Before we embark on the subject of List, Sets, and Maps–which are ways of organizing collections of Objects–we should introduce the concept of generics.

Back in Lesson 5, we introduced the List interface, which is used for managing a collection of Objects. Back there we said, “You’d be foolish to store different classes of objects in a single List, but it can be done.”

Generics are Java’s way of keeping you from being foolish.

Now, as far as the JVM is concerned, any single List can contain objects of many different types: Shoes, Ships, SealingWax, Cabbages, Kings, etc. But if you go stuffing instances all these different classes into a single List, you risk treating a King like a Cabbage, and that would be sacrilegious (translation: you’d get a ClassCastException or UnsupportedOperationException).

Generics are a mechanism that lets the compiler protect you from such blunders.

Specifying a Generic

So how does this work? Say you code something like this:

    private List listOfStrings;

You’ll get a warning message like this:

List is a raw type. References to generic type List<E> should be parameterized

To dispel this warning, code instead:

    private List<String> listOfStrings;

What makes this necessary is that List is defined with a generic:

    public interface List<E> ...

The “<E>” is the generic. When a class or interface is declared as generic, anytime that class or interface, or any of its subclasses or implementations, will request that you provide one or more object types inside angle brackets.

Now, the List interface has an add() method that lets you add objects to the List. And here’s where generics keep you from screwing up.

Try coding this:

    Integer someInteger = 0;
    listOfStrings.add(someInteger);

and you’ll get an error message like this:

   The method add(String) in the type List<String> is not
   applicable for the arguments (Integer)

What kind of sorcery is this? Didn’t we say that the JVM doesn’t give a hoot or holler what goes into a List? That’s true. But this error message is the result of the enforcement mechanism provided by generics. Take a look at the add() method definition in the List interface.

    boolean add(E e);

Remember, above we said that E stands for the object type used in the declaration of listOfStrings–in this case, String. This means that each time we use listOfStrings, all the occurrences of E in the List interface are momentarily interpreted as though they said String, so the method declaration above is equivalent to:

    boolean add(String e);

That’s why the error message refers to the method, “add(String)” and not “add(E)”. And since (momentarily) there is no add(Integer e) method, you get an error message.

More Ideas for Generics

We’ve seen how a generic defined at the class or interface level can be used in combination with its use at the method level to enforce uniformity (i.e., to keep Cabbages and Kings from both appearing in the same List). How about enforcing uniformity within a method?

Say you have a method that does nothing more than store an object in a List of similar objects. You can write the method declaration like this:

    public <T> void storeItem(T theItem, List<T> theList,
           T theSecondItem)...

And now you’ve made sure that theItem is of the same class as the objects in theList, and theSecondItem is also of that class.

You can be even more restrictive by specifying the kinds of classes the method, or a class or interface, will accept. Say you only want to use objects of type Number, and subclasses of Number, in this method. You can change it like so:

    public <T extends Number> void 
        storeItem(T theItem, List<T> theList, T theSecondItem)...

Likewise, you can define the generic at the class or interface level:

     public class MyClass<T extends Number> ...

But wait–there’s more! Because we’ve declared that theItem is a Number, the storeItem method can call any accessible method in the Number class on theItem. An example:

    private <T extends Number> void aMethod(T theNumber) {
        System.out.println(theNumber.byteValue());
    }

Defining a Generic Class or Interface

Let’s try defining a generic class of our own. We’ll start by defining an enum, and a class with a generic:

public enum Rank {
      GENERAL, COLONEL, MAJOR, CAPTAIN, LIEUTENANT, SERGEANT, PRIVATE
}

/////////////////////////////////////////

public class Serviceman<T> {
    private String name;
    private T level;

    public Serviceman(String name, T level) {
        super();
        this.name = name;
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public T getLevel() {
        return level;
    }

    public void setLevel(T level) {
        this.level = level;
    }
}

Notice how “T” is used as the object type for level? This means that level is going to be of type T, but we don’t know what that will be yet. See, too, how T is used in place of a class name in the constructor, as the return type for getLevel(), and the argument type in setLevel().

Now we’ll create a class that uses Serviceman:

public class Platoon {

    private Serviceman<Rank> soldier;
    private Serviceman<Integer> sailor;
    
    public static void main(String[] args) {
        Platoon  platoon = new Platoon();
        platoon.displayMembers();
    }

    public Platoon() {
        soldier = new Serviceman<Rank>
              ("George Washington", Rank.GENERAL);
        sailor = new Serviceman<Integer>
              ("David Farragut", 99);
    }

    public void displayMembers() {
        display(soldier);
        display(sailor);
    }
    
    private void display(Serviceman<?> person) {
        System.out.println(person.getName() + " "
              + person.getLevel());
    }
}

Running this code shows this on the console:

George Washington GENERAL
David Farragut 99

So what’s happening here? soldier and sailor are declared as class Serviceman with a generic indicating the data type of the level field (because Serviceman.level is declared as having the generic type). In the constructor for Platoon, the generic is used again when allocating the two objects.

What would happen if we tried this allocation?

sailor = new Serviceman<Rank>("David Farragut", Rank.CAPTAIN);

We’d get this error:

Type mismatch: cannot convert from Serviceman<Rank> to Serviceman<Integer>

That’s because sailor is declared with the generic type Integer. The compiler won’t let you assign a Serviceman with generic type Rank to it. You’d get a similar problem if you tried this:

soldier.setLevel(99);

because in soldier, the level field is a Rank, not an Integer.

Finally, what’s with that argument type Serviceman<?> in the display() method? Well, we know the method is receiving a Serviceman, but we don’t know what the generic type will be. “?” is a wild card indicating that any generic is okay.

What if we coded the method this way?

   private void display(Serviceman<Rank> person) {
      or
   private void display(Serviceman<? extends Rank> person) {

We’d get this error message where we call the method passing sailor:

The method display(Serviceman<Rank>) in the type Platoon is not applicable for the arguments (Serviceman<Integer>)

Using the first declaration, only objects of type Serviceman<Rank> are acceptable. Using the second, only objects of type Serviceman<Rank> are acceptable, plus any Serviceman using a generic class which is a subclass of Rank (of which there happen to be none, but that’s neither here nor there).

Now let’s go even further. Suppose Serviceman were declared this way?

   public class Serviceman<T extends Rank> {

This restricts the classes used in the generic to Rank and any subclasses it has. private Serviceman<Integer> sailor becomes invalid. It also means, however, that just as in methods with such restricted generics, the fields and methods of Rank (if there were any) can be applied to any object in the class that has type T.

And if that weren’t enough, consider

public class Serviceman<T super Rank> {

This restricts the classes used to Rank and any of its superclasses. Again, the fields and methods of Rank are available for any object that has type T.

Although we’ve covered the most useful aspects of generics, there’s lots more that you can read about here: Lesson: Generics (Updated).

What You Need to Know

  • Generics are a compiler mechanism that helps avoid misinterpreting the classes to which objects belong, and thus preventing Exceptions at run time.
  • Generics are known only to the compiler. They don’t appear in compiled code.
  • Generics are interpreted as though they were the names of classes or interfaces used when a field or method is declared.

Next, the most common use of generics: Collections, Lists, Sets, and Maps