Generics and type safety

The background

Before Java 5.0, all the collections (like List, Set etc.) were supposed to hold type Object. So, you can put anything to the collection, but when you get something out of that collection, you will get a reference of type Object.

The problem

There are couple of problems in the code above –

  1. There is no way to restrict the type of object the list will hold. So, along with Integer, you can put Boolean, Float, String and any other type that you can think of.
  2. When you get something out of that collection, you will get a reference of type Object. So you have to explicitly cast the object to a specific type.
  3. If you do not cast to the proper type, there is a possibility that you will get an exception at runtime.

Let’s understand the last point with an example.

List intList = new ArrayList();
intList.add(new Integer(10));
intList.add("ABC");

Object obj1 = intList.get(0);
Object obj2 = intList.get(1);

Integer val1 = (Integer) obj1;
Integer val2 = (Integer) obj2;

You will get an exception at the last line at runtime stating: java.lang.String cannot be cast to java.lang.Integer.

The solution

Java 5.0 introduced generics using which you can create a collection of specific types. The compiler will allow you to add elements of that type only. If you try to add elements of another type, you will get a compilation error. Also, when you get something out of the list, you will get an object of that type only.

The benefit of using generics

We can see from above code, there are clearly couple of benefit –

  1. Catch error at compile time: As compiler ensures that you are allowed to add elements only of a specific type, if you try to add objects of another type, you will get errors early at compile time rather than runtime.
  2. Elimination of casts: As you will get an object of a particular type, typecast is not required.
Understand the generic class

We have already used the generic ArrayList class. Below is a simplified version of the ArrayList class. For simplicity, we are using just the signature of the add and get method.

public class ArrayList<E> {
    public boolean add(E e) {
        ...
    }
    
    public E get(int index) {
        ...
    }
}

You might wonder, what is that E with angular brackets. Think of E as a placeholder. This E represents the type of elements the ArrayList is going to hold.

So, when you write something like: List<Integer> intList = new ArrayList<Integer>(), compiler consider the ArrayList class as below –

public class ArrayList<Integer> {
    public boolean add(Integer e) {
        ...
    }
    
    public Integer get(int index) {
        ...
    }
}

So, you can see, E is replaced by the actual type that you have provided.

You may ask, are you only allowed to use the letter E only to represent a placeholder? The answer is no. You can use any letter. But the convention use – use single letters in upper case. Preferably use T (Type). In the case of ArrayList, E represents Element.

Also you can see, if we define type in the class declaration, that type is available to all the methods of that class. For example, In case of ArrayList, we have declared type E and then we have used that type in the add and get method.

Erasure

As we have already discussed, before Java 5.0, there was no concept of Generic. We used to create collections without type parameters. Also, when you get something out of that collection, you will get a reference of type Object and you need to cast that to a specific type.

Consider a code that is written before Java version 5.0

public class JavaDemo {
    public static void main(String args[]) {
        ArrayList strList = new ArrayList();
        strList.add("Saturday");
        strList.add("Sunday");
        String firstVale = (String) strList.get(0);
    }
}

From Java 5.0 onwards, you can write code with generics and provide type parameters. So, same code will look like –

public class JavaDemo {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<String>();
        strList.add("Saturday");
        strList.add("Sunday");
        String firstVale = strList.get(0);
    }
}

To make Java backward compatible so that both the codes will behave in a similar way, Java used the concept of erasure.

If you compile the above code with with type parameter, the Java compiler will erase the type parameter from strList and insert type cast before strList.get(0) so that the compiled version will look similar to what we use to have before Java 5.0

This process of erasing the parameterized type is known as type erasure.

Generic method

Now the question is, is it only allowed to declare type parameters at class level? The answer is no. You can define at method level as well. To understand this, let’s consider you want to create an utility method where if you pass a List, it will return you the first element of that list. If the list contains Integers, it will return Integer, if the list contains String, it will return String.

