Generics

Generics let you write classes and methods that work with any type while still catching type mistakes at compile time instead of at runtime. You've already been using them without a formal introduction — every List<String> or Map<String, Integer> in the Collections lesson was a generic type in action.

Why Generics Exist

Before generics, collections stored plain Object references, which meant you could accidentally add the wrong type of item and only find out when your code crashed at runtime with a ClassCastException. A raw, untyped List gives the compiler no information about what's supposed to be inside it.

List names = new ArrayList(); // raw type - no type safety
names.add("Sonu");
names.add(42); // compiles fine, blows up later
          

With a generic type parameter, the compiler enforces the rule for you, right at the point where the mistake is made — no waiting for a crash in production.

List<String> names = new ArrayList<>(); // diamond operator
names.add("Sonu");
names.add(42); // compile error - int cannot be converted to String
          
Note: The empty <> on the right-hand side is called the diamond operator. The compiler infers the type from the declared variable, so you don't have to repeat String twice.

Writing Your Own Generic Class

You aren't limited to using generics on built-in collections — you can write your own generic classes too. The type parameter, conventionally a single capital letter like T (for "Type"), acts as a placeholder that gets filled in with a real type when the class is used.

Example: A Generic Box

class Box<T> {
  private T contents;

  public void set(T contents) {
    this.contents = contents;
  }

  public T get() {
    return this.contents;
  }
}

public class BoxDemo {
  public static void main(String[] args) {
    Box<String> stringBox = new Box<>();
    stringBox.set("Hello");

    Box<Integer> intBox = new Box<>();
    intBox.set(99);

    System.out.println(stringBox.get()); // Hello
    System.out.println(intBox.get()); // 99
  }
}

What's happening:

  • Box<T> is one class definition, but it works for any type you plug into T.
  • Box<String> and Box<Integer> are both backed by the same Box class — no duplicate code needed for each type.
  • The compiler knows exactly what type get() returns for each box, so no manual casting is required.

Bounded Type Parameters

Sometimes you want to restrict a type parameter to a family of types instead of allowing absolutely anything. A bounded type parameter does this with the extends keyword.

class NumberBox<T extends Number> {
  private T value;

  public double doubled(T value) {
    return value.doubleValue() * 2;
  }
}
          

Here T extends Number means only Number or one of its subclasses — Integer, Double, Long, and so on — can be used as the type argument. This lets the method safely call doubleValue(), a method guaranteed to exist because every bound type is a Number.

Next up: learn how Java lets a program do more than one thing at a time with Multithreading.