Java OO & Functional/Wildcard `? extends`, `? super` — PECS rule
17/33
Bài 17 / 33~18 phútGenerics & CollectionsMiễn phí lượt xem

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

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên