☕ Java

Type Parameters

Type parameters are the placeholders — written as single uppercase letters like T, E, K, V — that make Java generics work. A type parameter declares a variable that stands in for a concrete type, to be supplied by the caller or inferred by the compiler. They appear on classes (class Box<T>), interfaces (interface Comparable<T>), and methods (public static <T> T identity(T value)), and they are the mechanism by which the Java type system achieves compile-time type safety without duplicating code for each concrete type. This entry covers where and how type parameters are declared, the naming conventions and why they exist, the scope rules for type parameters (where they are in scope and where they are not), how type parameters interact with inheritance, how the compiler resolves a type parameter to a concrete type at each call site, and the distinction between a type parameter (a declaration) and a type argument (a concrete substitution).

Declaring Type Parameters — Classes, Interfaces, and Methods

A type parameter is declared by placing a comma-separated list of names inside angle brackets immediately after the class name, interface name, or (for methods) before the return type. Each name in the list is a new type variable in scope for the body of that declaration. class Box<T> declares one type parameter T. class Pair<K, V> declares two. interface Transformer<I, O> declares two. public static <T> List<T> singletonList(T o) declares one method-level type parameter T, separate from anything on the enclosing class. The distinction between a type parameter and a type argument is fundamental. A type parameter is a declaration — it is what appears in angle brackets on a class or method definition, and it is a placeholder. A type argument is a concrete substitution — it is what appears when you use a generic type or call a generic method: new Box<String>(), List<Integer>, Collections.<String>emptyList(). The compiler replaces every occurrence of the type parameter with the type argument throughout the parameterized context. Type parameters declared on a class are in scope for all instance fields, instance methods (both parameter types and return types), and constructors. They are not in scope for static fields or static methods, because static members belong to the class itself rather than to any particular parameterization. A static field of type T would be shared across Box<String>, Box<Integer>, and Box<Object> simultaneously, which is incoherent — the JVM has only one Box.class regardless of parameterization. Type parameters declared on a method are in scope only for that method's return type, parameter types, and body. Method type parameters are independent of class type parameters: a method can shadow a class type parameter by declaring its own parameter with the same name (though this is strongly discouraged — it makes the code confusing). When a class implements a generic interface or extends a generic class, it must either supply concrete type arguments (class IntBox extends Box<Integer>), propagate its own type parameters (class SpecialBox<T> extends Box<T>), or mix both (class NumberTransformer<R> implements Transformer<Number, R>).
Java
// ── Type parameter on a class ─────────────────────────────────────────
public class Box<T> {                      // T is the type parameter
    private T value;                       // T in scope for fields

    public Box(T value) {                  // T in scope for constructor
        this.value = value;
    }

    public T get()           { return value; }   // T in scope for methods
    public void set(T value) { this.value = value; }
}

// Type ARGUMENTS supplied at use site:
Box<String>  strBox = new Box<>("hello"); // T = String
Box<Integer> intBox = new Box<>(42);      // T = Integer
Box<Double>  dblBox = new Box<>(3.14);    // T = Double

// ── Type parameter on an interface ────────────────────────────────────
public interface Transformer<I, O> {       // two type parameters
    O transform(I input);                  // both in scope
}

// Implementing with concrete type arguments:
class UpperCaser implements Transformer<String, String> {
    public String transform(String input) { return input.toUpperCase(); }
}

// Implementing with propagated type parameter:
class ListWrapper<T> implements Transformer<T, List<T>> {
    public List<T> transform(T input) { return List.of(input); }
}

// ── Type parameter on a method ────────────────────────────────────────
public class Utils {
    // <T> declares T as a METHOD type parameter (before return type):
    public static <T> T identity(T value) { return value; }

    // Two method type parameters:
    public static <A, B> Pair<B, A> swap(Pair<A, B> pair) {
        return new Pair<>(pair.second(), pair.first());
    }

    // Method type parameter INDEPENDENT of class — Utils is not generic:
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }
}

String  s = Utils.identity("hello");      // T = String, inferred from argument
Integer i = Utils.identity(42);           // T = Integer, inferred from argument

// ── Static context CANNOT use class type parameter ────────────────────
public class Repository<T> {
    private List<T> items = new ArrayList<>();

    public void add(T item) { items.add(item); }        // OK — instance method
    public T    get(int i)  { return items.get(i); }    // OK — instance method

    // COMPILE ERROR — static field cannot reference T:
    // private static T defaultItem;  // error

    // COMPILE ERROR — static method cannot reference class T:
    // public static T create() { ... }  // error

