Bounded Types và Generic Invariance — vì sao List<Integer> không phải List<Number>
Upper bounded type parameters, multiple bounds, generic invariance vs array covariance, workaround qua wildcard, và cách JVM xử lý erasure đến bound thay vì Object.
TL;DR: Bounded type parameter <T extends Number> giới hạn T chỉ được là Number hoặc subtype của nó — giúp gọi T.doubleValue() mà không cần cast. Generic invariance nghĩa là List<Integer> KHÔNG phải subtype của List<Number> dù Integer extends Number — Java chủ đích thiết kế vậy để tránh type safety break lúc runtime. Ngược lại, Java arrays là covariant (Integer[] có thể gán cho Number[]) nhưng đây là thiết kế cũ có lỗ hổng (ArrayStoreException). Bài này giải thích invariance từ góc độ type safety, cách workaround qua wildcard, và khi nào bounded type parameter ảnh hưởng bytecode.
1. Scenario — hàm tính tổng generic
Bạn muốn viết một hàm tính tổng các phần tử trong List. Thử với type parameter không bị giới hạn:
public static <T> double sum(List<T> list) {
double total = 0;
for (T item : list) {
total += item.doubleValue(); // compile error: T khong co method doubleValue()
}
return total;
}
Compiler từ chối vì T có thể là bất kỳ type nào, kể cả String, Boolean — những type này không có doubleValue(). Để nói với compiler "T chỉ được là kiểu số", cần bounded type parameter.
2. Bounded type parameter — <T extends Bound>
Bounded type parameter (JLS §4.5.1) là cú pháp giới hạn kiểu T phải là subtype (hoặc chính xác là) một kiểu cụ thể. Từ khoá extends dùng cho cả class lẫn interface:
// T phai la Number hoac subtype cua Number (Integer, Long, Double, BigDecimal...)
public static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T item : list) {
total += item.doubleValue(); // OK -- Number co doubleValue()
}
return total;
}
// Su dung:
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
List<BigDecimal> bigs = List.of(new BigDecimal("1.1"), new BigDecimal("2.2"));
System.out.println(sum(ints)); // 6.0
System.out.println(sum(doubles)); // 7.5
System.out.println(sum(bigs)); // 3.3
Khi bạn viết <T extends Number>, compiler cho phép gọi tất cả public method của Number trên biến kiểu T. Đây là mấu chốt: bounded type mở rộng những gì bạn có thể làm với T trong method body.
2.1 Ví dụ thực tế: bounded trong Collections
Comparable là interface được dùng rất nhiều làm bound:
// Tim phan tu nho nhat trong list -- yeu cau T co the so sanh voi chinh no
public static <T extends Comparable<T>> T min(List<T> list) {
if (list.isEmpty()) throw new NoSuchElementException();
T result = list.get(0);
for (T item : list) {
if (item.compareTo(result) < 0) {
result = item;
}
}
return result;
}
// Hoat dong voi moi Comparable type
String minStr = min(List.of("banana", "apple", "cherry")); // "apple"
Integer minInt = min(List.of(3, 1, 4, 1, 5)); // 1
3. Multiple bounds — <T extends A & B>
T có thể bị giới hạn bởi nhiều type cùng lúc bằng &:
// T phai implement ca Comparable va Serializable
public static <T extends Comparable<T> & Serializable> void processAndStore(List<T> list) {
Collections.sort(list); // OK vi T extends Comparable
// serialize(list) // OK vi T extends Serializable
}
Quy tắc quan trọng với multiple bounds:
- Tối đa 1 class, các phần còn lại phải là interface.
- Class phải đứng đầu tiên trong danh sách.
// DUNG: class truoc, interface sau
<T extends AbstractList<E> & Serializable & Cloneable>
// SAI: 2 class -- compile error (T khong the extend 2 class)
<T extends ArrayList<E> & LinkedList<E>> // compile error
Lý do không thể có 2 class: Java không có multiple class inheritance. T extend 2 class đồng nghĩa với multiple inheritance — không cho phép.
4. Bounded type và erasure — ảnh hưởng bytecode
Khi compiler xử lý generic type, nó thực hiện type erasure — xoá thông tin generic và thay bằng type thực tế. Điều quan trọng: với bounded type parameter, erasure đến bound, không phải Object:
// Source code:
public static <T extends Number> double sum(List<T> list) {
for (T item : list) {
total += item.doubleValue();
}
}
// Sau erasure (bytecode tuong duong):
public static double sum(List list) {
for (Number item : list) { // T -> Number (bound), khong phai Object
total += item.doubleValue(); // goi truc tiep, khong can cast
}
}
So sánh với unbounded <T>:
// Source:
public static <T> void print(List<T> list) {
for (T item : list) { System.out.println(item); }
}
// Sau erasure:
public static void print(List list) {
for (Object item : list) { // T -> Object (khong co bound)
System.out.println(item);
}
}
Hệ quả thực tế: bounded type parameter giúp compiler tạo bytecode hiệu quả hơn (không cần checkcast instruction với method call trên bound), và bắt lỗi type sớm hơn tại compile time.
5. Generic invariance — định nghĩa và lý do
Generic invariance là tính chất: nếu A là subtype của B, thì Generic<A> không phải subtype của Generic<B>. Cụ thể:
Integer extends Number— đúng- Nhưng
List<Integer>KHÔNG phải subtype củaList<Number>
Điều này nghe có vẻ phi lý. Vì sao Java thiết kế vậy?
Xét đoạn code giả định nếu invariance KHÔNG được áp dụng:
// Gia su (SAI -- Java khong cho phep):
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints; // neu cho phep...
nums.add(3.14); // Double la Number, nen add OK...
nums.add(Long.MAX_VALUE); // Long cung la Number...
// Gio ints la List<Integer> nhung chua Double va Long
Integer x = ints.get(3); // ClassCastException tai runtime!
Nếu Java cho phép List<Integer> gán cho List<Number>, bạn có thể thêm Double (hợp lệ với List<Number>) vào một list thực tế chứa Integer — và đọc ra bằng Integer sẽ ném ClassCastException lúc runtime. Generic invariance ngăn điều này xảy ra, đảm bảo lỗi được bắt tại compile time.
// Thuc te -- Java tu choi ngay tai compile time:
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints; // compile error: incompatible types
Người mới Java hay thắc mắc "tại sao method của tôi nhận List<Number> nhưng không nhận được List<Integer>?". Đây chính là invariance. Giải pháp: dùng upper bounded wildcard List<? extends Number> — sẽ thấy ở section 7.
6. Arrays là covariant — thiết kế cũ, lỗ hổng runtime
Java arrays được thiết kế covariant (từ Java 1.0): nếu A extends B thì A[] là subtype của B[]. Đây là thiết kế trước khi generics ra đời (Java 5), nhằm giúp viết generic algorithms:
Integer[] ints = {1, 2, 3};
Number[] nums = ints; // OK -- covariant, compiler cho phep
nums[0] = 3.14; // ArrayStoreException tai runtime!
// -- nguoi ta dang ghi Double vao Integer[], JVM phat hien va throw
JVM bắt lỗi này bằng arraystore check — mỗi lần ghi vào array, JVM kiểm tra xem kiểu thực của phần tử có compatible với kiểu thực của array không. Nếu không: ArrayStoreException.
So sánh với generics:
| Arrays | Generics | |
|---|---|---|
| Covariance | Có (Integer[] extends Number[]) | Không (List<Integer> không phải List<Number>) |
| Kiểm tra type | Runtime (ArrayStoreException) | Compile time (compile error) |
| Hiệu năng | Overhead mỗi lần ghi (arraystore check) | Không overhead runtime |
| Khi nào phát hiện lỗi | Muộn (runtime) | Sớm (compile time) |
Generics rút kinh nghiệm từ covariant arrays và chọn invariance để bắt lỗi sớm hơn. Đây là quyết định thiết kế có chủ đích.
flowchart TD
A["Integer extends Number"] --> B["Arrays: Integer[] extends Number[]<br/>(covariant)"]
A --> C["Generics: List<Integer> NOT subtype of List<Number><br/>(invariant)"]
B --> D["Error: ArrayStoreException at runtime"]
C --> E["Error: compile error -- safe"]7. Workaround invariance — wildcard
Nếu cần viết method nhận cả List<Integer> lẫn List<Double> (và mọi list số), dùng upper bounded wildcard ? extends Number:
// Voi invariance, method nay chi nhan dung List<Number>:
public static double sumNumbers(List<Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
sumNumbers(List.of(1, 2, 3)); // compile error: List<Integer> khong phai List<Number>
sumNumbers(List.of(1.5, 2.5)); // compile error: List<Double> khong phai List<Number>
// FIX: dung upper bounded wildcard
public static double sumNumbers(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
sumNumbers(List.of(1, 2, 3)); // OK -- List<Integer>
sumNumbers(List.of(1.5, 2.5)); // OK -- List<Double>
sumNumbers(List.of(new BigDecimal("1"))); // OK -- List<BigDecimal>
List<? extends Number> đọc là "list của phần tử có kiểu không rõ, nhưng biết là subtype của Number". Đây là covariance qua wildcard — an toàn hơn array covariance vì compiler ngăn ghi vào list (chỉ đọc được).
Tại sao không thể ghi vào List<? extends Number>?
List<? extends Number> list = new ArrayList<Integer>();
list.add(3.14); // compile error -- compiler khong biet exact type la gi
list.add(1); // compile error -- tuong tu
Compiler không biết ? là Integer hay Double hay Long — nếu cho add 3.14 vào List<Integer>, là sai. Nên compiler từ chối tất cả write operations trên List<? extends T>.
Đây là PECS (Producer Extends, Consumer Super) đã học ở bài 03 — ? extends T phù hợp cho producer (chỉ đọc ra), ? super T phù hợp cho consumer (chỉ ghi vào).
8. Pitfall 1 — cố gắng ghi vào List<? extends Number>
public static void addNumbers(List<? extends Number> list) {
list.add(42); // compile error!
list.add(null); // null la ngoai le, cho phep -- nhung it dung
}
Nếu cần method vừa đọc vừa ghi, phải dùng:
- Type parameter cụ thể:
<T extends Number> void addToList(List<T> list, T item) - Hoặc lower bounded wildcard
List<? super Integer>nếu chỉ cần ghiIntegervào list
// Vua doc vua ghi -- dung type parameter
public static <T extends Number> void addIfPositive(List<T> list, T item) {
if (item.doubleValue() > 0) {
list.add(item); // OK -- list la List<T>, item la T
}
}
9. Pitfall 2 — recursive bound <T extends Comparable<T>>
Một dạng bounded type đặc biệt thường gây nhầm:
// Sai cach doc: "T extend Comparable cua T"
// Dung cach doc: "T la type co the compare voi chinh no"
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
<T extends Comparable<T>> là recursive generic bound — T phải implement Comparable<T>, tức là T biết cách so sánh với đối tượng cùng kiểu T. String, Integer, LocalDate đều satisfy bound này.
Phân biệt với <T extends Comparable> (raw type, không nên dùng) — mất type safety của compareTo.
Một trường hợp khó hơn:
// Cho phep T compare voi supertype cua no (flexible hon)
public static <T extends Comparable<? super T>> T max(List<T> list) { ... }
Comparable<? super T> nghĩa là T có thể implement Comparable với supertype của nó. Ví dụ class Dog extends Animal, Animal implements Comparable<Animal> — Dog không implement Comparable<Dog> trực tiếp nhưng bound ? super Dog bắt được Comparable<Animal>. Pattern này thấy trong Collections.sort JDK.
10. Tổng kết — bảng so sánh
Bounded <T extends B> | Wildcard ? extends B | Wildcard ? super B | |
|---|---|---|---|
| Dùng làm type parameter? | Có | Không (? không phải type) | Không |
| Gọi method của B? | Có | Có (khi đọc) | Không (type không rõ) |
| Có thể ghi vào? | Có | Không | Có |
| Phù hợp cho | Method body cần gọi method của bound | Parameter nhận nhiều subtype (producer) | Parameter nhận supertype (consumer) |
| Erasure đến | Bound (B) | Bound (B) | Object (lower bounded) |
11. 📚 Deep Dive
- JLS §4.5.1 — Type Arguments and Wildcards — docs.oracle.com/.../jls-4.html#jls-4.5.1 — spec chính thức về wildcard và bounded type arguments, subtyping rules.
- JLS §10.10 — Array Store Exception — docs.oracle.com/.../jls-10.html#jls-10.10 — spec về covariant arrays và ArrayStoreException.
- Angelika Langer Generics FAQ — angelikalanger.com/GenericsFAQ — tài liệu tham khảo đầy đủ nhất về Java Generics, đặc biệt section về wildcards và variance.
- Effective Java item 31 (Joshua Bloch, 3rd edition) — "Use bounded wildcards to increase API flexibility" — giải thích PECS, khi nào dùng
extendsvssuper, và lý do wildcard làm API linh hoạt hơn. - JEP 300 (project Valhalla context) — giải thích vì sao generic invariance là thiết kế đúng và Valhalla không thay đổi điều đó dù có value types.
12. Self-check
Q1Vì sao `<T>` không đủ khi muốn gọi `item.doubleValue()` trên `T`? Cần thêm gì?▸
Q2Vì sao `List<Integer>` không phải subtype của `List<Number>` mặc dù `Integer extends Number`?▸
Q3Tại sao `Integer[] ints = 3; Number[] nums = ints; nums[0] = 3.14;` ném `ArrayStoreException` thay vì compile error?▸
Q4Bạn có method `void printAll(List<Number> list)`. Bạn gọi `printAll(new ArrayList<Integer>())` — compile error hay OK?▸
Q5Phân biệt `<T extends Comparable<T>>` và `<T extends Comparable<? super T>>`. Khi nào cần dạng thứ hai?▸
Q6`<T extends A & B>` yêu cầu gì về A và B? Vì sao class phải đứng trước interface?▸
Q7Sau type erasure, bytecode của `<T extends Comparable<T>> T max(T a, T b)` trông như thế nào?▸
Comparable max(Comparable a, Comparable b) { return a.compareTo(b) >= 0 ? a : b; }. Compiler chèn thêm checkcast instruction tại các điểm return để đảm bảo kiểu trả về đúng. Điều này giải thích vì sao generic code không tạo ra class file riêng cho mỗi type argument — tất cả dùng chung 1 bytecode với `Comparable` (hay `Object` với unbounded). Performance implication: không có overhead tạo class mới (không như C++ templates), nhưng có overhead checkcast và không thể tối ưu specialization cho primitive types (lý do Valhalla đang giải quyết).Bài tiếp theo
Bài 11 là Mini Challenge — LRU Cache: áp dụng tổng hợp LinkedHashMap, bounded generics, và immutable collections để implement LRU Cache generic từ đầu với complexity O(1) get và put.
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