DEV Community

Roderick Schmogick
Roderick Schmogick

Posted on

Are Generics Really That Generic?

One thing you'll notice about the Collection interface is its use of generics. In the interface definition, the data type of Collection objects' elements is not specified. Their type is represented as E, a mere placeholder or type parameter. Collection<E> is thus what we call a generic type. Collection<String> and Collection<Collection<E>> are examples of specific types—though, in the latter case, still a generic type—instantiating Collection<E>. These instances of the type Collection<E> are parameterized types. The types String and Collection<E> substituted for E in Collection<E> are type arguments.

Collection's being a generic type allows Java developers considerable latitude in choosing element types when implementing or instantiating collection classes. I don't know about you, but I'm more familiar with generics (templates) in C++ than in Java. So when I saw the type parameter E in the Java docs, I was tempted to read it as a placeholder for nearly any type, even a primitive type. But that can't be right, given what we know about collections and their elements in Java. Think about it. If that were true, what would that mean?

You could then create a Collection object with elements of a primitive type like int. For example, you could create a Set object on the model of the mathematical set of integers {1,2,3}.\lbrace 1, 2, 3 \rbrace . And you wouldn't have to use the wrapper class Integer to accomplish this. You could just do Set<int> set = new HashSet<>() {{ add(1); add(2); add(3); }};.

As it happens, this initialization is perfectly syntactically acceptable. But it gives a type error. That comes as no surprise, since as we've seen, only non-primitive values are allowed in collections. So, seeing as E represents the type of elements in a collection, it can't be a primitive type.

This raises a question about how generic generics really are. Perhaps no type parameter is so generic as to allow arguments of both reference types and primitive types. Or perhaps some type parameters, like E in Collection<E>, are less generic than others. But what could account for type parameters' having different levels of generality?

Only one mechanism exists in Java for controlling type parameters' generality. That mechanism is to use the extends keyword to specify a type parameter's upper bound: roughly, the most general type argument allowed. More precisely, a type parameter's upper bound is the type of which any type argument must be a subtype, lest a compilation error result.

But no bound is specified in Collection. It's just public interface Collection<E>, not public interface Collection<E extends SomeSupertype>. (Granted, the definition does say Collection<E> extends Iterable<E>. But that use of extends just makes Collection a subinterface of Iterable. It doesn't establish an upper bound of E.)

So we know there's no explicit bound requiring E to be a reference type. Could there be an implicit one? As a matter of fact, there could. And there is, in a sense.

Consider a class or interface SomeType<T>, such that extends does not occur between the angle brackets enclosing the type parameter. When the definition of SomeType is compiled, Object replaces T. This happens by default, since no bound for T is specified in the definition of SomeType. Due to this default behavior, T is bounded in that it must be a subtype of Object. As such, it cannot be a primitive type, nor can it be a supertype of both reference types and primitive types. So, if we want, we can call Object the implicit upper bound of T. Alternatively, we can say T has no upper bound. T can be any type of object, after all!

Now we can see why E does not allow primitive type arguments in Collection<E>. Primitive types are not subtypes of Object. In fact, no primitive type is a subtype of a more fundamental type—hence the descriptor "primitive." But any type parameter T must be a subtype of Object. For there are only two possibilities. One is that Object is the (implicit) upper bound of T. The other is that T extends SomeSupertype.

Suppose Object is the upper bound of T. Then, by the definition of "upper bound," T must be a subtype of Object.

Suppose instead that T extends SomeSupertype. Even though T is technically a supertype of itself, SomeSupertype cannot be T. The type a type parameter extends must be distinct from the type parameter. So T cannot be a primitive type; it's a subtype of a distinct type. Moreover, all non-primitive types are subtypes of Object. So T must be a subtype of Object.

So, no matter what, T has to be a subtype of Object. That means any type parameter, including E, must be a subtype of Object—which entails E is a reference type rather than a primitive type.

What have we learned? Type parameters have varying generalities depending on their bounds. But there's a limit to how general these "generics" get. They can only allow type arguments which are subtypes of Object. Thus, no type parameter is generic enough to allow primitive type arguments in addition to reference type arguments.

Top comments (0)