Arrays

Arrays are sequences of elements of a given type. In Java arrays of primitive types and arrays of objects are allowed.

Declaring and creating array types

An array of type T is declared as T[], e.g., int[] would be an array of type int. Arrays are objects in Java. That is new arrays are created using new. When creating an array the size of the array is provided in square brackets. For instance, new T[3] creates an array of size 3 that holds elements of type T. The elements of an array of a primitive type are initialized to the default value of that type while the elements of an array of an Object type are initialized to null.

Accessing the elements of an array

Given an array variable x, the element stored at position i is accessed using x[i]. Note that positions are counted starting at 0.

Array literals

In Java an array literal is a list of elements separated by , that is enclosed in {}. For example, { 1, 3, 4 } is a literal int array containing the elements 1, 3, and 4.

Determining the length of an array

The length of an array x is accessed by x.length.

In [1]:
// declare a reference variable for an object of type int[] and assign it a newly created int array of size 3 
int[] x = new int[3];

// since int is a primitive type the arrays elements are set to 0, the default value of int
System.out.println(String.format("x = [%d, %d, %d]", x[0], x[1], x[2]));

// now let's set the first elment (position 0) to 4 and the 3rd element (position 2) to 5
x[0] = 4;
x[2] = 5;

System.out.println(String.format("x = [%d, %d, %d]", x[0], x[1], x[2]));

// get length (3)
return x.length;
x = [0, 0, 0]
x = [4, 0, 5]
Out[1]:
3
In [2]:
// using array literals 
int[] x = {1,2,4};

System.out.println(String.format("x = [%d, %d, %d]", x[0], x[1], x[2]));

// outside of intialization you new need to use the syntax new T[]{...}
return new int[]{1,4,5};
x = [1, 2, 4]
Out[2]:
[1, 4, 5]

Since arrays are objects, equality comparison on arrays tests whetehr two arrays correspond to the same object, not whether they contain the same values

In [4]:
int[] x = {1,2};
int[] y = {1,2};

return x == y;
Out[4]:
true

Since arrays are objects they do not provide the methods such as toString() available for all other object types through Object. For instance, the equals method is defined for arrays (not overridden).

In [5]:
int[] x = {1,2};
int[] y = {1,2};
return x.equals(y);
Out[5]:
false
In [6]:
String[] x = { "Peter", "Bob"};
String[] y = { "Peter", "Bob"};
return x.equals(y);
Out[6]:
false

Multidimensional Arrays

In [6]:
int[][] matrix;

matrix = new int[4][2];

matrix[3][0] = 15;

for(int i = 0; i < matrix.length; i++) {
    for(int j = 0; j < matrix[i].length; j++) {
        System.out.print(matrix[i][j] + " ");
    }
    System.out.println();
}

System.out.println();

for(int i = 0; i < matrix.length; i++) {
    matrix[i] = new int[i+1];
}

for(int i = 0; i < matrix.length; i++) {
    for(int j = 0; j < matrix[i].length; j++) {
        System.out.print(matrix[i][j] + " ");
    }
    System.out.println();
}
0 0 
0 0 
0 0 
15 0 

0 
0 0 
0 0 0 
0 0 0 0 
Out[6]:
null

The Arrays class

Typically, when comparing arrays for equality or serializing them as strings, we would like to do this element-wise using the corresponding method implemented for the element type. The Arrays class of Java serves this need by providing static methods that implement this functionality. See the documentation of this class: javadoc

