Core Java

How and when to use Generics

This article is part of our Academy Course titled Advanced Java.

This course is designed to help you make the most effective use of Java. It discusses advanced topics, including object creation, concurrency, serialization, reflection and many more. It will guide you through your journey to Java mastery! Check it out here!

1. Introduction

The idea of generics represents the abstraction over types (well-known to C++ developers as templates). It is a very powerful concept (which came up quite a while ago) that allows to develop abstract algorithms and data structures and to provide concrete types to operate on later. Interestingly, generics were not present in the early versions of Java and were added along the way only in Java 5 release. And since then, it is fair to say that generics revolutionized the way Java programs are being written, delivering much stronger type guaranties and making code significantly safer.

In this section we are going to cover the usage of generics everywhere, starting from interfaces, classes and methods. Providing a lot of benefits, generics however do introduce some limitations and side-effects which we also are going to cover.

2. Generics and interfaces

In contrast to regular interfaces, to define a generic interface it is sufficient to provide the type (or types) it should be parameterized with. For example:

package com.javacodegeeks.advanced.generics;

public interface GenericInterfaceOneType< T > {
    void performAction( final T action );
} 

The GenericInterfaceOneType is parameterized with single type T, which could be used immediately by interface declarations. The interface may be parameterized with more than one type, for example:

package com.javacodegeeks.advanced.generics;

public interface GenericInterfaceSeveralTypes< T, R > {
    R performAction( final T action );
}

Whenever any class wants to implement the interface, it has an option to provide the exact type substitutions, for example the ClassImplementingGenericInterface class provides String as a type parameter T of the generic interface:

package com.javacodegeeks.advanced.generics;

public class ClassImplementingGenericInterface
        implements GenericInterfaceOneType< String > {
    @Override
    public void performAction( final String action ) {
        // Implementation here
    }
} 

The Java standard library has a plenty of examples of the generic interfaces, primarily within collections library. It is very easy to declare and use generic interfaces however we are going to get back to them once again when discussing bounded types (Generics, wildcards and bounded types) and generic limitations (Limitation of generics).

3. Generics and classes

Similarly to interfaces, the difference between regular and generic classes is only the type parameters in the class definitions. For example:

package com.javacodegeeks.advanced.generics;

public class GenericClassOneType< T > {
    public void performAction( final T action ) {
        // Implementation here
    }
} 

Please notice that any class (concrete, abstract or final) could be parameterized using generics. One interesting detail is that the class may pass (or may not) its generic type (or types) down to the interfaces and parent classes, without providing the exact type instance, for example:

package com.javacodegeeks.advanced.generics;

public class GenericClassImplementingGenericInterface< T >
        implements GenericInterfaceOneType< T > {
    @Override
    public void performAction( final T action ) {
        // Implementation here
    }
} 

It is a very convenient technique which allows classes to impose additional bounds on generic type still conforming the interface (or parent class) contract, as we will see in section Generics, wildcards and bounded types.

4. Generics and methods

We have already seen a couple of generic methods in the previous sections while discussing classes and interfaces. However, there is more to say about them. Methods could use generic types as part of arguments declaration or return type declaration. For example:

public< T, R > R performAction( final T action ) {
    final R result = ...;
    // Implementation here
    return result;
} 

There are no restrictions on which methods can use generic types, they could be concrete, abstract, static or final. Here is a couple of examples:

protected abstract< T, R > R performAction( final T action );

static< T, R > R performActionOn( final Collection< T > action ) {
    final R result = ...;
    // Implementation here
    return result;
} 

If methods are declared (or defined) as part of generic interface or class, they may (or may not) use the generic types of their owner. They may define own generic types or mix them with the ones from their class or interface declaration. For example:

package com.javacodegeeks.advanced.generics;

public class GenericMethods< T > {
    public< R > R performAction( final T action ) {
        final R result = ...;
        // Implementation here
        return result;
    }

    public< U, R > R performAnotherAction( final U action ) {
        final R result = ...;
        // Implementation here
        return result;
    }
} 

Class constructors are also considered to be kind of initialization methods, and as such, may use the generic types declared by their class, declare own generic types or just mix both (however they cannot return values so the return type parameterization is not applicable to constructors), for example:

public class GenericMethods< T > {
    public GenericMethods( final T initialAction ) {
        // Implementation here
    }

    public< J > GenericMethods( final T initialAction, final J nextAction ) {
        // Implementation here
    }
} 

It looks very easy and simple, and it surely is. However, there are some restrictions and side-effects caused by the way generics are implemented in Java language and the next section is going to address that.

5. Limitation of generics

Being one of the brightest features of the language, generics unfortunately have some limitations, mainly caused by the fact that they were introduced quite late into already mature language. Most likely, more thorough implementation required significantly more time and resources so the trade-offs had been made in order to have generics delivered in a timely manner.

Firstly, primitive types (like int, long, byte, …) are not allowed to be used in generics. It means whenever you need to parameterize your generic type with a primitive one, the respective class wrapper (Integer, Long, Byte, …) has to be used instead.

final List< Long > longs = new ArrayList<>();
final Set< Integer > integers = new HashSet<>(); 

Not only that, because of necessity to use class wrappers in generics, it causes implicit boxing and unboxing of primitive values (this topic will be covered in details in the part 7 of the tutorial, General programming guidelines), for example:

final List< Long > longs = new ArrayList<>();
longs.add( 0L ); // 'long' is boxed to 'Long'

long value = longs.get( 0 ); // 'Long' is unboxed to 'long'
// Do something with value 

But primitive types are just one of generics pitfalls. Another one, more obscure, is type erasure. It is important to know that generics exist only at compile time: the Java compiler uses a complicated set of rules to enforce type safety with respect to generics and their type parameters usage, however the produced JVM bytecode has all concrete types erased (and replaced with the Object class). It could come as a surprise first that the following code does not compile:

void sort( Collection< String > strings ) {
    // Some implementation over strings heres
}

void sort( Collection< Number > numbers ) {
    // Some implementation over numbers here
} 

From the developer’s standpoint, it is a perfectly valid code, however because of type erasure, those two methods are narrowed down to the same signature and it leads to compilation error (with a weird message like “Erasure of method sort(Collection<String>) is the same as another method …”):

void sort( Collection strings )
void sort( Collection numbers ) 

Another disadvantage caused by type erasure come from the fact that it is not possible to use generics’ type parameters in any meaningful way, for example to create new instances of the type, or get the concrete class of the type parameter or use it in the instanceof operator. The examples shown below do no pass compilation phase:

public< T > void action( final T action ) {
    if( action instanceof T ) {
        // Do something here
    }
}

public< T > void action( final T action ) {
    if( T.class.isAssignableFrom( Number.class )  ) {
     // Do something here
    }
} 

And lastly, it is also not possible to create the array instances using generics’ type parameters. For example, the following code does not compile (this time with a clean error message “Cannot create a generic array of T”):

public< T > void performAction( final T action ) {
    T[] actions = new T[ 0 ];
} 

Despite all these limitations, generics are still extremely useful and bring a lot of value. In the section Accessing generic type parameters we are going to take a look on the several ways to overcome some of the constraints imposed by generics implementation in Java language.

6. Generics, wildcards and bounded types

So far we have seen the examples using generics with unbounded type parameters. The extremely powerful ability of generics is imposing the constraints (or bounds) on the type they are parameterized with using the extends and super keywords.

The extends keyword restricts the type parameter to be a subclass of some other class or to implement one or more interfaces. For example:

public< T extends InputStream > void read( final T stream ) {
    // Some implementation here
} 

The type parameter T in the read method declaration is required to be a subclass of the InputStream class. The same keyword is used to restrict interface implementations. For example:

public< T extends Serializable > void store( final T object ) {
    // Some implementation here
} 

Method store requires its type parameter T to implement the Serializable interface in order for the method to perform the desired action. It is also possible to use other type parameter as a bound for extends keyword, for example:

public< T, J extends T > void action( final T initial, final J next ) {
     // Some implementation here
} 

The bounds are not limited to single constraints and could be combined using the & operator. There could be multiple interfaces specified but only single class is allowed. The combination of class and interfaces is also possible, with a couple of examples show below:

public< T extends InputStream & Serializable > void storeToRead( final T stream ) {
    // Some implementation here
}
public< T extends Serializable & Externalizable & Cloneable > void persist(
        final T object ) {
    // Some implementation here
} 