    // OK — static method with its OWN type parameter (different T, unrelated):
    public static <T> Repository<T> empty() { return new Repository<>(); }
}

// ── Inheritance and type parameter propagation ────────────────────────
// Concrete substitution — IntBox always works with Integer:
class IntBox extends Box<Integer> {
    public IntBox(int value) { super(value); }
    public int doubledValue() { return get() * 2; }  // get() returns Integer
}

// Propagation — NamedBox<T> is still generic:
class NamedBox<T> extends Box<T> {
    private final String label;
    public NamedBox(T value, String label) { super(value); this.label = label; }
    public String label() { return label; }
}

// Mixed — first type arg fixed, second propagated:
class StringTransformer<O> implements Transformer<String, O> {
    private final Function<String, O> fn;
    public StringTransformer(Function<String, O> fn) { this.fn = fn; }
    public O transform(String input) { return fn.apply(input); }
}

IntBox          ib = new IntBox(21);
System.out.println(ib.doubledValue());  // 42

NamedBox<Double> nb = new NamedBox<>(3.14, "pi");
System.out.println(nb.label() + " = " + nb.get());  // pi = 3.14

StringTransformer<Integer> st = new StringTransformer<>(String::length);
System.out.println(st.transform("hello"));  // 5

Naming Conventions and Scope Rules

Java establishes universally followed naming conventions for type parameters. T is used for a general single type, typically the primary element type of a container or the subject of an operation. E stands for Element, used throughout the collections framework (List<E>, Set<E>, Iterator<E>) to signal that the type parameter represents what the collection holds. K and V stand for Key and Value, used in Map<K, V> and related interfaces to distinguish the two roles. N stands for Number, used when a numeric type is expected. R stands for Return type, used in functional interfaces (Function<T, R>) to distinguish the output type from the input. S, U, and W are used for additional type parameters when T through N are already in use. These conventions are not enforced by the compiler — you can write class Box<Element> or class Pair<FirstType, SecondType> and the code will compile. But violating the conventions makes generics significantly harder to read. When you see <T>, experienced Java developers instantly know it is a general placeholder. When you see <E>, they know it is an element type. Using full words disrupts this visual grammar. The scope of a class's type parameters is precisely the body of the class, including nested non-static classes (which have access to the enclosing class's type parameters). Static nested classes do not have access to the enclosing class's type parameters because static nested classes are effectively separate top-level classes that happen to be namespaced inside the outer class. A static nested class Node<T> in a class LinkedList<T> has its own independent T, which happens to share the same name but is a different type parameter. Shadowing a class type parameter in a method — declaring a method type parameter with the same name as the class type parameter — compiles but is considered a serious error in practice. Inside that method, the method's T shadows the class's T, and any mention of T refers to the method's parameter. The enclosing class's T becomes invisible. Most IDEs warn about this pattern, and code reviewers should reject it.
Java
// ── Naming conventions in practice ───────────────────────────────────
// T — general type (single-parameter containers, operations)
class Container<T> {
    private T value;
    public Container(T value) { this.value = value; }
    public T get() { return value; }
}

// E — element type (collections)
class SimpleList<E> {
    private Object[] data = new Object[16];
    private int size;
    public void add(E element) { data[size++] = element; }
    @SuppressWarnings("unchecked")
    public E get(int index)    { return (E) data[index]; }
}

// K, V — key and value (maps)
class SimpleMap<K, V> {
    public void put(K key, V value) { /* ... */ }
    public V    get(K key)          { return null; /* ... */ }
}

// R — return type of a function
interface Mapper<T, R> {
    R map(T input);
}

// N — numeric type
class NumericContainer<N extends Number> {
    private N value;
    public N get()               { return value; }
    public double asDouble()     { return value.doubleValue(); }
}

// S, U — second, third additional type parameters
interface TriFunction<T, U, S, R> {
    R apply(T t, U u, S s);
}

// ── Type parameter scope: nested classes ──────────────────────────────
public class LinkedList<T> {                 // class type parameter T

    // Static nested class — has its OWN T, separate from LinkedList's T:
    private static class Node<T> {           // this T is NOT LinkedList's T
        T data;
        Node<T> next;
        Node(T data) { this.data = data; }
    }

    // Inner (non-static) class — SHARES LinkedList's T:
    // (Inner classes in generic containers are uncommon; usually Node is static)

    private Node<T> head;

