CISC 3115
Modern Programming Techniques
Lecture 08
Collections (Containers), Generics, and Autoboxing

Vectors

A vector is a container, similar to an arrray, supporting: For simplicity, we will omit the insertion and removal operations.

We will present an evolution of vector classes, incorporating much of what we have covered this semester:

Topics

We will examine adding each of the following features to our vector.

A First-Attempt Vector Class

class Vector {
	Vector(int capacity) {
		this.capacity = capacity;
		arr = new int[capacity];
	}
	Vector() {this(DEFAULT_CAPACITY);}

	int get(int index) {return arr[index];}

	void set(int index, int value) {arr[index] = value;}

	void add(int value) {
		arr[size] = value;
		size++;
	}

	int getCapacity() {return capacity;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	int [] arr;
	int capacity, size = 0;
	static final int DEFAULT_CAPACITY = 10;
}

This code has flaws that we will correct in this lecture:

Exception Handling

Bounds Checking

Container Overflow

A Vector with Exception-Handling

class Vector {
	Vector(int capacity) {arr = new int[capacity];}
	Vector() {this(DEFAULT_CAPACITY);}

	int get(int index) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, int value) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(int value) {
		if (size == getCapacity()) throw new IllegalStateException("Exceeded vector capacity of " + arr.length);
		arr[size++] = value;
	}

	int getCapacity() {return arr.length;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	int [] arr;
	int size = 0;
	static final int DEFAULT_CAPACITY = 10;
}
Notes
Here is the app:
public class VectorApp {
	public static void main(String [] args) {
		Vector v = new Vector();
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());

		try {
			System.out.println(v.get(0));
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on get: " + e);
		}

		try {
			v.set(0, 999);
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on set: " + e);
		}

		for (int i = 0; i < v.getCapacity(); i++)
			v.add(i * 10);
		System.out.println(v);
		System.out.println("size: " + v.size());

		for (int i = 0; i < v.size(); i++)
			v.set(i, v.get(i)+1);
		System.out.println(v);
		System.out.println();

		try {
			v.add(999);
		} catch (IllegalStateException e) {
			System.out.println("Successfully caught exception on add: " + e);
		}
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());
		System.out.println(v);
	}
}
Notes
capacity: 10
size: 0
Successfully caught exception on get: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
Successfully caught exception on set: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
{0, 10, 20, 30, 40, 50, 60, 70, 80, 90}
size: 10
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}

Successfully caught exception on add: java.lang.IllegalStateException: Exceeded vector capacity of 10
capacity: 10
size: 10
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}

User-Defined Exception Classes

RunTimeException — Checked vs Unchecked Exceptions

Exception-Handling Using a User-Defined Exception Class

In those version, we define our own standalone exception class. Here is an exception class that we define; we will use it for both types of exceptions: out of bounds and overflow
class VectorException extends RuntimeException {
	VectorException(String message) {super(message);}
}
Notes
Here is the Vector class:
class Vector {
	Vector(int capacity) {arr = new int[capacity];}
	Vector() {this(DEFAULT_CAPACITY);}

	int get(int index) {	
		if (index < 0 || index >= size) throw new VectorException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, int value) {	
		if (index < 0 || index >= size) throw new VectorException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(int value) {
		if (size == getCapacity()) throw new VectorException("Exceeded vector capacity of " + arr.length);
		arr[size++] = value;
	}

	int getCapacity() {return arr.length;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	int [] arr;
	int size = 0;
	static final int DEFAULT_CAPACITY = 10;
}
Notes
Here is the app:
public class VectorApp {
	public static void main(String [] args) {
		Vector v = new Vector();
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());

		try {
			System.out.println(v.get(0));
		} catch (VectorException e) {
			System.out.println("Successfully caught exception on get: " + e);
		}

		try {
			v.set(0, 999);
		} catch (VectorException e) {
			System.out.println("Successfully caught exception on set: " + e);
		}

		for (int i = 0; i < v.getCapacity(); i++)
			v.add(i * 10);
		System.out.println(v);
		System.out.println("size: " + v.size());

		for (int i = 0; i < v.size(); i++)
			v.set(i, v.get(i)+1);
		System.out.println(v);
		System.out.println();

		try {
			v.add(999);
		} catch (VectorException e) {
			System.out.println("Successfully caught exception on add: " + e);
		}
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());
		System.out.println(v);
	}
}

Notes
The output:
capacity: 10
size: 0
Successfully caught exception on get: VectorException: Index 0 out of bounds for length 0
Successfully caught exception on set: VectorException: Index 0 out of bounds for length 0
{0, 10, 20, 30, 40, 50, 60, 70, 80, 90}
size: 10
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}

Successfully caught exception on add: VectorException: Exceeded vector capacity of 10
capacity: 10
size: 10
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}
Sidebar

A Nested/Inner Class for the Exception Class

