An Introduction To Java Generics

Java generics was introduced in Java 5. Generics allows us to raise the level of abstraction and create more reusable types. If you are familiar with C++, you can think of generics as being similar in certain respects to C++ templates.

In this tutorial we’ll introduce Generics in Java. We will look at why they were created, and how they can be used.

Why Generics

Generics allows us to create reusable types, and methods. For example, consider that we want to copy an array of integers. Therefore, we create a method to perform the copy operation. Moreover, in the future we need to copy an array of decimal values, represented as floats.

Before generics we would probably write two separate methods with identical logic to perform these operations. First we write the method to copy an int array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static int[] copyIntArray(int[] arrToCopy) {
    if(arrToCopy==null || arrToCopy.length ==0) {
        return new int[0];
    }

    int[] newArray = new int[arrToCopy.length];

    for(int index=0; index < arrToCopy.length; index++) {
        newArray[index] = arrToCopy[index];    
    }

    return newArray;
}

Next we write the method to copy the float array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static float[] copyFloatArray(float[] arrToCopy) {
    if(arrToCopy==null || arrToCopy.length ==0) {
        return new float[0];
    }

    float[] newArray = new float[arrToCopy.length];

    for (int index=0; index < arrToCopy.length; index++) {
        newArray[index] = arrToCopy[index];
    }
    return newArray;
}

If we compare the two methods above we can see that the logic of creating the new array, iterating the array, and assigning the array values to the new array is identical in both methods. The variation between the two methods is constrained to the data types int and float.

Let’s see how we can improve this. Our first approach is to create a generic version taking advantage of inheritance in Java.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static Object[] copyArrayWithInheritance(Object[] arrToCopy) {
    if(arrToCopy==null || arrToCopy.length ==0) {
        return new Object[0];
    }
    Object[] newArray = new Object[arrToCopy.length];
    for (int index=0; index < arrToCopy.length; index++) {
        newArray[index] = arrToCopy[index];
    }
    return newArray;
}

As we can see the only difference in our new method is that we change the method parameter and return type to Object.

1
2
3
4
5
Integer[] integerArray = new Integer[] {1,2,3,4,5};
Object[] arrIntegerCopy = GenericsExample.copyArrayWithInheritance(integerArray);

Float[] floatArray = new  Float[] {1.0f,2.0f,3.0f,4.0f,5.0f};
Object[] arrFloatCopy = GenericsExample.copyArrayWithInheritance(floatArray);

Above we can call our new method to copy both the integer array and the float array. However, there are still some problems. The copyArrayWithInheritance method returns an array of type Object.

Therefore, we need to perform an unsafe cast to convert the Object array to an Integer or Float array.

1
2
3
Integer[] arrIntegerCopy = (Integer[]) GenericsExample.copyArrayWithInheritance(integerArray);

Float[] arrFloatCopy = (Float[]) GenericsExample.copyArrayWithInheritance(floatArray);

You can read more in this tutorial about Casting Reference Types In Java.

Let’s see how we can improve this situation using generics.

Generic Methods

As we saw in the last section, it is common to write code that differs only in the data types that it operates on.

We would also like the convenience of being able to specify the data type that a method operates on and / or returns. Moreover, we would like the compiler to provide compile time type safety, and also take care of accepting and returning the data types we want, without the need to perform dangerous type casting operations.

The good news is that we can get all of these benefits and more by taking advantage of generics.

Let’s write our copy method as a generic method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static <T> T[] copy(T[] arrToCopy) {
    if(arrToCopy==null || arrToCopy.length ==0) {
        return (T[]) ReflectionUtils.newArray(arrToCopy, arrToCopy.length, arrToCopy.getClass());
    }

    T[] newArray = (T[]) ReflectionUtils.newArray(arrToCopy, arrToCopy.length, arrToCopy.getClass());

    for (int index=0; index < arrToCopy.length; index++) {
        newArray[index] = arrToCopy[index];
    }

    return newArray;
}

Now with our generic method created we can call it.

1
2
Integer[] integerArray = new Integer[] {1,2,3,4,5};
Integer[] arrIntegerCopy = GenericMethodExample.copy(integerArray);

As you can see generics enables us to raise the level of abstraction by creating reusable, type safe code, by allowing the data type to vary. It is also important to note that generics only works with Reference Types.

Generic Method Review

Let’s quickly review generic methods.

