- Why Use Generics?
- Generic Types
- Generic Methods
- Bounded Type Parameters
- Generics, Inheritance, and Subtypes
- Type Inference
- Wildcards
- Type Erasure
- Restrictions on Generics
- Questions and Exercises: Generics
Generics, Inheritance, and Subtypes
As you already know, it is possible to assign an object of one type to an object of another type, provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer’s supertypes:
Object someObject = new Object(); Integer someInteger = new Integer(10); someObject = someInteger; // OK
In object-oriented terminology, this is called an is a relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well:
public void someMethod(Number n) { /* . . . */ } someMethod(new Integer(10)); // OK someMethod(new Double(10.1); // OK
The same is also true with generics. You can perform a generic type invocation, passing Number as its type argument, and any subsequent invocation of add will be allowed if the argument is compatible with Number:
Box<Number> box = new Box<Number>(); box.add(new Integer(10)); // OK box.add(new Double(10.1)); // OK
Now consider the following method:
public void boxTest(Box<Number> n) { /* . . . */ }
What type of argument does it accept? By looking at its signature, you can see that it accepts a single argument whose type is Box<Number>. But what does that mean? Are you allowed to pass in Box<Integer> or Box<Double>, as you might expect? The answer is no because, as shown in Figure 6.1, Box<Integer> and Box<Double> are not subtypes of Box<Number>. This is a common misunderstanding when it comes to programming with generics and is an important concept to learn.
Figure 6.1. Box<Integer> Is Not a Subtype of Box<Number> Even Though Integer Is a Subtype of Number
Generic Classes and Subtyping
You can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.
Using the Collections classes as an example, as shown in Figure 6.2, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is preserved between the types.
Figure 6.2. A Sample Collections Hierarchy
Now imagine we want to define our own list interface, PayloadList (Figure 6.3), which associates an optional value of generic type P with each element. Its declaration might resemble the following:
Figure 6.3. A Sample PayloadList Hierarchy
interface PayloadList<E,P> extends List<E> { void setPayload(int index, P val); . . . }
The following parameterizations of PayloadList are subtypes of List<String>:
- PayloadList<String,String>
- PayloadList<String,Integer>
- PayloadList<String,Exception>