In this version, we move the exception class into the corresponding container class. This is primarily for the purpose of encapsulation / data hiding. In practice, exception classes are not nested by defined as a top-level class (as in the previous version).

class Vector {
	class VectorException extends RuntimeException {
		VectorException(String message) {super(message);}
	}

	Vector(int capacity) {arr = new int[capacity];}
	Vector() {this(DEFAULT_CAPACITY);}

	int get(int index) {	
		if (index < 0 || index >= size) throw new VectorException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, int value) {	
		if (index < 0 || index >= size) throw new VectorException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(int value) {
		if (size == getCapacity()) throw new VectorException("Exceeded vector capacity of " + arr.length);
		arr[size++] = value;
	}

	int getCapacity() {return arr.length;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	int [] arr;
	int size = 0;
	static final int DEFAULT_CAPACITY = 10;
}
Notes
  • Again, the absence of static in the nested class header means we are dealing with an inner (non-static nested) class
    • The fully qualified name of the class is Vector$VectorException
      • The fully-qualified name of a class is its full name (in the absence of any imports or other naming-shortcuts)
        • This would include any package names, e.g. java.lang.String
        • In our case, we are working in the default package (i.e., the package you are in if there is no package statement at the top of your source file).
    • The main advantage of doing this is when the nested class is only used by the outer class and thus no other code needs to see this class.
  • Other than the placement of the exception class, everything is the same
capacity: 10
size: 0
Successfully caught exception on get: Vector$VectorException: Index 0 out of bounds for length 0
Successfully caught exception on set: Vector$VectorException: Index 0 out of bounds for length 0
{0, 10, 20, 30, 40, 50, 60, 70, 80, 90}
size: 10
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}

Successfully caught exception on add: Vector$VectorException: Exceeded vector capacity of 10
capacity: 10
size: 10
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}
Notes
  • Note the name of the exception

A Resizeable Underlying Container

We now turn our attention to capacity/memory management of the underlying array, with the goal of having a resizeable underlying container. This would permit us to basically ignore capacity issues.
Sidebar

Memory Management

Here is the core logic for resizing an array, assuming the array is referenced by the variable arr:

and here is the corresponding Java fragment:

int [] temp = new int[2*arr.length];
for (int i = 0; i < arr.length; i++)
	temp[i] = arr[i];
arr = temp;
Notes
  • Not much to say here; the fragment is a direct translation of the diagram into code
  • Note the loop goes up to arr.length, i.e., the old array objects length
  • As we mentioned above, the doubling is crucial to an efficient algorithm

Packaging the Resizing Logic into a Method

The Right Way

The Method
int [] resize(int [] arr) {
	int [] temp = new int[2*arr.length];
	for (int i = 0; i < arr.length; i++)
		temp[i] = arr[i];
	return temp;
}
The Invoking Code
int [] arr = new int[…];
…
// We realize we need a bigger array
arr = resize(arr);

The Wrong Way

The Method
void resize(int [] arr) {
	int [] temp = new int[2*arr.length];
	for (int i = 0; i < arr.length; i++)
		temp[i] = arr[i];
	arr = temp;
}
The Invoking Code
int [] arr = new int[…];
…
// We realize we need a bigger array
resize(arr);

Embedding Memory-Management into a Container Class

The above memory-managed logic is typically not done on 'standalone' arrays, but rather in the context of a container class and its underlying array.
class Vector {
	Vector() {
		arr = new int[INITIAL_CAPACITY];
	}

	int get(int index) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, int value) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(int value) {
		checkCapacity();
		arr[size++] = value;
	}

	int getCapacity() {return arr.length;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	private void checkCapacity() {
		if (size < getCapacity()) return;
		int [] temp = new int[2*getCapacity()];
		for (int i = 0; i < size; i++)
			temp[i] = arr[i];
		arr = temp;
	}

	int [] arr;
	int size = 0;
	static final int INITIAL_CAPACITY = 10;
}
Notes
The app:
public class VectorApp {
	public static void main(String [] args) {
		basics();
		exceptions();
		capacityManagement();
	}

	static void basics() {
		System.out.println("=== Basic Functionality ===");
		System.out.println("--- Creating Vector and printing it and its initial stats ---");
		Vector v = new Vector();
		System.out.println(v + " (" + v.size() + "/" + v.getCapacity());
		System.out.println();

		System.out.println("--- Loading it up to capacity; add ---");
		for (int i = 0; i < v.getCapacity(); i++) {
			v.add(i * 10);
			System.out.println(v + " (" + v.size() + "/" + v.getCapacity() + ")");
		}
		System.out.println();

		System.out.println("--- Incrementing each element; get and set ---");
		for (int i = 0; i < v.size(); i++)
			v.set(i, v.get(i)+1);
		System.out.println(v);
		System.out.println();
	}

	static void exceptions() {
		System.out.println("=== Exceptions ===");
		Vector v = new Vector();
		try {
			System.out.println(v.get(0));
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on get: " + e);
		}

		try {
			v.set(0, 999);
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on set: " + e);
		}
		System.out.println();
	}

