AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

13 Generics / Lesson 2 of 6

Writing generic classes and methods

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

How do you write a generic class in Java?

To write a generic class in Java, declare type parameters in angle brackets after the class name — public class Pair<A, B> — and use them as types for fields, constructor parameters and methods. A generic method declares its own parameters before the return type, as in static <T> T firstOrDefault(List<T> list, T fallback), and the compiler infers them at each call.

Using List<String> is the easy half. Writing your own generic types — a Pair<A, B>, a Stack<T>, a Result<T>, a static <T> T firstOrDefault(List<T>, T) — is where generics stop being syntax and become a design tool. This lesson covers declaring type parameters on classes, interfaces and methods, how the compiler infers type arguments, and the common mistakes when a class and a method both introduce T.

A generic class

public final class Pair<A, B> {
    private final A first;
    private final B second;

    public Pair(A first, B second) { this.first = first; this.second = second; }
    public A first()  { return first; }
    public B second() { return second; }
    public <C> Pair<A, C> withSecond(C c) { return new Pair<>(first, c); }     // a generic method inside a generic class

    @Override public String toString() { return "(" + first + ", " + second + ")"; }
}

Pair<String, Integer> p = new Pair<>("age", 30);
Pair<String, Double> q = p.withSecond(30.5);

The type parameters go after the class name and are in scope throughout the body: fields, constructor parameters, method signatures, nested non-static types. Inside Pair<A, B>, A and B are just types you cannot instantiate or inspect (erasure — lesson 5), but you can store, pass and return them. (A record record Pair<A, B>(A first, B second) {} does all of this in one line — records are generic too.)

A generic stack

public class Stack<T> {
    private Object[] items = new Object[8];        // NOT new T[8] — impossible under erasure
    private int size;

    public void push(T item) {
        if (size == items.length) items = Arrays.copyOf(items, size * 2);
        items[size++] = item;
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        if (size == 0) throw new NoSuchElementException();
        T item = (T) items[--size];                 // the one unavoidable unchecked cast, contained and justified
        items[size] = null;                         // let the GC have it
        return item;
    }

    public boolean isEmpty() { return size == 0; }
}

The backing store is Object[] and the cast on the way out is unchecked; the class is still safe, because only push(T) ever stores into the array. @SuppressWarnings("unchecked") on the smallest possible scope, with the reason clear from the code, is the idiom. This is exactly how ArrayList is written.

Generic interfaces

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
}
public class UserRepository implements Repository<User, Long> { … }     // fix the arguments when implementing
public class InMemoryRepository<T, ID> implements Repository<T, ID> { … }   // or stay generic

Comparable<T>, Iterable<T>, Function<T, R> and every collection interface follow this shape. When you implement one, either supply concrete arguments or carry your own parameters through.

Generic methods

public static <T> T firstOrDefault(List<T> list, T fallback) {
    return list.isEmpty() ? fallback : list.get(0);
}
public static <T extends Comparable<T>> T max(List<T> list) { … }          // bounded — next lesson
public static <K, V> Map<V, K> invert(Map<K, V> map) { … }
public static <T> void swap(T[] array, int i, int j) { … }

String s = firstOrDefault(names, "none");          // T inferred as String from the arguments
Integer n = Main.<Integer>firstOrDefault(List.of(), 0);   // explicit type witness — rarely needed

The type parameter list goes before the return type. A generic method's parameters are inferred per call from the argument types and the target type, so callers almost never write them. Generic methods may be static — and must be, if they are utilities on a non-generic class; a class type parameter cannot be used in a static method (there is no instance to fix it), which is why static <T> declares its own.

Inference

The compiler infers type arguments from: the argument expressions, the target type (assignment, return, parameter), and bounds. It is good but not magic:

List<String> empty = Collections.emptyList();       // T = String from the target
var e = Collections.emptyList();                    // T = Object — no target to infer from
process(Collections.emptyList());                   // Java 8+ infers from process's parameter type
List<Number> nums = List.of(1, 2.0);                // T inferred as Number & Comparable<…> — works; the lub of Integer and Double

When inference fails ("incompatible types: inference variable T has incompatible bounds"), add a witness (Collections.<String>emptyList()), a typed local variable, or a lambda parameter type.

Class parameter versus method parameter

public class Box<T> {
    private T value;
    public <T> void confuse(T other) { … }        // a NEW, unrelated T that SHADOWS the class's T — a bug
    public void set(T v) { value = v; }           // the class's T
    public <U> Box<U> map(Function<T, U> f) { return new Box<>(f.apply(value)); }   // right: new parameter named U
}

Redeclaring the class's parameter name on a method creates a second, independent parameter and silently breaks the connection. Use a fresh letter for method-level parameters inside generic classes.

Static members and type parameters

public class Registry<T> {
    private static T shared;               // compile error: non-static type variable T cannot be referenced from a static context
    private static int count;              // fine
    public static <T> Registry<T> empty() { return new Registry<>(); }   // its own T
}

The class's T is per-instance in concept; statics are per-class, and there is one class for all Ts under erasure. A static factory declares its own type parameter.

Multiple parameters and naming

Map<K, V>, BiFunction<T, U, R>, Pair<A, B>: as many as needed, comma-separated. Keep to single capitals; name by role (K, V, E, R); do not reuse a name in nested scopes.

Generic constructors and nested types

Constructors may declare their own type parameters (<T> Box(T seed)) — rare. A non-static inner class of a generic class sees the outer's parameters (Box<T>.Inner may use T); a static nested class does not (static class Node<T> needs its own).

Interview angle

  • "Where does a generic method's type parameter go?" Before the return type: static <T> T f(…).
  • "Can a static method use the class's type parameter?" No — it declares its own.
  • "How do you create a T[] inside a generic class?" You cannot directly; use Object[] with a contained unchecked cast, or Array.newInstance with a Class<T>.
  • "What is the diamond?" <> — infer the constructor's type arguments from the target type.
  • "What goes wrong if a method redeclares <T> inside Box<T>?" It shadows the class parameter with an unrelated one.

Key takeaways

  • Class/interface parameters go after the name and are in scope for the body; method parameters go before the return type and are inferred per call.
  • Backing arrays are Object[] with one contained, suppressed cast — the ArrayList pattern.
  • Statics cannot use the class's parameter; declare their own. Never shadow a class parameter on a method.
  • Inference uses arguments, target type and bounds; give a witness or a typed variable when it fails.

Common questions

Where does the type parameter go in a generic method?

Before the return type: public static <T> T firstOrDefault(List<T> list, T fallback). The compiler infers T at each call from the arguments and the target type, so callers rarely write it; an explicit witness looks like Main.<Integer>firstOrDefault(List.of(), 0).

Can a static method use the class's type parameter?

No. A class's type parameter belongs to instances, and under erasure one class serves every T, so a static member using it fails with "non-static type variable T cannot be referenced from a static context". A static method declares its own, as in static <T> Registry<T> empty().

How do you create a generic array in Java?

You cannot write new T[n]. Use an Object[] as the backing store and cast on the way out, with @SuppressWarnings("unchecked") on the smallest scope — exactly how ArrayList works — or call Array.newInstance with a Class<T> token.

What happens if a method redeclares T inside a generic class?

It creates a second, unrelated type parameter that shadows the class's T, silently breaking the connection between them. Give method-level parameters a fresh letter, as in <U> Box<U> map(Function<T, U> f).

Can records be generic in Java?

Yes. record Pair<A, B>(A first, B second) {} declares a generic record, with its type parameters after the name just like a class, and gets the constructor and accessors in one line.

Mark as readSign in to keep your place.

How to Write a Generic Class and Generic Method in Java

How to write a generic class and a generic method in Java: where type parameters go, how inference works, why statics cannot use T, and generic arrays.

  • Course: Java study plan
  • Module: Generics
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: OpenJDK 18

How do you write a generic class in Java?

To write a generic class in Java, declare type parameters in angle brackets after the class name — public class Pair<A, B> — and use them as types for fields, constructor parameters and methods. A generic method declares its own parameters before the return type, as in static <T> T firstOrDefault(List<T> list, T fallback), and the compiler infers them at each call.

Lesson

