Writing Generic Classes

Now that we have (at least some of) the “end user” view of generics, let’s try writing a few classes ourselves. In this section, we’ll talk about how type variables are used in the definition of generic classes, where they may appear, and some of their limitations. We’ll also talk about subclassing generic types.

The Type Variable

We’ve already seen the basics of how type variables are used in the declaration of a generic class. One or more type variables are declared in the angle bracket (<>) type declaration and used throughout the body and instance methods of the class. For example:

    class Mouse { }
    class Bear { }

    class Trap< T >
    {
        T trapped;

        public void snare( T trapped ) { this.trapped = trapped; }
        public T release() { return trapped; }
    }

    // usage
    Trap<Mouse> mouseTrap = new Trap<Mouse>();
    mouseTrap.snare( new Mouse() );
    Mouse mouse = mouseTrap.release();

Here, we created a generic Trap class that can hold any type of object. We used the type variable T to declare an instance variable of the parameter type as well as in the argument type and return type of the two methods.

The scope of the type variable is the instance portion of the class, including methods and any instance initializer blocks. The static portion of the class is not affected by the generic parameterization, and type variables are not visible in static methods or static initializers. As you might guess, just as all instantiations of the generic type have only one actual class (the raw type), they have only one, shared static context as well. You cannot even invoke a static method through a parameterized type. You must use the raw type or an instance of the object.

The type variable can also be used in the type instantiation of other generic types used by the class. For example, if we wanted our Trap to hold more than one animal, we could create a List for them within our class by referencing the parameter type like so:

    List<T> trappedList = new ArrayList<T>();

Just to cover all the bases, we should mention that instantiations of generic types on the type variable act just like any other type and can serve in all the places that other instantiations of a type can. For example, a method in our class can take a List<T> as an argument:

    public void trapAll( List<T> list ) { ... }

The effective type of the trapAll() method in a Trap<Mouse> is then simply:

    trapAll( List<Mouse> list ) { ... }

We should note that this is not what we mean by the term generic method. This is just a regular Java method that happens to take a generic type as an argument. We’ll talk about real generic methods, which can infer their types from arguments, and assignment contexts later in this chapter. A type variable can also be used to parameterize a generic parent class, as we’ll see in the next section.

Subclassing Generics

Generic types can be subclassed just like any other class by either generic or nongeneric child classes. A nongeneric subclass must extend a particular instantiation of the parent type, filling in the required parameters to make it concrete:

    class DateList extends ArrayList<Date> { }

    DateList dateList = new DateList();
    dateList.add( new Date() );
    List<Date> ld = dateList;

Here, we have created a nongeneric subclass, DateList, of the concrete generic instantiation ArrayList<Date>. The DateList is a type of ArrayList<Date> and inherits the particular instantiation of all of the methods, just as it would from any other parent. We can even assign it back to the parent type if we wish, as shown in this example.

A generic subtype of a generic class may extend either a concrete instantiation of the class, as in the previous example, or it may share a type variable that it “passes up” to the parent upon instantiation:

    class AdjustableTrap< T > extends Trap< T > {
        public void setSize( int i ) { ... }
    }

Here, the type variable T used to instantiate the AdjustableTrap class is passed along to instantiate the base class, Trap. When the user instantiates the AdjustableTrap on a particular parameter type, the parent class is instantiated on that type as well.

Exceptions and Generics

Types appear in the body of classes in another place—the throws clauses of methods. We can use type variables to define the type of exceptions thrown by methods, but to do so we need to introduce the concept of bounds. We cover bounds more in the next section. In this case, the usage is very simple. We just need to ensure that the type variable we want to use as our exception type is actually a type of Throwable. We can do that by adding an extends clause to the declaration of our type variable, like this:

     < T extends Throwable >

Here is an example class, parameterized on a type that must be a kind of Throwable. Its test() method accepts an instance of that kind of object and throws it as a checked exception:

    ExceptionTester< T extends Throwable >  {
        public void test( T exception ) throws T {// throw type is generic param
            throw exception; 
        }
    }

    try {
        new ExceptionTester<ClassNotFoundException>().test(
            new ClassNotFoundException() );
    } catch ( ClassNotFoundException e ) { ... }

