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.
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.
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.
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.
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
Throwable
s 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.
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.
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.