public class JavaDemo {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 3, 8);
        List<String> stringList = Arrays.asList("Three", "One", "Two");
        
        int intResult = getFirst(intList);
        String stringResult = getFirst(stringList);
    }
    
    
    public static <T> T getFirst(List<T> list) {
        return list.get(0);
    }
}.

You can see, we have declared the type parameter at method level using angular brackets. In this case, this type parameter T is available to this method only. This generic method can be used for any type of list.

Subtyping with generics

To understand this, we will consider Number, Integer and Double class. Both Integer and Double are child classes of Number class.

If you create a list of Number, you would be able to add both Integer and Double objects to that list.

List<Number> numList = new ArrayList<Number>();
numList.add(1);
numList.add(2.3);

You can now think, as both Integer and Double are child classes of Number class, List<Integer> or List<Double> must be considered as a subtype of List<Number>. But that is not true. If you try below, you will get a compile time error at the second line stating: Type mismatch: cannot convert from List<Integer> to List<Number>.

List<Integer> intList = Arrays.asList(1, 2);
List<Number> numList = intList;

It might seem surprising to you, but there is a valid reason for this restriction. Let’s assume, this is allowed. As the numList is of type Number, it must be allowed to add both Integer and Double objects. But internally it is referring to the intList which cannot accept a Double object. For that reason List<Integer> is not considered as a subtype of List<Number>.

Wildcard with extends

We have seen above, we cannot assign an Integer list to a Number list. But can we declare a list that will accept a list of any of its child types? The answer is yes, you can use the wild card with the extends keyword.

List<? extends Number> numList;

Here the question mark (?) is called the wildcard. You can think ? extends T as anything that extends or implements the type T. So this numList can be used to refer to both Integer list and Double list or list of any child class of Number.

So, the code below will work fine.

List<Integer> intList = Arrays.asList(1, 2);
List<Double> doubleList = Arrays.asList(1.1, 2.2);
List<? extends Number> numList;
numList = intList;
numList = doubleList;

If you try to add an Integer object or Double object to the numList, the compiler will not allow that. So, the last line of the code below will throw a compilation error.

List<Integer> intList = Arrays.asList(1, 2);
List<? extends Number> numList = intList;
numList.add(1);

The reason is, numList can refer to both Integer list and Double list. What will happen if the list refers to an Integer list and you are trying to add a Double object?

But it is guaranteed that, if you get an element from numList, that must be of type Number.

So the rule is, if we declare the type in the form "? extends E", we can only get elements and the return type will be E, but we cannot put elements. Only exception is, you can only put null. So, below code will work.

List<Integer> intList = Arrays.asList(1, 2);
List<? extends Number> numList = intList;
numList.add(null);

You may think, how this wild card is useful. We can only assign an Integer or Double list to the Number list, but as per the rule, we won’t be able to add anything to that list. This is just not useful.

But, that is not true. Let’s try to understand with an example. Let’s consider, you want to add all the elements from Integer list and Double list to Number List. We can write that using the below approach.

public class JavaDemo {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2);
        List<Double> doubleList = Arrays.asList(1.1, 2.2);
        List<Number> numList = new ArrayList<Number>();
        addAll(numList, intList);
        addAll(numList, doubleList);
    }
    
    public static void addAll(List<Number> list, List<? extends Number> input) {
        for (Number element: input) {
            list.add(element);
        }
    }
}

You see, a single method addAll() is accepting both Integer and Double lists. If we don’t use the wild card, we have to create 2 separate methods – one will accept an Integer list and another will accept a Double list.

Wildcard with super

We can use the super keyword along with wildcard to refer to a list of a type or its super type.

List<? super Integer> supList;

The above line means supList can be used to refer to an Integer list or a list of any supertype of Integer.

This list will allow you to add only Integer objects or null as supList may refer to an Integer list or Number list or any other super class list. But whatever that type is, it must be a super class of Integer.

