الدوال العامة
في هذا الدرس 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
Tfrom the arguments, and give the explicit form. - Write a generic method with two type parameters.
- Explain that a method's
Tis independent of a class'sT. - Recognize two things that cannot be done with a type parameter:
new T()and using a primitive array forT[].
Written Explanation
Where <T> goes
A generic method lists its type parameters in angle brackets before the return type:
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:
String s = first(List.of("a", "b")); // T is String
Integer n = first(List.of(10, 20)); // T is IntegerThis 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:
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:
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:
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 saysunexpected 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 forString[], but not forint[], becauseintis not a reference type.
Examples
Example 1: first for any list (simple)
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);a 10Two 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:
Integer n = first(List.of("a", "b"));error: incompatible types: inference variable T has incompatible bounds
upper bounds: Integer,Object
lower bounds: StringRead it as: the result must be an Integer (upper bounds), but the argument supplies String (lower bounds).
Example 2: firstOrDefault and countOccurrences (practical)
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"));none
explicit
3The 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)
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));(Ada, 36) -> (36, Ada)
z,y,xPair.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.
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.
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.
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.
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:
static <T> T firstOrDefault(List<T> list, T fallback)static <T> int indexOfItem(List<T> list, T target)returning -1 when not found.static <K, V> Pair<K, V> pairOf(K key, V value)(you can reuse yourPairfrom 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
Tfrom the arguments. The explicit formUtils.<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
Tyou 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
- Where does the type parameter list go in a generic method?
- How does the compiler know what
Tis when you callfirst(List.of("a"))? - Can a static method in
Box<T>useT? - Why does
new T()not compile? - What can you call on a value of plain type
T?
أظهر الإجابات
- In angle brackets before the return type.
- It infers
Tfrom the arguments: the list is aList<String>, soTisString. - No. Static members belong to the class, not to a particular
Box<...>. A static method needs its own type parameter. - The actual type is not known at runtime, so Java cannot create an instance of it.
- Only the methods every object has, such as
equals,hashCode, andtoString.
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
Tfor the reason given are from the course author's general knowledge; the tutorial's restrictions page gives the related rule for static fields.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.