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 (Covariance — Hiệp biến)
Trong lý thuyết ngôn ngữ lập trình, Covariance (Hiệp biến) mô tả khả năng duy trì mối quan hệ phân cấp lớp của các kiểu generic cùng chiều với các kiểu tham số thành phần của chúng.
Cụ thể: Nếu Integer là một subtype của Number, và chúng ta mong muốn List<Integer> cũng có thể được xem là một kiểu con hợp lệ để gán cho List<? extends Number>, ta sử dụng cú pháp Upper Bound Wildcard (? extends T) để hiện thực hóa tính hiệp biến.
Đô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 — collection lúc này đóng vai producer: bạn lấy ra được (produce), không đặt vào được (consume).
3. Lower bound wildcard — ? super T (Contravariance — Nghịch biến)
Trái ngược hoàn toàn với hiệp biến, Contravariance (Nghịch biến) mô tả cơ chế đảo ngược mối quan hệ kế thừa của kiểu generic so với kiểu tham số thành phần gốc của chúng.
Cụ thể: Nếu Integer là một subtype của Number, nhưng chúng ta muốn gán ngược một List<Number> (kiểu cha) vào một biến đại diện cho danh sách chấp nhận Integer (kiểu con) để thực hiện ghi dữ liệu an toàn, ta sử dụng cú pháp Lower Bound Wildcard (? super T) để kích hoạt tính nghịch biến.
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(Covariance - Hiệp biến).destlà consumer — nơi bạn đặt T vào →? super T(Contravariance - Nghịch biến).
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 đi tính linh hoạt (flexibility).
4.1 Vai trò liên kết kiểu của <T> trong Collections.copy
Hãy nhìn kỹ vào chữ ký của phương thức Collections.copy ở trên. Tại sao chúng ta cần khai báo tham số kiểu <T> ở đầu phương thức thay vì viết trực tiếp bằng các wildcard không định danh?
Tham số <T> ở đây đóng vai trò cực kỳ quan trọng làm Type Link (Liên kết kiểu) ở cấp độ biên dịch:
- Thiết lập cầu nối kiểu tương thích:
<T>đóng vai trò làm trung gian ràng buộc giữa kiểu nguồn dữ liệu (src) và kiểu đích chứa dữ liệu (dest). - Kiểm tra loại an toàn tối đa (Type-Safe Enforcement): Nhờ mối liên kết qua
<T>, compiler đảm bảo tuyệt đối rằng bất kỳ đối tượng nào đọc ra từsrc(chắc chắn thuộc kiểuThoặc các subtype củaTnhờ? extends T) đều sẽ hoàn toàn tương thích và ghi vào đượcdest(vốn chấp nhận kiểuThoặc các supertype củaTnhờ? super T). - Loại bỏ ép kiểu thủ công: Nếu không có
<T>làm cầu nối liên kết, compiler sẽ mất đi tham chiếu chung và từ chối lời gọidest.set(i, src.get(i))vì không thể chứng minh tính an toàn. Sự hiện diện của<T>giúp compiler tự tin cho phép gán trực tiếp dữ liệu mà không bắt dev phải viết bất kỳ dòng ép kiểu thủ công nào, loại bỏ hoàn toàn nguy cơ ClassCastException ở runtime.
4.2 Ma trận tóm tắt nhanh 2x2 về Wildcard
Để ghi nhớ nhanh và không bao giờ bị nhầm lẫn giữa hai loại wildcard này trong thực chiến, học viên có thể đối chiếu qua bảng ma trận 2x2 dưới đây:
| Loại Wildcard | Đọc dữ liệu (Read / Get) | Ghi dữ liệu (Write / Add) |
|---|---|---|
? extends T(Hiệp biến - Covariance) | HỢP LỆ (Đọc ra với kiểu compile-time là T — phần tử thực tế là T hoặc subtype của T) | BỊ CẤM (Chỉ được ghi giá trị null) |
? super T(Nghịch biến - Contravariance) | HẠN CHẾ (Chỉ đọc được kiểu chung Object) | HỢP LỆ (Ghi được đối tượng kiểu T hoặc con của T) |
4.3 PECS thực tế — Stream.collect
Signature thật trong JDK (method instance của Stream<T>):
// java.util.stream.Stream<T>:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
Để ý accumulator có kiểu BiConsumer<R, ? super T>: tham số thứ hai tiêu thụ element T của stream → ? super T đúng theo PECS. Nhờ vậy một BiConsumer<List<Object>, Object> (consumer "rộng" hơn) vẫn dùng được cho Stream<String>. Các vị trí R giữ nguyên kiểu chính xác vì method vừa ghi vào container kết quả vừa trả nó về cho caller. Rule PECS áp dụng khắp JDK — đọc signature Collections.copy hay Comparator.comparing là cách nhanh nhất để thuộc.
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 (Covariance). |
list.add(42) | FAIL — compiler không biết T chính xác; có thể là List<Double> → từ chối ghi để đảm bảo an toàn. |
Number n = list.get(0) | OK — mọi element là subtype Number, lấy về Number an toàn (đọc hiệp biế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 (Contravariance).
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: List và Queue — ArrayList, LinkedList, ArrayDeque, PriorityQueue
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