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
ℹ️ 📚 Deep Dive Oracle (optional)
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