☕ Java

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.

Declaring Generic Methods and Type Inference

A generic method declares its own type parameter list between the method's modifiers and its return type. For static methods: public static <T> List<T> repeat(T value, int times). For instance methods: public <R> R transform(Function<T, R> mapper) (where T may come from the class and R is the method's own parameter). The angle-bracket type parameter list must appear before the return type — not after the method name or parameter list. Type inference is the compiler's ability to determine the type arguments for a generic method call from the types of the actual arguments. When you call Collections.emptyList() and assign the result to List<String> result, the compiler infers T = String from the assignment target. When you call swap(array, 0, 1) with a String[], the compiler infers T = String from the array argument. Type inference uses the argument types, the target type of the assignment or return, and the declared bounds to compute the most specific type that satisfies all constraints. Inference is bidirectional in Java 8+: the compiler considers both the argument types (left-to-right) and the target type (what the result is assigned to or passed into). Before Java 8, target type inference was limited, which is why older code sometimes required explicit type witnesses. Today, inference fails most commonly when: the method's result is used directly without assignment (System.out.println(Collections.emptyList()) — no target type), when the inferred type would require an intersection of incompatible types, or when the return type depends on a type parameter not constrained by any argument. The explicit type witness syntax provides the type arguments manually: Collections.<String>emptyList(), or for a static import, Generics.<String, Integer>pair("key", 42). Type witnesses are placed between the dot and the method name. They are rarely needed in Java 8+ but remain useful in rare cases where inference produces an unwanted result or where code clarity benefits from explicit type documentation.
Java
// ── Basic generic method declaration ──────────────────────────────────
public class Generics {

    // Static generic method — T is the method's own type parameter:
    public static <T> T identity(T value) {
        return value;
    }

    // Static with multiple type parameters:
    public static <K, V> Map.Entry<K, V> entry(K key, V value) {
        return Map.entry(key, value);
    }

    // Instance generic method in a non-generic class:
    public <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }
}

// ── Type inference — compiler deduces T from arguments ────────────────
String  s = Generics.identity("hello");   // T inferred as String
Integer i = Generics.identity(42);        // T inferred as Integer
// No cast needed, no type witness needed.

// Inference from target type:
List<String>  emptyStrings = Collections.emptyList();  // T = String from target
List<Integer> emptyInts    = Collections.emptyList();  // T = Integer from target

// ── Type witness — explicit type argument ─────────────────────────────
// Needed when inference can't determine type from context:
// Passing result directly to a method with overloads — ambiguous without witness:
System.out.println(Collections.<String>emptyList());   // explicit witness

// Usually inference works fine with assignment:
List<String> list = Collections.emptyList();  // no witness needed

// ── Inference with multiple type parameters ───────────────────────────
Map.Entry<String, Integer> e = Generics.entry("age", 30);
// K inferred as String from "age", V inferred as Integer from 30

// ── Generic method in a generic classclass T vs method T ──────────
public class Container<T> {
    private T value;
    public Container(T value) { this.value = value; }

    // Uses class type parameter T:
    public T get() { return value; }

    // Introduces its OWN type parameter R — independent of class T:
    public <R> Container<R> map(Function<T, R> mapper) {
        return new Container<>(mapper.apply(value));
    }

    // Uses class T and introduces its own S:
    public <S> Pair<T, S> pairWith(S other) {
        return new Pair<>(value, other);
    }
}

Container<String>  strContainer = new Container<>("hello");
Container<Integer> intContainer = strContainer.map(String::length); // R = Integer
Pair<String, Boolean> p = strContainer.pairWith(true);  // S = Boolean

System.out.println(intContainer.get());  // 5
System.out.println(p);                   // (hello, true)

Bounded Type Parameters and Static Utility Methods

