Java Generics is one of the most important features of the Java language. The idea behind generics is quite simple, however, it sometimes comes off as complex because of the shift from the usual syntax associated with it.

The purpose of this tutorial is to introduce you to this useful concept of generics in an easy to understand manner.

But before diving into generics itself, let’s figure out why Java generics were needed in the first place.

Purpose of Java Generics

Before the introduction of generics in Java 5, you could write and compile a code snippet like this without throwing an error or a warning:

List list = new ArrayList();
list.add("hey");
list.add(new Object());

You could add values of any type to a list or another Java Collection without having to declare what type of data it stores. But when you retrieve values from the list, you have to explicitly cast it to a certain type.

Consider iterating through the above list.

for (int i=0; i< list.size(); i++) {
    String value = (String) list.get(i);  //CastClassException when i=1
}

Allowing the creation of a list without first declaring the stored data type, as we did, could result in programmers making mistakes like above which throws ClassCastExceptions during the runtime.

Generics were introduced to prevent the programmers from making such mistakes.

With generics, you can explicitly declare the data type that is going to be stored when creating a Java Collection as the following example shows.

List<String> stringList = new ArrayList<>();

Now, you cannot mistakenly store an Integer in a String type list without throwing a compile-time error. This ensures that your program doesn’t run into runtime errors.

stringList.add(new Integer(4)); //Compile time Error

The main purpose of the introduction of generics to Java was to avoid running into ClassCastExceptions during runtime.

Creating Java Generics

You can use generics to create Java classes and methods. Let’s look at the examples of how to create generics of each type.

Generic Class

When creating a generic class, the type parameter for the class is added at the end of the class name within angle <> brackets.

public class GenericClass<T> {
    private T item;
    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return this.item;
    }
}

Here, T is the data type parameter. T, N, and E are some of the letters used for data type parameters according to Java conventions.

In the above example, you can pass it a specific data type when creating a GenericClass object.

public static void main(String[] args) {

    GenericClass<String> gc1 = new GenericClass<>();
    gc1.setItem("hello");
    String item1 = gc1.getItem(); // "hello"
    gc1.setItem(new Object()); //Error

    GenericClass<Integer> gc2 = new GenericClass<>();
    gc2.setItem(new Integer(1));
    Integer item2 = gc2.getItem(); // 1
    gc2.setItem("hello"); //Error
}

You cannot pass a primitive data type to the data type parameter when creating a generic class object. Only data types that extend Object type can be passed as type parameters.

For example:

GenericClass<float> gc3 = new GenericClass<>(); //Error

Generic Methods

Creating generic methods follows a similar pattern to creating generic classes. You can implement a generic method inside a generic class as well as a non-generic one.

public class GenericMethodClass {

    public static <T> void printItems(T[] arr){
        for (int i=0; i< arr.length; i++) {
            System.out.println(arr[i]);
        }
    }

    public static void main(String[] args) {
        String[] arr1 = {"Cat", "Dog", "Mouse"};
        Integer[] arr2 = {1, 2, 3};

        GenericMethodClass.printItems(arr1); // "Cat", "Dog", "Mouse"
        GenericMethodClass.printItems(arr2); // 1, 2, 3
    }
}

Here, you can pass an array of a specific type to parameterize the method. The generic method PrintItems() iterates through the passed array and prints the items stored just like a normal Java method.

Bounded Type Parameters

So far, the generic classes and methods we created above can be parameterized to any data type other than primitive types. But what if we wanted to limit the data types that can be passed to generics? This is where bounded type parameters come in.

You can bound the data types accepted by a generic class or method by specifying that it should be a subclass of another data type.

For example:

//accepts only subclasses of List
public class UpperBoundedClass<T extends List>{
    //accepts only subclasses of List
    public <T extends List> void UpperBoundedMethod(T[] arr) {
    }
}

Here, the UpperBoundedClass and UpperBoundedMethod can only be parameterized using subtypes of the List data type.

List data type acts as an upper bound to the type parameter. If you try to use a data type that is not a subtype of List, it will throw a compile-time error.

The bounds are not limited to only classes. You can pass interfaces as well. Extending the interface means, in this case, implementing the interface.

A parameter can also have multiple bounds like this example shows.

//accepts only subclasses of both Mammal and Animal
public class MultipleBoundedClass<T extends Mammal & Animal>{

    //accepts only subclasses of both Mammal and Animal
    public <T extends Mammal & Animal> void MultipleBoundedMethod(T[] arr){

    }
}

The accepting data type must be a subclass of both Animal and Mammal classes. If one of these bounds is a class, it must come first in the bound declaration.

In the above example, if Mammal is a class and Animal is an interface, Mammal must come first as shown above. Otherwise, the code throws a compile-time error.

Java Generics Wildcards

Wildcards are used to pass on parameters of generic types to methods. Unlike, a generic method, here, the generic parameter is passed to the parameters accepted by the method, which is different from the data type parameter we discussed above. A wildcard is represented by the ? symbol.

public void printItems(List<?> list) {
    for (int i=0; i< list.size(); i++) {
        System.out.println(list.get(i));
    }
}

The above printItems() method accepts lists of any data type as the parameter. This prevents programmers from having to repeat codes for lists of different data types, which would be the case without generics.

Upper Bounded Wildcards

If we want to limit the data types stored in the list accepted by the method, we can use bounded wildcards.

Example:

public void printSubTypes(List<? extends Color> list) {
    for (int i=0; i< list.size(); i++) {
        System.out.println(list.get(i));
    }
}

printSubTypes() method accepts only the lists that store subtypes of Color. It accepts a list of RedColor or BlueColor objects, but doesn’t accept a list of Animal objects. This is because Animal is not a subtype of Color. This is an example of an upper-bounded wildcard.

Lower Bounded Wildcards

Similarly, if we had:

public void printSuperTypes(List<? super Dog> list) {
    for (int i=0; i< list.size(); i++) {
        System.out.println(list.get(i));
    }
}

then, the printSuperTypes() method only accepts lists that store super types of the Dog class. It would accept a list of Mammal or Animal objects but not a list of LabDog objects because LabDog is not a superclass of Dog, but a subclass. This is an example of a lower-bounded wildcard.

Conclusion

Java Generics has become a feature that programmers cannot live without since its introduction.

This popularity is due to its impact on making programmers’ lives easier. Other than preventing them from making coding mistakes, the use of generics makes the code less repetitive. Did you notice how it generalizes classes and methods to avoid having to repeat code for different data types?

Having a good grasp of generics is important to become an expert in the language. So, applying what you learned in this tutorial in practical code is the way to go forward now.

See also