So below code will work fine –

List<Number> numList = new ArrayList<Number>();
numList.add(1);
numList.add(2.2);
List<? super Integer> supList = numList; 
supList.add(1);
supList.add(null);

But if you try to get an element from the numList, you just don’t know what the list will return. It may be an Integer object or any superclass object. As we don’t know the exact type, when we declare the type in the form "? super E", get is not allowed by the declared type E. So, below code will throw compilation error at last line –

List<Integer> intList = Arrays.asList(1, 2, 3);
List<? super Integer> supList = intList; 
int val = supList.get(0);

But as Object is the super class of each and every class, we can always get an element and use the reference of type Object. So, below code will work fine –

List<Integer> intList = Arrays.asList(1, 2, 3);
List<? super Integer> supList = intList; 
Object val = supList.get(0);
Bounded type parameter

Instead of the wild card, we can also use a bounded type parameter to restrict the type. Let’s revisit the code of our addAll() method –

public class JavaDemo {
    public static void addAll(List<Number> list, List<? extends Number> input) {
        for (Number element: input) {
            list.add(element);
        }
    }
}

Instead of using the wildcard, we can also use bounded type parameter as shown below –

public class JavaDemo {
    public static <T extends Number> void addAll(List<Number> list, List<T> input) {
        for (Number element: input) {
            list.add(element);
        }
    }
}
Multiple bounds

If you want to restrict the type to several several bounds, that is also possible. Let’s assume, in the addAll() method, you want to accept a list of objects that should extend both Number and Comparable interface.

public class JavaDemo {
    public static <T extends Number & Comparable> void addAll(List<Number> list, List<T> input) {
        for (Number element: input) {
            list.add(element);
        }
    }
}

But remember, a wildcard can have only one bound, while a type parameter can have several bounds. So below code will not work –

public class JavaDemo {
    public static void addAll(List<Number> list, List<? extends Number & Comparable> input) {
        for (Number element: input) {
            list.add(element);
        }
    }
}
Unbounded wildcards

The unbounded wildcard type is specified using only the wildcard character (?).

List<Integer> intList = Arrays.asList(1, 2);
List<?> numList = intList;

Consider List<?> as List<? extends Object>.

Points to remember for wildcards

During instance creation, top-level parameters cannot contain wildcards.

List<?> list = new ArrayList<?>();
List<?> list = new ArrayList<Integer>();

However, nested wildcards are permitted.

List<List<?>> lists = new ArrayList<List<?>>();

During class declaration also, top-level parameters cannot contain wildcards.

public class JavaDemo implements List<?> { }

However, nested wildcards are permitted in this case as well.

public class JavaDemo extends ArrayList<List<?>> { }

Difference between List<Object> and raw type List in Java?

To understand this, let’s create two list as shown below –

List listA = new ArrayList();
List<Object> listB = new ArrayList<Object>();

Here listA is a raw list as type parameter is not supplied. So, the compiler will show a warning indicating this list is of raw type and should be parameterized. The second list listB won’t show any such warning.

We can add any type of objects to both the lists.

listA.add(1);
listA.add("String");
		
listB.add(1);
listB.add("String");

But, if we try to assign a list of any type other than Object, listA will accept that, but listB won’t allow that.

listA = new ArrayList<String>();
listB = new ArrayList<String>(); // Compilation error
Difference between List<?> and List<Object> in Java?

List<?> is List of unknown type, while List<Object> is essentially List of type Object.  So, if you try to assign a list of Objects to List<?> and List<Object>, both will work fine.

List<Object> objList = new ArrayList<Object>();
List<?> listA = objList;
List<Object> listB = objList;

But, if you try to assign a list of a type other than Object, List<?> will accept that, but not  List<Object>.

That’s it for now. Hope the concept of generics is clear now. If you have any doubt, please ask in the comment section. I will try to answer that as soon as possible. Till then, bye bye.