Java — Từ Zero đến Senior/Generics & Collections/Wildcard `? extends`, `? super` — PECS rule
3/5
~18 phútGenerics & Collections

Wildcard `? extends`, `? super` — PECS rule

Invariance của generics, upper bound wildcard, lower bound wildcard, rule PECS (Producer Extends Consumer Super) của Effective Java, và unbounded wildcard List<?>.

List<String> có phải List<Object> không? Trực giác trả lời "có, vì String là Object". Sai — Java generics invariant, List<String> không là subtype của List<Object>. Đây là rule đầu tiên làm dev mới bối rối.

Để lách cứng nhắc này, Java cung cấp wildcard ? với extends (upper bound) và super (lower bound). Dùng đúng cho API mềm dẻo; dùng sai gây compile error lạ. Rule kinh điển PECS (Producer Extends, Consumer Super) giúp chọn đúng.

Bài này giải thích invariance, 2 loại bounded wildcard, PECS, và khi nào dùng <?> không có bound.

1. Invariance — List<String> KHÔNG phải List<Object>

List<String> strings = new ArrayList<>();
List<Object> objects = strings;   // COMPILE ERROR

Lý do: nếu cho phép gán, chương trình sau sẽ phá type safety:

// Gia su hop le:
List<String> strings = new ArrayList<>();
List<Object> objects = strings;    // strings va objects cung list
objects.add(42);                    // Object[] nhan Integer
String s = strings.get(0);          // ClassCastException — la Integer

Java cấm để ngăn. List<A>List<B> là 2 kiểu độc lậpA là subtype của B. Đây gọi là invariance của generics.

So sánh với array — Java array covariant (lịch sử):

String[] strings = new String[10];
Object[] objects = strings;   // OK voi array — covariance
objects[0] = 42;              // RUNTIME: ArrayStoreException

Array Java kiểm tra type tại runtime (có type info lưu trong object). Generics erase → không check runtime → Java chọn invariance compile-time để tránh ClassCastException.

2. Upper bound wildcard — ? extends T

Đôi khi bạn muốn method nhận bất kỳ subtype nào của T. Dùng ? extends:

public static double sumOfList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) sum += n.doubleValue();
    return sum;
}

sumOfList(List.<Integer>of(1, 2, 3));    // OK — Integer extends Number
sumOfList(List.<Double>of(1.5, 2.5));     // OK — Double extends Number
sumOfList(List.<Number>of(1, 2.5));       // OK — Number extends Number (chinh no)

List<? extends Number> = "list của một kiểu nào đó là subtype Number (hoặc Number)". Compiler không biết chính xác T là gì, chỉ biết T ≤ Number.

2.1 Hệ quả — không add được

List<? extends Number> list = new ArrayList<Integer>();
list.add(42);            // COMPILE ERROR
list.add(new Integer(42));   // COMPILE ERROR
list.add(null);           // OK — null tuong thich moi reference

Compiler không biết list chính xác là List<Integer> hay List<Double> hay List<Number>. Add Integer có thể vi phạm nếu list là List<Double>. Để an toàn, compiler cấm mọi add trừ null.

Kết luận: ? extendsread-only cho parameter — bạn lấy ra được (consume), không đặt vào được.

3. Lower bound wildcard — ? super T

Ngược lại: ? super T = "kiểu nào đó là supertype của T (hoặc T)".

public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();

addNumbers(ints);    // OK — Integer super Integer
addNumbers(nums);    // OK — Number super Integer
addNumbers(objs);    // OK — Object super Integer

List<? super Integer> có thể là List<Integer>, List<Number>, List<Object>. Tất cả đều chấp nhận add Integer (vì Integer "lên cao" hợp lệ).

3.1 Hệ quả — đọc ra Object

List<? super Integer> list = ...;
Integer x = list.get(0);   // COMPILE ERROR — khong biet la Integer hay Number hay Object
Object y = list.get(0);     // OK — chac chan la Object (supertype cua tat ca)

Compiler không biết chính xác kiểu element — chỉ biết là supertype của Integer. Lấy ra về Object là lựa chọn an toàn duy nhất.

Kết luận: ? superwrite-only cho kiểu cụ thể — add được, nhưng lấy ra chỉ được Object.

4. PECS — Producer Extends, Consumer Super

Rule Effective Java:

  • Nếu tham số sản xuất (produce) giá trị T → dùng ? extends T (đọc ra T).
  • Nếu tham số tiêu thụ (consume) giá trị T → dùng ? super T (ghi vào).

Ví dụ kinh điển — Collections.copy:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}
  • srcproducer — nơi bạn lấy T ra? extends T.
  • destconsumer — nơi bạn đặt T vào? super T.

Dùng:

List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(List.of(0, 0, 0));
Collections.copy(dest, src);    // OK — Integer extends T=Integer, Number super T=Integer

Nếu viết cả hai bên List<T> (không wildcard), caller phải truyền đúng List<Integer> cho cả hai — mất flexibility.

4.1 PECS thực tế — stream Collector

// Interface Collector<T, A, R>:
// T — element input
// A — accumulator type
// R — result

static <T, R> R collect(Stream<T> stream,
                        Supplier<R> supplier,
                        BiConsumer<? super R, ? super T> accumulator,
                        BiConsumer<? super R, ? super R> combiner) { ... }

Mọi functional param đều ? super — consumer. Rule PECS áp dụng khắp JDK.

5. Unbounded wildcard — List<?>

List<?> = "list của kiểu gì đó, không biết và không quan tâm".

public static void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

printAll(List.of(1, 2, 3));
printAll(List.of("a", "b"));
printAll(List.of(1.5, 2.5));

Khác List<Object>List<?> nhận mọi List<X> (không invariance).

Hạn chế: không add được element nào (trừ null).

Dùng khi:

  • Method chỉ đọc (không add), không quan tâm kiểu element.
  • Vd: size(), isEmpty(), contains(Object), print, log.

6. Khi nào dùng wildcard vs type parameter?

// Phien ban 1 — type parameter
public static <T> void swap(List<T> list, int i, int j) {
    T tmp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, tmp);
}

// Phien ban 2 — wildcard
public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}
private static <T> void swapHelper(List<T> list, int i, int j) {
    T tmp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, tmp);
}

Phiên bản 2 có signature public sạch hơn (List<?>) — không lộ type parameter cho caller, nhưng nội bộ vẫn cần <T> cho compile logic.

Rule Effective Java Item 31: API public ưu tiên wildcard; internal dùng type parameter.

7. Wildcard capture

Compiler có cơ chế "capture" wildcard — khi đọc element, tạo type variable tạm:

public static void reverse(List<?> list) {
    // compiler "capture" ? thanh CAP#1 (internal type)
    List<Object> tmp = new ArrayList<>(list);
    for (int i = 0; i < list.size(); i++) {
        list.set(i, tmp.get(list.size() - 1 - i));   // COMPILE ERROR
    }
}

list.set(i, Object) fail vì compiler không chắc element type. Workaround: helper method generic capture wildcard:

public static void reverse(List<?> list) { reverseHelper(list); }
private static <T> void reverseHelper(List<T> list) {
    List<T> tmp = new ArrayList<>(list);
    for (int i = 0; i < list.size(); i++) {
        list.set(i, tmp.get(list.size() - 1 - i));   // OK — T co dinh
    }
}

Pattern phổ biến — chấp nhận wildcard public, chuyển sang type parameter internal.

8. Pitfall tổng hợp

Nhầm 1: Gán List<String> cho List<Object>.

List<Object> objs = new ArrayList<String>();   // COMPILE ERROR

✅ Dùng List<? extends Object> hoặc List<?> nếu chỉ đọc.

Nhầm 2: Add vào List<? extends T>.

List<? extends Number> list = ...;
list.add(42);   // COMPILE ERROR

✅ Đổi sang List<? super Integer> nếu cần add, hoặc không wildcard.

Nhầm 3: Lấy ra T từ List<? super T>.

List<? super Integer> list = ...;
Integer x = list.get(0);   // COMPILE ERROR

✅ Lấy ra Object. Hoặc dùng List<? extends T>.

Nhầm 4: Wildcard trong return type — khó dùng.

public static List<? extends Number> getNumbers() { ... }
// Caller: List<? extends Number> list = ...; list.add(42) — fail

✅ Return type nên có type parameter cụ thể để caller dùng được.

Nhầm 5: Nhầm List<Object> với List<?>.

void foo(List<Object> list) { ... }
foo(new ArrayList<String>());   // COMPILE ERROR

List<?> nếu chỉ đọc và nhận mọi kiểu.

9. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: PECS do Joshua Bloch phổ biến trong Effective Java. Rule đơn giản nhưng cứu dev khỏi lỗi wildcard phức tạp — áp được 90% trường hợp. Collection API JDK (Collections.copy, Stream.collect, Comparator.thenComparing) đều thể hiện PECS rõ ràng — đọc signature của chúng là cách nhanh nhất để thuộc.

10. Tóm tắt

  • Generics invariant: List<String> không phải List<Object>. Khác array (covariant).
  • ? extends T — upper bound wildcard. Producer — lấy ra T, không add.
  • ? super T — lower bound wildcard. Consumer — add T, lấy ra Object.
  • ? unbounded — đọc về Object, không add (trừ null).
  • PECS rule: Producer Extends, Consumer Super.
  • API public ưu tiên wildcard; internal dùng type parameter.
  • Wildcard trong return type khó dùng → tránh, trả kiểu cụ thể.
  • Wildcard capture qua helper method — pattern phổ biến.

11. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau compile không?
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints;

Không compile — "incompatible types". Generics invariant: List<Integer> không phải subtype của List<Number>Integer là subtype của Number.

Lý do: nếu cho phép, compiler mất khả năng enforce type safety — caller có thể nums.add(new Double(1.5)) làm corrupted ints.

Fix:

  • Nếu chỉ đọc: List<? extends Number> nums = ints; — OK, chấp nhận List<Integer>.
  • Nếu cần add: đổi logic — không thể gán List<Integer> thành List<Number> trực tiếp vì semantics khác.
Q2
Áp dụng PECS — sign method sau đúng thế nào?
public static <T> void copy(List<T> dest, List<T> src) {
    for (T item : src) dest.add(item);
}

Method hiện tại yêu cầu destsrccùng type parameter. Gán linh hoạt hơn với PECS:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) dest.add(item);
}

Lý do:

  • srcproducer (lấy T ra) → ? extends T.
  • destconsumer (đặt T vào) → ? super T.

Với signature mới:

List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>();
copy(dest, src);   // OK — before would fail

Đây chính xác là signature Collections.copy trong JDK.

Q3
Đoạn sau dòng nào compile OK, dòng nào fail?
List<? extends Number> list = new ArrayList<Integer>();
list.add(42);
Number n = list.get(0);
list.add(null);
list.size();
DòngKết quả
List<? extends Number> list = new ArrayList<Integer>();OK — ArrayList<Integer> thoả ? extends Number.
list.add(42)FAIL — compiler không biết T chính xác; có thể là List<Double> → từ chối.
Number n = list.get(0)OK — mọi element là subtype Number, lấy về Number an toàn.
list.add(null)OK — null tương thích với mọi reference.
list.size()OK — không phụ thuộc element type.

Rule: ? extends là "read-only" cho element type (trừ null). Muốn add, dùng ? super.

Q4
Khi nào dùng List<?> thay vì List<Object>?

List<?>List<Object> khác nhau:

  • List<?> — "list của kiểu gì đó, không quan tâm". Nhận được mọi List<X>: List<Integer>, List<String>...
  • List<Object> — "list khai báo chính xác là List<Object>". Chỉ nhận List<Object> — không nhận List<String> (invariance).
static void printAll(List<?> list) { list.forEach(System.out::println); }
static void printAllObj(List<Object> list) { list.forEach(System.out::println); }

List<String> s = List.of("a", "b");
printAll(s);      // OK
printAllObj(s);   // COMPILE ERROR — invariance

Dùng List<?> khi method chỉ đọc element, không quan tâm kiểu — ngắn, linh hoạt, không bị invariance chặn.

Dùng List<Object> khi thực sự cần một list khai Object (hiếm trong API hiện đại).

Q5
Method sau compile không? Nếu không, fix thế nào?
public static <T> void swap(List<?> list, int i, int j) {
    T tmp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, tmp);
}

Không compile. List<?> không cho set/add element (trừ null). T ở signature không liên quan đến ? — hai type parameter độc lập.

Fix — pattern "wildcard capture qua helper":

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

private static <T> void swapHelper(List<T> list, int i, int j) {
    T tmp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, tmp);
}

Public signature có List<?> — caller dùng với bất kỳ List<X>. Private helper có <T> — compiler "capture" wildcard thành T cụ thể khi forward call, cho phép set trong body.

Đây là pattern Effective Java Item 31 khuyến nghị: API public dùng wildcard, internal dùng type parameter.


Bài tiếp theo: Collections framework — List, Set, Map và khi nào chọn implementation nào