Skip to content
Ameen Abozaid
PublishedBEG-031-L03

Generic methods

  • Lesson 3 of 6
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

Last lesson put a type parameter on a class. Often you do not need a whole generic class; you need one method that works on many types: take the first element of any list, count an item in any list, swap two values. For that, a method can declare its own type parameter.

In the crate picture, a generic class is a crate with a blank label, fixed once the crate is made. A generic method is a worker who can handle any labeled crate you hand over, and who gives back items of the right label. The worker does not care whether it is a crate of books or a crate of tools, as long as the label stays consistent.

Why this matters: utility methods are everywhere in backend code, and the JDK's own helpers, such as List.of(...) and Collections.sort(...), are generic methods. You will read them in lesson 5.

This lesson builds on "Generic classes" and on static methods from the object-oriented modules.

Learning Objectives

By the end of this lesson you will be able to:

  • Declare a generic method with <T> in the correct position, before the return type.
  • Write static and non-static generic methods.
  • Explain that the compiler infers T from the arguments, and give the explicit form.
  • Write a generic method with two type parameters.
  • Explain that a method's T is independent of a class's T.
  • Recognize two things that cannot be done with a type parameter: new T() and using a primitive array for T[].

Written Explanation

Where <T> goes

A generic method lists its type parameters in angle brackets before the return type:

java
static <T> T first(List<T> list) {
    return list.get(0);
}

Read it left to right: "for some type T, a static method that takes a List<T> and returns a T". The first <T> declares the parameter. The T after it is the return type, and the T in List<T> is a use. Forgetting the first <T> is the classic error: the compiler then looks for a class called T.

The Oracle tutorial states the rule plainly: the type parameter section goes before the method's return type. It works the same for static and non-static methods, and also for constructors.

The compiler works out T

You do not write the type when you call the method:

java
String s = first(List.of("a", "b"));   // T is String
Integer n = first(List.of(10, 20));    // T is Integer

This is type inference. The compiler looks at the argument List<String> and concludes T = String, so the result is a String with no cast. The same method served two types and stayed type safe.

If you ever need to say it explicitly, the type goes before the method name: Utils.<String>first(list). This is rarely necessary, but it is useful when inference cannot decide, for example when passing an empty collection with nothing to learn from.

One method, two parameters

Several parameters are separated by commas, as with classes:

java
static <A, B> Pair<B, A> swap(Pair<A, B> p) {
    return new Pair<>(p.getSecond(), p.getFirst());
}

Given a Pair<String, Integer>, the compiler infers A = String, B = Integer, and the result type is Pair<Integer, String>. The signature itself tells you that the types swap places. That is documentation the compiler checks.

Methods in generic classes

A static method cannot use the class's T, because static members belong to the class itself rather than to any one Box<String> or Box<Integer>. A static factory method in a generic class therefore declares its own parameter:

java
public class Pair<A, B> {
    // ...
    public static <X, Y> Pair<X, Y> of(X first, Y second) {
        return new Pair<>(first, second);
    }
}

The names X and Y here are independent of the class's A and B. Using the same letters would still work, but would shadow the class parameters and confuse readers; picking different letters for a static method makes the independence obvious. In general, a method's type parameter exists only inside that method, while a class's parameter exists across the whole class.

The same method for many types

Where would you need this? Anywhere the logic does not depend on the element type. Counting how many times a value appears in a list needs only equals:

java
static <T> int countOccurrences(List<T> list, T target) {
    int count = 0;
    for (T item : list) {
        if (item.equals(target)) {
            count++;
        }
    }
    return count;
}

Note that the same T ties both parameters: the target must have the same type as the list's elements. countOccurrences(tags, "java") works for a List<String>. You cannot call methods other than those every object has (equals, toString, hashCode), because T could be anything. Giving T more abilities is the topic of lesson 4.

What you cannot do with T

Because Java checks generics at compile time and later removes the type information (type erasure), a type parameter has restrictions. The two that matter now:

  • You cannot create an object of type T. new T() does not compile. The compiler says unexpected type; required: class; found: type parameter T. If a method needs a new object, take it as a parameter, or receive it from the caller.
  • A generic method can take T[], but only an array of reference types. swapInArray(T[] array, int i, int j) works for String[], but not for int[], because int is not a reference type.

Examples

Example 1: first for any list (simple)

java
static <T> T first(List<T> list) { return list.get(0); }

String s = first(List.of("a", "b"));
Integer n = first(List.of(10, 20));
System.out.println(s + " " + n);
text
a 10

Two calls, two different types, no cast, one method.

If you assign the result to the wrong type, inference fails with a message that names both sides:

java
Integer n = first(List.of("a", "b"));
text
error: incompatible types: inference variable T has incompatible bounds
    upper bounds: Integer,Object
    lower bounds: String

Read it as: the result must be an Integer (upper bounds), but the argument supplies String (lower bounds).

Example 2: firstOrDefault and countOccurrences (practical)

java
static <T> T firstOrDefault(List<T> list, T fallback) {
    return list.isEmpty() ? fallback : list.get(0);
}

List<String> empty = new ArrayList<>();
System.out.println(Utils.firstOrDefault(empty, "none"));
System.out.println(Utils.<String>firstOrDefault(empty, "explicit"));

List<String> tags = List.of("java", "sql", "java", "git", "java");
System.out.println(Utils.countOccurrences(tags, "java"));
text
none
explicit
3

The second call spells out <String>, which gives the same result as the inferred version. It is shown to demonstrate the syntax, not because you should write it every time.

Example 3: static factory and swap (realistic)

java
Pair<String, Integer> p = Pair.of("Ada", 36);
Pair<Integer, String> q = Utils.swap(p);
System.out.println(p + " -> " + q);

String[] words = {"x", "y", "z"};
Utils.swapInArray(words, 0, 2);
System.out.println(String.join(",", words));
text
(Ada, 36) -> (36, Ada)
z,y,x

Pair.of saves you from writing the type arguments twice on one line. The swap result's type changed from Pair<String, Integer> to Pair<Integer, String>, and the compiler tracked it.

For the array version, the body of swapInArray uses a temporary of type T. Passing an int[] instead of a String[] is rejected: the message ends with inference variable T has incompatible bounds, equality constraints: int.

Backend Insight

Static utility methods are the backend programmer's toolbox: validation helpers, mapping helpers, and "return the only element or fail" helpers. A generic version is written once and used for User, Order, and Invoice alike.

You will also meet generic methods when you use libraries. Calling a method that returns T determined by what you pass in, such as a request-reading helper that maps a payload onto the type you ask for, is the same mechanism. The signature tells you what comes back, and the compiler enforces it. A utility that takes Class<T> and returns T is a common way around the new T() restriction, which you will see once reflection appears later in the roadmap.

Common Mistakes

Mistake 1: Forgetting the <T> before the return type

  • Incorrect: static T first(List<T> list)
  • Correct: static <T> T first(List<T> list)

Without the declaration the compiler reports it cannot find symbol T.

Mistake 2: Trying new T()

  • Incorrect: static <T> T make() { return new T(); }
  • Correct: take the object from the caller, for example static <T> T orElse(T value, T fallback).

The type is not known when the code runs, so Java cannot build one. The compiler's message is unexpected type.

Mistake 3: Assuming T can do more than Object can

Inside a method with a plain <T>, you can call only what every object has, such as equals and toString. Calling item.compareTo(other) on a plain T fails with cannot find symbol. Lesson 4 gives T a bound to permit it.

Mistake 4: Mixing types that the same T ties together

countOccurrences(List<String>, Integer) is rejected: one T must fit both arguments, and a String list cannot take an Integer target. If you really need two independent types, declare <A, B>.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Where does <T> go in a generic method: before the method name, before the return type, or after the parameters?

Show solution A1

A1. Before the return type: static <T> T last(...).

A2. Write static <T> T last(List<T> list) returning the final element.

Show solution A2

A2.

java
static <T> T last(List<T> list) {
    return list.get(list.size() - 1);
}

A3. What type does the compiler infer for T in last(List.of(1, 2, 3))?

Show solution A3

A3. Integer, because the argument is a List<Integer>.

Level B: Application

B1. Write static <T> boolean contains(List<T> list, T target) using equals.

Show solution B1

B1.

java
static <T> boolean contains(List<T> list, T target) {
    for (T item : list) {
        if (item.equals(target)) return true;
    }
    return false;
}

B2. Write static <T> List<T> reversed(List<T> list) that returns a new list with the elements in reverse order.

Show solution B2

B2.

java
static <T> List<T> reversed(List<T> list) {
    List<T> out = new ArrayList<>();
    for (int i = list.size() - 1; i >= 0; i--) {
        out.add(list.get(i));
    }
    return out;
}

reversed(List.of(1, 2, 3)) gives [3, 2, 1].

B3. Why does the line static <T> T make() { return new T(); } not compile?

Show solution B3

B3. T is only a placeholder; the real type is unknown when the code runs, so Java cannot build an instance of it. The compiler reports unexpected type, required: class, found: type parameter T.

Level C: Challenge

C1. Write static <T> List<T> merge(List<T> a, List<T> b) returning a new list with the elements of a followed by those of b. Then say what happens when you call it with a List<String> and a List<Integer>, and why.

Show solution C1

C1.

java
static <T> List<T> merge(List<T> a, List<T> b) {
    List<T> out = new ArrayList<>(a);
    out.addAll(b);
    return out;
}

Called with a List<String> and a List<Integer>, it does not compile: T would have to be String and Integer at once (inference variable T has incompatible equality constraints Integer,String). Merging two different element types needs a wildcard signature, which lesson 5 shows.

C2. A teammate wrote static <T> T pick(T a, T b) and calls pick("x", 5). Does it compile? What does the compiler infer, and what can you do with the result?

Show solution C2

C2. It compiles. The compiler picks a type that is a common supertype of String and Integer, so the result is not a String or an Integer but a shared supertype. In practice you can call only the methods of that supertype (for example toString); to use it as a String you would need a cast, which brings back the original problem. Use two type parameters or two separate methods if the types should differ. (Compiled with JDK 21: the call compiles, and assigning the result to Object or Comparable<?> is accepted.)

Mini Challenge

Write a small Utils class with these methods and a main that calls each with at least two element types:

  1. static <T> T firstOrDefault(List<T> list, T fallback)
  2. static <T> int indexOfItem(List<T> list, T target) returning -1 when not found.
  3. static <K, V> Pair<K, V> pairOf(K key, V value) (you can reuse your Pair from lesson 2).

Check your work: every call compiles without a cast, and a deliberately wrong call (for example assigning firstOrDefault(names, "x") to an Integer) is rejected with an inference message you can read.

Summary & Next Lesson

Key takeaways

  • A generic method declares its own type parameters in angle brackets before the return type: static <T> T first(List<T> list).
  • The compiler infers T from the arguments. The explicit form Utils.<String>first(list) is rarely needed.
  • A method's type parameter is independent of any class parameter. Static methods cannot use the class's T.
  • With a plain T you can call only what every object has. new T() is not allowed.
  • Generic methods are how one utility serves many types without casts.

Next lesson

Next, "Bounded types" limits what T may be, with <T extends Number> and <T extends Comparable<T>>, so your generic method can finally call something useful on T, like compareTo.

Self-check

  1. Where does the type parameter list go in a generic method?
  2. How does the compiler know what T is when you call first(List.of("a"))?
  3. Can a static method in Box<T> use T?
  4. Why does new T() not compile?
  5. What can you call on a value of plain type T?
Show answers
  1. In angle brackets before the return type.
  2. It infers T from the arguments: the list is a List<String>, so T is String.
  3. No. Static members belong to the class, not to a particular Box<...>. A static method needs its own type parameter.
  4. The actual type is not known at runtime, so Java cannot create an instance of it.
  5. Only the methods every object has, such as equals, hashCode, and toString.

References

  • Oracle, The Java Tutorials: Generic Methods. Supports the position of the type parameter section before the return type, static and non-static generic methods, explicit type arguments, and type inference.
  • Oracle, The Java Tutorials: Restrictions on Generics. Supports that new T() is illegal, and that type arguments cannot be primitives.
  • Oracle, The Java Tutorials: Type Erasure. Supports the statement that generic type information is removed after compile-time checking, which is why the type is unknown at runtime.
  • Examples and compiler messages were compiled and run with JDK 21. The reflection remark in Backend Insight and the explanation that a static method cannot use the class's T for the reason given are from the course author's general knowledge; the tutorial's restrictions page gives the related rule for static fields.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.