Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How Do Generic Subtypes Work?

DZone's Guide to

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.

· Java Zone
Free Resource

Try Okta to add social login, MFA, and OpenID Connect support to your Java app in minutes. Create a free developer account today and never build auth again.

Generic 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 Objects 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:

Image title

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:

Image title

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

  1. In the first case, MyList<? super Double> simply states that we expect a MyList containing Double or any supertype of Double, of which Number is one. Thus, since Number will suffice as a supertype of Double, providing MyList<? super Number> is a more constrained version of MyList<? super Double>, making the former a subtype of the latter.
  2. In the second case, the opposite is true. If we expect a MyList containing Number or any subtype of Number, being that Double is a subtype of Number, MyList containing Double or subtype of Double can be thought of as a special case of MyList containing Number or a subtype of Number.
  3. In the last case, only MyList<?> acts as a common supertype between MyList<Double> and MyList<Number>. As previously stated, the polymorphic relationship between Double and Number does not, in turn, constitute a polymorphic relationship between MyList<Double> and MyList<Number>. Thus, the only common ancestry between the two types is a MyList that contains any reference type (objects).

The generic argument specialization and generalization in the first two cases are illustrated in the figure below:

Image title

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:

Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
java ,generics ,subclassing ,inheritance ,tutorials

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}