In [7]:
int[] x = {1,2,3};
return x.toString(); // this is not useful
Out[7]:
[I@4e8fc73a
In [8]:
import java.util.Arrays;
int[] x = {1,2,3};
return Arrays.toString(x);
Out[8]:
[1, 2, 3]
In [9]:
String[] x = { "Peter", "Bob"};
String[] y = { "Peter", "Bob"};
return x.equals(y); // they are not the same objects, so they are not equals
Out[9]:
false
In [10]:
import java.util.Arrays;
String[] x = { "Peter", "Bob"};
String[] y = { "Peter", "Bob"};
return Arrays.equals(x,y); // all of their elements are equals (using String.equals) -> they are equal
Out[10]:
true

Collections

Java provides a rich set of collection types that are defined though a hierarchy of interfaces and classes implementing these interfaces. We will only cover some of the collections here. See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/package-summary.html for the full list. The highest level interface defined for collection is Collection. This interface defines basic methods that are implemented by all Collections including:

  • add(E e) adds and element to the collection
  • addAll(Collection c) adds all element from collection c to the current collection
  • contains(Object o) checks wether object o is contained in the collection. Objects are compared using equals
  • iterator() returns an iterator for the collection (more about iterators below)
  • toArray() returns an array with the elements stored in the collection
  • isEmpty() return true if the collections is empty.
  • size() returns the number of elements stored in the collection
  • remove(Object o) removes Object o from the collection (if it exits). Objects are compared using equals

Relevant subinterfaces are lists (List), sets (Set), maps (Map). We will cover some implementations of these interface in the following.

Collections store elements of a certain type. This is implemented using generics that will be covered in a later notebook. For now, just note that when creating a collection you have to specify the class of the elements that you want to store in the collection when declaring a variable of this collection type and when creating an instance like so:

List<String> mystringlist = new ArrayList<String> ();

Note that a collection of type T can be used to store any subclass of T too.

Vector

A vector https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Vector.html is a an extensible array that allow elements to be accessed by position. Some important methods are:

  • get(int position) returns the element stored at position
  • add(Object o) append object o to the end of the vector
  • set(int index, E element) replaces the element at position index with element
In [1]:
import java.util.Vector;

// create a Vector for storing Integers
Vector<Integer> x = new Vector<Integer> ();
x.add(1);
x.add(2);
x.add(3);

return x;
Out[1]:
[1, 2, 3]
In [2]:
import java.util.Vector;

// create a Vector for storing Integers
Vector<Integer> x = new Vector<Integer> ();
x.add(1);
x.add(2);
x.add(3);

// remove 2
x.remove(2); // remove 2 
x.set(1, 5); // replace the second element (now 3) with 5
return x;
Out[2]:
[1, 5]
In [3]:
import java.util.Vector;

// create a Vector for storing Integers
Vector<Integer> x = new Vector<Integer> ();
x.add(1);
x.add(2);
x.add(3);

return x.size();
Out[3]:
3
In [4]:
import java.util.Vector;

// create a Vector for storing Integers
Vector<String> x = new Vector<String> ();
x.add("A");
x.add("B");
x.add("C");

return x;
Out[4]:
[A, B, C]
In [2]:
package lecture;

public class EqualIsNotEqual {
    
    int a;
    int b;
    
    public EqualIsNotEqual (int a, int b) {
        this.a = a;
        this.b = b;
    }
    
    public boolean equals(Object o) {
        if (!(o instanceof EqualIsNotEqual))
            return false;
        EqualIsNotEqual ob = (EqualIsNotEqual) o;
        return ob.a == this.a;
    }
}
Out[2]:
lecture.EqualIsNotEqual

Two EqualIsNotEqual objects are considered equal if their a values are the same.

In [3]:
import lecture.EqualIsNotEqual;

EqualIsNotEqual x = new EqualIsNotEqual(1,1);
EqualIsNotEqual y = new EqualIsNotEqual(1,2);
return x.equals(y);
Out[3]:
true

Searching in a collection uses equals

In [4]:
import lecture.EqualIsNotEqual;
import java.util.Vector;

EqualIsNotEqual x = new EqualIsNotEqual(1,1);
EqualIsNotEqual y = new EqualIsNotEqual(1,2);

Vector<EqualIsNotEqual> v = new Vector<EqualIsNotEqual>();
v.add(x);

return v.contains(y);
Out[4]:
true

Iterators

Iterators all the iteration over all elements from a set. To get an interator for a collection its iterator() method. The two main methods of an iterator are hasMore() which returns true if there are still more elements to iterate over and next() which returns the next element from the collection.

In [5]:
import java.util.Vector;
import java.util.Iterator;

// create a Vector for storing Integers
Vector<Integer> x = new Vector<Integer> ();
x.add(1);
x.add(2);
x.add(3);

Iterator<Integer> it = x.iterator();
while(it.hasNext()) {
    System.out.println("" + it.next());
}
1
2
3
Out[5]:
null

Java also allows the iteration over the elements of a collection or array using a for loop like this: for( <type> <varname>: <collection) which in each iteration binds one element from <collection> to variable <varname>.

In [21]:
import java.util.Vector;
import java.util.Iterator;

// create a Vector for storing Integers
Vector<Integer> x = new Vector<Integer> ();
x.add(1);
x.add(2);
x.add(3);

for(Integer i: x) {
    System.out.println("" + i);
}
1
2
3
Out[21]:
null

Sets

A set is an unordered collection. An element can either be in a set or not. Two example implementation of the Set interface are

  • HashSet is backed up by a HashMap (see below).
In [10]:
package lecture;

import java.util.List;
import java.util.ArrayList;



public class MySet {
    
    private List<String> el;

    public MySet() {
        el = new ArrayList<String>();
    }
    
    public int numEls () {
        return el.size();
    }
    
    public void add(String e) {
        if (!el.contains(e))
            el.add(e);
    }
    
    public boolean contains(String e) {
        return el.contains(e);
    }
    
}
Out[10]:
lecture.MySet
In [11]:
import lecture.MySet;

MySet x = new MySet();
x.add("Peter");
return x.contains("Alice");
Out[11]:
false
In [ ]:
package lecture;

public class
In [2]:
import java.util.Set;
import java.util.HashSet;

Set x = new HashSet<String> ();

x.add("Peter");
x.add("Bob");
x.add("Alice");

// x contains "Peter" but not "Alice"
return String.format("x has Peter: %b, x has Alice: %b", x.contains("Peter"), x.contains("Alice"));
Out[2]:
x has Peter: true, x has Alice: true

BitSets

BitSet uses a single bit in a array of type byte to record whether the element at certain position is in the set.

In [25]:
import java.util.BitSet;

BitSet x = new BitSet ();

x.set(1);
x.set(3);

// x contains 1 but not 2

return String.format("x has 1: %b, x has 2: %b", x.get(1), x.get(2));
Out[25]:
x has 1: true, x has 2: false

Lists

Similar to vectors, lists contain a sequence of elements. We will consider two implementation of the List interface:

  • ArrayList internally uses an array to store the elements of the list. This has the advantage that accessing the element at some position is fast (computational complexity is $O(1)$). However, inserting at a position within the list can be expesive since all following elements have to be moved (computational complexity is $O(n)$).
  • LinkedList uses points to connect list elements. That makes insertion at the beginning and end of the list fast ($O(1)$), while accessing an element at an arbitrary position is slow ($O(n)$).

Dynamic arrays

Array lists and vectors are both implemented using the concept of a dynamic array. The idea is create an array of size n (some fixed constant). Once the list outgrows the array, a larger array is created, and the elements are copied from the original to the larger array. From the on the larger array is used. That is, if the current array is of size n and an operation cases the list to grow to n+1 elements, then a new array is created. Of course we have the freedom of choosing the size of the new array as long as it is can hold at least n+1 elements. As it turns out it is beneficial to always double the size of the array., e.g., from $n$ to $2n$ because otherwise insertion will be in O(n). When doubling the array size, then insertion has amortized complexity of O(1). Here amortized means that while a single insertion may cost more than O(1), inserting n elements is in O(n). That is, the high complexity of some insertions is balanced out by the low complexity of other operators.

In [26]:
import java.util.ArrayList;

ArrayList<String> x = new ArrayList<String>();
x.add("Peter");
x.add("Bob");
x.add("Alice");

return x;
Out[26]:
[Peter, Bob, Alice]

Linked list

A linked list datastructure uses a class ListElement to store elements in the list which has a field to store a data element and a field next of type ListElement pointing to the next element of the list. The linked list then has a single field ListElement referencing the first element in the list (often called its head). To search an element e in a linked list, one starts at the head and follows the next references until either the end of the list is reached (the last list element has next set to null) or the element is found. For instance, a sketch of a possible implementation of a String list (without methods for searching, inserting, ...) is shown below:

public class StringList {
    StringListElement head;
}

public class StringListElement {
    StringListElement next;
    String data;
}

The above implementation is what is called a singly linked list. In a doubly linked list (which Java's LinkedList class uses) each element has an additional field previous pointing to the previous element in the list which allows for backwards navigation. Often list implementation use and additional field to store the list size (otherwise computing the size of a list would be $O(n)$) and also maintain a pointer to the tail (the last element in the last) for $O(1)$ insertation at the end of the list.

In [58]:
import java.util.LinkedList;

LinkedList<String> x = new LinkedList<String>();
x.add("Peter");
x.add("Bob");
x.add("Alice");

return x;
Out[58]:
[Peter, Bob, Alice]

Maps

As the name suggest Maps associate keys to values. Thus, for maps you have to specify two type: the type of keys to be used and the type of values to be used.

  • put(K key, V value) - associates the object value with the key key
  • get(Object key) - returns the value associated with key (or null is the key is not in the map)

There are many possible ways of how maps can be implemented. Here we will consider two such implementations: HashMap and TreeMap. A TreeMap also provides sorted access to the keys stored in the map while the hash map does not.

Background on Hash Tables

A HashMap uses a data structure called hash table. A hash table uses a function h (the hash function) that maps objects to integer values within a certain range, say [0,n-1] with the requirement that h(o) = h(o') if o and o are two objects that are equal. Internally, a hash table maintains an array table of size n storing linked lists. The linked list at table[i] called a bucket stores all key-value pairs for keys k where h(k) = i. To insert a new key value pair (k,v) we compute h(k), then search through the linked list at table[h(k)]. If the key k already exists in the linked list then the value associated with this key replaced with v. If not then (k,v) is appended at the end of the list. To search for a key k we again compute h(k) and then loop through the list at table[h(k)] comparing each key in the list with k. If we find a key that is equal to k then we return the corresponding value.

In Java's implementation of HashMap the hashCode method of objects is used as the hash function.

Example class defining its own equality and hashCode methods. Note that for hashCode to work properly, it has to be compatible with equals. That is, for any two objects o1 and o2 if o1.equals(o2) then o1.hashCode() == o2.hashCode().

In [8]:
package lecture;

public class Person {
    
    public String name;
    public String ssn;
    public String address;
    public int salary;
    
    public Person(String name, String ssn, String address, int salary) {
        this.ssn = ssn;
        this.name = name;
        this.address = address;
        this.salary = salary;
    }
    
    // two persons are considered equal if they have the same ssn and name
    @Override
    public boolean equals (Object o) {
        if (!(o instanceof Person))
            return false;
        
        Person op = (Person) o;
        
        return op.ssn.equals(ssn)
                && op.name.equals(name);
    }
    
    @Override
    public int hashCode() {
        // return the bitwise xor of the name and the ssns hashcode
        return this.name.hashCode() ^ this.ssn.hashCode(); 
    }
    
    public String toString() {
        return String.format("<Name: %s, SSN: %s, Address: %s, Salary: %d>", name, ssn, address, salary);
    }
    
}
Out[8]:
lecture.Person
In [3]:
import lecture.Person;

// now let's test the equality and hashcode
Person p1 = new Person("Pete", "123-456-7890", "Chicago, IL, 60616", 3000);
Person p2 = new Person("Pete", "123-456-7890", "Springfield, IL, 50324", 50000);

System.out.println(p1.equals(p2));
System.out.println("" + p1.hashCode());
System.out.println("" + p2.hashCode());
true
-1269805907
-1269805907
Out[3]:
null
In [9]:
import lecture.Person;
import java.util.HashMap;

// now let's create a hashtable that maps ssns to persons 
Person pete = new Person("Pete", "123-456-7890", "Chicago, IL, 60616", 0);
Person alice = new Person("Alice", "111-111-1111", "Springfield, IL, 50324", 100000);
Person bob = new Person("Bob", "555-555-5555", "Chicago, IL, 60615", 20000);

HashMap<String,Person> map = new HashMap<> ();
map.put(pete.ssn, pete);
map.put(bob.ssn, bob);
map.put(alice.ssn, alice);

return map;

Creating Collections

The Collections API provides convenient ways for creating collections from object. For lists and sets this is the of method. The of method is variadic, i.e., it takes an arbitrary number of parameters (the elements to store in the collections).

Sorting Lists and Comparable

Java provides methods for sorting collections. This functionality and other convenience functions are provided through a class called Collections. Here we are interested in the sort methods:

  • sort(List<T> list) sorts the list list of element type T. The class T must implement the interface Comparable<T2> where T2 has to be a superclass of T.
  • sort(List<T> list, Comparator<? super T> c) sorts the list list using the Comparator c.

The Comparator<T> interface defines a single method compare(T o1, T o2) that compares two objects of type T according to some total order and returns 0 if the objects are the same, a negative integer is the first element is less than the second element, and a positive integer if the first elements is larger than the second element.

In [10]:
package lecture;

import java.util.Comparator;
import lecture.Person;

public class SalaryComparator implements Comparator<Person> {
    
    public int compare(Person o1, Person o2) {
        if (o1 == o2)
            return 0;
        if (o1.salary != o2.salary)
            return (o1.salary < o2.salary) ? -1 : 1;
        return 0;
    }
    
}
Out[10]:
lecture.SalaryComparator
In [11]:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

import lecture.Person;
import lecture.SalaryComparator;

// let's sort a list of persons according to their salary

Person pete = new Person("Pete", "123-456-7890", "Chicago, IL, 60616", 30000);
Person alice = new Person("Alice", "111-111-1111", "Springfield, IL, 50324", 100000);
Person bob = new Person("Bob", "555-555-5555", "Chicago, IL, 60615", 20000);

List<Person> l = new ArrayList<Person> ();
l.add(pete);
l.add(alice);
l.add(bob);

Collections.sort(l, new SalaryComparator()); // sort based on salary

return l;
Out[11]:
[<Name: Bob, SSN: 555-555-5555, Address: Chicago, IL, 60615, Salary: 20000>, <Name: Pete, SSN: 123-456-7890, Address: Chicago, IL, 60616, Salary: 30000>, <Name: Alice, SSN: 111-111-1111, Address: Springfield, IL, 50324, Salary: 100000>]