Introduction to Generics
This article describes the advantages of generics in Java in relation to type safety and the rationale for introducing it in Java 5.
Why Generics?
Consider the following lines of code.
What is wrong with the above code?
The first thing we notice is that we are defining a list without specifying what the content should be. In the above definition, List is known as a raw type and if you have read Effective Java, item 23 strongly discourages this. Why is this discouraged?
Firstly, lines 14 and 15 adds integers into the list but while manipulating these items (like looping in 20 to 23), we may need to add explicit casts if we need to perform Integer-specific actions. The need for explicit casts makes code brittle and can lead to serious issues at runtime. Looking at lines 26–33, we see another danger. The method addItem takes a raw type List as its parameter and in line 27, we called the method with a list of integers. However, a double is added to the list. While assuming that the list contains integers, line 33 tries to cast the added double to an integer, which results in a ClassCastException at runtime. We are not warned by the compiler during this process, making it very dangerous. A good solution will be to use a generic method so that JVM warns us about any potential illegal casts at compile time.
Generics enforces type safety at compile time
With our knowledge of supertypes Java and that Objects are supertypes of all other types, we may be tempted to write the following generic code:
However, this fails because of a property in Java called invariance (Lists are invariant while arrays are not). Due to invariance, line 7 will result in a compilation error thus, further enforcing compile-time type safety. This is possible because of generics. This is put in place to avoid unnecessary cast exceptions that may arise at runtime.
Generic Types
According to the official oracle docs, a generic type is a generic class or interface that is parameterized over types. Imagine we have a class called Box. A Box object can contain any type. To generify this class, we can use Object as follows:
However, as explained above, using Object as a common supertype is too generic and can lead to cast exceptions during runtime. Therefore, the class above can be changed like below:
In the above example, our code will not compile when we try to add a Double into a box of Integers. In the above declaration, T is known as a type parameter while the entire class is known as a generic type.
Bounded Generics
In the example above, we may want to restrict the type argument, T, to a specific supertype, say, Number. A simple reason for this may be that we want our Box objects to only operate on numbers and nothing else.
In the above example, T is an upper-bounded type parameter and the bound is Number.
Also note that when we do not specify an upper bound as above, Object is explicitly used as the bound.
Therefore,
class Box<T> {} is the same as class Box<T extends Object> {}
Generic Methods
Generic methods are infinitely overloaded, i.e. they can take arguments of any type (although we can also induce bounds to method parameter types). They also ensure type safety by allowing the compiler to flag out issues at compile time thus, avoiding type-specific exceptions that may spring up at runtime.
Generic Methods: Examples
The Java Collections class defines a method called replaceAll() that reads as follows:
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
We read this as “Given a List of type T, replace all oldval in the list to newVal”
Notice that oldVal and newVal must be the same type parameter T.
We can call the above method like:
List<Integer> someList = getListOfIntegers()booeal allReplaced = replaceAll(someList, 12, 10);boolean allReplaced = replaceAll(someList, 12, 10.4) // will result to a compilation issue
A fix for the addItems in the first example is seen below:
The method is generic and ensures that we don't add an item into a list of items that are not of the same type as that item
Generic and Type Erasure
In the above paragraphs, we have seen how generics can enforce compile-time safety. We now understand that it is better for an illegal code to fail the compilation stage than bursting out and production and thus, producing more undesirable outcomes. However, how does generics ensure compile-time type safety? To implement generics, the Java compiler does the following:
- Replace all type parameters (T in the above sections) in the generic types with the bounds (in the case of upper bounded generics) or with Object (we have seen above that when no bound is specified, Object is used as the upper bound) if no bound is specified. The byte code produced at this step, therefore, contains only Interfaces, classes, and methods.
- Insert casts where necessary and notify programmers of potential cast exceptions.
Type Replacement due to Type Erasure
In the first figure, T is replaced with Object since it is unbounded (remember any unbounded type is bounded by Object). However, in the second figure, T is replaced with Numer by the compiler because it is bounded by Number. The same replacement strategies also apply for generic methods.
Class casting
In addition to type replacement, the compiler also performs explicit casts during compilation to ensure type safety.
Box<Integer> intBox = new Box<>();
intBox.setObject(12.4) // 12.4 is autoboxed to Double and casted to Integer and since this is an illegal cast, the compiler throws a ClassCastException.
Here, Integer is the inferred type (Note that JVM uses complex algorithms for type inference that will not be discussed int this article)
Conclusion
Generics is a really cool feature in Java that ensures type safety at compile time. When used well, it prevents unnecessary illegal casts that may result at runtime. Apart from the fact that generic are invariant: we are not allowed to substitute a subtype with a supertype and vice-versa, the compiler also has a property called type erausre which further ensures type safety of generic types. Again, we can specify bounds to generic methods and generic types to further protect our code from illegal assignments as seen in the above sections. Apart from the advantage of making our code extremely flexible, generics also enforees type safety and we should take advantage of this where necessary.