	static void capacityManagement() {
		System.out.println("=== Capacity Management ===");
		Vector v = new Vector();

		System.out.println("--- Creating Vector and loading it up with 1,000 elements ---");
		int oldCapacity = -1;
		for (int i = 0; i < 1000; i++) {
			v.add(i);
			if (v.getCapacity() != oldCapacity) 
				if (v.size() <= 10)
					System.out.println(v + " (" + v.size() + "/" + v.getCapacity() + ")");
				else
					System.out.println("{" + v.get(0) + ", " + v.get(1) + "..." + v.get(v.size()-2) + ", " + 
						v.get(v.size()-1) + "} (" + v.size() + "/" + v.getCapacity() + ") ... resized");  
			oldCapacity = v.getCapacity();
		}
		System.out.println("{" + v.get(0) + ", " + v.get(1) + "..." + v.get(v.size()-2) + ", " + 
			v.get(v.size()-1) + "} (" + v.size() + "/" + v.getCapacity() + ")");  
	}
}
Notes
=== Basic Functionality ===
--- Creating Vector and printing it and its initial stats ---
{} (0/10

--- Loading it up to capacity; add ---
{0} (1/10)
{0, 10} (2/10)
{0, 10, 20} (3/10)
{0, 10, 20, 30} (4/10)
{0, 10, 20, 30, 40} (5/10)
{0, 10, 20, 30, 40, 50} (6/10)
{0, 10, 20, 30, 40, 50, 60} (7/10)
{0, 10, 20, 30, 40, 50, 60, 70} (8/10)
{0, 10, 20, 30, 40, 50, 60, 70, 80} (9/10)
{0, 10, 20, 30, 40, 50, 60, 70, 80, 90} (10/10)

--- Incrementing each element; get and set ---
{1, 11, 21, 31, 41, 51, 61, 71, 81, 91}

=== Exceptions ===
Successfully caught exception on get: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
Successfully caught exception on set: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0

=== Capacity Management ===
--- Creating Vector and loading it up with 1,000 elements ---
{0} (1/10)
{0, 1...9, 10} (11/20) ... resized
{0, 1...19, 20} (21/40) ... resized
{0, 1...39, 40} (41/80) ... resized
{0, 1...79, 80} (81/160) ... resized
{0, 1...159, 160} (161/320) ... resized
{0, 1...319, 320} (321/640) ... resized
{0, 1...639, 640} (641/1280) ... resized
{0, 1...998, 999} (1000/1280)

Sidebar

Adding Another Constructor

Despite the container now being self-resizing, we often add an additional constructor that allows the user to specify the initial capacity of the container

Vector(int capacity) {arr = new int[capacity];}

In fact, we will see that some containers in the JFC provide and initial capacity constructor and others don't; and this can actually tell us something about the nature of the container.

Generic Containers

We are headed towards our last improvement — a generic container, i.e., a container that permits arbitrary element types. We will make a couple of detours on the way, to both motivate it as well as introduce several other language features that are both useful and informative.

A Vector with Integer Element Type

We'll start by replacing the primitive int element type of the Vector with the corresponding Integer wrapper class.
  • As you may have learned in 3115, we want our containers to have a class element type (in particular Object, so we'll move a step in that direct and use Integer as it is a class, and it is also related to int (i.e., it is its wrapper class).
  • This has little to do with going towards generics other than the essentially irrelevant replacing of a primitive element type with a class.
  • The main change will be in the app below.
class Vector {
	Vector() {
		this.capacity = INITIAL_CAPACITY;
		arr = new Integer[capacity];
	}

	Integer get(int index) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, Integer value) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(Integer value) {
		checkCapacity();
		arr[size++] = value;
	}

	int getCapacity() {return capacity;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	private void checkCapacity() {
		if (size < capacity) return;
		capacity *= 2;
		Integer [] temp = new Integer[capacity];
		for (int i = 0; i < size; i++)
			temp[i] = arr[i];
		arr = temp;
	}

	Integer [] arr;
	int capacity, size = 0;
	static final int INITIAL_CAPACITY = 10;
}
Notes
  • As mentioned above, nothing much new here: a change in the underlying array element type and the accompanying changes to the object creation, and associated parameters/return types.
The app is where we want to focus our attention:
public class VectorApp {
	public static void main(String [] args) {
		Vector v = new Vector();
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());

		try {
			System.out.println(v.get(0));
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on get: " + e);
		}

		try {
			v.set(0, 999);
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on set: " + e);
		}

		for (int i = 1; i <= v.getCapacity(); i++)
			v.add(i * 10);
		System.out.println(v);
		System.out.println("size: " + v.size());

		for (int i = 0; i < v.size(); i++)
			v.set(i, v.get(i)+1);
		System.out.println(v);

		v.add(999);
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());
		System.out.println(v);
		System.out.println();
	}
}
Notes
  • The highlighted items are all int-related literals or expressions,appearing in a context in which an Integer is expected.
  • Integer is a wrapper class for int it is not an int
    • In particular, there is no + or * operator defined for Integer
      • Strictly speaking, we should be:
        • creating Integer objects to wrap any ints when adding to the Vector (whose element type is Integer
          • it would seem that to create an Integer from an int (say the int variable i), there would be an Integer constructor that accepts an int, and then one could write
            
            Integer integer = new Integer(i);
            						
            however, for various technical reasons, that is not recommended; rather, there is a static method of Integer, valueOf, that accepts an int and returns a new Integer that wraps that int, and thus one would write:
            
            Integer integer = Integer.valueOf(i);
            						
        • extracting int from any Integer objects when getting any element from the Vector and performing any arithmetic (or other int-based action on them.
          • this direction works as expected: there is a (instance) method of Integer, intValue that returns the (wrapped) value of the receiving Integer object as an int, e.g.:
            
            int i = Integer.intValue();
            						
In other words, strictly speaking the correct version of the app should be:
public class VectorApp {
        public static void main(String [] args) {
                Vector v = new Vector();
                System.out.println("capacity: " + v.getCapacity());
                System.out.println("size: " + v.size());

                try {
                        System.out.println(v.get(0));
                } catch (IndexOutOfBoundsException e) {
                        System.out.println("Successfully caught exception on get: " + e);
                }

                try {
                        v.set(0, Integer.valueOf(999));
                } catch (IndexOutOfBoundsException e) {
                        System.out.println("Successfully caught exception on set: " + e);
                }

                for (int i = 1; i <= v.getCapacity(); i++)
                    v.add(Integer.valueOf(i * 10));
                System.out.println(v);
                System.out.println("size: " + v.size());

                for (int i = 0; i < v.size(); i++) 
                    v.set(i, Integer.valueOf(v.get(i).intValue()+1));
                System.out.println(v);

                v.add(Integer.valueOf(999));
                System.out.println("capacity: " + v.getCapacity());
                System.out.println("size: " + v.size());
                System.out.println(v);
                System.out.println();
        }
}
Notes
  • the code that extracts the vector element, adds 1 to it, and puts it back, i.e., v.set(i, new Integer(v.get(i).intValue()+1)) is a mouthful. Here is its equivalent, broken down for greater clarity and readability:
    
    Integer integer = v.get(i);
    int j = integer.intValue();
    v.set(i, Integer.valueOf(j+1)); 
    		
This is clearly quite cumbersome
  • and we will shortly see it gets even more complex when combined with the true element type of containers

Autoboxing

Autoboxing Tutorial

Java 5 introduced a new language feature; one that had been long awaited and addressed the above issue: autoboxing. The basic idea was to make the boundary between a primitive type and its wrapper class as transparent as possible. so that one could (almost) used int and Integer interchangeably.

To do that the compiler was enlisted to do much of the above cumbersome wrapping and unwrapping logic. The wrapping was called boxing, and the unrpapping unboxing.

  • any time a primitive type appears in a context requiring an object, the type is wrapped (by the compiler) into its wrapper class object
    
    Vector v = new Vector();
    v.add(3);		// compiler automatically inserts an Integer.valueOf
                   //  so this internally becomes v.add(Integer.valueOf(3));
    		
  • any time an object appears in a context requiring a primitive type, the object is unwrapped (by the compiler) to its corresponding primitive type
    
    Vector v = new Vector();
    …
    int i = v.get(0);		// compiler automatically insert a '.intValue' 
                             //  so this becomes int i = v.get(0).intValue();
    		
As one more example, here is our increment logic for the elemtns of the Vector in our app:

v.set(i, v.get(i)+1);

  • The first thing that happens in this expression is the i'th element of the v is retrieved, v.get(i)
    • The retrieved object is an Integer since the Vector element type is Integer
    • For the sake of this example, let's assume the object was wrapping the int value 5
  • The retrieved element then has 1 (v.get(i)+1) added to it
    • This is an arithmetic expression requiring a primitive, however, the left operand is the Integer object we just retrieved, therefore the compiler unboxes the object, extracting the int value (5).
    • 5 is added to 1 resulting in the int value 6.
  • The result of the addition is the 'value' operand of the set method, which is putting the new update value back into the same location of v. That value is supposed to be an Integer (i.e., object) but is the int (6) from the previous step
    • The compiler therefore boxes the int into an Integer object (sing valieOf and that object then becomes the value that is stored into v by the set.
  • In summary, all the machinery of wrapping and unwrapping the primitive is still there, except it's supplied behind the scenes by the compiler and thus hidden from the programmer.
  • As a result our app did not have to be modified when we changed the element type from int to Integer

An Vector With Object Element Type

We now move to the general case of having Object as the element type
  • Object is chosen because all classes are descendants of Object, and an object of any class can be a Vector element.
class Vector {
	Vector() {
		this.capacity = INITIAL_CAPACITY;
		arr = new Object[capacity];
	}

	Object get(int index) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, Object value) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(Object value) {
		checkCapacity();
		arr[size] = value;
		size++;
	}

	int getCapacity() {return capacity;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	private void checkCapacity() {
		if (size < capacity) return;
		capacity *= 2;
		Object [] temp = new Object[capacity];
		for (int i = 0; i < size; i++)
			temp[i] = arr[i];
		arr = temp;
	}

	Object [] arr;
	int capacity, size = 0;
	static final int INITIAL_CAPACITY = 10;
}
Notes
  • Same basic comments as before, no real change other than to the array element type, and the various parameter/return values corresponding to elements
The app is again where we will focus our attention:
public class VectorApp {
	public static void main(String [] args) {
		Vector v = new Vector();
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());

		try {
			System.out.println(v.get(0));
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on get: " + e);
		}

		try {
			v.set(0, 999);
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on set: " + e);
		}

		for (int i = 1; i <= v.getCapacity(); i++)
			v.add(i * 10);
		System.out.println(v);
		System.out.println("size: " + v.size());

		for (int i = 0; i < v.size(); i++)
			v.set(i, ((Integer)v.get(i))+1);
		System.out.println(v);

		v.add(999);
		System.out.println("capacity: " + v.getCapacity());
		System.out.println("size: " + v.size());
		System.out.println(v);
		System.out.println();
	}
}
Notes
  • In the code v.get(i), now that the element type is Object all we can say when we extract an element is that it is an Object
    • In particular, we do not know that it is an Integer (other than the fact that we know we have only added Integers to the Vector; this last fact will prove crucial in a bit)
    • The compiler also does not know that the retrieved element is an Integer and thus cannot do any unboxing
      • The declared (i.e., compile-time) element type of the array is Object; it is only at runtime that the actual element is created (as an Integer)
        • That creation is done in the statement
          
          v.add(i * 10);
          								
          • once again, autoboxing plays a major role here. The add methods is expecting an object of type Object but is presented with an int (i * 10) and thus boxes the result of the expression the corresponding wrapper class (Integer) and it is this object that is added to the vector (i.e., the underlying array).
          • There is no problem inserting an Integer into an arrays of Object element type, as Integer is a subclass (indirectly possibly) of Object, i.e., it is a descendant of Object and thus is-a object of the suoerclass as well (i.e., an Integer object is-a Object object as well by virtue of the inheritance.

Upcasting and Downcasting

  • As explained above, the set (and the add) are not problematic; Integer objects are Object objects as well, can appear wherever an Object can, and thus can be made an element of an array with an Object element type.
    • Assigning an object from lower down in the inheritance hierarchy (e.g. Integer) to a variable higher up in the hioerarcht (e.g. Object is called upcasting and is always legal and requires no special syntax.. Thus inserting any type of object into a container with Object as element type is never a problem.
      • Which is why the containers of the JCF are have Object as their element type
  • The problem (as discussed in the first bullet) is retrieving an element from the container.
    • In that situation, one wishes to 'restore' the particular identity (type, e.g. Integer) to the element so it can be used when assigning the reference to an object from a variable higher up in the hierarchy to a variable lower down.
      • While still viewed as an Object there are very few methods one can apply; in particular for our case, we cannot invoke the intValue method to extract the wrapped int
    • What we are in essence doing is assigning a reference to an object (the element of the vector) from a variable higher up in the hierarchy (Objectto a variable lower down the hierarchy (Integer).
    • This is known as downcasting and will only work is the object being referred to is 'compatible' with the lower variable
      • For our discussion, think of compatible as being an object of the class of the lower variable
      • and in general, one does know if something is compatible or not until runtime

Examples of Upcasting and Downcasting

Here is a hierarchy of shapes, a superclass, Shape and two subclasses, Circle and Square:
abstract class Shape {…}

class Circle extends Shape {…}

class Square extends Shape {…}

Now let's start working with some variables and objects of these classes.

1. Circle circle = new Circle();
2. Square square = new Square();

3. Shape shape1 = circle;		// ok, upcast — All Circles are Shapes by virtue of inheritance
4. Shape shape2 = square;		// ok, upcast — All Squares are Shapes
Notes
  • First off, we need to distinguish between types of the variables and objects
    • circle: a reference to a Circle
    • square: a reference to a Square
    • shape1 / shape2: reference to a Shape
    • the object of type Circle created in line 1
    • the object of type Square created in line 2
  • Note the type of the reference variables are known statically (i.e., at code creation time, and thus at compile-time), while the objects are not created until runtime (as a result of executing the new operator).
    • Since every reference variable typically contains a reference to an object, we speak of the static (or compile-time) type of the variable (i.e., it's declared type), vs the dynamic (runtime) type of the object it references.
    • As such, after the above four line of code we have:
      • A variable of type reference to Circle (circle) containing a reference to a Circle object
      • A variable of type reference to Square (square) containing a reference to a Square object
      • A variable of type reference to Shape (shape1) containing a reference to a Circle object
      • A variable of type reference to Shape (shape2) containing a reference to a Square object
    • The last two are a result of assigning the references from circle / square to shape1 / shape2 respectively
      • These two assignments are called upcasts as they are assigning a reference from a variables lower down the hierarchy up (a subclass) to a variable higher in the hierarchy (a superclass)
      • And upcasts are both legal and always successful, since the subclass is-a instance of its superclass as well
        • i.e., All Circles are Shapes and All Squares are Shapes
Now consider these sequences:
Circle circle = new Circle();	
Square square = circle;		// illegal --  no Circles are Squares

and

Square square = new Square();	
Circle circle = square;		// illegal --  no Squares are Circles
Notes
  • Both of the above assignments are illegal: the two classes are not related in the hierarchy in any meaningful way; i.e., neither is a subclass of the other.
  • It thus makes no sense to assign an object reference from one to the other (any more than it makes sense at the semantics level, i.e., turning a circle into a square or vice versa.
  • Again, no Circles are Squares and no Squares are Circles
  • Being illegal (and therefore of no use), these 'sidecasts' are not given any technical name
The above seems fairly straightforward. Now consider the following:
Shape shape = new Circle();						// this is a legal upcast … All Circles are Shapes
…
Circle circle = shape1;		// seems ok --  assigning a Circle object to a Circle variable
Square square = shape1;		// seems wrong --  assigning a Circle object to a Square variable

and

Shape shape = new Square();						// this is a legal upcast … All Circles are Shapes
…
Circle circle = shape;		// seems wrong --  assigning a Square object to a Circle variable
Square square = shape1;		// seems ok --  assigning a Square object to a Square variable
Notes
  • In each case, the assignment from shape is to a variable of a subclass (circle or square). Such an assignment is called a downcast (casting down the hierarchy)
  • The two fragments of code are identical other than the object created and assigned to shape
  • The between the object creation and subsequent assignments is meant to signify that there may be a significant amount of code executed between the creation of the object and its assignment to circle and square
  • How is the compiler supposed to be able to distinguish between the legal and illegal assignments from shape to circle and square?
    • In fact there is no way for the compiler to make that distinction, given that the object is not even created until runtime!
To emphasize that last point about the compiler not being able to distinguish a valid from invalid downcast and thus the check must be done at runtime, consider the following code:
Random r = new Random();
boolean b = r.nextBoolean();
Shape shape;
if (b)
	shape = new Circle();		// upcast
else
	shape = new Square();		// upcast
Circle circle = shape;	// ok? or not? downcast
Notes
  • Which object is initially assigned to shape depends on the value of the random variable which is not generated until runtime.
  • As a result, whether circle is assigned a reference to a Circle object (legal) or a reference to a Square object (illegal) cannot be determined until runtime
  • Through careful coding, you can often ensure that your downcasts will always work. But the compiler wants you to signal the fact that you know something potentially dangerous is happening, and therefore you must cast the downcast:
    Shape shape = new Circle();
    Circle circle = (Circle)shape;		// will pass compilation and succeed at runtime
    Square square = (Square)shape;;		// will pass compilation but fail at runtime with a ClassCastException
    

Returning to our Random example

for final emphasis; the following code will sometimes work and sometimes fail with a ClassCastException:

Random r = new Random();
boolean b = r.nextBoolean();
Shape shape;
if (b)
	Shape = new Circle();
else
	Shape = new Square();
Circle circle = (Circle)shape;	// will sometimes succeed and sometimes fail at runtime
In summary, downcasts need to be checked at runtime, and if they fail, they throw a ClassCastException

The Need for Generic Containers

Having Object as the element type allows us to have any type of object in a container (because of upcasting). However, there is a price:

A Generic Vector

We now introduce our final improvement on our container: the ability to specify and enforce a specific element type.
class Vector<E> {
	@SuppressWarnings("unchecked")
	Vector() {
		this.capacity = INITIAL_CAPACITY;
		arr = (E [])new Object[capacity];
	}

	E get(int index) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		return arr[index];
	}

	void set(int index, E value) {	
		if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + size);
		arr[index] = value;
	}

	void add(E value) {
		checkCapacity();
		arr[size] = value;
		size++;
	}

	int getCapacity() {return capacity;}
	int size() {return size;}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += arr[i] + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	private void checkCapacity() {
		if (size < capacity) return;
		capacity *= 2;
		@SuppressWarnings("unchecked")
		E [] temp = (E [])new Object[capacity];
		for (int i = 0; i < size; i++)
			temp[i] = arr[i];
		arr = temp;
	}

	E [] arr;
	int capacity, size = 0;
	static final int INITIAL_CAPACITY = 10;
}
Notes
  • The type parameter for a generic class is introduced in the class header following the class name
  • The usual conventions is to use a single upper-case letter, usually T (for type), or E (for element)m and occasionally a numeric suffix (e.g., T1) if there are more than one type (see the Pair example below).
  • The type parameter is then used throughout the class definition in the context that the element type would appear.
  • For technical reasons,Java does not permit the creation of a 'generic array', i.e., an array whose element type is a type parameter. However, this is a necessity as we need an array as our underlying container
    • The workaround is to create an array of type Object, i.e., Object [] and then cast it to the generic array, i.e., E []
    • This in turn causes the compiler to issue a warning, which in turn can be muted with the annotation
      @SuppressWarnings("unchecked")
      				
      • The annotation can be placed immediately before a declaration (as in checkCapacity, or in front of the surrounding method (as in the constructor).
And here is the app:
public class VectorApp {
	public static void main(String [] args) {
		basics();
		exceptions();
		capacityManagement();
		generics();
	}

	static void basics() {
		System.out.println("=== Basic Functionality ===");
		System.out.println("--- Creating Vector and printing it and its initial stats ---");
		Vector<Integer> v = new Vector<>();
		System.out.println(v + " (" + v.size() + "/" + v.getCapacity() + ")");
		System.out.println();

		System.out.println("--- Loading it up to capacity; add ---");
		for (int i = 0; i < v.getCapacity(); i++) {
			v.add(i * 10);
			System.out.println(v + " (" + v.size() + "/" + v.getCapacity() + ")");
		}
		System.out.println();

		System.out.println("--- Incrementing each element; get and set ---");
		for (int i = 0; i < v.size(); i++)
			v.set(i, v.get(i)+1);
		System.out.println(v);
		System.out.println();
	}

	static void exceptions() {
		System.out.println("=== Exceptions ===");
		Vector<String> v = new Vector<>();
		try {
			System.out.println(v.get(0));
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on get: " + e);
		}

		try {
			v.set(0, "");
		} catch (IndexOutOfBoundsException e) {
			System.out.println("Successfully caught exception on set: " + e);
		}
		System.out.println();
	}

	static void capacityManagement() {
		System.out.println("=== Capacity Management ===");
		Vector<Double> v = new Vector<>();

		System.out.println("--- Creating Vector and loading it up with 1,000 elements ---");
		int oldCapacity = -1;
		for (int i = 0; i < 1000; i++) {
			v.add((double)i);
			if (v.getCapacity() != oldCapacity) 
				if (v.size() <= 10)
					System.out.println(v + " (" + v.size() + "/" + v.getCapacity() + ")");
				else
					System.out.println("{" + v.get(0) + ", " + v.get(1) + "..." + v.get(v.size()-2) + ", " + 
						v.get(v.size()-1) + "} (" + v.size() + "/" + v.getCapacity() + ") ... resized");  
			oldCapacity = v.getCapacity();
		}
		System.out.println("{" + v.get(0) + ", " + v.get(1) + "..." + v.get(v.size()-2) + ", " + 
			v.get(v.size()-1) + "} (" + v.size() + "/" + v.getCapacity() + ")");  
	}

	static void generics() {
		System.out.println("--- Generics");
		System.out.println();

		System.out.println("--- A Vector of doubles");
		Vector<Double> vDouble = new Vector<>();
		for (int i = 0; i < 10; i++)
			vDouble.add((double)i);
		System.out.println(vDouble);
		System.out.println();

		System.out.println("--- A Vector of Strings");
		Vector<String> vString = new Vector<>();
		for (int i = 0; i < 7; i++)
			vString.add("Str" + i);
		System.out.println(vString);
		for (int i = 1; i < vString.size(); i++)
			System.out.println(vString.get(i).substring(1));
		System.out.println();

		System.out.println("--- A Vector of Vector<Integer>");
		Vector<Vector<Integer>> vvInteger = new Vector<>();
		for (int i = 0; i < 5; i++) {
			Vector<Integer> vInteger = new Vector<>();
			for (int j = 0; j < i; j++)
				vInteger.add(j);
			vvInteger.add(vInteger);
		}
		System.out.println(vvInteger);
	}
}

Notes
  • the diamond operator (<>) tells the compiler to perform type inference on the assignment of declaration, i.e., to look at the specified type argument on the left and use it to determine the proper value of the omitted type on the right.
  • The vectors in the exceptions AMD capacityManagement methods were modified to illustrate the generic facility, which is then expanded on in the newly introduced generics method.
    • Neither of those methods really used anything 'integer', so it was trivial to change the Integer to String and Double
    • The only exception was the use of the index variable in capacityManagement to load up the vector with the thousand elements. We could have simply assigned 0.0 to each element (we're not doing anything with them), but instead chose to simply cast the index variable to double
      • As a challenge, once it's a double autoboxing kicked in. But in Java, conversion of an int to double is legal, and implicit, so why couldn't we simply add the int value to the vector and let autoboxing do its magic there as well?
  • vectors within vectors: nested containers
  • === Basic Functionality ===
    --- Creating Vector and printing it and its initial stats ---
    {} (0/10)
    
    --- Loading it up to capacity; add ---
    {0} (1/10)
    {0, 10} (2/10)
    {0, 10, 20} (3/10)
    {0, 10, 20, 30} (4/10)
    {0, 10, 20, 30, 40} (5/10)
    {0, 10, 20, 30, 40, 50} (6/10)
    {0, 10, 20, 30, 40, 50, 60} (7/10)
    {0, 10, 20, 30, 40, 50, 60, 70} (8/10)
    {0, 10, 20, 30, 40, 50, 60, 70, 80} (9/10)
    {0, 10, 20, 30, 40, 50, 60, 70, 80, 90} (10/10)
    
    --- Incrementing each element; get and set ---
    {1, 11, 21, 31, 41, 51, 61, 71, 81, 91}
    
    === Exceptions ===
    Successfully caught exception on get: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
    Successfully caught exception on set: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
    
    === Capacity Management ===
    --- Creating Vector and loading it up with 1,000 elements ---
    {0.0} (1/10)
    {0.0, 1.0 ... 9.0, 10.0} (11/20) ... resized
    {0.0, 1.0 ... 19.0, 20.0} (21/40) ... resized
    {0.0, 1.0 ... 39.0, 40.0} (41/80) ... resized
    {0.0, 1.0 ... 79.0, 80.0} (81/160) ... resized
    {0.0, 1.0 ... 159.0, 160.0} (161/320) ... resized
    {0.0, 1.0 ... 319.0, 320.0} (321/640) ... resized
    {0.0, 1.0 ... 639.0, 640.0} (641/1280) ... resized
    {0.0, 1.0 ... 998.0, 999.0} (1000/1280)
    --- Generics
    
    --- A Vector of doubles
    {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0}
    
    --- A Vector of Strings
    {Str0, Str1, Str2, Str3, Str4, Str5, Str6}
    tr1
    tr2
    tr3
    tr4
    tr5
    tr6
    
    --- A Vector of Vector
    {{}, {0}, {0, 1}, {0, 1, 2}, {0, 1, 2, 3}}
    

    Underlying Concepts

    Some More Examples — Two Pair Classes

    Homogeneous Pairs

    class HomogeneousPair<T> {
    	HomogeneousPair(T first, T second) {
    		this.first = first;
    		this.second = second;
    	}
    
    	void swap() {
    		T t = first;
    		first = second;
    		second = t;
    	}
    
    	public String toString() {return "(" + first + ", " + second + ")";}
    
    	T first, second;
    }
    

    Heterogeneous Pairs

    class HeterogeneousPair<T1, T2> {
    	HeterogeneousPair(T1 first, T2 second) {
    		this.first = first;
    		this.second = second;
    	}
    
    	public String toString() {return "(" + first + ", " + second + ")";}
    
    	T1 first;
    	T2 second;
    }
    
    Notes
    • swap method?

    Using the Pair Classes

    class PairsDemo {
    	public static void main(String [] args) {
    		HomogeneousPair<Integer> homPair1 = new HomogeneousPair<>(1, 2);
    		System.out.println(homPair1);
    		homPair1.swap();
    		System.out.println(homPair1);
    		System.out.println();
    
    		HomogeneousPair<String> homPair2 = new HomogeneousPair<>("dog", "cat");
    		System.out.println(homPair2);
    		homPair2.swap();
    		System.out.println(homPair2);
    		System.out.println();
    
    		HeterogeneousPair<Integer, String> hetPair2 = new HeterogeneousPair<>(1, "cat");
    		System.out.println(hetPair2);
    	}
    }
    
    (1, 2)
    (2, 1)
    
    (dog, cat)
    (cat, dog)
    
    (1, cat)
    

    ArrayList

    Like all the other collection classes:

    ArrayList alist = new ArrayList();		// Object element type
    
    for (int i = 1; i < 10; i++) {
    	Integer integer = new Integer(i);	// wrapping primitive int in Integer wrapper object
    	alist.add(integer);				// upcast of Integer to Object element type
    }
    
    for (int i = 0; i < alist.size(); i++)
    	System.out.println(alist.get(i));	// no cast downcast necessary -- calling toString which is defined in Object
    
    for (int i = 0; i < alist.size(); i++) {
    	Integer integer = (Integer)alist.get(i));	// downcast of Object to Integer
    	int n = integer.intValue();				// extraction of primitive int from Integer wrapper
    	n++;									// int operation
    	integer = new Integer(n));				// wrapping primitive int in (new) Integer wrapper object
    	alist.set(i, integer);					// upcast Integer to Object element type, replacing original element at i
    }
    

    Generics and Autoboxing

    Overview

    As of Java 1.5 (Java 5), two highly useful (and long-awaited) features made their debut

    Generics

    Type Erasure

    Autoboxing

    The Diamond Operator

    Type Inference

    var keyword for local variable declarations

    Code for this Lecture