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).
Đứng trước một tham số List cụ thể, câu hỏi phải tự hỏi chỉ có hai — và có một nhánh mà PECS không nói ra:

Nhánh cuối là chỗ hay bị bỏ sót: nếu method vừa đọc vừa ghi thì không có wildcard nào đúng cả — phải quay về List<T> khai đúng kiểu. Wildcard mua sự mềm dẻo cho caller bằng cách bỏ bớt thông tin của compiler; khi bạn cần cả hai chiều thì không còn gì để bỏ.
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; - 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); } - 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(); - Q4Khi nào dùng
List<?>thay vìList<Object>? - 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); }
Bài tiếp theo: List và Queue — ArrayList, LinkedList, ArrayDeque, PriorityQueue
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
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