Before discussing the super keyword, we need to get familiarized with the concepts of wildcards. If the type parameter is not of the interest of the generic class, interface or method, it could be replaced by the ? wildcard. For example:

public void store( final Collection< ? extends Serializable > objects ) {
    // Some implementation here
} 

The method store does not really care what type parameters it is being called with, the only thing it needs to ensure that every type implements Serializable interface. Or, if this is not of any importance, the wildcard without bounds could be used instead:

public void store( final Collection< ? > objects ) {
    // Some implementation here
} 

In contrast to extends, the super keyword restricts the type parameter to be a superclass of some other class. For example:

public void interate( final Collection< ? super Integer > objects ) {
    // Some implementation here
} 

By using upper and lower type bounds (with extends and super) along with type wildcards, the generics provide a way to fine-tune the type parameter requirements or, is some cases, completely omit them, still preserving the generics type-oriented semantic.


 

7. Generics and type inference

When generics found their way into the Java language, they blew up the amount of the code developers had to write in order to satisfy the language syntax rules. For example:

final Map< String, Collection< String > > map =
    new HashMap< String, Collection< String > >();

for( final Map.Entry< String, Collection< String > > entry: map.entrySet() ) {
    // Some implementation here
} 

The Java 7 release somewhat addressed this problem by making changes in the compiler and introducing the new diamond operator <>. For example:

final Map< String, Collection< String > > map = new HashMap<>(); 

The compiler is able to infer the generics type parameters from the left side and allows omitting them in the right side of the expression. It was a significant progress towards making generics syntax less verbose, however the abilities of the compiler to infer generics type parameters were quite limited. For example, the following code does not compile in Java 7:

public static < T > void performAction( final Collection< T > actions,
        final Collection< T > defaults ) {
    // Some implementation here
}

final Collection< String > strings = new ArrayList<>();
performAction( strings, Collections.emptyList() ); 

The Java 7 compiler cannot infer the type parameter for the Collections.emptyList() call and as such requires it to be passed explicitly:

performAction( strings, Collections.< String >emptyList() ); 

Luckily, the Java 8 release brings more enhancements into the compiler and, particularly, into the type inference for generics so the code snippet shown above compiles successfully, saving the developers from unnecessary typing.

8. Generics and annotations

Although we are going to discuss the annotations in the next part of the tutorial, it is worth mentioning that in the pre-Java 8 era the generics were not allowed to have annotations associated with their type parameters. But Java 8 changed that and now it becomes possible to annotate generics type parameters at the places they are declared or used. For example, here is how the generic method could be declared and its type parameter is adorned with annotations:

public< @Actionable T > void performAction( final T action ) {
    // Some implementation here
} 

Or just another example of applying the annotation when generic type is being used:

final Collection< @NotEmpty String > strings = new ArrayList<>();
// Some implementation here 

In the part 4 of the tutorial, How and when to use Enums and Annotations, we are going to take a look on a couple of examples how the annotations could be used in order to associate some metadata with the generics type parameters. This section just gives you the feeling that it is possible to enrich generics with annotations.

9. Accessing generic type parameters

As you already know from the section Limitation of generics, it is not possible to get the class of the generic type parameter. One simple trick to work-around that is to require additional argument to be passed, Class< T >, in places where it is necessary to know the class of the type parameter T. For example:

public< T > void performAction( final T action, final Class< T > clazz ) {
    // Some implementation here
} 

It might blow the amount of arguments required by the methods but with careful design it is not as bad as it looks at the first glance.

Another interesting use case which often comes up while working with generics in Java is to determine the concrete class of the type which generic instance has been parameterized with. It is not as straightforward and requires Java reflection API to be involved. We will take a look on complete example in the part 11 of the tutorial, Reflection and dynamic languages support but for now just mention that the ParameterizedType instance is the central point to do the reflection over generics.

10. When to use generics

Despite all the limitations, the value which generics add to the Java language is just enormous. Nowadays it is hard to imagine that there was a time when Java had no generics support. Generics should be used instead of raw types (Collection< T > instead of Collection, Callable< T > instead of Callable, …) or Object to guarantee type safety, define clear type constraints on the contracts and algorithms, and significantly ease the code maintenance and refactoring.

