Java Generics

In this article, we’ll talk about Java Generics and how you can use them, through examples.

1. What are Java Generics?

Java Generics were introduced in Java 5, and by utilizing them, you can make any class, interface, or method use members or variables that can be specified dynamically.

The syntax for a generic class or interface is the following:

// A generic class that can have and use variables of type T1, T2 , T3
public class AClass<T1, T2, T3>

// A generic interface that can have and use variables of type T1, T2, T3
public interface AnInterface<T1, T2, T3>

As for Methods:

// A void method that accepts parameters of any three different or same types and returns nothing
<T1,T2,T3> void aVoidMethod(T1 t1, T2 t2, T3 t3)

// A returning method that accepts parameters of any three different or same types 
//and returns a variable of a type the same as with the third parameter
<T1,T2,T3> T3 aReturningMethod(T1 t1, T2 t2, T3 t3)

1.1 Before Java 5

Note that before Java 5, if you wanted to use a List, you would write this:

List hello = new ArrayList();

But this means that you would be able to add anything to this list since the list would hold Object elements:

hello.add("h"); // A String
hello.add(5); // An Integer
hello.add(true); // A boolean

while if you needed to retrieve a value, you would need to cast the result from Object to the type this variable you expect to be:

String s = (String) hello.get(0);
Integer i = (Integer) hello.get(1);
Boolean i = (Boolean) hello.get(2);

1.2 After Java 5 and Before Java 7

After generics were introduced in Java 5, you can specify the type as shown below:

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

and as a result, if you try the same, you will get these compile errors:

java: incompatible types: int cannot be converted to java.lang.String
java: incompatible types: boolean cannot be converted to java.lang.String

Since now the List contains only Strings, you would not need to perform any casting.

1.3 After Java 7

Since Java 7, you do not need to add the type parameter in the right hand of the initialization since the compiler can determine which is the type. As a result, you could just write the following to initialize a list of strings:

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

1.4 Generics Naming Conventions

As for the naming convention, according to oracle docs, you should use single and uppercase letters, so it will be easy to differentiate a type variable and an ordinary class or interface name.

The most commonly used type parameter names are:

  • E – Element (used extensively by the Java Collections Framework)
  • K – Key
  • N – Number
  • T – Type
  • V – Value
  • S,U,V etc. – 2nd, 3rd, 4th types
Generic Types – Oracle Docs

Finally, you should know that you can use generics only with references(String, Double, Object, Person, etc.) and NOT with primitives(int, double, boolean, etc.)

2. Why Use Generics?

Code Reusability: Java Generics promote code reusability since a single class, interface or method can be used differently depending on the type(s) that we will provide.

No need for Casting: Before Java 5, when using a List, all the elements that were added inside a list were Objects

List hello = new ArrayList();
hello.add("You"); 
hello.add("Learn"); 
hello.add("Code");

Now notice that you cannot just retrieve an element at a specified index and as a result, the following

String you = hello.get(0);

would produce the java: incompatible types: java.lang.Object cannot be converted to java.lang.String compile error and the only way to make this work is to cast it explicitly:

String you = (String) hello.get(0);

Nevertheless, with Java Generics, we can just specify the type of the List on the creation and then retrieve any item without the need for casting:

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

hello.add("You"); 
hello.add("Learn"); 
hello.add("Code");

String you = hello.get(0);

Type Safety: From the previous benefit of using generics, you can understand that since a List holds elements of type Object, we can add anything inside as in Java anything extends Object. As a result, we could write the following before generics:

List hello = new ArrayList();
hello.add("You"); 
hello.add(5); 
hello.add(2.5d);

This list now holds three elements, all of which have different types. With generics, we can forbid any addition of an element of another type. So if we like a list of Person objects, we would write

List<Person> hello = new ArrayList();

And we would never to able to add anything else other than Person objects.

3. How to Use Java Generics in a Class

As it has already been stated, to use generics in a class you just have to add a non-reserved keyword for any different type that you might need inside <> just after the class name; for instance, if you want your class to have 3 members all of which should be able to have different types, you can do it as shown below:

public class GenericClass<T1,T2,T3> {

    private T1 var1;
    private T2 var2;
    private T3 var3;

    public GenericClass(T1 var1, T2 var2, T3 var3) {
        this.var1 = var1;
        this.var2 = var2;
        this.var3 = var3;
    }

}

Then you will be able to construct any object with members of any type that you might want to use:

GenericClass<Integer, Double, Person> gc1 = new GenericClass<>(
        5,
        2.5d,
        new Person("Geo", "Pal")
);
GenericClass<Boolean, String, Object> gc2 = new GenericClass<>(
        true,
        "Hello",
        null
);

4. How to Use Java Generic Methods

In the class that we created before, you can just use the types inside any method without any extra effort, for instance:

void aVoidMethod(T1 aVar, T3 aVar2) {
    System.out.println(aVar);
    System.out.println(aVar2);
}

T3 aReturningMethod(T1 aVar, T3 aCVar) {
    if(aVar == null) {
        return aCVar;
    }
    return null;
}

However, if these generic methods belonged to a non-generic class you would need to change them:

<T1,T2> void aVoidMethod(T1 aVar, T3 aVar2) {
    System.out.println(aVar);
    System.out.println(aVar2);
}

<T1,T3> T3 aReturningMethod(T1 aVar, T3 aCVar) {
    if(aVar == null) {
        return aCVar;
    }
    return null;
}

Since T1, T3 and T3 would be unknown if we didn’t and we would get:

