Arrays are sequences of elements of a given type. In Java arrays of primitive types and arrays of objects are allowed.
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
.
Given an array variable x
, the element stored at position i
is accessed using x[i]
. Note that positions are counted starting at 0.
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
.
The length of an array x
is accessed by x.length
.
// 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;
// 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};
Since arrays are objects, equality comparison on arrays tests whetehr two arrays correspond to the same object, not whether they contain the same values
int[] x = {1,2};
int[] y = {1,2};
return x == y;
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).
int[] x = {1,2};
int[] y = {1,2};
return x.equals(y);
String[] x = { "Peter", "Bob"};
String[] y = { "Peter", "Bob"};
return x.equals(y);
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();
}
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
int[] x = {1,2,3};
return x.toString(); // this is not useful
import java.util.Arrays;
int[] x = {1,2,3};
return Arrays.toString(x);
String[] x = { "Peter", "Bob"};
String[] y = { "Peter", "Bob"};
return x.equals(y); // they are not the same objects, so they are not equals
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
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 collectionaddAll(Collection c)
adds all element from collection c
to the current collectioncontains(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 collectionisEmpty()
return true
if the collections is empty.size()
returns the number of elements stored in the collectionremove(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.
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 vectorset(int index, E element)
replaces the element at position index
with element
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;
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;
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();
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;
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;
}
}
Two EqualIsNotEqual
objects are considered equal if their a values are the same.
import lecture.EqualIsNotEqual;
EqualIsNotEqual x = new EqualIsNotEqual(1,1);
EqualIsNotEqual y = new EqualIsNotEqual(1,2);
return x.equals(y);
Searching in a collection uses equals
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);
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.
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());
}
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>
.
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);
}
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).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);
}
}
import lecture.MySet;
MySet x = new MySet();
x.add("Peter");
return x.contains("Alice");
package lecture;
public class
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"));
BitSet
uses a single bit in a array of type byte to record whether the element at certain position is in the set.
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));
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)$).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.
import java.util.ArrayList;
ArrayList<String> x = new ArrayList<String>();
x.add("Peter");
x.add("Bob");
x.add("Alice");
return x;
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.
import java.util.LinkedList;
LinkedList<String> x = new LinkedList<String>();
x.add("Peter");
x.add("Bob");
x.add("Alice");
return x;
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.
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()
.
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);
}
}
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());
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;
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).
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.
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;
}
}
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;