Generics in Java allow classes, interfaces, and methods to work with different data types while providing compile-time type safety. They use type parameters such as T, E, K, and V to make code reusable without requiring unnecessary type casting.
- Reduce the need for explicit type casting.
- Allow classes, interfaces, and methods to work with different reference types.
- Help detect incompatible types during compilation.
Why Use Generics?
- Before generics, collections such as ArrayList stored elements as Object, so retrieving an element often required explicit type casting.
- This could lead to ClassCastException at runtime if an object was cast to an incompatible type.
- Generics allow you to specify the type of elements a collection can store, such as ArrayList<String>.
- The compiler checks type safety at compile time, reducing the need for explicit casting and helping detect type-related errors earlier.
- Generics also make code easier to read by clearly indicating the type of data a collection is intended to contain.
Types of Java Generics
1. Generic Class
A generic class is a class that can operate on objects of different types using a type parameter. Like C++, we use <> to specify parameter types in generic class creation. To create objects of a generic class, we use the following syntax:
// To create an instance of generic class
BaseType <Type> obj = new BaseType <Type>()
// We use < > to specify Parameter type
class Test<T> {
T obj;
Test(T obj) {
this.obj = obj;
}
public T getObject() { return this.obj; }
}
class Geeks {
public static void main(String[] args)
{
// instance of Integer type
Test<Integer> iObj = new Test<Integer>(15);
System.out.println(iObj.getObject());
// instance of String type
Test<String> sObj
= new Test<String>("GeeksForGeeks");
System.out.println(sObj.getObject());
}
}
Output
15 GeeksForGeeks
Note: In Parameter type, we can not use primitives like "int", "char" or "double". Use wrapper classes like Integer, Character, etc.
How Type Parameter T Behaves Like a Normal Type
In a generic class, the type parameter T behaves like a normal data type within the class. Once a specific type is provided while creating an object, the compiler replaces T with that type.
This means T can be used just like a regular type for:
- Declaring variables
- Method parameters
- Method return types
class Box<T> {
T value; // T used as a variable type
Box(T value) { // T used as constructor parameter
this.value = value;
}
public T getValue() { // T used as return type
return value;
}
}
We can also pass multiple Type parameters in Generic classes.Â
class Test<T, U>
{
T obj1; // An object of type T
U obj2; // An object of type U
Test(T obj1, U obj2)
{
this.obj1 = obj1;
this.obj2 = obj2;
}
public void print()
{
System.out.println(obj1);
System.out.println(obj2);
}
}
class Geeks
{
public static void main (String[] args)
{
Test <String, Integer> obj =
new Test<String, Integer>("GfG", 15);
obj.print();
}
}
Output
GfG 15
2. Generic Method
A generic method declares its own type parameter. The type parameter is written before the return type.
class Geeks {
// A Generic method example
static <T> void genericDisplay(T element)
{
System.out.println(element.getClass().getName()
+ " = " + element);
}
public static void main(String[] args)
{
// Calling generic method with Integer argument
genericDisplay(11);
// Calling generic method with String argument
genericDisplay("GeeksForGeeks");
// Calling generic method with double argument
genericDisplay(1.0);
}
}
Output
java.lang.Integer = 11 java.lang.String = GeeksForGeeks java.lang.Double = 1.0
Limitations of Generics
1. Generics Work Only with Reference Types
When we declare an instance of a generic type, the type argument passed to the type parameter must be a reference type. We cannot use primitive data types like int, char.
Test<int> obj = new Test<int>(20);
The above line results in a compile-time error that can be resolved using type wrappers to encapsulate a primitive type. But primitive type arrays can be passed to the type parameter because arrays are reference types.
Diamond Operator (<>) in Java
- From Java 7 onwards, Java introduced the diamond operator (<>) to reduce redundancy when creating objects of generic classes.
- Instead of writing the type parameter again on the right-hand side, the compiler can automatically infer the type from the left-hand side.
// Using diamond operator (<>), Java infers the type automatically
ArrayList<int[]> a = new ArrayList<>();
2. Generic Types Differ Based on their Type Arguments
Generic types differ based on their type arguments, but this difference exists only at compile time. During compilation, Java removes generic type information through a process called type erasure, replacing type parameters with their bounds or Object. As a result, generics ensure type safety at compile time while maintaining backward compatibility at runtime.
class Test<T> {
// An object of type T is declared
T obj;
Test(T obj) { this.obj = obj; } // constructor
public T getObject() { return this.obj; }
}
class Geeks {
public static void main(String[] args)
{
// instance of Integer type
Test<Integer> iObj = new Test<Integer>(15);
System.out.println(iObj.getObject());
// instance of String type
Test<String> sObj
= new Test<String>("GeeksForGeeks");
System.out.println(sObj.getObject());
iObj = sObj; // This results an error
}
}
Output:Â
error:
incompatible types:
Test cannot be converted to Test
Explanation: At compile time, Test<Integer> and Test<String> are treated as different parameterized types.
Java generics enforce type safety during compilation, so assigning one to another results in a compile-time error.
However, due to type erasure, the generic type information is removed at runtime and both become the raw type Test.
Even though they are the same raw type at runtime, the compiler prevents the assignment to maintain type safety.
Static Variables in Generic Classes
- Due to type erasure, Java creates only one class at runtime for a generic class, regardless of the type parameter used.
- This means that static members are shared across all type parameters of a generic class.
class Test<T> {
static int count = 0;
Test() {
count++;
}
}
public class Geeks {
public static void main(String[] args) {
Test<Integer> obj1 = new Test<>();
Test<String> obj2 = new Test<>();
Test<Double> obj3 = new Test<>();
System.out.println(Test.count);
}
}
Output
3
Explanation: Even though we created objects of Test<Integer>, Test<String> and Test<Double>, only one class Test exists at runtime due to type erasure. Therefore, the static variable count is shared among all instances, regardless of their type parameter.
Benefits of Generics
Generics provide several benefits in Java:
- Type Safety: Generics detect invalid type assignments at compile time, reducing the risk of runtime
ClassCastException. - Code Reusability: A single generic class, method, or interface can work with different reference types.
- No Explicit Type Casting: The compiler automatically performs the required type conversion when retrieving elements from a generic collection.
- Better Readability: Specifying the type makes the intended data type clear and makes code easier to understand and maintain.
Example: Type Safety and No Casting
import java.util.ArrayList;
class Geeks {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Sweta");
names.add("Gudly");
// Compile-time error
// names.add(10);
String name = names.get(0);
System.out.println(name);
}
}
Output
Sweta
Explanation: Here, ArrayList<String> allows only String values, and get() returns a String directly, so explicit casting is not required.