Using List<String> is the easy half. Writing your own generic types — a Pair<A, B>, a Stack<T>, a Result<T>, a static <T> T firstOrDefault(List<T>, T) — is where generics stop being syntax and become a design tool. This lesson covers declaring type parameters on classes, interfaces and methods, how the compiler infers type arguments, and the common mistakes when a class and a method both introduce T.

A generic class

public final class Pair<A, B> {
    private final A first;
    private final B second;

    public Pair(A first, B second) { this.first = first; this.second = second; }
    public A first()  { return first; }
    public B second() { return second; }
    public <C> Pair<A, C> withSecond(C c) { return new Pair<>(first, c); }     // a generic method inside a generic class

    @Override public String toString() { return "(" + first + ", " + second + ")"; }
}

Pair<String, Integer> p = new Pair<>("age", 30);
Pair<String, Double> q = p.withSecond(30.5);

The type parameters go after the class name and are in scope throughout the body: fields, constructor parameters, method signatures, nested non-static types. Inside Pair<A, B>, A and B are just types you cannot instantiate or inspect (erasure — lesson 5), but you can store, pass and return them. (A record record Pair<A, B>(A first, B second) {} does all of this in one line — records are generic too.)

A generic stack

public class Stack<T> {
    private Object[] items = new Object[8];        // NOT new T[8] — impossible under erasure
    private int size;

    public void push(T item) {
        if (size == items.length) items = Arrays.copyOf(items, size * 2);
        items[size++] = item;
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        if (size == 0) throw new NoSuchElementException();
        T item = (T) items[--size];                 // the one unavoidable unchecked cast, contained and justified
        items[size] = null;                         // let the GC have it
        return item;
    }

    public boolean isEmpty() { return size == 0; }
}

The backing store is Object[] and the cast on the way out is unchecked; the class is still safe, because only push(T) ever stores into the array. @SuppressWarnings("unchecked") on the smallest possible scope, with the reason clear from the code, is the idiom. This is exactly how ArrayList is written.

Generic interfaces

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
}
public class UserRepository implements Repository<User, Long> { … }     // fix the arguments when implementing
public class InMemoryRepository<T, ID> implements Repository<T, ID> { … }   // or stay generic

Comparable<T>, Iterable<T>, Function<T, R> and every collection interface follow this shape. When you implement one, either supply concrete arguments or carry your own parameters through.

Generic methods

public static <T> T firstOrDefault(List<T> list, T fallback) {
    return list.isEmpty() ? fallback : list.get(0);
}
public static <T extends Comparable<T>> T max(List<T> list) { … }          // bounded — next lesson
public static <K, V> Map<V, K> invert(Map<K, V> map) { … }
public static <T> void swap(T[] array, int i, int j) { … }

String s = firstOrDefault(names, "none");          // T inferred as String from the arguments
Integer n = Main.<Integer>firstOrDefault(List.of(), 0);   // explicit type witness — rarely needed

The type parameter list goes before the return type. A generic method's parameters are inferred per call from the argument types and the target type, so callers almost never write them. Generic methods may be static — and must be, if they are utilities on a non-generic class; a class type parameter cannot be used in a static method (there is no instance to fix it), which is why static <T> declares its own.

Inference

The compiler infers type arguments from: the argument expressions, the target type (assignment, return, parameter), and bounds. It is good but not magic:

List<String> empty = Collections.emptyList();       // T = String from the target
var e = Collections.emptyList();                    // T = Object — no target to infer from
process(Collections.emptyList());                   // Java 8+ infers from process's parameter type
List<Number> nums = List.of(1, 2.0);                // T inferred as Number & Comparable<…> — works; the lub of Integer and Double

When inference fails ("incompatible types: inference variable T has incompatible bounds"), add a witness (Collections.<String>emptyList()), a typed local variable, or a lambda parameter type.

Class parameter versus method parameter

public class Box<T> {
    private T value;
    public <T> void confuse(T other) { … }        // a NEW, unrelated T that SHADOWS the class's T — a bug
    public void set(T v) { value = v; }           // the class's T
    public <U> Box<U> map(Function<T, U> f) { return new Box<>(f.apply(value)); }   // right: new parameter named U
}