    public void addFirst(T value) {
        Node<T> node = new Node<>(value);   // Node<T> where T = LinkedList's T
        node.next = head;
        head = node;
    }
}

// ── Shadowing class type parameter in a method — DO NOT DO THIS ────────
public class Processor<T> {
    private T data;
    public Processor(T data) { this.data = data; }

    // BAD: method declares its own <T> that SHADOWS class T
    public <T> void process(T input) {   // this T is NOT the class's T
        // Inside here, T refers to the method's T, not the class's T
        // 'data' still has the class's T, but you can't refer to it as T
        System.out.println(input);       // method's T
        System.out.println(data);        // class's T — but confusingly both named T
    }

    // GOOD: use a different name when a method needs its own type parameter
    public <R> R convert(Function<T, R> converter) {
        return converter.apply(data);    // clear: T = class param, R = method param
    }
}

// ── Type parameter scope boundaries ──────────────────────────────────
public class Outer<T> {
    T outerValue;                        // T in scope

    class Inner {
        void show() {
            System.out.println(outerValue); // T accessible — Inner is non-static
        }
    }

    static class StaticNested<T> {       // own T — unrelated to Outer's T
        T nestedValue;
        // outerValue NOT accessible — static nested has no enclosing instance
    }
}

Type Parameter Resolution and Type Argument Inference

Type parameter resolution is the process by which the compiler determines the concrete type to substitute for each type parameter at a given use site. Resolution happens entirely at compile time — by the time bytecode is produced, all type parameters have been erased. The substituted type is used for compile-time type checking; the erasure (Object or the bound) is used for the actual bytecode. The compiler resolves type arguments through inference. For method calls, inference examines three sources: the types of the actual arguments (argument inference), the type expected at the call site — the target type of an assignment, return, or method call argument (target type inference, added in Java 8), and the declared bounds (bounds inference). The compiler computes the most specific type consistent with all constraints. When multiple constraints conflict, the compiler chooses their least upper bound (the most specific common supertype). When no constraint is available, the type parameter defaults to its upper bound (or Object if unbounded). For generic class instantiation, the diamond operator <> was introduced in Java 7 to allow inference from context: new ArrayList<>() instead of new ArrayList<String>(). The compiler infers the type argument from the target type of the assignment. Without the diamond, the programmer must specify the type argument explicitly. In Java 9+, the diamond can be used with anonymous classes in limited cases. When inference produces a type you did not intend, an explicit type witness overrides it. Type witnesses are rarely needed in Java 8+ code but occasionally arise when: calling a generic method as an argument to another method (no assignment target exists), when the inferred type is a raw type or an unexpected intersection type, or when calling a generic method that has no arguments relating to the return type. The witness is placed between the object (or class name) and the method name: obj.<String>method() or ClassName.<Type>staticMethod().
Java
// ── Inference from method arguments ──────────────────────────────────
public static <T> List<T> repeat(T value, int count) {
    List<T> result = new ArrayList<>();
    for (int i = 0; i < count; i++) result.add(value);
    return result;
}

List<String>  strings = repeat("hello", 3);  // T = String  (from argument "hello")
List<Integer> ints    = repeat(42, 5);        // T = Integer (from argument 42)
List<Double>  doubles = repeat(3.14, 2);      // T = Double  (from argument 3.14)

// ── Inference from target type ────────────────────────────────────────
// Collections.emptyList() has no arguments — target type drives inference:
List<String>  emptyS = Collections.emptyList();  // T = String  (from target)
List<Integer> emptyI = Collections.emptyList();  // T = Integer (from target)

// ── Inference from return context ─────────────────────────────────────
public static <T> T getOrDefault(Map<String, T> map, String key, T defaultVal) {
    return map.getOrDefault(key, defaultVal);
}

Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);
int aliceScore = getOrDefault(scores, "Alice", 0);  // T = Integer, from map and defaultVal
int missing    = getOrDefault(scores, "Carol", -1); // T = Integer

// ── Least upper bound when constraints produce multiple types ──────────
// If passing both String and Integer, the inferred type is their LUB: Serializable & Comparable...
// Usually results in Object or a common interface:
var mixed = List.of("hello", 42);  // List<? extends Serializable & Comparable<...>>
// It's clearer to be explicit:
List<Object> explicit = new ArrayList<>();
explicit.add("hello");
explicit.add(42);

// ── Diamond operator — inference for constructors ─────────────────────
// Before Java 7 (explicit):
Map<String, List<Integer>> oldStyle = new HashMap<String, List<Integer>>();

