Java Generics
Generic means something that is not specific or fixed. In Java, Generic classes and methods are created to work with different data types and objects.
We are simply creating a parameter for the type that a class or method can accept. Generics provide an additional layer of abstraction and helps us to avoid runtime errors. They also provide type-safety. Let's learn more about Generics in Java.
Java Generic Classes
Suppose, we need a class that can work with any object type. We can use the diamond operator(<>) to create a Generic class in Java.
Consider the example shown below. The letter T in between the diamond operator is called the type parameter. The class objects can be created by using String type, Integer type, and even Double.
//Generic class with parameter type T
class GenericClass<T>
{
T field1;//Object of type T
GenericClass(T f1)
{
this.field1 = f1;
}
}
public class GenericsDemo
{
public static void main(String[] args)
{
GenericClass<String> g1 = new GenericClass("100");
GenericClass<Integer> g2 = new GenericClass(100);
GenericClass<Double> g3 = new GenericClass(100.0);
}
}
In the above example, we just had a single type parameter. We can also use multiple type parameters to create Generic classes. An example of this is shown below.
//Generic Class with multiple type parameters
class GenericClass<E, T>
{
E field1;//Object of type E
T field2;//Object of type T
GenericClass(E f1, T f2)
{
this.field1 = f1;
this.field2 = f2;
}
}
public class GenericsDemo
{
public static void main(String[] args)
{
GenericClass<String, Integer> g1 = new GenericClass("100", 100);
GenericClass<Integer, String> g2 = new GenericClass(100, "100");
GenericClass<Double, String> g3 = new GenericClass(100.0, "100");
}
}
Java Generic Methods
Just like classes, methods in Java can also take a type parameter. This will enable us to use the same method for different object types.
Consider the code below, which uses a Generic print() method to print an object. <T> in the method definition, tells Java that this is a Generic method with parameter type T. We can use this type T anywhere in the method implementation. The diamond operator must be used even if the method returns void.
public class GenericsDemo
{
public static <T> void print(T object)
{
System.out.println(object);
}
public static void main(String[] args)
{
String s = "100";
Integer i = 100;
Double d = 100.0;
print(s);
print(i);
print(d);
}
}
100
100
100.0
Just like Generic classes, generic methods can also take multiple type parameters. For example, let's create a method that takes two Lists of any type as parameters, and it should print the two lists. We can define two parameter types(T and E) within the diamond operator.
import java.util.ArrayList;
import java.util.LinkedList;
public class GenericsDemo
{
public static <T, E> void printLists(ArrayList<T> l1, LinkedList<E> l2)
{
System.out.println(l1);
System.out.println(l2);
}
public static void main(String[] args)
{
ArrayList<Integer> al = new ArrayList<>();
al.add(5);
al.add(10);
al.add(15);
LinkedList<String> ll = new LinkedList<>();
ll.add("five");
ll.add("ten");
ll.add("fifteen");
printLists(al, ll);
}
}
[5, 10, 15]
[five, ten, fifteen]
Bounded Generic Methods
Generic classes and methods provide a lot of freedom to the user, but this can sometimes work against us. We can use the extends keyword to limit the range of types accepted by Generics.
<T extends ClassName>
This will only allow the parent class and all its subclasses inside the Generic class or method.
For example, let's create three classes(ClassA, ClassB, and ClassC), and extend ClassB from ClassA. Then, the boundedPrint() method shown below will work with objects of ClassA and ClassB.
class ClassA
{
public void printClassA()
{
System.out.println("Class A");
}
}
class ClassB extends ClassA
{
public void printClassB()
{
System.out.println("Class B");
}
}
class ClassC
{
public void printClassC()
{
System.out.println("Class C");
}
}
public class GenericsDemo
{
//Bounded Generic Method
public static <T extends ClassA> void boundedPrint(T object)
{
System.out.println("Object of ClassA or a subclass of ClassA");
}
public static void main(String[] args)
{
ClassA a = new ClassA();
ClassB b = new ClassB();
boundedPrint(a);
boundedPrint(b);
}
}
Object of ClassA or a subclass of ClassA
Object of ClassA or a subclass of ClassA
However, we get a compilation error if we use this method with an object of ClassC.
public static void main(String[] args)
{
ClassC c = new ClassC();
boundedPrint(c);//Compilation error
}
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method boundedPrint(T) in the type GenericsDemo is not applicable for the arguments (ClassC)
at GenericsDemo.main(GenericsDemo.java:35)
We can also use bounded Generics with classes that implement an interface. Instead of the class name, we will use the interface name with the extends keyword.
<T extends Interface>
We can also use a class and interface together to impose a stricter restriction.
<T extends Class & Interface>
Type Safety in Generics
A major advantage of using Generics is that it provides type safety. If we have defined a type for a Generic, then the compiler knows what object to expect and returns an error at the compile time itself instead of giving a runtime error.
For example, if we create an ArrayList to store Integers and do not specify a type for its element, then the code below will compile successfully. But we will get an error at runtime.
import java.util.ArrayList;
public class GenericsDemo
{
public static void main(String[] args)
{
ArrayList l = new ArrayList();
l.add(100);//Integer
l.add(101);//Integer
l.add("102");//String
Integer i1 = (Integer)l.get(0);
Integer i2 = (Integer)l.get(1);
Integer i3 = (Integer)l.get(2);
}
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at GenericsDemo.main(GenericsDemo.java:48)
Instead, if we use the diamond operator and define the type as Integer that we will get an error at compile time itself. This reduces the overhead at runtime. Also, we don't require explicit casts, as the compiler knows which type will be returned and applies implicit casting.
import java.util.ArrayList;
public class GenericsDemo
{
public static void main(String[] args)
{
ArrayList<Integer> l = new ArrayList<Integer>();
l.add(100);//Integer
l.add(101);//Integer
l.add("102");//String
Integer i1 = l.get(0);
Integer i2 = l.get(1);
Integer i3 = l.get(2);
}
}
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method add(Integer) in the type ArrayList<Integer> is not applicable for the arguments (String)
at GenericsDemo.main(GenericsDemo.java:44)
Type Erasure in Generics
Generics use a technique called type erasure at compile time. Type erasure replaces the parameter type T of Generics with Object class(or parent class in case of bounded Generics) and enforces type constraints at compile time itself. This makes sure that no additional work needs to be done at runtime and reduces runtime overhead. It ensures that no new classes are created for parameterized types.
For example, consider the following Generic method.
public static <T> void print(T obj)
{
//Implementation
}
The type parameter T will be replaced by Object type at compile-time, and the code will look like the following.
public static void print(Object obj)
{
//Implementation
}
If the type parameter is bounded, then it is replaced with the parent class at compile time.
public static <T extends ClassA> void boundedPrint(T obj)
{
//Implementation
}
public static void boundedPrint(ClassA obj)
{
//Implementation
}
Due to type erasure, Generics cannot work with primitive types. This is because primitive types do not extend the Object class, or any other class for that matter, and so during the process of type erasure, the compiler cannot replace them. But we can always use wrapper classes(like Integer and Double) in place of primitives.
Summary
In this tutorial, we learned the basics of Generics in Java. It solves a lot of problems for programmers and makes our code reusable. A single Generic method can be used for multiple types of objects. Generics also provide type safety and no explicit casting is required. Generics help us to write common algorithms for different types of objects.