Bounded type parameters on methods constrain the types that can be used as arguments and make methods from the bound available within the method body. The bound appears after the type parameter name: <T extends Comparable<T>>, <T extends Number>, or multiple bounds <T extends Serializable & Cloneable>. The bound enables the compiler to type-check calls to methods defined in the bound interface or class. Generic static utility methods — methods that are generic, static, and work with or produce standard collection types — are the most common application of generic methods in practice. The java.util.Collections class is the canonical example: sort(List<T> list), binarySearch(List<? extends Comparable<? super T>> list, T key), max(Collection<? extends T> coll), min, frequency, disjoint, and many others. These methods work correctly with any type that satisfies the bounds, without requiring the caller to write casts or use raw types. A common pattern is a generic method that takes a varargs parameter: @SafeVarargs public static <T> List<T> listOf(T... elements). The @SafeVarargs annotation suppresses the heap pollution warning that arises because creating a generic array of T[] for the varargs parameter involves an unchecked cast. The annotation is a promise that the method does not expose the array reference to code that could insert incompatible types, which is safe here because the array is only read, not stored or returned. The Collections.sort(List<T> list, Comparator<? super T> c) signature demonstrates the ? super T comparator bound: the comparator needs to accept T objects, so a Comparator<Object> or Comparator<Number> can sort a List<Integer>. This is the consumer super half of PECS applied to a method parameter. The full signatures of utility methods in java.util.Collections are excellent study material for understanding how bounded type parameters and wildcards compose in real-world APIs.
Java
// ── Bounded type parameter — enables compareTo ───────────────────────
public static <T extends Comparable<T>> T clamp(T value, T min, T max) {
    if (value.compareTo(min) < 0) return min;   // compareTo available from bound
    if (value.compareTo(max) > 0) return max;
    return value;
}

System.out.println(clamp(5, 1, 10));      // 5 — within range
System.out.println(clamp(-3, 1, 10));     // 1 — below min
System.out.println(clamp(15, 1, 10));     // 10 — above max
System.out.println(clamp("dog", "ant", "elephant")); // dog — works with String too

// ── Multi-bound ───────────────────────────────────────────────────────
public static <T extends Number & Comparable<T>> T maxNumber(List<T> list) {
    if (list.isEmpty()) throw new NoSuchElementException();
    T result = list.get(0);
    for (T element : list) {
        if (element.compareTo(result) > 0) result = element;  // Comparable
    }
    System.out.println("Max as double: " + result.doubleValue());  // Number
    return result;
}

System.out.println(maxNumber(List.of(3, 1, 4, 1, 5, 9, 2, 6)));  // 9
System.out.println(maxNumber(List.of(1.1, 2.2, 0.5, 3.7)));       // 3.7

// ── Static generic utility methods — Collections pattern ──────────────
public class CollectionUtils {

    // Generic swap — works for any List:
    public static <T> void swap(List<T> list, int i, int j) {
        T temp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, temp);
    }

    // Generic frequency count with predicate:
    public static <T> int countIf(Collection<T> collection, Predicate<? super T> predicate) {
        int count = 0;
        for (T element : collection) {
            if (predicate.test(element)) count++;
        }
        return count;
    }

    // Generic partition — split into two lists based on predicate:
    public static <T> Map.Entry<List<T>, List<T>> partition(
            Collection<T> source, Predicate<? super T> predicate) {
        List<T> matched   = new ArrayList<>();
        List<T> unmatched = new ArrayList<>();
        for (T element : source) {
            (predicate.test(element) ? matched : unmatched).add(element);
        }
        return Map.entry(matched, unmatched);
    }

    // @SafeVarargs — no heap pollution because elements[] only read:
    @SafeVarargs
    public static <T> List<T> listOf(T... elements) {
        List<T> result = new ArrayList<>(elements.length);
        for (T e : elements) result.add(e);
        return result;
    }
}

// Usage:
List<Integer> nums = new ArrayList<>(List.of(5, 3, 1, 4, 2));
CollectionUtils.swap(nums, 0, 4);
System.out.println(nums);   // [2, 3, 1, 4, 5]

int evens = CollectionUtils.countIf(nums, n -> n % 2 == 0);
System.out.println("Evens: " + evens);  // 2

Map.Entry<List<Integer>, List<Integer>> parts =
    CollectionUtils.partition(nums, n -> n > 3);
System.out.println("Greater than 3: " + parts.getKey());    // [5, 4]
System.out.println("Up to 3:        " + parts.getValue());  // [2, 3, 1]

Recursive Type Bounds, Return Type Relationships, and Inference Edge Cases

