Matthieu Vergne's Homepage

Last update: 27/01/2024 18:41:16

How to Use Java Generic Classes?

Generics have been introduced in Java 5 to add stability to your code by making more of your bugs detectable at compile time, dixit the Oracle tutorial. It is thus a great way to ensure that you are using the right type of object in your code even in cases where you don't know the exact type in advance.

In this article, we focus specifically on the most common situation: we need to use a class that defines one or more generics for itself. Probably the most common use of generics comes with the standard Java data structures. As Java developers, we often instantiate a List<E> or a Map<K, V>, which are both generic classes (classes relying on Generics). As a matter of fact, which class to assign to each generics is rather straightforward. If you need to create a list of Coin objects, then just create a List<Coin>. If you need to map various String values to their own list of Coin, then create a Map<String, List<Coin>>.

Questions

Maybe you already saw here and there these extends and super keywords. For example, when you call list.addAll(otherList), the addAll method does not simply take a list of the same type (List<E>). And this is not the only one. Here are some straightforward methods we can use on a List object since Java 8:


public interface List<E> extends Collection<E> {
	// ...
	boolean addAll(Collection<? extends E> c);
	boolean removeAll(Collection<?> c);
	void replaceAll(UnaryOperator<E> operator)
	void sort(Comparator<? super E> c);
	// ...
}

Using a Comparator for sorting is the usual deal, no surprise here. Using a UnaryOperator to replace each value with another one is not strange either for those familiar with it. Using a Collection instead of a List allows to consume a Set, which is sometimes useful. But what about their Generics? What are the extends and super keywords? And why one is using the proper generics E while the other only uses ?? Should you also use them when you define your own method parameters?

A matter of reuse: genericness vs. constraints

The aim of Generics is to provide type controls at compilation time in cases where the type is not known in advance. Since you don't know the type, the question is then: what can you at least assume about it? The 4 generics definitions we saw previously allow to describe specific constraints on the Generics type. The more constraints the Generics fulfills, the more you know about it, and so the more you can do with it. But the more you constrain it, the less you can reuse your method elsewhere, because you can only use it where those constraints apply. The goal is thus to constrain the Generics enough to do what you need, but not more to not reduce the reusability of your code.

No particular type constraint? Then anything goes with ?

Let's start with the least constrained case:


boolean removeAll(Collection<?> c);

This List method accepts a Collection with any Generics. It does not need to be of the same type than the Generics of the current List. Indeed, when you call list.remove(x), you can provide any kind of object (remove accepts any Object). If it is an object of the wrong type, it will simply not be found in the list and thus won't be removed. There is no reason to constrain it to be of the same type. Similarly, when removing several objects, there is no reason to constrain their type either.

When you define your own methods, you may start by accepting any Generics by setting them with the wildcard ?. If you need something more specific at some point, your method will not compile. Just add the relevant constraint at that time.

Extract some E from it? Then ? extends E

Let's see the most intuitive constraint that is extends:


boolean addAll(Collection<? extends E> c);

The extends keyword, as we are used to, means that the Generics type should extend E. It works either with E itself or any of its child classes. When adding elements to our List, we need to ensure they are of the right type E. Thus, the elements that we extract from the provided Collection should be of type E. Only then when can supply them to our List.

When you define your own methods, check what you extract from the generic class that you receive (what is returned by a method that you need from it). If you need to extract a specific type of objects, then the corresponding Generics should extend it. It does not matter what you do with it: just look at what type of object you need to extract from the generic class. Its Generics should extend this type to be compatible.


interface GenericClass<X> {
	X someCall();
}
<T> void doSomething(GenericClass<? extends T> arg) { // extends T
	T extracted = arg.someCall(); // because you need to extract T from it
}

Supply some E to it? Then ? super E

Let's look then at super:


void sort(Comparator<? super E> c);

The super keywords is the "reverse" of extends. The Comparator Generics should thus have a type which is E or a parent class, including Object. Indeed, our List contains objects of type E, so we will supply E instances to the Comparator. A Comparator that compares E instances works, but one that compares Object instances works too. For example, here we supply 2 E instances to a Comparator. But if it compares their toString() values, it can consume any object, not only E instances. So a Comparator<Object>, which can compare any kind of object, can also compare E instances, so we should be able to use it for our List<E>.

When you define your own methods, check what you supply to the generic class that you receive (what argument you give to a method that you need from it). If you need to supply a specific type of objects, then the corresponding Generics should super it. It does not matter where it comes from: just look at what type of object you need to supply to the generic class. Its Generics should super this type to be compatible.


interface GenericClass<X> {
	void someCall(X x);
}
<T> void doSomething(T t, GenericClass<? super T> arg) { // super T
	arg.someCall(t); // because you need to supply T to it
}

Extract and supply E? Then go full E

Now let's look at the last case:


void replaceAll(UnaryOperator<E> operator)

The UnaryOperator allows to replace a value by another: we give it the old value, it returns the new one. Our List containes E instances, so we need to supply a E to the operator, so its Generics should super E. But the returned values should be put back into our List, which accepts only E instances. So we also need to extract a E from the operator, so its Generics should also extends E. As a reminder, super E allows E or a parent class, and extends E allows E or a child class. The only way to fulfill both constraints is to be exactly of type E. This is why we need a UnaryOperator<E>.

When you define your own methods, check what you supply and extract to the generic class that you receive. If for a specific Generics you need to extract a given type, extend it. If for a specific Generics you need to supply a given type, super it. If for the same Generics you need both, then apply both constraints by asking for the exact type.


interface GenericClass<X> {
	void someCall(X x);
	X someOtherCall();
}
<T> void doSomething(T supplied, GenericClass<T> arg) { // T alone (extends + super)
	arg.someCall(supplied);            // because you need to supply T to it
	T extracted = arg.someOtherCall(); // and to extract T from it
}

If the constraints apply to different generics, then consider them separately:


interface GenericClass<X, Y> {
	void someCall(X x);
	Y someOtherCall();
}
<T> void doSomething(T supplied, GenericClass<? super T, ? extends T> arg) {
	arg.someCall(supplied);            // supply T for X, so super T for X
	T extracted = arg.someOtherCall(); // extract T for Y, so extends T for Y
}

Answers

In short, Generics should be constrained based on what you need from them. When you need a generic class, add constraints to its Generics based on how you need to interact with the generic class itself:

Just be aware that the more constraints you add, the less your code can be used elsewhere. So prefer to apply only those constraints that you actually need rather than immediately going for <T>. If you are not sure, just start with the wildcard <?>, then add the relevant constraints to make it compile.

Bibliography