Java Generics Tutorial

Generics in java were introduced as one of the features in JDK 5. “Java Generics” is a technical term denoting a set of language features related to the definition and use of generic types and methods. In Java, generic types or methods differ from regular types and methods in that they have type parameters.

“Java Generics are a language feature that allows for definition and use of generic types and methods.”

Generic types are instantiated to form parameterized types by providing actual type arguments that replace the formal type parameters.

public class LinkedList<E> ...

LinkedList<String> list = new LinkedList();
  • A class like LinkedList<E> is a generic type that has a type parameter E.
  • Instantiations such as LinkedList<Integer> or a LinkedList<String> are called parameterized types.
  • String and Integer are the respective actual type arguments.

1. Introduction to Generics

If you closely look at collection framework classes then you will observe that most classes take parameter/argument of type Object and return values from methods as Object. Now, in this form, they can take any Java type as an argument and return the same. They are essentially heterogeneous i.e. not of a particular similar type.

Programmers like us often wanted to specify that a collection contains elements only of a certain type e.g. Integer or String or Employee. In the original collection framework, having homogeneous collections was impossible without adding extra checks before adding some checks in the code. Generics were introduced to remove this limitation to be very specific. They add this type of checking of parameters in your code at compile-time, automatically. This saves us from writing a lot of unnecessary code which actually does not add any value in run-time if written correctly.

“In layman’s term, generics force type safety in java language.”

Without this type of safety, your code could have been infected by various bugs that get revealed only in runtime. Using generics, makes them highlighted in compile time itself and makes our code robust even before you get the bytecode of your Java source code files.

“Generics add stability to your code by making more of your bugs detectable at compile time.”

So now we have a fair idea of why generics are present in Java in the first place. The next step is to get some knowledge about how they work in Java. What actually happens when you use generics in your source code?

2. How do generics work?

2.1. Type Safety

At the heart of generics is “type safety“. What exactly is type safety? It is just a guarantee by the compiler that if the correct Types are used in the correct places then there should not be any ClassCastException in runtime.

A usecase can be a list of Integer i.e. List<Integer>. If you declare a list like List<Integer>, then Java guarantees that it will detect and report any attempt to insert any non-integer type into the above list.

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

list.add(1);
list.add("one");  //compiler error

2.2. Type Erasure

Another important term in generics is “type erasure“. It essentially means that all the extra information added using generics into sourcecode will be removed from bytecode generated from it. Inside bytecode, it will be old java syntax which you will get if you don’t use generics at all. This necessarily helps in generating and executing code written prior to Java 5 when generics were not added to the language.

Let’s understand with an example.

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

list.add(1000);

If you compare the bytecode of the above example with/without generics, then there will be no difference. Clearly, the compiler removed all generics information. So, the above code is very much similar to the below code without generics.

List list = new ArrayList();

list.add(1000);

“Precisely, Generics in Java is nothing but a syntactic sugar to your code for Type Safety and all such type information is erased by Type Erasure feature by the compiler.”

3. Types of Generics

Now we have some understanding of what generics are all about. Now start exploring other important concepts revolving around generics. I will start by identifying the various ways, generics can be applied to source code.

3.1. Class or Interface

A class is generic if it declares one or more type variables. These type variables are known as the type parameters of the class. Let’s understand with an example.

DemoClass is a simple class, which has one property t (can be more than one also); and type of property is Object.

class DemoClass {

   private Object t;

   public void set(Object t) { this.t = t; }

   public Object get() { return t; }
}

Here we want once initialized the class with a certain type, the class should be used with that particular type only. e.g. If we want one instance of the class to hold value t of type ‘String‘, then programmer should set and get the only String type.

Since we have declared property type to Object, there is no way to enforce this restriction. A programmer can set any object, and can expect any return value type from get() method since all Java types are subtypes of Object class.

To enforce this type of restriction, we can use generics as below:

class DemoClass<T> {

   //T stands for "Type"
   private T t;
 
   public void set(T t) { this.t = t; }
    
   public T get() { return t; }
}

Now we can be assured that class will not be misused with the wrong types. Sample usage of DemoClass will look like this:

DemoClass<String> instance = new DemoClass<>();
instance.set("lokesh");   //Correct usage
instance.set(1);        //This will raise compile time error

The above analogy is true for the interfaces as well. Let’s quickly look at an example to understand, how generics type information can be used in interfaces.

//Generic interface definition
interface DemoInterface<T1, T2> 
{
   T2 doSomeOperation(T1 t);
   T1 doReverseOperation(T2 t);
}
 