Generic methods are methods that allow us to write a single method declaration that can be reused with arguments and return values of different types. The compiler will provide us with compile time type safety. Generic methods have the following characteristics:

  • Generic methods have a type parameter delimited by angle brackets (< >) that precedes the method’s return type ( < T > in the above example).
  • A Generic method can have more than one type parameter separated by commas.
  • The type parameters can also be used to specify the return type of the method, as we did in our array copy example.
  • Type parameters can only represent reference types. Primitive types are not allowed.

Bounded Type Parameters

As we saw to create generic methods we specify type parameters.

1
public static <T> T[] copy(T[] arrToCopy) 

However, sometimes we want to restrict the type parameters to certain types. We can achieve this restriction with bounded type parameters.

Let’s say we want to create a method that takes a list of numbers and returns the even integers in the list. In order to achieve this we want to support different java.lang.Number data types such as: Integer, Float Double etc. However, to take advantage of type safety and to get an integer representation of a number, we have to restrict the data type to a type that extends java.lang.Number.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static <T extends Number> T[] findEven(T[] arr) {
    if(arr==null || arr.length ==0) {
        return (T[]) ReflectionUtils.newArray(arr, arr.length, arr.getClass());
    }

    List<T> evenNumberList = new ArrayList<T>();

    for(T num : arr) {
        if(num.intValue() % 2 ==0) {
            evenNumberList.add(num);
        }
    }

    T[] evenArr = (T[]) ReflectionUtils.newArray(arr, evenNumberList.size(), arr.getClass());
    return evenNumberList.toArray(evenArr);
}

Above our generic method restricts the type parameter T to a type that extends java.lang.Number. This is achieved through using the extends keyword.

1
<T extends Number>

Now in our findEven method we can treat objects of type T as java.lang.Number instances.

1
2
3
4
5
for(T num : arr) {
    if(num.intValue() % 2 ==0) {
        evenNumberList.add(num);
    }
}
1
2
3
4
5
6
7
@Test
public void findEven() {
    Number[] integerArray = new Number[]{1, 2, 3, 4, 5.0f};
    Number[] arrEvenInteger = GenericMethodExample.findEven(integerArray);
    Integer[] expectedArr = new Integer[]{2, 4};
    assertArrayEquals(expectedArr, arrEvenInteger);
}

Multiple Bounded Type Parameters

We can also specify multiple upper bounds. For example, let’s say we have a custom interface that is used to test if a number is even.

1
2
3
4
5
public interface IsEven {

    public boolean isEven();

}

We could update our findEven method to restrict the type to a type that realizes both java.lang.Number and our IsEven type.

1
2
3
public static <T extends Number & IsEven> T[] findEven(T[] arr) {
    //method implementation
}

Wildcards With Generics

Wildcards are specified using a question mark ?. A Wildcard represents an unknown type.

Wildcards are mainly used when working with collections. As you probably already know a class can be a superclass or a subclass. Moreover, a compiler makes it possible to cast between reference types in an inheritance hierarchy.

However, working with collections is a little different. For example, we can say a String is an Object because a String inherits from an Object. Whereas a List<String> is not a List<Object>.

Therefore, we can perform operations like this:

1
2
3
List<Object> listOfObjects = new LinkedList<>();
listOfObjects.add("String One");
listOfObjects.add(Integer.valueOf(2));

Because an Integer, and a String are an Object. However, we cant perform assignment :

1
2
3
4
5
6
7
8
List<Object> listOfObjects = new LinkedList<>();
List<String> listOfStrings = new LinkedList<>();

listOfStrings = listOfObjects; //does not compile
listOfObjects = listOfStrings; // does not compile

listOfStrings = (List<String>) listOfObjects; // does not compile
listOfObjects = (List<String>) listOfStrings; //does not compile

These operations are not allowed, because it would be possible to assign elements of the wrong type, to a collection. For example, if the list of objects contained types other than Strings such as Integers. Then the operation:

1
listOfStrings = (List<String>) listOfObjects;

Would assign an element of type Integer to a collection of Strings. Wildcards provide a solution to these issues as we can tell the compiler that a collection can contain elements of a type. We can then create generic methods to process and return collections.

We can define unknown, upper, and lower bound wildcards.

Unknown Wildcards

Unknown wildcards allow us to read from collections, but don’t allow us to insert into a collection.

1
2
3
4
List<String> listOfString = new LinkedList<>();

List<?> listUnknown = listOfString;
listUnknown.add("New Element"); //does not compile

Upper Bound Wildcards

Upper bound wildcards state that a collection contains elements of a type or a subtype of that type.