However, please be aware of the limitations of the current implementation of generics in Java, type erasure and the famous implicit boxing and unboxing for primitive types. Generics are not a silver bullet solving all the problems you may encounter and nothing could replace careful design and thoughtful thinking.

It would be a good idea to look on some real examples and get a feeling how generics make Java developer’s life easier.

Example 1: Let us consider the typical example of the method which performs actions against the instance of a class which implements some interface (say, Serializable) and returns back the modified instance of this class.

class SomeClass implements Serializable {
} 

Without using generics, the solution may look like this:

public Serializable performAction( final Serializable instance ) {
    // Do something here
    return instance;
} 

final SomeClass instance = new SomeClass();
// Please notice a necessary type cast required
final SomeClass modifiedInstance = ( SomeClass )performAction( instance );  

Let us see how generics improve this solution:

public< T extends Serializable > T performAction( final T instance ) {
    // Do something here
    return instance;
}      

final SomeClass instance = new SomeClass();
final SomeClass modifiedInstance = performAction( instance );  

The ugly type cast has gone away as compiler is able to infer the right types and prove that those types are used correctly.

Example 2: A bit more complicated example of the method which requires the instance of the class to implement two interfaces (say, Serializable and Runnable).

class SomeClass implements Serializable, Runnable {
    @Override
    public void run() {
        // Some implementation
    }
} 

Without using generics, the straightforward solution is to introduce intermediate interface (or use the pure Object as a last resort), for example:

// The class itself should be modified to use the intermediate interface
// instead of direct implementations
class SomeClass implements SerializableAndRunnable {
    @Override
    public void run() {
        // Some implementation
    }
} 

public void performAction( final SerializableAndRunnable instance ) {
    // Do something here
} 

Although it is a valid solution, it does not look as the best option and with the growing number of interfaces it could get really nasty and unmanageable. Let us see how generics can help here:

public< T extends Serializable & Runnable > void performAction( final T instance ) {
    // Do something here
} 

Very clear and concise piece of code, no intermediate interface or other tricks are required.

The universe of examples where generics make code readable and straightforward is really endless. In the next parts of the tutorial generics will be often used to demonstrate other features of the Java language.

11. What´s next

In this section we have covered one of very distinguishing features of Java language called generics. We have seen how generics make you code type-safe and concise by checking that the right types (with bounds) are being used everywhere. We also looked through some of the generics limitations and the ways to overcome them. In the next section we are going to discuss enumerations and annotations.

12. Download the Source Code

  • This was a lesson on How to design Classes and Interfaces. You may download the source code here: advanced-java-part-4

Andrey Redko

Andriy is a well-grounded software developer with more then 12 years of practical experience using Java/EE, C#/.NET, C++, Groovy, Ruby, functional programming (Scala), databases (MySQL, PostgreSQL, Oracle) and NoSQL solutions (MongoDB, Redis).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
David
David
8 years ago

Very helpful. While I’ve learned Java generics through osmosis so far, this article clarified a few point for me. It’s a small revelation to learn that the wildcard “?” simply means “This method, interface, or class doesn’t care what the type is.” I suppose then that `Collection` and similar could always be replaced with `Collection`, since the method, interface or class isn’t going to ever return to the type.

Andriy Redko
8 years ago
Reply to  David

Hi David,

Thank you for your comment. Right, another way to think about “?” is that sometimes you just don’t know what the type is and as such any type parameter is acceptable.

Best Regards,
Andriy Redko

Germann Arlington
Germann Arlington
8 years ago

One note on
9. Accessing generic type parameters
You will find that in your code example
public void performAction( final T action, final Class clazz ) {
assert(action.getClass() == clazz);
// Some implementation here
}

Nathan
Nathan
4 years ago

Need to point out that the generic type doesn’t support polymorphism. For example, MyList is not a subtype of MyList, even though Double is a subtype of Number. Also, you could do MySublist <X> extends MyList <T>, so X and T could be 2 different types.

Nathan
Nathan
4 years ago
Reply to  Nathan

MyList<Doube> and MyList<Number>, not displayed correctly due to the html syntax.

Back to top button