Cannot resolve symbol 'T1'
Cannot resolve symbol 'T2'
Cannot resolve symbol 'T3'

5. WildCard Type

The wildcard(?) is a symbol that can be used instead of a specific T we used before, and it represents an unknown type.

Note that you CANNOT declare a class with a wildcard:

//This is not allowed
public class WildCardClass<?> {}

However, it is used in many cases, e.g. in the class ArrayList, you can find the following implementation where a Collection of an unknown type is used:

public boolean removeAll(Collection<?> c) {
  return batchRemove(c, false, 0, size);
}

In this case, if no restrictions from superclasses or interfaces existed, we could have written:

public <T> boolean removeAll(Collection<T> c) {
  return batchRemove(c, false, 0, size);
}

and the result would be the same.

Moreover, we can pass a wildcard type as an argument in a method or use it as a local variable:

static void printEverythingInAList(List<?> list) {
    
    list.forEach(System.out::println);
    
    //Not allowed
    //list.add(new Object());
    
    List<?> anotherList =  List.of("aString");

    //Not allowed
    //anotherList.add("s");
}

If we uncomment the 2 lines after //Not allowed, we will get Required type: capture of ? so we will never be able to add anything to a wildcard list. However, we can traverse and access a wildcard list without any error.

6. Java Generics with Extends and Super Keyword

You can also restrict the type of a type parameter or a wildcard, by adding the extends or super keyword.

Before we begin, consider that we have the following classes declared

public class Person{}

public class Teacher extends Person {}

public class Student extends Person {}

6.1 Java Generics with Extends Keyword

When using extends keyword either with a type parameter or a wildcard, you specify that the type to be used will have a superclass the type you specified after the extends keyword.

Therefore, if we have a List<? extends Person> means that the list can be of any type that extends the Person class.

Now consider that we have the following:

List<? extends Person> personList = new ArrayList<Person>();  // Person is-a Person
List<? extends Person> personList1 = new ArrayList<Teacher>(); // Teacher extends Person
List<? extends Person> personList2 = new ArrayList<Student >();  // Student extends Person

From the above we can deduct the following when it comes to reading from these collections:

  • We can read Person objects from all lists since you know that everything inside these lists contain objects that extend Person class
  • We cannot read Teacher objects since it wouldn’t possible for us to know whether an object is of type Teacher, as it could be of type Student
  • We cannot read Student objects for the same reason

As for writing:

  • You cannot add any Person object, as the list could be of type Teacher
  • You cannot add any Teacher object, as the list could be of type Student
  • You cannot add any Student object for the same reason

To summarize, should you use the extends keyword in a list, you will not be able to add anything to it and you would only be able to read objects of superclass type.

6.2 Java Generics with super Keyword

When using super keyword either with a type parameter or a wildcard, you specify that the type to be used will have a subclass of the type you specified after the super keyword.

Therefore, if we have a List<? super Teacher> means that the list can be of any type that has Teacher as a subclass.

Now consider that we have the following:

List<? super Teacher> personList1 = new ArrayList<Teacher>();  // Teacher is-a Teacher
List<? super Teacher> personList2 = new ArrayList<Person>(); // Teacher is a subclass of Person
List<? super Teacher> personList3 = new ArrayList<Object>();  // Teacher is a subclass of Object

From the above we can deduct the following when it comes to reading from these collections:

  • We cannot read Teacher objects, since this list could be of type Person or Object
  • We cannot read Person objects since the list could be of type Object
  • We can only read objects since the only thing that is certain is that anything inside will be an object

As for writing:

  • You cannot add any Person object or Object object, as the list could be of type Teacher and Person respectively
  • You can only add Teacher objects since all of the declarations allow an element of type Teacher

To summarize, should you use the super keyword in a list, you will be able to add only the elements on the right side of the ? super Something and you would only be able to read objects of type Object.

7. Java Generics in the Function Interface

In this section, we will take a look at the Function interface, which was introduced in Java 8, and explain how and why generics are used, so that you have a complete understating of generics in a real-world use case.

If we take a look at this interface, the first thing that we will notice is this:

@FunctionalInterface
public interface Function<T, R> {

The <T, R> are specified so we can have any type as a parameter and any return type.

As a result, we will be able to do any of the following:

Function<String,Integer> stringToInteger = Integer::parseInt

Function<Integer,String> integerToString = String::valueOf;

And whichever combination you can imagine.

Now take a look at these two functions:

R apply(T t);

static <T> Function<T, T> identity() {
        return t -> t;
}

You can notice that in the first one, we didn’t use <R> again, since it is specified at the class level, while for the identity() it is needed since this method is static.

The last method that we will inspect is the following:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

For the andThen function, you can notice that we need to have the <V> in the signature as it isn’t defined at the class level while the T and V are already defined at the class level and the method is not static.

Furthermore, the parameter is defined as Function <? super R, ? extends V> which means that:

  • The input of the function must be of any type that has R as a subclass(as passing anything else wouldn’t work)
  • The return type must be of any type that has V as a superclass

Finally, if you like to delve into Functional Interfaces, such as Function, you can read Java Functional Interface.

8. Conclusion

After reading this article, you should be able to use Java Generics to your own benefit and enhance the reusability of your code.

9. Sources

[1] : Generic Types (The Java™ Tutorials >
Learning the Java Language > Generics (Updated))
– Oracle Docs

[2]: Difference between <? super T> and <? extends T> in Java – StackOverflow