1
List<? extends Number> numbers = new LinkedList<>();

We can then treat the elements in the collection as types of java.lang.Number. However, still we can’t insert elements into the collection.

1
numbers.add(Integer.valueOf(4)); //compile error

However, we can assign lists of a specific type to a list with an upper bound of those types.

1
2
3
4
5
6
List<? extends Number> numbers = new LinkedList<>();
List<Integer> integers = new LinkedList<>();
List<Double> doubles = new LinkedList<>();

numbers = integers;
numbers = doubles;

To explore more about bounded type parameters you may find the tutorial Java Bounded Type Parameters useful.

Lower Bound Wildcards

Lower bound wildcards allow us to insert elements of type T, or a subclass of type T into a collection. This is, because we specify the lower bound which is the most specific type that a collection can hold.

1
2
List<? super Temporal> temporalList = new LinkedList<>();
temporalList.add(LocalDate.now());

However, lower bounds make it more difficult to read from a collection.

You can read more about this topic in the tutorial Java Generic Wildcards.

Generic Classes

Generic classes are like normal classes, with the addition of one or more type parameters. Whereas generic methods allow us to create methods that work with variations of type. Generic classes allow us to create reusable types that work with different data types.

Generic classes are used extensively throughout the Java API. For example, the Java collections API provides reusable data structures that function with different reference types.

Let’s create a simple generic class that enables us to represent the X and Y value of a Point as different data types, that can be used in different contexts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class GenericPoint<T> {
    private T x;
    private T y;


    public GenericPoint(T x, T y) {
        this.x = x;
        this.y = y;
    }

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}

The class accepts the type parameter T.

1
public class GenericPoint<T> 

We then use the type parameter to set the data type of the X and Y fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private T x;
private T y;

public GenericPoint(T x, T y) {
    this.x = x;
    this.y = y;
}

public T getX() {
    return x;
}

public void setX(T x) {
    this.x = x;
}

public T getY() {
    return y;
}

public void setY(T y) {
    this.y = y;
}

We can then create Point instances that represent a point as different data types.

1
2
3
4
5
GenericPoint<Float> pointFloat = new GenericPoint<>(10.0f, 5.0f);
Float xFloat = pointFloat.getX();

GenericPoint<Integer> pointInteger = new GenericPoint<>(10,5);
Integer xInteger = pointInteger.getX();

However, we may decide that we would like to restrict our Point class to represent points of numbers. Just like generic methods we can achieve this using Upper Bound Type Paramaters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static class GenericPoint<T extends Number> {
    private T x;
    private T y;

    public GenericPoint(T x, T y) {
        this.x = x;
        this.y = y;
    }

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}
1
2
GenericPoint<Integer> pointInteger = new GenericPoint<>(10,5);
GenericPoint<Object> pointInteger = new GenericPoint<>(10,5); //compile error

Generics And Functional Programming

Functional Programming is becoming more popular, it allows us to compose programs from well tested reusable code. Lambdas, and functional interfaces, are the building blocks of functional code in Java. Generic classes and generic methods enable is to realize components that raise the level of abstraction even further, creating more reusable building blocks.

1
2
3
List<Integer> evenIntegers = Map.of("1", 1, "2", 2)
        .values().stream().filter((i) -> i %2 ==0)
        .collect(Collectors.toList());

Type Erasure

Generics were added to Java to enable greater reusability, and compile time type safety. Moreover, to preserve backwards compatibility, and performance, generic type information is removed at runtime. For example, the method

1
2
3
4
public static <T> void printList(List<T> list) {


}

would be replaced with:

1
2
3
public static void printList(List list) {

}

Moreover, bounded types will be replaced with the bound.

1
2
public static <T extends Number> T maxInteger(T left, T right) {
}
1
2
public static Number maxInteger(Number left, Number right) {
}

Moreover, this can cause issues with the reflection API, in some use-cases. For example, when creating reusable framework code. In these cases we may need to use approaches such as TypeToken.

Advantages Of Generics

Let’s briefly review some of the advantages of generics.

  • Greater Reusability – We can reuse interface, classes and methods with different types.
  • Increased Abstraction – Facilitates less code, better quality code, increased composability and reusability.
  • Type Safety – Helps to reduce bugs, and improve code quality and maintainability by catching errors at compile time, instead of runtime.

Conclusion

In this short tutorial we introduced generics in Java and looked at some examples of using it with methods and classes. We also examined type parameters, including bounded, and wildcard types.

Finally, we looked at some of the advantages of using generics in Java.