8.5. CastsWe've now talked about relationships between generic types and even between generic types and raw types. But we haven't brought up the concept of a cast yet. No cast was necessary when we interchanged generics with their raw types. Instead, we just crossed a line that triggers unchecked warnings from the compiler:
List list = new ArrayList<Date>( );
List<Date> dl = list; // unchecked warning
Normally, we use a cast in Java to work with two types that could be assignable. For example, we could attempt to cast an
Object
to a
Date
because it is plausible that the
Object
is a
Date
value. The cast then
Collection<Date> cd = new ArrayList<Date>( );
List<Date> ld = (List<Date>)cd; // Ok!
This code snippet shows a valid cast from a more general Collection<Date> to a List<Date> . The cast is plausible here because a Collection<Date> is assignable from and could actually be a List<Date> . Similarly, the following cast catches our mistake where we have aliased a TReeSet<Date> as a Collection<Date> and tried to cast it to a List<Date> :
Collection<Date> cd = new TreeSet<Date>( );
List<Date> ld = (List<Date>)cd; // Runtime ClassCastException!
ld.add( new Date( ) );
There is one case where casts are not effective with generics, however, and that is when we are trying to differentiate the types based on their parameter types:
Object o = new ArrayList<String>( );
List<Date> ldfo = (List<Date>)o; // unchecked warning, ineffective
Date d = ldfo.get(0); // unsafe at runtime, implicit cast may fail
Here, we aliased an ArrayList<String> as a plain Object . Next, we cast it to a List<Date> . Unfortunately, Java does not know the difference between a List<String> and a List<Date> at runtime, so the cast is fruitless. The compiler warns us of this by generating an unchecked warning at the location of the cast; we should be aware that we might find out later when we try to use the cast object that it is incorrect. Casts are ineffective at runtime because of erasure and the lack of type information. |
8.6. Writing Generic Classes
Now that we have (at least some of) the "end
8.6.1. 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
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
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 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
8.6.2. Subclassing GenericsGeneric 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 "
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
8.6.3. Exceptions and GenericsTypes appear in the body of classes in another placethe throws clauses of methods. Type variables may be used 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, but in this case, the usage is really 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 exception; }
}
try {
new E<ClassNotFoundException>( ).test(
new ClassNotFoundException( ) );
} catch ( ClassNotFoundException e ) { ... }
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 has not been lost on the compiler. The compiler enforces the checked exception type that it just applied. 8.6.3.1 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
8.6.4. 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,
Since the type variable
T
has faithfully
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 for some other applications. This problem comes up often though and there is a solution, although it's not quite as elegant as we'd like. 8.6.4.1 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,
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, Java 5.0 introduced some changes in the Class 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 side effects. 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. |