Taming Tiger, Part 2

Understanding generics

Welcome to the second part of this three-part series on Sun Microsystems' latest release of the Java 2 Platform, Standard Edition (J2SE). To refresh your memory, Part 1 was a quick introduction to J2SE 1.5 and covered many new additions to the Java language: auto-boxing and -unboxing of primitives, enumerations, static imports, the enhanced "for" loop, variable method arguments, and the newly formatted input method. I devote Part 2 entirely to generics, Java's counterpart to templates in C++ and a similar facility (also called generics) in C# 2.0.

Read the whole series: "Taming Tiger," Tarak Modi (JavaWorld):

Why generics?

To understand the problem that generics (short for generic types) solve, let's look at the following code fragment:

// Declare Class A
class A
{
}
// Declare Class B
class B
{
}
// Somewhere in the program create a Vector 
Vector v = new Vector();
// Add an object of type A
v.add(new A());
// And sometime later get the object back
B b = (B) v.get(0);

The above code will compile fine but will throw a runtime exception (java.lang.ClassCastException) when you execute it. This is obviously a serious problem and should be caught as early as possible. Preferably, the above code fragment should not even compile.

Enter Java generics

Now, let's rewrite the above code fragment using generics:

// Class A and B declared previously
// Somewhere in the program create a Vector 
Vector<A> v = new Vector<A>();
// Add an object of type A
v.add(new A());
// And sometime later get the object back
B b = (B) v.get(0);

That looks similar to the first fragment, except for the code in the angle brackets. The angle brackets are the syntax for providing type parameters to parameterized types. I talk more about both parameterized types and type parameters in this article's later sections.

Even though the code change is minimal, its impact is far from small. This code fragment will not compile. Compiling this program with J2SE 1.5 (remember to use -source 1.5) will give you the following error:

inconvertible types
found   : A
required: B
        B b = (B) v.get(0);

In plain English, the compiler tells us that to be able to use the line B b = (B) v.get(0); legally, the vector must be parameterized (in the declaration) to accept objects of type B.

Diving into generics

