How Do Generic Subtypes Work?
Generics add powerful features to an OO language, but they can also introduce confusion in the conceptual models of a language for both new and experienced devs.
Join the DZone community and get the full member experience.
Join For FreeGeneric classes are a powerful tool in any programming language, but they can also bring a great deal of confusion. For example, how come List<Double>
is not a subclass of List<Number>
even though Double
is a subtype of Number
? In this article, we will explore the various rules that surround subclassing generic types and build a cohesive view of the generic inheritance mechanism provided by Java. Before delving into this important topic, though, we will define the various different techniques for defining generic types and generic class arguments.
Understanding Generics
The purpose of generics in an object-oriented language is to allow for arbitrary aggregate types to be supplied to a class without having to write a new class for each of the supplied types. For example, if we wanted to write a list class to store objects, without generics we would be forced to either create a new class for each type passed (e.g. IntegerList
, DoubleList
, etc.) or have the internal structure of the list class store Object
s as depicted in the listing below:
public class MyList {
private Object[] elements;
public void addElement(Object element) {
// ... add to the elements array ...
}
public Object getElementAtIndex(int index) {
// ... retrieve the element at the given index ...
}
}
Generic Parameters
Although using Object
does solve the problem of storing any Object
or type that derives from Object
, it still has important flaws. Foremost among these is the loss of type safety at compile time. For example, if we were to call addElement
with an argument of Integer
, the supplied Integer
is no longer treated as its actual type, but rather, as an Object
. This means we must cast the Integer
when retrieved and code that is outside of our control that uses this MyList
class may not have enough information to know what type to cast the retrieved element to.
This problem compounds if we add another element, but this time of type Double
. If a consumer of our MyList
class is expecting a list of Integer
objects, performing a cast to Integer
on the retrieved element will cause ClassCastException
at runtime. If on the other hand, we expected the MyList
class to contain values of type Number
(of which Integer
and Double
are both subclasses), we have not transmitted this information to ensure type safety at compile time. In essence, the fact that our list contains both Integer
and Double
objects is arbitrary from the perspective of the compiler. Since it does not know our intent, it cannot perform checks to ensure that we are indeed abiding by our stated intentions.
In order to solve this, Java Development Kit (JDK) 5 introduced the concept of generic classes to Java, which allows for a type parameter to be specified within arrow brackets after the class name. For example, we can now rewrite our List
class as follows:
public class MyList<T> {
private T[] elements;
public void addElement(T element) {
// ... add to the elements array ...
}
public T getElementAtIndex(int index) {
// ... retrieve the element at the given index ...
}
}
Now we can create a MyList
of Integer
objects:
MyList<Integer> listOfIntegers = new MyList<Integer>();
Notice that if we wanted to create another MyList
to store Double
objects, we do not have to create another class: We can simply instantiate a MyList<Double>
. We can also create a list of Number
objects in a similar manner:
MyList<Number> listOfNumbers = new MyList<>();
listOfNumbers.addElement(new Integer(1));
listOfNumbers.addElement(new Double(3.41));
Upper Bounded Generic Parameters
When designing our generic classes, we may also want to restrict the types of values that can be supplied to the class as generic arguments (the type that is mapped to the generic parameter when the generic class is instantiated). For example, if we create a ListOfNumbers
class, we may want to restrict the supplied generic arguments to types that are Number or extend from Number
(note that the diamond operator, <>
, was introduced in JDK 7 and allows for type inference, where the generic argument on the right-hand side is assumed to be exactly the generic argument on the left-hand side of the assignment):
public class ListOfNumber<T extends Number> {
public Number sum() {
// ... sum all values and return computed value ...
}
In this case, the sum method assumes that all the stored elements are Number
objects or objects that derive from Number
, thus allowing for a numeric value to be computed. If we did not include this generic type upper bound, a client could instantiate a list of Object
or another non-numeric type, and we would be expected to compute the sum (which does not make sense from a domain or problem perspective). Note that the extends portion of the upper bound can be used to specify an interface that the generic parameter must implement or specify multiple interfaces (or one class and multiple interfaces). For more information, see the Bound Type Parameters article by Oracle.
Wildcards
When we are instantiating generic types, there may be cases where we do not care about the actual generic argument of the list. For example, if we want a sum from a ListOfNumbers
, but do not expect to add or retrieve any elements from the list, we can ignore the actual generic argument type of the list with the use of wildcards (denoted by a question mark as a generic argument):
public class ListOfNumberFactory {
public static ListOfNumber<Double> getList() {
return new ListOfNumber<Double>();
}
}
ListOfNumber<?> list = ListOfNumberFactory.getList();
System.out.println(list.sum());
Before moving further, an important distinction must be made between named generic parameters, such as T
, and wildcards:
Named generic parameters are used when defining a generic class or method to denote the actual generic argument when the class is instantiated or the method is used. Wildcards are used when employing generic classes or methods to denote the actual generic argument (or lack of care about the generic argument).
This means that we cannot create a generic class that has the type public class MyIncorrectList<?> {}
and we cannot instantiate a generic class of the form new MyList<T>();
unless it is contained within the definition of another generic class, such as in the following case:
public class OuterGeneric<T> {
private MyList<T> list;
// ... other fields and methods ...
}
The distinction between generic parameters and wildcards is an important one and will become much more so when we deal with generic subtypes, where generic parameters and wildcards are merged into the same type hierarchy.
Upper Bounded Wildcards
Just as with upper bounded generic parameters, there may be cases where we do not care about the type of the generic argument, except that it is a subclass of a specified type or that it implements a specified interface. For example, suppose we want to process a list of Number
objects in a loop. In this case, we would need to specify that we expect the upper bound on our list to be Number
, as seen below:
public class ListOfNumber<T extends Number> implements Iterable<T> {
public Number sum() {
// ... compute the sum ...
}
@Override
public Iterator<T> iterator() {
// ... return an iterator ...
}
}
ListOfNumber<? extends Number> list = ListOfNumberFactory.getList();
for (Number number: list) {
// ... do something with the Number ...
}
It is tempting to simply set the type of list to ListOfNumber<Number>
, but that would restrict our usage to exactly ListOfNumber<Number>
objects. For example, we would be unable to return ListOfNumber<Double>
from the ListOfNumberFactory.getList()
and perform the same operation. We will see the importance of this distinction more clearly later when we discuss the generic class hierarchy.
Note that we are pragmatically restricted from adding any objects to our ListOfNumber
class because we do not know the actual generic argument of the list when using an upper bounded wildcard: We only know that its actual implementation type is a subtype of Number
. For example, it may be tempting to think that we can insert an Integer
object to ListOfNumber<? extends Number>
, but the compiler will be thrown an error if we do since it cannot guarantee the type safety of such an insertion. The generic argument could be Double
, in which case, we cannot add an Integer
object to a list of Double
. For more information, see this StackOverflow explanation.
Lower Bounded Wildcards
Unlike named generic parameters, wildcards can also specify a lower bound. For example, suppose we wanted to add an Integer
to a list. The natural inclination would be to specify that the type of the list would be MyList<Integer>
, but this arbitrarily restricts the types of lists we can operate on. Could we not add Integer
objects to a list of Number
or a list of Object
as well? In this case, we can specify the lower bound of the wildcard, allowing any generic argument type that is the same or a superclass of the lower bound type to be used:
public class IntegerListFactory {
public static MyList<? super Integer> getList() {
// ... return MyList<Integer>, MyList<Number>, MyList<Object>, etc....
}
}
MyList<? super Integer> integerList = IntegerListFactory.getList();
integerList.addElement(new Integer(42));
While lower bounded wildcards are not as prevalent as unbounded or upper bounded wildcards, they still play an important role in the subclassing of generic types, as we will see in the following section.
Subtyping Generic Classes
In general, generic subclasses can be broken up into two categories: (1) generic parameter subtypes and (2) wildcard generic subtypes. In a similar sense to the division between the definition and usage of generics that we previously saw with generic parameters and generic wildcards, respectively, each of these two categories has its own nuances and important rules of inheritance.
Generic Parameter Subtypes
With a solid understanding of generics and their purpose, we can now begin to examine the inheritance hierarchy that can be established with generics. One of the most common misnomers when starting with generics is that polymorphic generic arguments imply polymorphic generic classes. In fact, no relationship exists between the polymorphism of generic arguments and the polymorphism of the generic class:
Polymorphic generic arguments do not imply polymorphic generic classes
For example, if we have a List<Number>
, List<Double>
(where Double
is a subtype of Number
) is not a subtype of List<Number>
. In fact, the only relationship between List<Number>
and List<Double>
is that they both inherit from Object
(and as we will see shortly, List<?>
). To illustrate this scenario, we can define the following set of classes:
public class MyList<T> {}
public class MySpecializedList<T> extends MyList<T> {}
public class My2ParamList<T, S> extends MySpecializedList<T> {}
This set of classes leads to the following inheritance hierarchy:
Starting from the top, we can see that all generic classes still inherit from the Object
class. As we move down to the next level, we see that although Double
is a subtype of Number
, MyList<Double>
is not a subtype of MyList<Number>
. In order to understand this distinction, we must look at a concrete example. If we were to instantiate a MyList<Number>
, we can insert any object that is a Number
or a subtype of Number
in the following manner:
public class MyList<T> {
public void insert(T value) {
// ... insert the value ...
}
}
MyList<Number> numberList = new MyList<>();
numberList.insert(new Integer(7));
numberList.insert(new Double(5.72));
In order for MyList<Double>
to indeed be a subtype of MyList<Number>
, it would have to be substitutable for any instance of MyList<Number>
according to the Liskov Substitution Principle (i.e. anywhere a MyList<Number>
could be used, MyList<Double>
would have to provide the same behavior, such as adding an Integer
or Double
to the list). If we instantiate a MyList<Double>
, we quickly see that this principle does not hold, as we cannot allow an Integer
object to be added to our MyList<Double>
, since Integer
is not a subtype of Double
. Therefore, MyList<Double>
is not substitutable in all cases, and thus, is not a subtype of MyList<Number>
.
As we continue down the hierarchy, we can see that naturally, a generic class such as MySpecializedList<T>
is a subtype of MyList<T>
, so long as the generic arguments for T
match. For example, MySpecializedList<Number>
is a subtype of MyList<Number>
, but MySpecializedList<String>
is not a subtype of MyList<Number>
. Likewise, MySpecializedList<Integer>
is not a subtype of MyList<Number>
for the same reason that MyList<Double>
is not a subtype of MyList<Number>
.
Lastly, a generic class that includes additional generic parameters is a subtype of another generic class so long as the former class extends the latter class and the shared generic parameters match. For example, My2ParamList<T, S>
is a subtype of MySpecializedList<T>
so long as T
is the same type (since it is a shared generic parameter). If a generic parameter is not shared, such as S
, it may vary independently without affecting the generic hierarchy. For example, both My2ParamList<Number, Integer>
and My2ParamList<Number, Double>
are both subtypes of MySpecializedList<Number>
since the shared generic parameter matches.
Wildcard Subtypes
Although the generic parameter hierarchy is relatively straightforward, the hierarchy introduced by generic wildcards is much more nuanced. Within the wildcard scheme, we must account for three distinct cases: (1) unbounded wildcards, (2) upper bounded wildcards, and (3) lower bounded wildcards. We can see the relationship between these various cases in the figure below:
In order to understand this hierarchy, we must focus on the constraints in each of the wildcards presented (note that classes dealing with Double
and arrows exiting classes dealing with Double
are green and classes dealing with Number
and arrows exiting classes dealing with Number
are blue). Starting at the top, MyList<?>
inherits from Object
, since the MyList
object may contain a generic argument of any reference type (any object may be contained in this list). In actuality, this list can conceptually be thought to contain only objects of type Object
.
With the top of the hierarchy established, we will move to the bottom and focus on MyList<Double>
(green class in the bottom-left). Two classes act as direct parents to this class: (1) MyList<? extends Double>
and MyList<? super Double>
. The former case simply states that MyList<Double>
is a subclass of a MyList
that contains any Double
objects or objects that are subtypes of Double
. Viewed a different way, we are saying that a MyList
that contains only Double
is a special case of a MyList
that contains Double
objects or any other subclass of Double
. If we were to substitute MyList<Double>
where MyList<? extends Double>
were expected, we know that our MyList
would contain Double
or subtypes of Double
(in actuality, it would contain onlyDouble
, but that still suffices to meet the Double
or subtype of Double
requirement).
The latter case (having MyList<? super Double>
as a parent) simply states the same thing in the opposite direction: If we expect a MyList
that contains Double
or supertypes of Double
, supplying a MyList<Double>
will suffice. Viewed analogously to the previous case, a MyList
containing only Double
objects can be thought of as a special case of a MyList
that contains Double
objects or objects that are a subtype of Double
. In reality, MyList<Double>
is a more constrained version of MyList<? super Double>
. Thus, anywhere a MyList<? super Double>
is expected can be logically satisfied by providing a MyList<Double>
, which makes MyList<Double>
a subtype of MyList<? super Double>
by definition.
Completing the Double
portion of the hierarchy, we see that MyList<Double>
is a subclass of MyList<? extends Number>
. To understand this ancestry, we must think of what this upper bound entails. In short, we are requiring that MyList
contain objects of type Number
or any subclass of type Number
. Thus, MyList<Double>
, which contains only objects of type Double
(which is a subclass of type Number
) is a more constrained version of a MyList
containing Number
objects or subtypes of Number
. By definition, this makes MyList<Double>
a subtype of MyList<? extends Number>
.
The Number
portion of the hierarchy is simply a reflection of the Double
portion already discussed. Taken from the bottom, anywhere a MyList
of Number
or supertypes of Number
is expected (MyList<? super Number>
), a MyList<Number>
can suffice. Likewise, Number
is a supertype of Double
, and therefore, anywhere a Double
or a supertype of Double
is expected (MyList<? super Double>
), a MyList<Number>
will suffice. Lastly, anywhere a MyList
containing Number
or any subtype of Number
is required (MyList<? extends Number>
), a MyList
containing Number
will suffice since it just a special case of this requirement.
Corollary Topics
Although we have covered most of this hierarchy, three corollary topics remain: (1) MyList<? super Number>
being a subtype of MyList<? super Double>
, (2) MyList<? extends Double>
being a subtype of MyList<? extends Number>
, and (3) the common supertype between MyList<Double>
and MyList<Number>
.
- In the first case,
MyList<? super Double>
simply states that we expect aMyList
containingDouble
or any supertype ofDouble
, of whichNumber
is one. Thus, sinceNumber
will suffice as a supertype ofDouble
, providingMyList<? super Number>
is a more constrained version ofMyList<? super Double>
, making the former a subtype of the latter. - In the second case, the opposite is true. If we expect a
MyList
containingNumber
or any subtype ofNumber
, being thatDouble
is a subtype ofNumber
,MyList
containingDouble
or subtype ofDouble
can be thought of as a special case ofMyList
containingNumber
or a subtype ofNumber
. - In the last case, only
MyList<?>
acts as a common supertype betweenMyList<Double>
andMyList<Number>
. As previously stated, the polymorphic relationship betweenDouble
andNumber
does not, in turn, constitute a polymorphic relationship betweenMyList<Double>
andMyList<Number>
. Thus, the only common ancestry between the two types is aMyList
that contains any reference type (objects).
The generic argument specialization and generalization in the first two cases are illustrated in the figure below:
Conclusion
Generics add some very powerful features to an object-oriented language, but they can also introduce deep confusion in the conceptual models of a language for both new and experienced developers. Foremost among these confusions is the inheritance relationships among the various generic cases. In this article, we explored the purpose and thought process behind generics and covered the proper inheritance scheme for both named generic parameters and wildcard generics. For more information, consult the Oracle articles on generics and generic inheritance:
Opinions expressed by DZone contributors are their own.
Comments