A recursive type bound is a bound where the type parameter appears in its own bound: <T extends Comparable<T>>. This expresses that T can be compared to other T values — that the type is self-comparable. This pattern is ubiquitous in Java generics: Comparable is the canonical example, and Enum<E extends Enum<E>> is the most complex standard use, ensuring that each enum type can only be compared to values of the same enum type. The recursive bound <T extends Comparable<T>> is the correct bound for natural-ordering algorithms. The alternative — accepting a raw Comparable or a Comparable<Object> — loses type safety and would allow comparing a String to an Integer at compile time. The recursive bound threads the concrete type T through the bound, so T's compareTo method can only accept other T values, which is exactly what the algorithm needs. Generic methods can express relationships between return types and parameter types that would be impossible without generics. The cast() pattern: @SuppressWarnings("unchecked") public static <T> T cast(Object o) { return (T) o; } — the return type T is inferred from the target type of the call site. This is used internally by the JDK (Class.cast(), Collections.checkedList()) and is sometimes necessary in framework code. Used carelessly, it defeats type safety; used carefully with validation (instanceof checks), it is legitimate. Type inference edge cases: when a generic method is called inside a lambda or stream pipeline, inference can become more complex. Expression lambdas and method references work well; statement lambdas (with a block body) sometimes require explicit type annotations or intermediate variables because the compiler processes the lambda body in a context where the target type may not yet be established. The standard advice is to break complex chains into intermediate variables with explicit types when inference fails, rather than using type witnesses (which make code harder to read and maintain).
Java
// ── Recursive type bound <T extends Comparable<T>> ───────────────────
public static <T extends Comparable<T>> T min(Collection<T> collection) {
    if (collection.isEmpty()) throw new NoSuchElementException();
    Iterator<T> it  = collection.iterator();
    T result        = it.next();
    while (it.hasNext()) {
        T next = it.next();
        if (next.compareTo(result) < 0) result = next;
    }
    return result;
}

System.out.println(min(List.of(3, 1, 4, 1, 5, 9)));         // 1
System.out.println(min(List.of("banana", "apple", "mango"))); // apple
// min(List.of(new Object())) — won't compile: Object doesn't extend Comparable<Object>

// ── Self-referential bound for Builder pattern ─────────────────────────
// Allows builder subclasses to return their own type from inherited methods:
public abstract class Builder<T, B extends Builder<T, B>> {
    protected String name;

    @SuppressWarnings("unchecked")
    public B withName(String name) {
        this.name = name;
        return (B) this;  // safe: B is always the concrete subclass
    }

    public abstract T build();
}

public class PersonBuilder extends Builder<Person, PersonBuilder> {
    private int age;

    public PersonBuilder withAge(int age) {
        this.age = age;
        return this;
    }

    @Override public Person build() { return new Person(name, age); }
}

Person p = new PersonBuilder()
    .withName("Alice")   // returns PersonBuilder (not Builder) — chaining works
    .withAge(30)
    .build();

// ── Return type inferred from target — the cast() pattern ─────────────
@SuppressWarnings("unchecked")
public static <T> T uncheckedCast(Object obj) {
    return (T) obj;  // T erased to Object at bytecode; caller's target type flows in
}

// T inferred as String from assignment target:
String str = uncheckedCast("hello");   // safe
// T inferred as Integer — but obj is a String — ClassCastException at use site:
// Integer bad = uncheckedCast("hello");  // compiles, explodes at runtime

// Safer pattern — validate before casting:
public static <T> Optional<T> safeCast(Object obj, Class<T> type) {
    return type.isInstance(obj) ? Optional.of(type.cast(obj)) : Optional.empty();
}

Optional<String> s = safeCast("hello", String.class);   // Optional["hello"]
Optional<String> n = safeCast(42, String.class);         // Optional.empty

// ── Inference in lambda and stream contexts ───────────────────────────
List<String> words = List.of("hello", "world", "generics");

// Inference works fine in simple stream chains:
List<Integer> lengths = words.stream()
    .map(String::length)           // T=String inferred, R=Integer inferred
    .collect(Collectors.toList()); // T=Integer inferred

// Inference can fail in complex nested lambdas — use explicit types:
// This may fail to infer in some compilers:
// Map<String, List<String>> grouped = words.stream()
//     .collect(Collectors.groupingBy(s -> s.substring(0, 1)));

// Explicit type annotation on lambda parameter resolves ambiguity:
Map<String, List<String>> grouped = words.stream()
    .collect(Collectors.groupingBy((String s) -> s.substring(0, 1)));

// Or use an intermediate variable with explicit type:
Function<String, String> firstLetter = s -> s.substring(0, 1);
Map<String, List<String>> grouped2 = words.stream()
    .collect(Collectors.groupingBy(firstLetter));

System.out.println(grouped2);
// {h=[hello], w=[world], g=[generics]}

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.
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).
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.