Generics were first proposed for Java in early 2001, and (after three long years) they have finally made their way into J2SE 1.5. As you saw above, generics provide a giant leap into solving a long-standing problem in Java; implementing compile-time type safety. One of the first places you will encounter generics in J2SE 1.5 (and in C++ and C#) are in the collection classes, such as the Vector class we used above. Such classes are called parameterized types. If a type is parameterized, then its declaration (i.e., when you declare an instance of that type) can optionally take a type with which to parameterize it. For example, we already know that Vector is a parameterized type, and hence, the declaration of v (in the example above) is as follows:

   Vector<A> v = new Vector<A>();

The type is optional to maintain backwards compatibility with legacy code. If the type were mandatory, then any code with a declaration such as Vector v = new Vector(); would no longer compile. Once a parameterized type is declared, using it is no different than a regular (pre 1.5) nonparameterized type.

There are subtleties to keep in mind while using generics. Consider the following seemingly harmless code fragment:

// Somewhere in the program create a Vector 
Vector<String> v2 = new Vector<String>();
Vector<Object> v = v2;

Compile the code and run it. What? It won't compile. Even though Object is the base class of all classes including String, you cannot assign a vector of type String to a vector of type Object. The converse is also true and is more intuitive than the former. The reason such rules exist are to ensure compile-time type safety and to prevent errors such as the following:

// Somewhere in the program create a Vector 
Vector<String> v2 = new Vector<String>();
Vector<Object> v = v2;
v.add(new A());
String s = v2.get(0);

Assuming that J2SE 1.5 relaxed its assignment rules, the above code would compile, but would result in a runtime exception when the line String s = v2.get(0) executes.

Using wildcards

To understand what wildcards are and why we need them, consider the following code fragment:

// Declare a Mammal class
abstract class Mammal
{
   abstract public void eat();
}
// Declare a Dog class
class Dog extends Mammal
{
   public void eat() 
{
   System.out.println("Dog is eating.");
}
}
// Declare a Cat class
class Cat extends Mammal
{
   public void eat() 
{
   System.out.println("Cat is eating.");
}
}
// Somewhere in the code create a vector of dogs and cats
Vector<Dog> dogs = new Vector<Dog>();
dogs.add(new Dog());
dogs.add(new Dog());
Vector<Cat> cats = new Vector<Cat>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
// Now make all the dogs and cats eat
Vector<Mammal> pets = dogs;
for(Mammal pet: pets)
{
   pet.eat();
}
pets = cats;
for(Mammal pet: pets)
{
   pet.eat();
}

By now we already know that this code when compiled will produce the following errors:

incompatible types
found   : java.util.Vector<Dog>
required: java.util.Vector<Mammal>
                Vector<Mammal> pets = dogs;
                                      ^
incompatible types
found   : java.util.Vector<Cat>
required: java.util.Vector<Mammal>
                pets = cats; 

But what if I wanted to make this code work? After all, it is legitimate—albeit contrived—code. The answer is simple and involves using wildcards. Here's the one line change that will appease the Java compiler:

   Vector<? extends Mammal> pets = dogs;

Now after you successfully compile and run the code, you will receive the following output:

Dog is eating.
Dog is eating.
Cat is eating.
Cat is eating.
Cat is eating.

So how did that work? The ? is a wildcard character that, along with the extends keyword, declares pets as a Vector of any type that derives from Mammal. That differs from the line we wrote before that declared pets as a Vector that could only contain objects of the direct type Mammal. To accept a Vector of any kind of object you could use the following declaration:

   Vector<?>

Note: That code differs from Vector<Object> for the same reasons discussed above.

Roll your own parameterized type

So far we've used the built-in parameterized types in Java. Now let's look at the other side of the coin—creating parameterized types. It's fairly easy to create your own parameterized types. As an example, let's create a new class called Mammals that can store any Mammal:

class Mammals<T extends Mammal> 
{
   private Vector<T> items = new Vector<T>();
   void add(T item)
   {
      items.add(item);
      }
   T get(int index)
   {
      return items.get(index);
   }    
}

Now you can use this new class in your code, as shown in the following fragment:

// A list of dogs
Mammals<Dog> dogs2 = new Mammals<Dog>();
dogs2.add(new Dog());
dogs2.add(new Dog());
// A list of cats
Mammals<Cat> cats2 = new Mammals<Cat>();
cats2.add(new Cat());
cats2.add(new Cat());
cats2.add(new Cat());
// Assign the dogs to a generic list of mammals
Mammals<? extends Mammal> mammals = dogs2;

To support iterating through the Mammals list, I modify the class as follows:

class Mammals<E extends Mammal> extends Vector<E> 
{
}

No, I did not forget the code in the class. I extended the Vector class and inherited all the required functionality (Isn't that what object-oriented programming is all about?). Now we can iterate through the list, as shown below:

Mammals<? extends Mammal> mammals = dogs2;  
for (Mammal m: mammals)
{
   m.eat();
}
mammals = cats2;  
for (Mammal m: mammals)
{
   m.eat();
}

Generic methods

J2SE 1.5 supports generic methods. To see how such methods can be useful, let's say you wanted to create a method that took an array of Mammals and put them in a Mammals list. Here's one way of accomplishing that:

// Mammals class from earlier example
class Mammals<E extends Mammal> extends Vector<E> 
{
}
// A generic method
static <T extends Mammal> void arrayToList(T[] array, Mammals<T> list)
{
   for(T item: array)
      list.add(item);
}

Generic methods prove especially useful when new functionality to an existing parameterized class is needed and it is not possible to extend the class, or when a group of such methods are provided as common utility methods.

Conclusion

I talked at length about generics in this article. Generics are a strong addition to Java and provide a level compile-time type safety not possible in previous versions of the language. They are also used extensively within J2SE 1.5. In addition, not surprisingly, Reflection in J2SE 1.5 has also been extended to support reflecting on generic types. However, those details reach beyond the scope of this introductory article. Perhaps, if there is enough interest, I will write an article exclusively on using Reflection on generics (and a potential weakness with Java's generics implementation called erasure).

In the third and final part of this series, I will go into details about the newly introduced metadata feature in J2SE 1.5 called annotations. Annotations allow programmers to decorate Java code with their own attributes that can be used for code documentation, code generation, and, during runtime, for providing special services such as enhanced business-level security or special business logic.

Tarak Modi has been architecting scalable, high-performance, distributed applications for more than eight years and is currently a senior specialist with North Highland, a management and technology consulting company. His professional experience includes hardcore C++ and Java programming; working with Microsoft technologies such as COM, MTS, COM+, and more recently .Net; Java-based technologies including J2EE; and CORBA. He has written many articles in well-known .Net and Java publications including several in JavaWorld. He currently hosts the Java Design blog at JavaWorld. He is also coauthor of Professional Java Web Services (Wrox Press, 2002; ISBN 1861003757). To find out more about him, visit his personal Website at http://www.tekNirvana.com.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more