At the end of the last lecture, we briefly introduced stacks and queues. As with most of the data structures that we've worked with so far, we started with an example that had a fixed element type, String, to keep things simple. But we'd really like our data structures to allow arbitrary element types so we don't have to rewrite them whenever we want a list of something other than String or whatever fixed type we picked.
Historically, Java has only had one way to do this, by taking advantage of inheritance and the fact that all classes are subclasses of Object (something we'll look at in detail shortly). As of Java 5, there is explicit support for generic classes and methods - classes and methods that use "type variables". Instead of declaring a StringStack with operations that have Strings as parameters , we can introduce a type parameter in angle brackets. So a generic stack interface can be declared as follows:
public interface Stack<E> { public void push(E value); public E pop(); public E top(); public boolean isEmpty(); public int size(); }
Now instead of having just a stack of Strings, we can have a Stack<String>, or Stack<Integer>, or any kind of stack at all. Similarly we can have a generic queue interface:
public interface Queue<E> { public void enqueue(E value); public E dequeue(); public boolean isEmpty(); public int size(); }
We can define generic classes to implement these interfaces. For instance we could implement a stack using an ArrayList:
public class ArrayStack<E> implements Stack<E> { private ArrayList<E> items; // items in the stack public void push(E value) { ... } public E pop() { ... } }
Then if we want a stack holding Strings, we can create an instance of this generic class as follows.
Stack<String> stk = new ArrayStack<String>();
Similarly, we could define a linked list implementation of stack, or an array or linked list implementation of a queue. For example,
public class LinkedQueue<E> implements Queue<E> { ... }
It turns out that generics are used in other contexts, some of which we've seen before. One is the interface that defines the compareTo method. If you remember, many objects have some sense of ordering (i.e, it makes sense to ask whether one object is "before" another). The classes defining these objects implement method compareTo, which returns an int value that is either negative, zero, or positive, depending on whether the the object handling the comparison is less than, equal to, or greater than another object. The way this works is that all classes that support a notion of ordering implement the interface Comparable that specifies the compareTo method. This is actually a generic interface, where the type of the objects being compared is a parameter:
public interface Comparable<T> { public int compareTo(T other); }
A class like String implements the Comparable interface, and then all objects of type String can be compared to other Strings using compareTo.
There is another technical detail that we ought to talk about, although most of the time we can ignore it. Almost all of the things in the Java universe are objects - that is their behavior is defined by classes, they are created using the new operation to create an instance of a class, and we can send them messages. For example, we can create an ArrayList and add something to it like this:
List<String> lst = new ArrayList<String>(); lst.add("xyzzy");
While almost everything in Java is an object, a handful of basic values are not, including ints, doubles, chars, and booleans. These are known as primitive types as opposed to regular objects, which are known as reference types. Most of the time we can ignore this distinction, and in Java 5, we can ignore it almost all of the time - although the difference is still there in the background.
The situation where this is most likely to be visible is in dealing with containers like lists, stacks, and queues. Even with generics, these containers are only able directly to store objects - values with reference types. If we want, for example, a list of ints, we have to do something special because the standard containers don't work with ints - they aren't objects. If we try to create an ArrayList<int>, we will get an error, because int is a primitive type, not a reference type, and generic parameters cannot be primitive types.
The Java libraries do, however, provide a systematic way to deal with this. For each of the primitive types like int, double, char, and boolean, there is a corresponding "wrapper" class that can be used to create a real object holding a value of the primitive type inside. These have reasonably obvious names: Integer, Double, Character, and Boolean. Instances of these types contain a single value of the corresponding primitive type.
The wrapper classes have been around since the early days of Java, but programmers had to use them explicitly to create wrapped values to store in containers like lists. In Java 5, conversions to and from wrapper classes normally happen automatically. So we can, for example, create an ArrayList<Integer> container and store ints inside of it. What is actually happening when we do something like add(5) to such a container is that the primitive value 5 is automatically converted to a new Integer(5) object, and that object is stored in the list. When we extract the value from the list, it is automatically unwrapped to give back the primitive int value 5. For the most part, we can ignore that this is happening, but occasionally, particularly when debugging or in error messages, the details may surface.