// Java 7+ (diamond infers from target type):
Map<String, List<Integer>> modern = new HashMap<>();         // T inferred
List<Pair<String, Integer>> pairs = new ArrayList<>();       // T inferred

// ── Type witness — explicit override of inference ─────────────────────
// Needed: generic method result passed directly to another method (no target type):
// Without witness, inference might choose Object:
System.out.println(Collections.<String>emptyList());  // explicit: List<String>

// Needed: return type not constrained by any argument:
public static <T> T uncheckedCast(Object obj) {
    @SuppressWarnings("unchecked") T t = (T) obj;
    return t;
}
// Without explicit target, inference picks Object — so always assign:
String s = uncheckedCast("hello");   // T = String from target
// This would silently produce Object:
// var wrong = uncheckedCast("hello");  // T = Object

// ── Resolution with generic class inheritance ─────────────────────────
// When a subclass fixes a type argument, the resolution is immediate:
class StringBox extends Box<String> {         // T = String, fixed permanently
    public StringBox(String s) { super(s); }
    // get() returns String — no cast needed anywhere in subclass
}

// When a subclass propagates, resolution defers to the subclass's call site:
class WrappedBox<T> extends Box<T> {
    public WrappedBox(T value) { super(value); }
}

WrappedBox<Integer> wb = new WrappedBox<>(99); // T resolved to Integer here
Integer val = wb.get();                         // returns Integer — type safe

Related Topics in Generics

Generic Class
A generic class in Java is a class parameterized by one or more type parameters, written as class Box<T> or class Pair<K, V>. Type parameters allow a single class definition to work correctly with many different types while preserving full compile-time type safety — eliminating the need for casts, preventing ClassCastException at runtime, and making the intended type contract explicit in the API. The compiler erases type parameters at bytecode level (type erasure), replacing them with their bounds or Object, but all type checking occurs at compile time before erasure. This entry covers type parameter declaration and naming conventions, bounded type parameters (extends, super), multiple type parameters, raw types and why they are unsafe, type erasure and its consequences, wildcards (?, ? extends T, ? super T), the PECS principle, recursive type bounds, generic interfaces, inheritance with generic classes, and how to design generic classes that work correctly across all parameterizations.
Generic Method
A generic method is a method that declares its own type parameters, independent of any type parameters on its enclosing class. The type parameters appear in angle brackets before the return type: public static <T> T identity(T value). Generic methods can appear in non-generic classes, in generic classes (adding their own type parameters beyond the class's), and as both static and instance methods. The compiler infers the type arguments for generic method calls from the argument types, eliminating the need for explicit type witness syntax in most cases. This entry covers type parameter declaration on methods, type inference rules and when inference fails, bounded type parameters on methods, returning computed type relationships, generic methods that operate on multiple type parameters, static generic utility methods (the standard library pattern), the interaction between method type parameters and class type parameters, explicit type witness syntax, and the difference between using a class type parameter vs a method type parameter.
Bounded Types
Bounded type parameters constrain which concrete types may be substituted for a type parameter. An upper bound (T extends Number) restricts T to Number and its subtypes, making Number's methods available on values of type T within the generic code. A lower bound cannot be applied to type parameters (only to wildcards), but multiple upper bounds can be combined with & (T extends Number & Comparable<T> & Serializable). Bounds serve two purposes simultaneously: they restrict the valid type arguments that callers can supply, and they expand what the generic code can do with values of type T by exposing the bound's API. Without a bound, only Object's methods are available on T values. This entry covers single and multiple upper bounds, recursive bounds, bounds on class type parameters vs method type parameters, how bounds interact with type erasure, bridge methods generated by the compiler, and practical patterns where bounds enable algorithms impossible with unbounded type parameters.
Wildcards
Wildcards — written as ? — represent an unknown type in a generic type argument position. Where a type parameter T is a name given to an unknown type that can be referenced throughout a declaration, a wildcard ? is an anonymous unknown type used at the point of consumption. Wildcards appear in method parameter types, field types, return types, and variable declarations — anywhere a parameterized type appears as a type (not where a type is being declared). The three wildcard forms are: the unbounded wildcard (List<?>), the upper-bounded wildcard (List<? extends Number>), and the lower-bounded wildcard (List<? super Integer>). Wildcards are the mechanism that introduces covariance and contravariance into Java's otherwise invariant generic type system. This entry covers all three forms in depth, the producer-extends consumer-super (PECS) principle, wildcard capture and the capture helper pattern, nested wildcards, and when to use wildcards vs type parameters.