Generics in Java

Last Updated : 4 May, 2026

Generics in Java refer to parameterized types that allow writing code which works with multiple data types using a single class, interface, or method. They improve reusability and ensure type safety at compile time.

  • Use type parameters (like <T>) to create flexible and reusable code
  • Prevent runtime errors by enforcing type safety at compile time

Why Use Generics?

  • Before Generics, Java collections like ArrayList or HashMap could store any type of object, everything was treated as an Object. It had some problems.
  • If you added a String to a List, Java didn’t remember its type. You had to manually cast it when retrieving. If the type was wrong, it caused a runtime error.
  • With Generics, you can specify the type the collection will hold like ArrayList<String>. Now, Java knows what to expect and it checks at compile time, not at runtime.

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

Java
// 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
Java
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. 

Java
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

We can also write generic methods that can be called with different types of arguments based on the type of arguments passed to the generic method. The compiler handles each method.

Java
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.

Java
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.
Java
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.

Type Parameter Naming Conventions

The type parameters naming conventions are important to learn generics thoroughly. The common type parameters are as follows:

  • T: Type
  • E: Element
  • K: Key
  • N: Number
  • V: Value

Benefits of Generics

Programs that use Generics has got many benefits over non-generic code. 

1. Code Reuse: We can write a method/class/interface once and use it for any type we want.

2. Type Safety: Generics make errors to appear compile time than at run time (It's always better to know problems in your code at compile time rather than making your code fail at run time).

Suppose you want to create an ArrayList that store name of students and if by mistake the programmer adds an integer object instead of a string, the compiler allows it. But, when we retrieve this data from ArrayList, it causes problems at runtime.

Example: Without Generics

Java
import java.util.*;

class Geeks
{
    public static void main(String[] args)
    {
        // Creatinga an ArrayList without any type specified
        ArrayList al = new ArrayList();

        al.add("Sweta");
        al.add("Gudly");
        al.add(10); // Compiler allows this

        String s1 = (String)al.get(0);
        String s2 = (String)al.get(1);

        // Causes Runtime Exception
        String s3 = (String)al.get(2);
    }
}

Output :

Exception in thread "main" java.lang.ClassCastException: 
   java.lang.Integer cannot be cast to java.lang.String
    at Test.main(Test.java:19)

Here, we get runtime error.

Why This Happens (Type Safety Issue)

  • Without generics, collections store elements as Object.
  • This allows inserting different data types (e.g., adding an Integer in a list of String).
  • The issue is detected only at runtime, leading to a ClassCastException.

How Generics Solve This Problem:

  • Generics enforce type safety at compile time.
  • By specifying the type (e.g., ArrayList<String>), only that type of object can be added.
  • This prevents invalid data insertion and avoids runtime errors.

Example: With Generics

Java
import java.util.*;

class Geeks
{
    public static void main(String[] args)
    {
        // Creating a an ArrayList with String specified
        ArrayList <String> al = new ArrayList<String> ();

        al.add("Sweta");
        al.add("Gudly");

        // Now Compiler doesn't allow this
        al.add(10); 

        String s1 = (String)al.get(0);
        String s2 = (String)al.get(1);
        String s3 = (String)al.get(2);
    }
}

Output: 

15: error: no suitable method found for add(int)
        al.add(10); 
          ^

3. Individual Type Casting is not needed: Generics remove the need for manual type casting during retrieval, making code cleaner and safer.

Java
import java.util.*;

class Geeks {
    public static void main(String[] args)
    {
        // Creating a an ArrayList with String specified
        ArrayList<String> al = new ArrayList<String>();

        al.add("Sweta");
        al.add("Gudly");

        // Typecasting is not needed
        String s1 = al.get(0);
        String s2 = al.get(1);
    }
}

4. Generics Promotes Code Reusability: Generics enable writing a single reusable method that works with multiple data types.

Example: Generic Sorting

Java
public class Geeks {

    public static void main(String[] args)
    {
        Integer[] a = { 100, 22, 58, 41, 6, 50 };

        Character[] c = { 'v', 'g', 'a', 'c', 'x', 'd', 't' };

        String[] s = { "Amiya", "Kuna", "Gudly", "Sweta","Mama", "Rani", "Kandhei" };

        System.out.print("Sorted Integer array:  ");
        sort_generics(a);

        System.out.print("Sorted Character array:  ");
        sort_generics(c);

        System.out.print("Sorted String array:  ");
        sort_generics(s);
    }

    public static <T extends Comparable<T> > void sort_generics(T[] a)
    {
          //As we are comparing the Non-primitive data types we need to use Comparable class
      
        //Bubble Sort logic
        for (int i = 0; i < a.length - 1; i++) {

            for (int j = 0; j < a.length - i - 1; j++) {

                if (a[j].compareTo(a[j + 1]) > 0) {

                    swap(j, j + 1, a);
                }
            }
        }
        // Printing the elements after sorted
        for (T i : a) 
        {
            System.out.print(i + ", ");
        }
        System.out.println();
      
    }

    public static <T> void swap(int i, int j, T[] a)
    {
        T t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

Output
Sorted Integer array:  6, 22, 41, 50, 58, 100, 
Sorted Character array:  a, c, d, g, t, v, x, 
Sorted String array:  Amiya, Gudly, Kandhei, Kuna, Mama, Rani, Sweta, 

Here, we have created a generics method. This same method can be used to perform operations on integer data, string data and so on.

Note: If you are new to Java, start practicing Generics with basic examples like generic Box, generic Pair and generic methods.

Comment