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> và List<B> là 2 kiểu độc lập dù A 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: ? extends là read-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: ? super là write-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));
}
}
srclà producer — nơi bạn lấy T ra →? extends T.destlà consumer — 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
Spec / reference chính thức:
- JLS §4.5.1 — Type Arguments of Parameterized Types — cú pháp wildcard.
- JLS §5.1.10 — Capture Conversion — rule capture wildcard thành type variable.
- Oracle Tutorial: Wildcards.
- Effective Java Item 31: "Use bounded wildcards to increase API flexibility".
- Java Puzzlers #88, #89 — ví dụ tricky wildcard trong collection.
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ảiList<Object>. Khác array (covariant). ? extends T— upper bound wildcard. Producer — lấy raT, không add.? super T— lower bound wildcard. Consumer — addT, lấy raObject.?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
Q1Đoạn sau compile không?List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints;
▸
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> dù 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ậnList<Integer>. - Nếu cần add: đổi logic — không thể gán
List<Integer>thànhList<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);
}
▸
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 dest và src có cù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:
srclà producer (lấy T ra) →? extends T.destlà consumer (đặ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();
▸
List<? extends Number> list = new ArrayList<Integer>();
list.add(42);
Number n = list.get(0);
list.add(null);
list.size();| Dòng | Kế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.
Q4Khi nào dùng List<?> thay vì List<Object>?▸
List<?> thay vì List<Object>?List<?> và List<Object> khác nhau:
List<?>— "list của kiểu gì đó, không quan tâm". Nhận được mọiList<X>:List<Integer>,List<String>...List<Object>— "list khai báo chính xác làList<Object>". Chỉ nhậnList<Object>— không nhậnList<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 — invarianceDù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).
Q5Method 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);
}
▸
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
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