Before Generics were introduced in Java 5, collections stored everything as plain Object references. This meant developers had to manually cast objects when extracting them, leading to runtime ClassCastException crashes. Generics solve this by introducing type safety at compile time.

In this guide, we will look at how to design classes and methods that accept variable type parameters (like <T>).

Illustration of dynamic generic types and data list containers
Real-World Analogy: Shipping Crates with Label Inserts

Imagine buying storage crates:

  • Without Generics: You have a single generic wooden box. You can put anything in it—books, apples, or glass bottles. When you open it, you have to verify what's inside before handling it. If you grab a glass bottle thinking it's an apple, you might cut yourself.
  • With Generics: You buy a crate that has a sliding label insert on the front: **Crate of <T>**. When you purchase it, you slide in the card labeled **"Apples"**. The container is now locked to only accept apples, guaranteeing you will never pull out a glass bottle by mistake.

1. Creating a Generic Class

To define a generic class, you place a type parameter (typically T, E, or K, V) in angle brackets right after the class name:

public class GenericClassExample<T> {
    T obj; // Holds reference of type T

    T printDoubleLogic(T t1) {
        if (t1 instanceof Integer) {
            int tempint = ((Integer) t1).intValue();
            tempint = tempint * 2;
            obj = (T) new Integer(tempint); // Safe cast
            return obj;
        }

        String t2 = t1 + " " + t1;
        obj = (T) new String(t2);
        return obj;
    }

    public static void main(String[] args) {
        // Instantiate for String type
        GenericClassExample<String> d1 = new GenericClassExample<>();
        String resultString = d1.printDoubleLogic("Hello");
        System.out.println(resultString); // Prints: Hello Hello

        // Instantiate for Integer type
        GenericClassExample<Integer> d2 = new GenericClassExample<>();
        int resultInt = d2.printDoubleLogic(5);
        System.out.println(resultInt); // Prints: 10
    }
}

2. Creating a Generic Method

Sometimes you don't need the entire class to be generic—just a single static or instance method. To do this, place the generic type declaration <T> before the method's return type:

import java.util.ArrayList;

public class GenericMethodExample {
    static int sum = 0;

    // Generic method declaration
    static <T> T docon(ArrayList<T> ob) {
        for (T a : ob) {
            sum += ((Integer) a).intValue(); // Expects Integer type elements
        }
        return (T) new Integer(sum);
    }

    public static void main(String[] args) {
        ArrayList<Integer> a1 = new ArrayList<>();
        a1.add(4);
        a1.add(5);
        
        int result = docon(a1); // Compiler infers type parameter as Integer
        System.out.println("Sum is " + result); // Sum is 9
    }
}

Conclusion

Generic classes and methods allow you to design reusable logic that adapts to different data types. By shifting type checks to compile time, Java prevents class casting crashes before your code is ever deployed.