//A class implementing generic interface
class DemoClass implements DemoInterface<String, Integer>
{
   public Integer doSomeOperation(String t)
   {
      //some code
   }
   public String doReverseOperation(Integer t)
   {
      //some code
   }
}

I hope I was clear enough to put some light on generic classes and interfaces. Now it’s time to look at generic methods and constructors.

3.2. Method or Constructor

Generic methods are much similar to generic classes. They are different only in one aspect, the scope of type information is only inside the method (or constructor). Generic methods are methods that introduce their own type parameters.

Let’s understand this with an example. Below is a code sample of a generic method that can be used to find all occurrences of a type parameter in a list of variables of that type only.

public static <T> int countAllOccurrences(T[] list, T item) {
   int count = 0;
   if (item == null) {
      for ( T listItem : list )
         if (listItem == null)
            count++;
   }
   else {
      for ( T listItem : list )
         if (item.equals(listItem))
            count++;
   }
   return count;
}  

If you pass a list of String and another string to search in this method, it will work fine. But if you will try to find an Number into list of String, it will give compile-time error.

The same as above can be an example of a generic constructor. Let’s take a separate example for a generic constructor as well.

public static <T> int countAllOccurrences(T[] list, T item) {

   int count = 0;

   if (item == null) {
      for ( T listItem : list )
         if (listItem == null)
            count++;
   }
   else {
      for ( T listItem : list )
         if (item.equals(listItem))
            count++;
   }
   return count;
}  

In this example, Dimension class’s constructor has the type information also. So you can have an instance of dimension with all attributes of a single type only.

4. Generic Arrays

Arrays in any language have the same meaning i.e. an array is a collection of similar types of elements. In Java, pushing any incompatible type in an array on runtime will throw ArrayStoreException. It means array preserve their type information in runtime, and generics use type erasure or remove any type of information in runtime. Due to the above conflict, instantiating a generic array is not permitted.

public class GenericArray<T> {

    // this one is fine
    public T[] notYetInstantiatedArray;
  
    // causes compiler error; Cannot create a generic array of T
    public T[] array = new T[5];
}

In the same line as the above generic type classes and methods, we can have generic arrays. As we know that an array is a collection of similar types of elements and pushing any incompatible type will throw ArrayStoreException in runtime; which is not the case with Collection classes.

Object[] array = new String[10];
array[0] = "lokesh";
array[1] = 10;      //This will throw ArrayStoreException

The above mistake is not very hard to make. It can happen anytime. So it’s better to provide the type information to the array also so that the error is caught at compile time itself.

Another reason why arrays do not support generics is that arrays are covariant, which means that an array of supertype references is a supertype of an array of subtype references. That is, Object[] is a supertype of String[] and a string array can be accessed through a reference variable of type Object[].

Object[] objArr = new String[10];  // fine
objArr[0] = new String();

5. Generics with Wildcards

In generic code, the question mark (?), called the wildcard, represents an unknown type. A wildcard parameterized type is an instantiation of a generic type where at least one type argument is a wildcard. Examples of wildcard parameterized types are Collection<?<, List<? extends Number<, Comparator<? super String> and Pair<String,?>. The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.

Having wild cards at different places have different meanings as well. e.g.

  • Collection denotes all instantiations of the Collection interface regardless of the type argument.
  • List denotes all list types where the element type is a subtype of Number.
  • Comparator<? super String< denotes all instantiations of the Comparator interface for type argument types that are supertypes of String.

A wildcard parameterized type is not a concrete type that could appear in a new expression. It just hints at the rule enforced by generics that which types are valid in any particular scenario where wild cards have been used.

For example, below are valid declarations involving wild cards:

Collection<?> coll = new ArrayList<String>(); 
//OR
List<? extends Number> list = new ArrayList<Long>(); 
//OR
Pair<String,?> pair = new Pair<String,Integer>();

And below are invalid uses of wildcards, and they will give a compile-time error.

List<? extends Number> list = new ArrayList<String>();  //String is not subclass of Number; so error
//OR
Comparator<? super String> cmp = new RuleBasedCollator(new Integer(100)); //Integer is not superclass of String

Wildcards in generics can be unbounded as well as bounded. Let’s identify the difference in various terms.

5.1. Unbounded wildcard parameterized type

A generic type where all type arguments are the unbounded wildcard "?” without any restriction on type variables. e.g.

ArrayList<?>  list = new ArrayList<Long>();  
//or
ArrayList<?>  list = new ArrayList<String>();  
//or
ArrayList<?>  list = new ArrayList<Employee>();  

5.2. Bounded wildcard parameterized type

Bounded wildcards put some restrictions over possible types, we can use to instantiate a parametrized type. This restriction is enforced using keywords “super” and “extends”. To differentiate more clearly, let’s divide them into upper-bounded wildcards and lower-bounded wildcards.

