Java Generics in layman language

Generics is one of the most challenging concepts to put your head around when you first time working with it, But it's one of the most used concept as well.

So let's understand what exactly is generics. As per one of the definitions, “Java Generics is a language feature that allows for definition and use of generic types and methods.”

If you did not get it don’t worry, we will discuss it will the help of examples.

So lets first discuss why we need Generics.

1. Type safety

So before Generics, this is how we use to create a list

List integers = new ArrayList();
integers.add(1);
integers.add(2);

Here we are adding integers in the list but we can add any other value without getting any compile-time error

integers.add("three");

So when we extract this data we are not 100% sure that we will get back an integer.

for(int i=0;i<list.size();i++) {
if(list.get(i) == (Integer)list.get(i)) {
System.out.println(2*(Integer)list.get(i));
} else {
System.out.println(1);
throw new IllegalArgumentException("Value is not integer");
}
}

2. Heterogeneous values

As we mentioned in 1st point also. we can add heterogeneous values. So when we pass our collection to 3rd party library, our collection is not type-safe. In that library, anyone can add any kind of data.

public static void main(String[] args) {
Set data = new HashSet();
Set updatedData = getData(data);
}
public static Set getData(Set data) {
data.add(1);
data.add("Two");
data.add(new ArrayList<>());
return data;
}

That's where Generics comes into picture. Generics add compile-time checks which solves both the issues. Compile-time checks help to prevent adding heterogeneous data in a collection and give confidence to the end-user that this list contains only 1 type of data which is mentioned in signature.

List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
integers.add("three"); // compile time exception

Generics have a special property called Type erasure which means all the extra information added using generics will be removed at compile time during byte code generation. It is also required for backward compatibility.

So after compilation, the byte code of these 2 statements will be the same

List<Integer> list1 = new ArrayList<>();List list2 = new ArrayList<>();

A class is Generic if it declares 1 or more type parameters. The type parameter itself is not a data type but can act as a place holder for any other datatype.

public class GenericClass<T,E> {
private T key;
private E value;
}

In this, we have 2 type parameters, T and E.

Now we can use this class with any data type like :

GenericClass<Integer,Integer> integers = new GenericClass<>();
GenericClass<String,Integer> strings = new GenericClass<>();

Same rules apply for the interface as well

public interface GenericInterface<T,E> {
T firstMethod();
E secondMethod();
}

The above code represents that it's a general class having 2 type parameters. The first type parameter is the return type of the first method and the second type parameter is the return type of the second method. So we can implement it as

public class SampleClass implements GenericInterface<Integer,String> {
@Override
public Integer firstMethod() {
return null;
}
@Override
public String secondMethod() {
return null;
}
}

Here we used Integer and String, but we can use any other data type as well.

In previous examples, we saw classes that are completely generic in nature, But we can have a specific generic method as well in non-generic class. We can define generic methods inside a non-generic class and the scope of the type variable is inside the method only.

public <T,E> void genericMethod(T key,E value) {
System.out.println(key);
System.out.println(value);
}

If you notice we have an extra piece of code in this method <T, E>. This is the same indicator which we use in any class definition to indicate how many type parameter this method or class will use. We have to add this indicator in generic method only when its a part of a non-generic class.

Both static and not static methods follow the same general rules.

public static <T> Map<T,T> staticGenericMethod(T val1,T val2) {
Map<T,T> map = new HashMap<>();
map.put(val1,val2);
return map;
}
public <T> Map<T,T> staticGenericMethod(T val1,T val2) {
Map<T,T> map = new HashMap<>();
map.put(val1,val2);
return map;
}

Generic constructor follows the same rule as other methods. They can come inside a generic class or can be in any other class also.

public class ClassWithGenericConstructor<T> {
private T key;
private T value;
public ClassWithGenericConstructor(T key,T value) {
this.key = key;
this.value = value;
}
}

In the above example, we have a generic constructor in a generic class.

public class ClassWithGenericConstructor {
public <T> ClassWithGenericConstructor(T key,T value) {
System.out.println(key);
}
}

In this example, we have a generic constructor in a non-generic class. As we discussed above, we have <T> in method declaration because we are in a non-generic class.

Generics and the way array works contradict each other. Array preserves their type-information means it will throw an error if we add different types of data into it and Generics use type erasure, which is contradictory So we cannot instantiate a generic array in Java.

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];
}

WildCards defines unknown data types in Generics, Using it with super and extends is used to restrict the types used in Generic class.

declarations :

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

WildCards are of 2 types bounded and unbounded

Unbounded in which we can add any data type.

Collection<?> coll = new ArrayList<String>();

Here on the right side we used String, but we can add any other data type as well. There are no restrictions.

In bounded we restrict the data types using extends and super.

extends

In extends, we can use a class which extends the given class like

List<? extends Number> list = new ArrayList<Long>();

here we can use any class which extends Number like Long, Integer, Double, etc

super

In super we can use classes which is a superclass of given class

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

here we can use any class which is a superclass of Integer

  1. Static fields of parameterized type are not allowed
private static T member; //This is not allowed
  1. We cannot create an instance of type parameter directly
new  T();   // not allowed
  1. Not compatible with primitive types
List<int> ids = new ArrayList<>();    //Not allowed
  1. Generic Exception class is not allowed
public class GenericException<T> extends Exception {}

I hope now you have a good understanding of generics.

For more information about Generics, refer to the official documentation

Full stack developer