Skip to content

Java · Generics

Java Generics — Type Safety, Generic Classes, Methods & the Diamond Operator

9 min read Updated 2026-06-20 Share:

Practice Generics Basics interview questions

Why generics exist

Before Java 5, a collection held plain Object. You could add anything to a List, but every time you read a value back you had to cast it, and if you guessed wrong the program blew up at runtime with a ClassCastException. Generics fix this by letting you parameterize a class, interface, or method by type, so the same code works for many types while the compiler still enforces correctness. The two payoffs are compile-time type checking — a wrong type is a build error, not a 3 a.m. crash — and no manual casts when you read values out.

List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);            // compile error — caught at build time
String n = names.get(0);  // no cast needed; the compiler knows it's a String

That single shift — moving type failures from runtime to compile time — is the whole reason generics matter, and it underpins everything else in this guide.

Generic classes

You declare a generic class by adding a type parameter in angle brackets after the class name. That parameter then stands in for a real type everywhere inside the class, and the caller supplies the concrete type when they create an instance.

class Box<T> {              // T is the type parameter
  private T value;
  public void set(T v) { value = v; }
  public T get() { return value; }
}

Box<String> b = new Box<>();
b.set("hi");
String s = b.get();        // returns T = String, no cast

For that instance the type argument String replaces T everywhere, so the compiler rejects b.set(42) and lets get() hand you a String directly. Generic interfaces work identically — interface Repository<T> declares T, and an implementor either fixes the type (class UserRepo implements Repository<User>) or stays generic (class MemRepo<T> implements Repository<T>). Core JDK contracts like Comparable<T>, Iterable<E>, and Comparator<T> are exactly this pattern, which is why class Money implements Comparable<Money> gives you a type-safe compareTo(Money) with no downcast.

Naming conventions for type parameters

Type parameters are single uppercase letters by convention, each hinting at a role. The compiler accepts any valid identifier, but following the convention makes a generic signature readable at a glance.

interface Map<K, V> { V get(K key); }   // K = key, V = value

The common letters: T for a generic type, E for an element (used throughout the collections framework), K and V for a map's key and value, N for a number, R for a return type, and S/U when a second or third type is needed. When you see <E> on a collection or <K, V> on a map, you already know what the author meant — that shared vocabulary is the entire point.

Generic methods

A class isn't the only thing that can be generic. A generic method declares its own type parameter, written before the return type, and that parameter is scoped to the single method. It can live in any class, generic or not.

// <T> introduces the type parameter for this method only
static <T> T firstOrNull(List<T> list) {
  return list.isEmpty() ? null : list.get(0);
}

String s = firstOrNull(List.of("a", "b")); // T inferred as String

The <T> between the modifiers and the return type is what makes the method generic — without it, T would be an undefined symbol. Crucially, a generic method's type parameter is independent of any type parameter on the enclosing class, which is what lets a plain utility class expose generic helpers without itself being generic.

Type inference and the diamond operator

You rarely have to write type arguments by hand because the compiler performs type inference: it looks at the argument types and the target type (what the result is assigned to) and deduces the type parameter for you. The most visible form is the diamond operator <> (Java 7+), which lets you omit the type arguments on the right of an assignment and infer them from the declared type on the left.

// verbose, pre-Java 7
Map<String, List<Integer>> m = new HashMap<String, List<Integer>>();

// diamond — compiler infers <String, List<Integer>> from the left side
Map<String, List<Integer>> m2 = new HashMap<>();

This cuts boilerplate without giving up any safety — new HashMap<>() is still fully generic, just inferred. When inference genuinely can't decide (typically when there are no arguments to learn from), you can supply an explicit type witness before the method name, as in Collections.<String>emptyList(). One caution with var: the right-hand side must carry the type arguments, because there's no left-hand declaration to borrow from — write new ArrayList<String>(), not new ArrayList<>(), or you'll infer ArrayList<Object> by accident.

Raw types and why to avoid them

A raw type is a generic type used without its type argument — List instead of List<String>. It exists only for backward compatibility with pre-Java-5 code, and using one opts out of type safety: the compiler stops checking element types and re-introduces exactly the runtime failures generics were built to prevent.

List raw = new ArrayList();      // raw type — unchecked
raw.add("hi");
raw.add(42);                     // no complaint — types not checked
String s = (String) raw.get(1);  // ClassCastException at runtime

Raw types produce unchecked warnings, which are the compiler begging you to parameterize. If you genuinely want a collection that holds anything, use List<Object> instead — it keeps full checking on. Note that new HashMap() (no diamond) is a raw type, while new HashMap<>() is not; the missing brackets are not a cosmetic detail.

Generic invariance

Here is the subtlety that trips up most people: even though String is a subtype of Object, List<String> is not a subtype of List<Object>. Generics are invariant. This feels wrong until you see what it prevents.

List<String> strings = new ArrayList<>();
List<Object> objs = strings;   // compile error — and a good thing too
objs.add(42);                  // ...because this would slip an Integer in
String s = strings.get(0);     // ...and this would explode at runtime

The compiler blocks the assignment so that the unsafe sequence can never happen. Arrays, by contrast, are covariant — String[] is an Object[] — which is precisely why array stores are checked at runtime and can throw ArrayStoreException. Generics chose compile-time safety over array-style covariance, and wildcards (List<?>, a list of some unknown type) exist to recover the flexibility invariance gives up.

Multiple type parameters

A generic type can take more than one parameter — just list them comma-separated. The canonical example is Map<K, V>, parameterized by both a key type and a value type, but you'll write your own Pair-style carriers all the time.

class Pair<A, B> {
  final A first;
  final B second;
  Pair(A a, B b) { first = a; second = b; }
}

Pair<String, Integer> p = new Pair<>("age", 30);
String key = p.first;   // A = String
int val = p.second;     // B = Integer

Each parameter is independent, so Pair<String, Integer> and Pair<Integer, String> are distinct, incompatible types. The compiler tracks every slot separately, which is what makes a two-parameter container as type-safe as a one-parameter one.

Generic constructors and static methods

Two corners surprise people. First, a constructor can declare its own type parameter, independent of the class's, written before the constructor name. This is rare but useful when the constructor needs a type purely for its arguments.

class Holder<T> {
  private T value;
  // S is the constructor's own parameter, separate from class T
  <S extends T> Holder(S initial) { this.value = initial; }
}

Second, a static method cannot see the class's type parameter, because that parameter exists per instance and a static method belongs to the class. A static method that needs generics must declare its own — which is exactly how factory methods like List.of and the pattern below work.

class Box<T> {
  T value;
  // static <U> needed — the class's T is not in scope here
  static <U> Box<U> of(U v) {
    Box<U> b = new Box<>();
    b.value = v;
    return b;
  }
}

Box<String> b = Box.of("hi");   // U inferred as String

Reusing the class's letter T for a static or constructor parameter is legal but confusing — pick a different letter so readers know it's a separate, independently-scoped type.

Recap

Generics parameterize code by type to deliver compile-time type checking and cast-free reads — type errors become build errors instead of ClassCastExceptions. Declare a generic class with <T> after the name, a generic method with <T> before the return type (independent of the class), and lean on type inference and the diamond operator so you rarely write type arguments by hand. Follow the T/E/K/V naming conventions for readable signatures, and never fall back to raw types — use List<Object> if you truly want anything. Remember that generics are invariant (List<String> is not a List<Object>), that multiple type parameters are independent, and that static methods and constructors declare their own type parameters. Get these basics right and the rest of generics — wildcards, bounds, and erasure — builds cleanly on top.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel