Generic Methods

Su Suriya Ravichandran Updated 14 Nov 2026
7 min read ·Lesson 89 of 133

A generic method declares its own type parameters, independent of any class. It lets a single method work with many types while keeping full type safety: the compiler works out the actual types from the arguments at each call and checks that everything fits. Generic methods can live in ordinary (non-generic) classes, and they are the backbone of utility APIs such as Collections.max, List.of, Arrays.asList, and Optional.of.

Syntax

The type parameter list goes before the return type:

public static <T> T firstOrDefault(List<T> list, T fallback) {
    return list.isEmpty() ? fallback : list.get(0);
}
public static  <T>        T          firstOrDefault(List<T> list, T fallback)
               └─ type    └─ return  └─ parameters can use T
                  params     type

<T> declares the type parameter; after that, T can be used in the return type, the parameter types, and the body.

String s = firstOrDefault(List.of("x", "y"), "none");      // T = String
Integer n = firstOrDefault(List.<Integer>of(), 0);          // T = Integer
LocalDate d = firstOrDefault(dates, LocalDate.now());       // T = LocalDate

Type Inference

At each call, the compiler infers the type arguments from the method's arguments and, when needed, from the context the result is assigned to. You rarely write them explicitly:

List<String> names = Arrays.asList("Ada", "Linus");   // T inferred as String
List<Integer> empty = Collections.emptyList();         // T inferred from the target type

In the second line there are no arguments at all; the compiler uses the declared type of empty to choose T = Integer.

Explicit type arguments

When inference cannot determine the type, or you want to be explicit, put the type arguments before the method name, after the dot:

List<String> list = Collections.<String>emptyList();
var pairs = Map.<String, Integer>of();

This is called a type witness. It requires a qualifier: Collections.<String>emptyList() or this.<String>method(), not just <String>method().

Generic Methods Versus Generic Classes

Generic methodGeneric class
Declares type parametersOn the method: <T> void m(T x)On the class: class Box<T>
Scope of the parameterThat method onlyAll instance members of the class
Type chosenPer call, usually inferredPer object, when it is created
Typical useUtilities and algorithmsContainers and data holders

A generic class's instance methods can use the class's parameter without declaring it. A method can also declare additional parameters of its own:

public class Box<T> {
    private final T value;
    public Box(T value) { this.value = value; }

    public T get() { return value; }                     // uses the class's T

    public <R> Box<R> map(Function<T, R> f) {            // declares its own R
        return new Box<>(f.apply(value));
    }
}

Static methods need their own type parameters

A generic class's type parameter belongs to instances, so static methods cannot use it. They declare their own instead:

public class Box<T> {
    public static <T> Box<T> of(T value) {    // this T is the method's, not the class's
        return new Box<>(value);
    }
}

Box<String> b = Box.of("hi");

This pattern, a static generic factory, is common: List.of, Optional.of, Map.entry, Comparator.comparing.

Multiple Type Parameters

public static <A, B> List<Map.Entry<A, B>> zip(List<A> left, List<B> right) {
    List<Map.Entry<A, B>> out = new ArrayList<>();
    for (int i = 0; i < Math.min(left.size(), right.size()); i++) {
        out.add(Map.entry(left.get(i), right.get(i)));
    }
    return out;
}

System.out.println(zip(List.of("Q1", "Q2", "Q3"), List.of(42_000, 51_500)));
// [Q1=42000, Q2=51500]

Each parameter is inferred independently: here A = String and B = Integer.

Bounded Type Parameters

To call methods on T, bound it to a type that has them:

public static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.get(0);
    for (T item : items) {
        if (item.compareTo(best) > 0) best = item;
    }
    return best;
}

The bound T extends Comparable<T> makes compareTo available and rejects, at compile time, any call with a type that cannot be compared.

A slightly more flexible bound is T extends Comparable<? super T>. It also accepts types whose compareTo is inherited from a superclass, such as subclasses of a class that implements Comparable for itself. The JDK's own Collections.max uses this form. Wildcards like ? super T are explained in the next lesson.

static <T extends Comparable<? super T>> T clamp(T value, T min, T max) {
    if (value.compareTo(min) < 0) return min;
    if (value.compareTo(max) > 0) return max;
    return value;
}

clamp(120, 0, 100);         // 100
clamp("m", "a", "k");       // "k"
clamp(3.5, 1.0, 5.0);       // 3.5

One method handles integers, strings, and doubles, and any other Comparable type, without overloads.

Generic Methods with Functional Parameters

Generic methods become especially powerful when combined with functional interfaces, because the caller supplies the behavior and the method supplies the structure:

public static <T, R> List<R> mapAll(List<T> items, Function<T, R> f) {
    List<R> out = new ArrayList<>(items.size());
    for (T item : items) out.add(f.apply(item));
    return out;
}

List<Integer> lengths = mapAll(List.of("java", "c", "bash"), String::length);   // [4, 1, 4]

T is inferred from the list, and R from what the function returns. This is the same idea that the Stream API's map, filter, and collect are built on.

Generic Methods and Arrays

Generic varargs methods are convenient but produce a compiler warning about heap pollution, because varargs creates an array and arrays of generic types are not fully type-checked at runtime:

@SafeVarargs
public static <T> List<T> listOf(T... items) {
    return new ArrayList<>(Arrays.asList(items));
}

@SafeVarargs tells the compiler the method only reads from the array and does not store anything unsafe in it. It may be applied only to methods that cannot be overridden (static, final, or private). List.of and Arrays.asList are annotated this way.

When to Write a Generic Method

Write a method generically when its logic does not depend on the specific type of the data it handles: searching, grouping, pairing, transforming, copying, choosing a maximum, wrapping results. If you find yourself writing the same method twice for List<String> and List<Integer>, or writing a method that takes Object and requires casts by the caller, a generic method is usually the answer.

Practical Example: A Small Utility Library

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

public class Utils {
    static <T, K> Map<K, List<T>> groupBy(Collection<T> items, Function<T, K> key) {
        Map<K, List<T>> groups = new TreeMap<>();
        for (T item : items) {
            groups.computeIfAbsent(key.apply(item), k -> new ArrayList<>()).add(item);
        }
        return groups;
    }

    static <T> Optional<T> firstMatch(List<T> items, Predicate<T> test) {
        for (T item : items) {
            if (test.test(item)) return Optional.of(item);
        }
        return Optional.empty();
    }

    static <A, B> List<Map.Entry<A, B>> zip(List<A> left, List<B> right) {
        List<Map.Entry<A, B>> out = new ArrayList<>();
        for (int i = 0; i < Math.min(left.size(), right.size()); i++) {
            out.add(Map.entry(left.get(i), right.get(i)));
        }
        return out;
    }

    static <T extends Comparable<? super T>> T clamp(T value, T min, T max) {
        if (value.compareTo(min) < 0) return min;
        if (value.compareTo(max) > 0) return max;
        return value;
    }

    record Student(String name, String city, int marks) { }

    public static void main(String[] args) {
        List<Student> students = List.of(
            new Student("Ada", "Chennai", 91), new Student("Linus", "Pune", 78),
            new Student("Grace", "Chennai", 85), new Student("Alan", "Delhi", 64));

        Map<String, List<Student>> byCity = groupBy(students, Student::city);
        byCity.forEach((city, list) -> System.out.println(city + ": " + list.size()));

        Map<Boolean, List<Student>> byPass = groupBy(students, s -> s.marks() >= 75);
        System.out.println("Passed: " + byPass.get(true).stream().map(Student::name).toList());

        System.out.println(firstMatch(students, s -> s.marks() < 70).map(Student::name).orElse("none"));
        System.out.println(zip(List.of("Q1", "Q2", "Q3"), List.of(42_000, 51_500)));
        System.out.println(clamp(120, 0, 100) + " " + clamp("m", "a", "k") + " " + clamp(3.5, 1.0, 5.0));
    }
}

Output:

Chennai: 2
Delhi: 1
Pune: 1
Passed: [Ada, Linus, Grace]
Alan
[Q1=42000, Q2=51500]
100 k 3.5

The class itself is not generic; each static method declares the type parameters it needs. groupBy has two: T for the items and K for the key. Calling it with Student::city infers K = String; calling it with a lambda that returns a boolean infers K = Boolean, so the same method produces both a map by city and a pass/fail split. firstMatch returns Optional<T>, so the caller gets an Optional<Student> and can map it to a name. zip infers two unrelated types, and clamp's bound lets it compare integers, strings, and doubles with one implementation. No casts appear anywhere, and every call is fully type-checked.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Generic Methods and get answers drawn from it.

Signed-in readers only.