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.

Why Generics, what were the issues?

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

Type Erasure

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

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

Generic class

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<>();

Generic Interface

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.

Generic methods

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

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 in Array :

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

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

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.

Bounded

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

Limitations of Generics

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