انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-031-L03

الدوال العامة

  • الدرس 3 من 6
  • 10 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

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?

أظهر الحل A1

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

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

أظهر الحل 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))?

أظهر الحل 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.

أظهر الحل 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.

أظهر الحل 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?

أظهر الحل 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.

أظهر الحل 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?

أظهر الحل 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?
أظهر الإجابات
  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.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.