CISC 3130
Data Structures
Container-Relevant Language Features & Techniques

Topics

Our Current Vector Class

Here is our current implementation of the 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;
}

I had mentioned in the previous lecture that this code was actually incorrect, and that we would correct the situation in this lecture. There are actually two error in our class:

Exception Handling

Bounds Checking

Container Overflow

Vector Version 3: 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

Vector Version 3.1: 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

Vector Version 3.2: 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

Vector Version 4: 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.
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

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.

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);

An Vector With Object Element Type

We now move to the general case of having Object as the element type
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

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:

Vector Version 5 — 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
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
  • 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

    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)