Java Generics in a Nutshell.

Navoda Nilakshi
8 min readDec 15, 2020

Hey everyone! This article will give you an in-depth insight into generics in Java and its advantages and usage. Generics is a vast topic but, we will mostly focus on its application to Java Collections in this article.

Life before Generics.

Java generic is an essential concept to provide type safety in collections and to solve type casting issues. Suppose you want to store a bunch of names that are in the form of String. In Java, you can use Arrays or Collections for this purpose. Arrays are type-safe which means there’s a guarantee that a String array will contain only String values or an int array will contain only int values, and so on. If you try to put int values to a String array, you’re bound to face a compile-time error. So, you can be rest assured that your program is working fine if you don’t get any compile-time error.

Consider the following example:

From the example above, you can get a clear understanding of how Arrays in Java behave in a type-safe manner. If you try to add an int value to the “names” array it will give you a compilation error. Also, there’s no need to perform typecasting when retrieving elements from the array.

So, it’s clear that we can trust arrays with our type related requirements, thus making arrays type-safe. Arrays are fine, but what about collections, you might ask. Before Java 5, this was not the case with collections. Collections were not type-safe and we couldn’t give any guarantee for the type of elements present inside the collection before Java 5. If we take our previous example of having to hold only string type objects, and if we chose ArrayList for that purpose like the snippet below, we can’t guarantee all the elements in the ArrayList to be String elements. That’s because if we accidentally add an Integer value or Double value into the ArrayList, we won’t get any compile-time error. This situation is quite dangerous because if there’s an error in the program it’s better to have a compile-time error so that we can resolve that beforehand rather than our program failing at runtime.

As it’s demonstrated in the above example, if we accidentally add an integer to the ArrayList, the compiler won’t complain. But as it’s shown in the following example, the compiler will complain if we don’t perform typecasting.

If we use this approach, it’s compulsory to provide typecasting every time we read an ArrayList element and assign it to a variable. If we fail to perform typecasting, then the Object type element of ArrayList is something that cannot be assigned to the String type variable, and hence incompatible type compile error will be generated.

Having said that, now there’s another issue we have to deal with in this case. Element in the index position of 2 in the ArrayList is an Integer as you can see but it’s getting type casted and assigned to a String variable without getting any complaints from the compiler. Only in the runtime, we’re bound to get a ClassCastException thus making our program fail in runtime.

Remember this is not the case with arrays. If we try to retrieve elements of a string array and assign them to a string variable we’re not required to provide typecasting because array elements are guaranteed to share the same type.

So, What’s the Generic Concept in Java?

Generics is the concept that came into the scene with the Java 5 version to cure this apparent headache of typecasting and lack of type- safety in collections. (Generics concept is far wider but we’re focusing on its usage on collections in this article)

If we consider ArrayList as an example,

List list = new ArrayList();

Is the non-generic version of the declaration of ArrayList which doesn’t have type safety and in this case type casting is mandatory. Given below is the generic form of the declaration.

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

As you can see we have added the type String within angle brackets to ensure every element inside our ArrayList is of String type. We can do the same thing with Integer, Double, and so on.

Now that you have a basic idea of how generics work in terms of collections, let’s have a closer look at how the internal processing is carried out.

In Java 1.4 and below, the ArrayList class did not have a type parameter when declaring the class ArrayList. So, add(), get() etc.. accepted any argument of Object type. Therefore, ArrayList was able to contain any type of element which belonged to the Object class. This made ArrayList vulnerable in terms of type safety. Also, as the get method had a return type of Object, it could return any value of Object type. So it made typecasting when retrieving mandatory.

Generic Classes.

Generics are parameterized classes where we can provide type parameter inside angle brackets when declaring the class.

In the runtime, T and its every application will be replaced with the type we provide. Above is an example of a generic class.

Bounded Types.

The last example had an unbounded parameter type. It means T can be of any Object type. It can be String, it can be Double, it can be Integer likewise. But there can be situations where we want to restrict the type parameter only to be assigned to what we need according to our requirements. In such cases, we can use bounded type parameters.

According to the above example, T can be only Number type or its subclasses like Integer, Float, etc A general notation can be summarised as follows.

X can be a class or an interface. If X is a class then as the type parameter we can use X class or any of its subclasses. If X is an interface we can use the X interface or any of its implementing classes as the type parameter. Below is an example in which X is an interface.

Note the fact that we can’t use the implements keyword as we would normally do. When declaring generic classes we can use interfaces but instead of implements, we have to use the extends keyword. Also, we can make the T parameter extend a class and an interface, or multiple interfaces. But we can’t extend the type parameter with two classes because multiple inheritance is not allowed in Java classes. Consider the following examples.

Multiple inheritance is not a problem when interfaces are concerned. Thus, the above example is valid. And also we can add more interfaces if we want according to the requirement.

In this example, T can be either a subclass of Number/or the Number class itself or an implementing class of Comparable interface/or Comparable interface. But the bottom line is that we cannot use the Comparable interface first and then the Number class. We have to place the class first when declaring and then only we can place the interface. This is the same as extending a parent class and implementing an interface. In that situation too we have to provide the parent class first.

Placing a class first and then multiple interfaces are also valid when declaring a generic class in java.

Generic Method and WildCard Characters (?).

Suppose you have a method like this,

To call this method, we have to pass an ArrayList of type String. There’s no other way of invoking this method. If we want to pass an Integer ArrayList we have to overload this method. If we want to pass a Double ArrayList to this m1() method, we have to overload it again. This can be pretty messy and here’s when the WildCard concept comes to play.

  1. Unbounded WildCards.

If we use the WildCard character concept like in the above example, we can pass ArrayList of unknown type(?) to invoke m1() method. We can pass String ArrayList, Integer ArrayList, or any other kind of ArrayList to this method and still call it.

But there are also some restrictions that are unavoidable with this privilege. That is we cannot add any elements to ArrayList except null within the method, because we don’t know the type of it exactly. We can add null because it’s acceptable for any Object type.

So this kind of approach is best suitable for read-only methods.

2. Upper-Bounded WildCards.

We can replace Number with any class or interface. In this case, we can call this method by passing an ArrayList of Number or any of its child classes. If we replace Number with an interface, then we can call this method by passing an ArrayList of that interface type or by passing an ArrayList of any of its implementation classes.

Like in the unbound wildcards, within the method we cannot add anything to the ArrayList except null because we don’t know the type of ArrayList which will be passed on.

3. Lower-Bounded WildCards.

Instead of String, we can use a class or an interface. If we use a class like String class, then we can call this method by passing an ArrayList of either String or an ArrayList of its superclasses.

If we use an interface, instead of the String class in the above example, we can call this method by passing an ArrayList of either that interface type or superclass of the implementation class of the interface we provide.

For example, If the interface we’re going to use here is Runnable, then it’s child class is Thread. Superclass of the Thread class is the Object class. So, we can use an ArrayList of the Object class to invoke this method or we can use the interface type of an ArrayList. To read more about class Thread and how it’s derived, click here

Thanks for reading this article. I hope you gained a lot out of it and now feel more comfortable with Java Generics. Cheers!

--

--