الفئات العامة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In the last lesson you saw why a collection that says what it holds is safer than one that holds anything. Those collections come from the JDK. This lesson shows how to write your own: a class with a type parameter, so that one piece of code can serve many types and still be checked by the compiler.
Back to the crate picture. A generic class is a crate factory with a blank label. The class is written once; whoever uses it writes the label (String, Integer, Contact) when they create a crate. From then on the crate accepts and returns only that type.
Why this matters: repositories, API responses, caches, and result wrappers in backend code are all generic classes. You will read them constantly, and you will write a few.
This lesson builds on "Life before generics" and on classes, constructors, and getters from the object-oriented modules.
Learning Objectives
By the end of this lesson you will be able to:
- Declare a generic class with one type parameter, such as
Box<T>. - Declare a generic class with two or more type parameters, such as
Pair<A, B>. - Use the type parameter as a field type, parameter type, and return type.
- Create objects with explicit type arguments and with the diamond
<>. - Explain why type arguments must be reference types (
Box<Integer>, notBox<int>). - Explain why
Box<String>is not aBox<Object>. - Recognize the conventional names
T,E,K,V.
Written Explanation
From Object to a type parameter
Here is the pre-generics way to write a box that holds one value:
class Box {
private Object value;
Box(Object value) { this.value = value; }
Object get() { return value; }
}Anything fits, and every reader needs a cast. The generic version adds one thing: a type parameter after the class name.
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}<T> declares T as a placeholder for a type. Inside the class, T is used where a type is expected: a field, a constructor parameter, a method parameter, a return type. Nobody knows yet what T will be. The user of the class decides.
Using it: type arguments
Box<String> b = new Box<>("hi");
String s = b.get(); // no cast
Box<Integer> n = new Box<>(7);
int doubled = n.get() * 2;The vocabulary matters, because error messages use it. In Box<T>, T is the type parameter. In Box<String>, String is the type argument. Writing Box<String> is called a generic type invocation, and the compiler then treats every T in the class as String for that variable.
The <> on the right side is the diamond. It tells the compiler to work out the type arguments from the left side or from the constructor arguments, so you do not repeat yourself. Since Java 7, new Box<>("hi") means the same as new Box<String>("hi").
Names of type parameters
Any identifier works, but the convention is a single uppercase letter. The Oracle tutorial lists the common ones: T for type, E for element (used throughout the Collections Framework), K for key, V for value, N for number. Following the convention lets readers recognize the role at a glance: Map<K, V> needs no explanation.
More than one type parameter
Separate the parameters with commas:
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
}Pair<String, Integer> has a String first and an Integer second. getFirst() returns String and getSecond() returns Integer, with no casts. The two parameters are independent: A and B may be the same type or different types.
Only reference types
A type argument must be a class, interface, or array type, a reference type. The eight primitive types (int, double, boolean, ...) are not allowed:
Box<int> a = new Box<>(1); // does not compile
Box<Integer> a = new Box<>(1); // fineUse the wrapper class instead (Integer, Double, Boolean). Java converts between int and Integer automatically (autoboxing), so new Box<>(1) and int x = box.get() still read naturally. Two consequences to remember: a wrapper can be null, and boxing has a small cost. For everyday backend code that cost is not a concern.
A box of strings is not a box of objects
Since String is an Object, you might expect this to work:
Box<String> b = new Box<>("x");
Box<Object> c = b; // does not compileIt does not. Generic types are not related just because their type arguments are related. The reason is safety: if the assignment were allowed, you could call c.set(42) and put an Integer into a box that b still believes holds only strings. Then the read String s = b.get() would break, which is exactly the problem generics exist to prevent. Lesson 5 shows how wildcards give you controlled flexibility here.
Examples
Example 1: Box and Pair in use (simple)
Box<String> b = new Box<>("hi");
String s = b.get();
Box<Integer> n = new Box<>(7);
int doubled = n.get() * 2;
System.out.println(s + " " + doubled);
Pair<String, Integer> p = new Pair<>("Ada", 36);
System.out.println(p.getFirst().length() + " " + (p.getSecond() + 1));Output:
hi 14
3 37p.getFirst().length() works without a cast because the compiler knows getFirst() returns a String.
Example 2: an undo stack (practical)
A stack holds items and gives back the latest one first. Writing it once with E serves every element type.
import java.util.ArrayList;
import java.util.List;
public class UndoStack<E> {
private final List<E> items = new ArrayList<>();
public void push(E item) { items.add(item); }
public E pop() {
if (items.isEmpty()) {
throw new IllegalStateException("stack is empty");
}
return items.remove(items.size() - 1);
}
public boolean isEmpty() { return items.isEmpty(); }
}UndoStack<String> undo = new UndoStack<>();
undo.push("type A");
undo.push("type B");
System.out.println(undo.pop());
System.out.println(undo.pop());
System.out.println(undo.isEmpty());type B
type A
trueThe class uses a List<E> inside. Generic classes freely use other generic types with their own parameter as the argument.
Example 3: the compiler as a guard (realistic)
Imagine UndoStack<String> is used by a text editor and someone accidentally pushes a number. Each of these is rejected with compiler output copied from JDK 21:
Box<int> a = new Box<>(1);
Box<Object> c = new Box<String>("x");
Box<String> d = new Box<>(5);error: unexpected type
Box<int> a = new Box<>(1);
required: reference
found: int
error: incompatible types: Box<String> cannot be converted to Box<Object>
error: incompatible types: cannot infer type arguments for Box<>
reason: inference variable T has incompatible bounds
equality constraints: String
lower bounds: IntegerThe third message looks heavy, but the two lines at the end say it all: T has to be String (from the left side), and the argument 5 is an Integer. Learn to find the "equality constraints" and "lower bounds" lines; you will see them again.
Backend Insight
Backend code uses the same shapes as Box and Pair under other names. A wrapper such as Response<T> carries a status plus a payload of any type. A Page<T> carries one page of results and a total count. A cache is a Cache<K, V>. A repository (lesson 6) is a Repository<T, ID>. Each is a class written once with the type parameters left open, then instantiated for User, Order, Product.
Writing the class once avoids the duplicated UserBox, OrderBox, ProductBox classes, and because the type is checked, a Page<Order> can never silently contain a User.
Common Mistakes
Mistake 1: Using a primitive as a type argument
- Incorrect:
Box<int>,Pair<String, double> - Correct:
Box<Integer>,Pair<String, Double>
Mistake 2: Dropping the type argument
- Incorrect:
Box b = new Box("hi");(a raw type: no checking,get()returnsObject) - Correct:
Box<String> b = new Box<>("hi");
Mistake 3: Expecting Box<String> to be a Box<Object>
Subtyping does not carry over to type arguments. If a method needs to accept boxes of several types, you will use a type parameter on the method (lesson 3) or a wildcard (lesson 5), not Box<Object>.
Mistake 4: Using a type parameter name that hides a real class
Naming a parameter String or Order compiles but confuses everyone, because inside the class that name now means the placeholder. Stay with T, E, K, V, or short capital-letter names.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. In class Holder<T>, which word is the type parameter? In Holder<Double> h, which is the type argument?
أظهر الحل A1
A1. T is the type parameter. Double is the type argument.
A2. Which of these compile? (a) Holder<String> (b) Holder<int> (c) Holder<Integer> (d) Holder<> used as a variable type.
أظهر الحل A2
A2. (a) and (c) compile. (b) does not, because int is not a reference type. (d) is not allowed as a variable type: the diamond is only for creating objects with new.
A3. What does the diamond in new Holder<>() save you from writing?
أظهر الحل A3
A3. The type argument on the right side: the compiler infers it, so you write Holder<String> h = new Holder<>(); instead of repeating String.
Level B: Application
B1. Write a generic class Holder<T> with set, get, and isPresent() (true when the value is not null). Create a Holder<String> and call all three methods.
أظهر الحل B1
B1.
class Holder<T> {
private T value;
void set(T v) { value = v; }
T get() { return value; }
boolean isPresent() { return value != null; }
}Usage: Holder<String> h = new Holder<>(); then h.isPresent() is false; after h.set("x") it is true and h.get() returns "x". (Compiled and run with JDK 21.)
B2. Write Triple<A, B, C> with a constructor and three getters. Create a Triple<String, Integer, Boolean>.
أظهر الحل B2
B2.
class Triple<A, B, C> {
private final A first; private final B second; private final C third;
Triple(A a, B b, C c) { first = a; second = b; third = c; }
A getFirst() { return first; }
B getSecond() { return second; }
C getThird() { return third; }
}
Triple<String, Integer, Boolean> t = new Triple<>("Ada", 36, true);B3. Why can this not compile? Holder<String> h = new Holder<>(); Holder<Object> o = h;
أظهر الحل B3
B3. Generic types are not related by their type arguments. If the assignment were allowed, o.set(42) would put an Integer into a box that h believes holds only strings.
Level C: Challenge
C1. Write Counter<T> that counts how many times each item was added, with add(T item) and int count(T item) (0 for an item never added). Use a Map<T, Integer> inside.
أظهر الحل C1
C1.
class Counter<T> {
private final Map<T, Integer> counts = new HashMap<>();
void add(T item) { counts.put(item, count(item) + 1); }
int count(T item) {
Integer c = counts.get(item);
return c == null ? 0 : c;
}
}After add("a"), add("a"), add("b"), count("a") is 2 and count("z") is 0. This needs import java.util.HashMap; and import java.util.Map;.
C2. Explain why Counter<int> is impossible and what to write instead. Then say what happens in counts.get(item) for an item that was never added, and how your count handles it.
أظهر الحل C2
C2. Type arguments must be reference types, so write Counter<Integer>. For an item that was never added, counts.get(item) returns null (the map has no entry), and count checks for null and returns 0. Writing int c = counts.get(item) would throw NullPointerException while unboxing the missing value.
Mini Challenge
Design a generic class Result<T> for a method that can succeed or fail. It holds either a value of type T or an error message.
- Give it a constructor
Result(T value, String message)where one of the two isnull. (Static factory methods such asok(...)need their own type parameter; lesson 3 shows how.) - Add
isOk(),getValue(), andgetMessage(). - Create a
Result<Integer>for a successful parse and a failed one.
Check your work: getValue() returns Integer with no cast, and new Result<Integer>("5", null) is rejected by the compiler.
Summary & Next Lesson
Key takeaways
- A generic class declares a type parameter after its name, such as
Box<T>, and uses it as a type inside. - The user supplies a type argument:
Box<String>. The diamond<>lets the compiler infer it onnew. - A class can have several parameters:
Pair<A, B>. Common names areT,E,K,V. - Type arguments must be reference types. Use
Integer, notint. Box<String>is not aBox<Object>: this protects the type safety.
Next lesson
Next, "Generic methods" gives a single method its own type parameter, so a utility such as first(List<T>) works for any list type.
Self-check
- Where is the type parameter declared in a generic class?
- What is the difference between a type parameter and a type argument?
- Why is
Box<int>illegal? - What does
new Box<>("hi")infer? - Why is
Box<String>not assignable toBox<Object>?
أظهر الإجابات
- After the class name, in angle brackets:
class Box<T>. - A type parameter is the placeholder in the declaration (
T). A type argument is the real type supplied by the user (String). - Type arguments must be reference types, and
intis a primitive. UseBox<Integer>. Box<String>, taken from the constructor argument and the variable type.- Otherwise a value of another type could be stored through the
Box<Object>reference into a box that is supposed to hold strings only.
References
- Oracle, The Java Tutorials: Generic Types. Supports the
Box<T>declaration, type parameter versus type argument, naming conventions (E, K, N, T, V), multiple parameters, the diamond, and that primitives are not allowed. - Oracle, The Java Tutorials: Restrictions on Generics. Supports that type arguments cannot be primitives and that
new T()is illegal. - Oracle, The Java Tutorials: Generics, Inheritance, and Subtypes. Supports that
List<Integer>is not aList<Number>, the same rule used here forBox<String>andBox<Object>. The explanation of the unsafeset(42)scenario is this course's own, not from the page. - All code and compiler messages above were compiled and run with JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.