Redeclaring the class's parameter name on a method creates a second, independent parameter and silently breaks the connection. Use a fresh letter for method-level parameters inside generic classes.

Static members and type parameters

public class Registry<T> {
    private static T shared;               // compile error: non-static type variable T cannot be referenced from a static context
    private static int count;              // fine
    public static <T> Registry<T> empty() { return new Registry<>(); }   // its own T
}

The class's T is per-instance in concept; statics are per-class, and there is one class for all Ts under erasure. A static factory declares its own type parameter.

Multiple parameters and naming

Map<K, V>, BiFunction<T, U, R>, Pair<A, B>: as many as needed, comma-separated. Keep to single capitals; name by role (K, V, E, R); do not reuse a name in nested scopes.

Generic constructors and nested types

Constructors may declare their own type parameters (<T> Box(T seed)) — rare. A non-static inner class of a generic class sees the outer's parameters (Box<T>.Inner may use T); a static nested class does not (static class Node<T> needs its own).

Interview angle

  • "Where does a generic method's type parameter go?" Before the return type: static <T> T f(…).
  • "Can a static method use the class's type parameter?" No — it declares its own.
  • "How do you create a T[] inside a generic class?" You cannot directly; use Object[] with a contained unchecked cast, or Array.newInstance with a Class<T>.
  • "What is the diamond?" <> — infer the constructor's type arguments from the target type.
  • "What goes wrong if a method redeclares <T> inside Box<T>?" It shadows the class parameter with an unrelated one.

Key takeaways

  • Class/interface parameters go after the name and are in scope for the body; method parameters go before the return type and are inferred per call.
  • Backing arrays are Object[] with one contained, suppressed cast — the ArrayList pattern.
  • Statics cannot use the class's parameter; declare their own. Never shadow a class parameter on a method.
  • Inference uses arguments, target type and bounds; give a witness or a typed variable when it fails.

Common questions

Where does the type parameter go in a generic method?

Before the return type: public static <T> T firstOrDefault(List<T> list, T fallback). The compiler infers T at each call from the arguments and the target type, so callers rarely write it; an explicit witness looks like Main.<Integer>firstOrDefault(List.of(), 0).

Can a static method use the class's type parameter?

No. A class's type parameter belongs to instances, and under erasure one class serves every T, so a static member using it fails with "non-static type variable T cannot be referenced from a static context". A static method declares its own, as in static <T> Registry<T> empty().

How do you create a generic array in Java?

You cannot write new T[n]. Use an Object[] as the backing store and cast on the way out, with @SuppressWarnings("unchecked") on the smallest scope — exactly how ArrayList works — or call Array.newInstance with a Class<T> token.

What happens if a method redeclares T inside a generic class?

It creates a second, unrelated type parameter that shadows the class's T, silently breaking the connection between them. Give method-level parameters a fresh letter, as in <U> Box<U> map(Function<T, U> f).

Can records be generic in Java?

Yes. record Pair<A, B>(A first, B second) {} declares a generic record, with its type parameters after the name just like a class, and gets the constructor and accessors in one line.

Exercises

A generic stack and a generic helper

Write class Stack<T> backed by an Object[] with push, pop, peek and isEmpty (one contained @SuppressWarnings("unchecked") cast). Write static <T> String describe(Stack<T> s, int take) that pops up to take elements and returns them joined by ,.

Read an integer n and n tokens; push them onto a Stack<String>; then read k and print describe(stack, k). Also push the integers 1..n onto a Stack<Integer> and print describe(ints, k) — the same generic method serving both.

Example: 3 then a b c, k = 2 →

c,b
3,2

max and reverse, for any type

Write two generic static methods: <T extends Comparable<T>> T max(List<T> xs) (the first maximum on ties) and <T> void reverse(List<T> xs) (in place, swapping from both ends). Read an integer n and n integers, then m and m words. Print maxInt=<…>, maxWord=<…> (natural String order), then the reversed lists as ints=[…] and words=[…]. One implementation each, used for both types.

Example: 3 then 4 9 2, then 2 then pear apple →

maxInt=9
maxWord=pear
ints=[2, 9, 4]
words=[apple, pear]

In this module: Generics

← Why generics exist · Bounded type parameters →