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
// ── 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")); // 5Naming Conventions and Scope Rules
// ── 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
// ── 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