The important part of this example is that the throws clause of our test method is defined to throw T, the generic parameter type of the class. This means that we can parameterize the type of exceptions thrown by a class.

The addition of the bound imposes the restriction that the parameter type used to instantiate the class T must be a type of Throwable. And we referenced the type T in the throws clause. So, an ExceptionTester<ClassNotFoundException> can throw a ClassNotFoundException from its test() method. Note that this is a checked exception and that fact has not been lost on the compiler. The compiler enforces the checked exception type that it just applied.

No generic throwables

We saw that a type variable can be used to specify the type of Throwable in the throws clause of a method. Perhaps ironically, however, we cannot use generics to create new types of exceptions. No generic subtypes of Throwable are allowed. If you think about this for a moment, you’ll see that in order to be useful, generic Throwables would require try/catch blocks that can differentiate instantiations of Throwable. And because (once again) there is no runtime representation of generics, this isn’t possible with erasure.

Parameter Type Limitations

We have seen the parameter types (type variables) of a generic class used to declare instance variables, method arguments, and return types as well as “passed along” to parameterize a generic superclass. One thing that we haven’t talked about is the question of how or whether we can use the type variable of a generic class to construct instances of the parameter type or work with objects of the type in other concrete ways. We deliberately avoided this issue in our previous “exception tester” example by simply passing our exception object in as an argument. Could we have done away with this argument? The answer, unfortunately, is that due to the limitations of erasure, there really is no parameter type information to work with at runtime. In this section, we’ll look at this problem and explore a workaround.

Because the type variable T has faithfully served as our parameter type everywhere else, you might imagine that we could use it to construct an instance of T using the new keyword. But we can’t:

    T element = new T(); // Error! Invalid syntax.

Remember that all type information is erased in the compiled class. The raw type does not have any way of knowing the type of object you want to construct at runtime. Nor is there any way to get at the Class of the parameter type through the type variable, for the same reason. So reflection won’t help us here either. This means that, in general, generics are limited to working with parameter types in relatively hands-off ways (by reference only). This is one reason that generics are more useful for containers than in some other applications. This problem comes up often and there is a solution, although it’s not quite as elegant as we’d like.

Using Class<T>

The only real way to get the type information that we need at runtime is to have the user explicitly pass in a Class reference, generally as one of the arguments to a method. Then we can explicitly refer to the class using reflection and create instances or do whatever else is necessary. This may sound like a really bad solution, without much type safety and placing a big burden on the developer to do the right thing. Fortunately, we can use a trick of generics to enforce this contract with the user and make it safe. Again, the basic idea is to have one of our methods accept the Class of the parameter type so that we can use it at runtime. Following our “exception tester” example:

       public void test( Class type ) throws T { ... }

This isn’t much better than it was before. Specifically, it doesn’t guarantee that the Class type passed to the method will match the parameterized type of the class (used in the throws clause here).

Fortunately, the Class class is, itself, now a generic type. Specifically, all instances of the Class class created by the Java VM are instantiated with their own type as a parameter. The class of the String type, for example, is now Class<String>, not just some arbitrary instance of the raw Class type that happens to know about strings.

This has two ramifications. First, we can specify a particular instantiation of Class using the parameter type in our class. And second, since the Class class is now generic, all of the reflective and instance creation methods can be typed properly and no longer require casts, so we can write our test() method like this:

    public void test( Class<T> type ) throws T {
        throw type.newInstance();
    }

The only Class instance that can be passed to our test() method now is Class<T>, the Class for the parameter type T, on which we instantiated ExceptionTester. So, although the user still has the burden of passing in this seemingly extraneous Class argument, at least the compiler will ensure that we do it and do it correctly:

    ExceptionTester<ArithmeticException> et =
        new ExceptionTester<ArithmeticException>();

    et.test( ArithmeticException.class ); // no other .class will work

In this code snippet, attempting to pass any other Class argument to the test() method generates a compile-time error.

Get Learning Java, 4th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.