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
// ── 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 class — class 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 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
// ── 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]}