5.3. Upper bounded wildcards

For example, say you want to write a method that works on List<String>, List<Integer>, and List<double> you can achieve this by using an upper bounded wildcard e.g. you would specify List<? extends Number>. Here Integer and Double are subtypes of Number class. In layman’s terms, if you want the generic expression to accept all subclasses of a particular type, you will use upper-bound wildcard using “extends” keyword.

public class GenericsExample<T>
{
   public static void main(String[] args)
   {
      //List of Integers
      List<Integer> ints = Arrays.asList(1,2,3,4,5);
      System.out.println(sum(ints));
       
      //List of Doubles
      List<Double> doubles = Arrays.asList(1.5d,2d,3d);
      System.out.println(sum(doubles));
       
      List<String> strings = Arrays.asList("1","2");
      //This will give compilation error as :: The method sum(List<? extends Number>) in the 
      //type GenericsExample<T> is not applicable for the arguments (List<String>)
      System.out.println(sum(strings));
       
   }
    
   //Method will accept 
   private static Number sum (List<? extends Number> numbers){
      double s = 0.0;
      for (Number n : numbers)
         s += n.doubleValue();
      return s;
   }
}

5.4. Lower bounded wildcards

If you want a generic expression to accept all types which are “super” type of a particular type OR parent class of a particular class then you will use a lower bound wildcard for this purpose, using ‘super’ keyword.

In the below given example, I have created three classes i.e. SuperClass, ChildClass and GrandChildClass. There relationship is shown in code below. Now, we have to create a method which somehow get a GrandChildClass information (e.g. from DB) and create an instance of it. And we want to store this new GrandChildClass in an already existing list of GrandChildClasses.

Here problem is that GrandChildClass is subtype of ChildClass and SuperClass as well. So any generic list of SuperClasses and ChildClasses is capable of holding GrandChildClasses as well. Here we must take the help of a lower-bound wildcard using ‘super‘ keyword.

public class GenericsExample<T>
{
   public static void main(String[] args)
   {
      //List of grand children
      List<GrandChildClass> grandChildren = new ArrayList<GrandChildClass>();
      grandChildren.add(new GrandChildClass());
      addGrandChildren(grandChildren);
       
      //List of grand childs
      List<ChildClass> childs = new ArrayList<ChildClass>();
      childs.add(new GrandChildClass());
      addGrandChildren(childs);
       
      //List of grand supers
      List<SuperClass> supers = new ArrayList<SuperClass>();
      supers.add(new GrandChildClass());
      addGrandChildren(supers);
   }
    
   public static void addGrandChildren(List<? super GrandChildClass> grandChildren) 
   {
      grandChildren.add(new GrandChildClass());
      System.out.println(grandChildren);
   }
}
 
class SuperClass{
    
}
class ChildClass extends SuperClass{
    
}
class GrandChildClass extends ChildClass{
    
}

6. What is not allowed in Generics?

So far we have learned about a number of things that you can do with generics to avoid many ClassCastException instances in your application. We also saw the usage of wildcards as well. Now it’s time to identify some tasks which are not allowed to do in generics.

6.1. We can’t have static field of type

We can not define a static generic parameterized member in your class. Any attempt to do so will generate compile-time error: Cannot make a static reference to the non-static type T.

public class GenericsExample<T>
{
   private static T member; //This is not allowed
}

6.2. We can not create an instance of T

Any attempt to create an instance of T will fail with error: Cannot instantiate the type T.

public class GenericsExample<T>
{
   public GenericsExample(){
      new T();
   }
}

6.3. Generics are not compatible with primitives in declarations

Yes, it’s true. You can’t declare generic expressions like List or Map<String, double>. Definitely, you can use the wrapper classes in place of primitives and then use primitives when passing the actual values. These value primitives are accepted by using auto-boxing to convert primitives to respective wrapper classes.

final List<int> ids = new ArrayList<>();    //Not allowed
 
final List<Integer> ids = new ArrayList<>(); //Allowed

6.4. We can’t create a Generic exception class

Sometimes, the programmer might be in need of passing an instance of generic type along with an exception being thrown. This is not possible to do in Java.

// causes compiler error
public class GenericException<T> extends Exception {}

When you try to create such an exception, you will end up with a message like this: The generic class GenericException may not subclass java.lang.Throwable.

That’s all for now closing the discussion on Java generics this time. I will come up with more interesting facts and features related to generics in the coming posts.

Drop me a comment if something is unclear /OR if you have any other questions.

Happy Learning !!

Comments

Subscribe
Notify of
guest
29 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode