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

الفئات العامة

  • الدرس 2 من 6
  • 9 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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>, not Box<int>).
  • Explain why Box<String> is not a Box<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:

java
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.

java
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

java
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:

java
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:

java
Box<int> a = new Box<>(1);       // does not compile
Box<Integer> a = new Box<>(1);   // fine

Use 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:

java
Box<String> b = new Box<>("x");
Box<Object> c = b;   // does not compile

It 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)

java
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:

text
hi 14
3 37

p.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.

java
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(); }
}
java
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());
text
type B
type A
true

The 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:

java
Box<int> a = new Box<>(1);
Box<Object> c = new Box<String>("x");
Box<String> d = new Box<>(5);
text
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: Integer

The 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() returns Object)
  • 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.

java
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.

java
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.

java
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.

  1. Give it a constructor Result(T value, String message) where one of the two is null. (Static factory methods such as ok(...) need their own type parameter; lesson 3 shows how.)
  2. Add isOk(), getValue(), and getMessage().
  3. 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 on new.
  • A class can have several parameters: Pair<A, B>. Common names are T, E, K, V.
  • Type arguments must be reference types. Use Integer, not int.
  • Box<String> is not a Box<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

  1. Where is the type parameter declared in a generic class?
  2. What is the difference between a type parameter and a type argument?
  3. Why is Box<int> illegal?
  4. What does new Box<>("hi") infer?
  5. Why is Box<String> not assignable to Box<Object>?
أظهر الإجابات
  1. After the class name, in angle brackets: class Box<T>.
  2. A type parameter is the placeholder in the declaration (T). A type argument is the real type supplied by the user (String).
  3. Type arguments must be reference types, and int is a primitive. Use Box<Integer>.
  4. Box<String>, taken from the constructor argument and the variable type.
  5. 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 a List<Number>, the same rule used here for Box<String> and Box<Object>. The explanation of the unsafe set(42) scenario is this course's own, not from the page.
  • All code and compiler messages above were compiled and run with JDK 21.

التطبيق

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

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

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

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