CMPU 102, Spring 2005 Lecture 5

This lecture will introduce the idea of generic data structures and algorithms, and also type parameters, a Java language feature that supports them.

Outline

Generic containers and algorithms

In general, a data structure is a collection of values. For example:

Here, LinkedList is the type of collection, and String, Animal, and int are all types of elements we might want to store in a list.

We don't want to have to write a separate List class for every kind of element. So, we need a generic way to refer to any kind of element, regardless of its exact type.

Using java.lang.Object as a generic type

A good choice for a generic type is java.lang.Object, since every Java class is either directly or indirectly a subclass of java.lang.Object.  For example, here is how we might define part of a LinkedList class:

public class LinkedList {
    public void add(Object obj) {
        ...
    }

    public Object removeFirst() {
        ...
    }

    ...
}

By declaring the parameter of add and the return type of removeFirst as Object, we allow any kind of object or array to be added to or removed from a LinkedList.

For putting a value into the linked list, nothing special is required, because all reference types (objects and arrays) are subclasses of Object:

LinkedList names = new LinkedList();
names.add("Alice");
names.add("Bob");

// Both Cat and Dog are subclasses of Animal
LinkedList animals = new LinkedList();
animals.add(new Cat());
animals.add(new Dog());

When taking an object out of generic collection, an additional step is required.  Even though in the examples above we put String, Cat, and Dog objects into LinkedList objects, when getting those objects back out they are returned as plain Object references.  Therefore, to recover the actual type of an element returned from the collection, a cast is required:

while (!names.isEmpty()) {
    String name = (String) names.removeFirst();
    System.out.println(name);
}

while (!animals.isEmpty()) {
    Animal animal = (Animal) animals.removeFirst();
    animal.sound();
}

While this approach to generic collections works, it has some problems.  The main problem is that it is easy to accidentally put the wrong kind of object into a collection, or attempt to take the wrong type of object out of a collection.  For example:

public static void vetOffice(LinkedList animals) {
    while (!animals.isEmpty()) {
        Animal animal = (Animal) animals.removeFirst();
    }
}

public static void main(String[] args) {
    LinkedList animals = new LinkedList();
    animals.add(new Cat());
    animals.add(new Dog());
    vetOffice(animals); // OK: all elements are Animals

    LinkedList names = new LinkedList();
    names.add("Alice");
    names.add("Bob");
    vetOffice(names); // Problem!
}

If we accidentally pass a LinkedList containing String objects to a method that expects to take Animal objects out of them, a ClassCastException will be thrown when the program attempts to cast a String to an Animal.

Generic types

Generic types are a language feature in the latest version of Java that allows us to specify type parameters for generic classes and methods.  A type parameter is a placeholder that will represent some specific type (e.g. String, Animal) when an instance of the class is created.  Here is how the LinkedList class looks with a type parameter representing the type of elements that will be stored in the list:

public class LinkedList<E> {
    public void add(E element) {
        ...
    }

    public E removeFirst() {
        ...
    }
}

Note that instead of using Object as the generic type for elements of the collection, we now use a type parameter called E.  When an instance of LinkedList is created, a real class will be substituted for E.  For example:

public static void vetOffice(LinkedList<Animal> animals) {
    ...
}

public static void main(String[] args) {
    LinkedList<String> names = new LinkedList<String>();
    names.add("Alice");
    names.add("Bob");
    vetOffice(names); // not allowed: LinkedList<String> not compatible with LinkedList<Animal>
    
    LinkedList<Animal> animals = new LinkedList<Animal>();
    animals.add(new Cat());
    animals.add(new Dog());
    animals.add("Cow"); // not allowed: String is not an Animal
}

When you call the add method to put an element into a collection, it will check the type of the element against the type parameter to ensure that the element's type is compatible with the actual value of the type parameter.  For example, since String is not a subclass of Animal, the compiler does not allow a String object to be added to LinkedList<Animal>.  Nor does it allow a LinkedList<String> to be passed to a method expecting LinkedList<Animal>.

Another benefit to using generic types is that you do not need to use a cast when getting an object out of a collection:

while (!names.isEmpty()) {
    String name = names.removeFirst();
    System.out.println(name);
}

while (!animals.isEmpty()) {
    Animal animal = animals.removeFirst();
    animal.sound();
}

Behind the scenes, however, generic types work the same way as generics using java.lang.Object.  Wherever a type parameter E appears in a generic class or method, such as LinkedList, the compiler replaces it with Object.  When a generic object or method is used, the compiler will insert casts as needed to convert from Object to whatever the real element type is.  Because the generic type E is "erased" and replaced with Object, this approach to generic types is known as type erasure.

While type erasure works, it has some limitations.  The main limitation is that a generic class or method cannot do anything that would require knowledge of the "real" type of E.  For example, a generic class or method cannot:

Iterators

When using a data structure, it is often necessary to perform some action with each element contained in the data structure.  Java makes this easy through the use of an Iterator.  An Iterator is simply an object of type java.util.Iterator which provides methods for traversing all of the elements in a collection.  It is also a generic class, with a type parameter specifying the type of object in the collection.  For example, here is how to use an iterator to print the elements of a LinkedList<String> object:

LinkedList<String> names = ...

Iterator<String> i = names.iterator();
while (i.hasNext()) {
    String name = i.next();
    System.out.println(name);
}

Functors

A functor is an object which is applied to one or more other objects.  They are often useful in combination with generic data classes and methods.

An example of a functor in Java is the java.util.Comparator interface.  Classes implementing the Comparator interface are used to compare two objects to determine the comparison order of two objects.  For example, a class implementing Comparator<String> might compare String objects to determine how to order them alphabetically.  Here is how Comparator is defined:

public interface Comparator<E> {
    public int compare(E left, E right);
}

The compare method returns a negative value of left compares as less than right, a positive value if left compares as greater than right, and zero if they are equal.

Let's say that Animal objects have a getAge method that returns the age of the animal in years.  We can define a Comparator class called CompareAnimalsByAge as follows:

public class CompareAnimalsByAge implements Comparator<Animal> {
    public int compare(Animal left, Animal right) {
        return left.getAge() - right.getAge();
    }
}

Using a generic class and a Comparator functor, we can define a generic method that can find the greatest element in a collection storing any kind of object according to any comparison criteria:

public static<E> E findMax(Collection<E> list, Comparator<E> comparator) {
    E max = null;

    Iterator<E> i = list.iterator();
    while (i.hasNext()) {
        E element = i.next();
        if (max == null || comparator.compare(element, max) > 0) {
            max = element;
        }
    }

    return max;
}

Note that the type parameter E appears in several places.  The first occurrence (after the keywords public static) signifies that the method is generic, and has a type parameter called E.  The remaining occurrences, which parametize the Collection, Comparator, and Iterator types, and also the return type of the method, specify that all of these generic classes and interfaces have the same type parameter, which is also the return type.

Because LinkedList implements the Collection interface, we can pass a LinkedList to a method expecting a Collection object. Using this generic method, we can find the oldest animal in a LinkedList of animals, in one line of code:

LinkedList<Animal> animals = ...

Animal oldest = findMax(animals, new CompareAnimalsByAge());