- Hiểu vấn đề generics giải quyết — và code Java trước năm 2004 trông thế nào.
- Viết generic class và generic method.
- Dùng bounded type parameter:
<T extends Comparable<T>>. - Nắm wildcard:
?,? extends T,? super T. - Áp dụng nguyên tắc PECS — Producer Extends, Consumer Super.
- Hiểu type erasure và mọi hệ quả của nó.
- Biết vì sao
List<String>không phải con củaList<Object>— tính bất biến (invariance). - Xử lý đúng raw type và cảnh báo "unchecked".
- Biết vì sao không tạo được mảng generic và cách JDK lách qua.
- Đối chiếu với generics của TypeScript và type hints của Python.
1. Vấn đề mà generics giải quyết
// ❌ Không có generics: List chứa Object
List ds = new ArrayList();
ds.add("Xin chào");
ds.add(42); // ⚠️ trình biên dịch không phản đối gì
ds.add(new SinhVien()); // ⚠️ cũng được luôn
String s = (String) ds.get(0); // phải ép kiểu bằng tay
String t = (String) ds.get(1); // 💥 ClassCastException LÚC CHẠY
// Vấn đề: lỗi chỉ lộ ra khi chương trình ĐANG CHẠY, có khi ở máy khách hàng
// ✅ Với generics: lỗi bị bắt ngay LÚC BIÊN DỊCH
List<String> ds2 = new ArrayList<>();
ds2.add("Xin chào");
// ds2.add(42); // ❌ error: incompatible types: int cannot be
// converted to String
String s2 = ds2.get(0); // ✅ không cần ép kiểu
ClassCastException ở máy khách hàng lúc 2 giờ sáng đắt hơn rất nhiều so với một dòng đỏ trong
IDE.
2. Generic class
// <T> là THAM SỐ KIỂU — một chỗ trống sẽ được điền khi dùng
public class Hop<T> {
private T noiDung;
public void dat(T x) { this.noiDung = x; }
public T lay() { return noiDung; }
public boolean rong() { return noiDung == null; }
}
Hop<String> h1 = new Hop<>(); // <> = diamond, Java 7+ tự suy ra
h1.dat("Xin chào");
String s = h1.lay(); // không cần ép kiểu ✅
// h1.dat(42); // ❌ lỗi biên dịch
Hop<Integer> h2 = new Hop<>();
h2.dat(42);
// --- Nhiều tham số kiểu ---
public class Cap<A, B> {
private final A dau;
private final B sau;
public Cap(A dau, B sau) { this.dau = dau; this.sau = sau; }
public A dau() { return dau; }
public B sau() { return sau; }
// Đổi chỗ — trả về Cap với thứ tự kiểu ngược lại
public Cap<B, A> doiCho() { return new Cap<>(sau, dau); }
}
Cap<String, Integer> c = new Cap<>("tuoi", 22);
Cap<Integer, String> d = c.doiCho();
// ✅ Java 16+: record generic — gọn hơn hẳn
public record Cap<A, B>(A dau, B sau) {
public Cap<B, A> doiCho() { return new Cap<>(sau, dau); }
}
| Ký tự | Quy ước dùng cho | Ví dụ trong JDK |
|---|---|---|
T | Type — kiểu bất kỳ | List<T>, Optional<T> |
E | Element — phần tử của collection | Collection<E>, Set<E> |
K, V | Key, Value | Map<K,V> |
N | Number | |
R | Return — kiểu trả về | Function<T,R> |
S, U, V | Kiểu thứ 2, 3, 4 | BiFunction<T,U,R> |
3. Generic method
// <T> đặt TRƯỚC kiểu trả về — đây là điểm hay quên
public static <T> void in(List<T> ds) {
for (T x : ds) System.out.println(x);
}
public static <T> T phanTuDau(List<T> ds) {
return ds.isEmpty() ? null : ds.get(0);
}
// Nhiều tham số kiểu
public static <K, V> Map<V, K> daoNguoc(Map<K, V> m) {
Map<V, K> kq = new HashMap<>();
m.forEach((k, v) -> kq.put(v, k));
return kq;
}
// Java tự SUY RA kiểu từ tham số — không cần nói
in(List.of("a", "b")); // T = String
Integer n = phanTuDau(List.of(1, 2, 3)); // T = Integer
// Chỉ định tường minh khi cần (hiếm)
List<String> rong = Collections.<String>emptyList();
Một class không generic vẫn có thể chứa generic method, và ngược lại — một generic class có thể có method với tham số kiểu riêng, độc lập với tham số kiểu của class. Khi method có tham số kiểu riêng, tham số đó chỉ tồn tại trong phạm vi lời gọi method đó.
class Hop<T> { // T của CLASS
private T noiDung;
<U> Cap<T, U> ghepVoi(U khac) { // U của METHOD, độc lập
return new Cap<>(noiDung, khac);
}
}
4. Bounded type parameter — giới hạn kiểu
// ❌ T bất kỳ → không gọi được method nào ngoài của Object
static <T> T lonNhat(List<T> ds) {
T max = ds.get(0);
for (T x : ds)
if (x.compareTo(max) > 0) max = x; // ❌ error: cannot find symbol
return max;
}
// ✅ Giới hạn trên: T phải cài đặt Comparable<T>
static <T extends Comparable<T>> T lonNhat(List<T> ds) {
T max = ds.get(0);
for (T x : ds)
if (x.compareTo(max) > 0) max = x; // ✅ giờ gọi được
return max;
}
lonNhat(List.of(3, 1, 4)); // 4
lonNhat(List.of("b", "a", "c")); // "c"
// lonNhat(List.of(new Object())); // ❌ Object không cài Comparable
// --- Giới hạn bằng class ---
static <T extends Number> double tong(List<T> ds) {
double t = 0;
for (T x : ds) t += x.doubleValue(); // method của Number
return t;
}
// --- NHIỀU giới hạn: dùng &, class phải đứng ĐẦU ---
static <T extends Number & Comparable<T> & Serializable> T xuLy(T x) {
...
}
// --- Giới hạn đệ quy — mẫu hay gặp trong JDK ---
public abstract class Enum<E extends Enum<E>> implements Comparable<E> { }
// Nghĩa: "E là một enum có thể so sánh với chính loại E của nó"
// → mỗi enum chỉ so sánh được với enum cùng loại, không so chéo
5. Generic là bất biến (invariant)
// Mảng thì HIỆP BIẾN (covariant) — cha con truyền được
Object[] mang = new String[3]; // ✅ biên dịch được
mang[0] = 42; // 💥 ArrayStoreException LÚC CHẠY
// Generic thì BẤT BIẾN (invariant) — bắt lỗi ngay lúc biên dịch
// List<Object> ds = new ArrayList<String>();
// ❌ error: incompatible types
Vì sao Java cố tình làm generic bất biến
Giả sử nó được phép — hãy xem chuyện gì sẽ xảy ra:
List<String> chuoi = new ArrayList<>();
List<Object> obj = chuoi; // giả sử được phép...
obj.add(42); // hợp lệ với List<Object>
String s = chuoi.get(0); // 💥 lấy được Integer từ List<String>!
Điều đó phá vỡ toàn bộ lời hứa an toàn kiểu của generics. Nên Java cấm hẳn ở bước biên dịch.
Mảng thì hiệp biến vì lý do lịch sử: nó có trước generics (Java 1.0), và cần thế để viết được
các hàm như Arrays.sort(Object[]). Cái giá phải trả là
ArrayStoreException lúc chạy — chính xác là loại lỗi mà generics sinh ra để loại bỏ. Đây
cũng là lý do không nên trộn mảng với generic.
6. Wildcard ?
Tính bất biến an toàn nhưng cứng nhắc: một method nhận List<Number> sẽ không nhận được
List<Integer>. Wildcard nới lỏng điều đó một cách có kiểm soát.
// ① ? — kiểu bất kỳ, không biết là gì
static void inSoLuong(List<?> ds) {
System.out.println(ds.size());
for (Object o : ds) System.out.println(o); // đọc ra Object thì được
// ds.add("x"); // ❌ KHÔNG thêm được gì (trừ null)
}
inSoLuong(List.of("a")); // ✅
inSoLuong(List.of(1, 2)); // ✅ nhận được mọi List
// ② ? extends T — giới hạn TRÊN: "T hoặc bất kỳ con nào của T"
static double tong(List<? extends Number> ds) {
double t = 0;
for (Number n : ds) t += n.doubleValue(); // ✅ ĐỌC được (là Number)
// ds.add(1); // ❌ KHÔNG GHI được — không biết chính xác kiểu gì
return t;
}
tong(List.of(1, 2, 3)); // List<Integer> ✅
tong(List.of(1.5, 2.5)); // List<Double> ✅
// ③ ? super T — giới hạn DƯỚI: "T hoặc bất kỳ cha nào của T"
static void themSo(List<? super Integer> ds) {
ds.add(1); // ✅ GHI được (Integer chắc chắn vừa)
ds.add(2);
Object o = ds.get(0); // ⚠️ đọc ra chỉ chắc là Object
// Integer i = ds.get(0); // ❌ có thể là List<Number> hoặc List<Object>
}
themSo(new ArrayList<Integer>()); // ✅
themSo(new ArrayList<Number>()); // ✅
themSo(new ArrayList<Object>()); // ✅
| Dạng | Đọc được gì | Ghi được gì | Dùng khi |
|---|---|---|---|
List<T> | T | T | Vừa đọc vừa ghi cùng một kiểu |
List<? extends T> | T ✅ | ❌ chỉ null | Nguồn — chỉ đọc dữ liệu ra |
List<? super T> | Object ⚠️ | T ✅ | Đích — chỉ ghi dữ liệu vào |
List<?> | Object | ❌ chỉ null | Không quan tâm kiểu (đếm, kiểm tra rỗng) |
6.1. PECS — Producer Extends, Consumer Super
? extends. Nếu là đích nhận dữ liệu (Consumer), dùng ? super. Nếu vừa
đọc vừa ghi, không dùng wildcard.
// Chữ ký thật của Collections.copy trong JDK:
public static <T> void copy(List<? super T> dich, // CONSUMER → super
List<? extends T> nguon) { // PRODUCER → extends
for (int i = 0; i < nguon.size(); i++)
dich.set(i, nguon.get(i));
}
// Nhờ vậy lời gọi này hợp lệ:
List<Object> dich = new ArrayList<>(Collections.nCopies(3, null));
List<Integer> nguon = List.of(1, 2, 3);
Collections.copy(dich, nguon); // ✅ Object ⊃ Integer
// --- Ví dụ tự viết ---
static <T> void chuyenHet(Collection<? extends T> tu, // đọc → extends
Collection<? super T> den) { // ghi → super
for (T x : tu) den.add(x);
}
List<Integer> a = List.of(1, 2, 3);
List<Number> b = new ArrayList<>();
chuyenHet(a, b); // ✅ Integer → Number
// --- Comparator cũng theo PECS ---
// Chữ ký thật của List.sort:
void sort(Comparator<? super E> c);
// Comparator TIÊU THỤ phần tử → super
// Nhờ vậy Comparator<Object> dùng được cho List<String>
Comparator<Object> theoChuoi = Comparator.comparing(Object::toString);
List<String> ds = new ArrayList<>(List.of("b", "a"));
ds.sort(theoChuoi); // ✅ nhờ ? super E
Producer Extends — bạn lấy dữ liệu ra từ nó, nên chỉ cần biết dữ liệu
đó ít nhất là T. Ghi vào thì không an toàn, vì bạn không biết chính xác nó là
List<Integer> hay List<Double>.
Consumer Super — bạn đưa dữ liệu vào nó, nên nó phải chứa được ít
nhất T. Đọc ra thì chỉ chắc chắn được là Object.
Một cách nhớ khác: "extends thì đọc, super thì ghi". Hoặc nhìn vào hướng mũi tên trong cây kế
thừa: extends nhìn xuống (nhận mọi con), super nhìn lên
(nhận mọi cha).
7. Type erasure — chìa khoá của mọi điều kỳ lạ
Java cài đặt generics bằng cách xoá thông tin kiểu sau khi biên dịch. Bytecode sinh ra gần như giống hệt code Java 1.4 không có generics.
List<String> ds;String s = ds.get(0);rồi XOÁ
List ds;String s = (String) ds.get(0);List trần.
// Trước khi xoá Sau khi xoá
List<String> → List
Map<String, Integer> → Map
<T> void f(T x) → void f(Object x)
<T extends Number> void g(T x) → void g(Number x) // thay bằng GIỚI HẠN
// Kiểm chứng bằng javap:
// $ javap -c Hop.class
// public java.lang.Object lay(); ← T đã biến thành Object
7.1. Bảy hệ quả của type erasure
// ① Mọi List<X> là CÙNG MỘT class lúc chạy
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
System.out.println(a.getClass()); // class java.util.ArrayList
// ② Không dùng instanceof với kiểu tham số
// if (x instanceof List<String>) // ❌ error: illegal generic type
if (x instanceof List<?>) { } // ✅ chỉ kiểm tra được đến đây
// ③ Không tạo được instance của T
class Hop<T> {
// T tao() { return new T(); } // ❌ error: type parameter T cannot be
// instantiated directly
// ✅ Giải pháp 1: truyền Class<T> vào (gọi là "type token")
T tao(Class<T> cls) throws Exception {
return cls.getDeclaredConstructor().newInstance();
}
// ✅ Giải pháp 2: truyền factory — cách hiện đại, sạch hơn
T tao(Supplier<T> factory) { return factory.get(); }
}
// ④ Không tạo được MẢNG generic
// T[] m = new T[10]; // ❌
// List<String>[] ds = new List<String>[10]; // ❌ generic array creation
// ✅ Cách JDK lách (chính ArrayList làm y hệt):
@SuppressWarnings("unchecked")
T[] m = (T[]) new Object[10];
// ✅ Hoặc dùng List<T> thay mảng — luôn tốt hơn
// ⑤ Không dùng kiểu nguyên thủy làm tham số kiểu
// List<int> ds; // ❌ — vì sau khi xoá, T thành Object,
// mà int không phải Object
List<Integer> ds; // ✅ phải dùng wrapper
// ⑥ Không nạp chồng method chỉ khác ở tham số kiểu
class X {
void f(List<String> a) { }
// void f(List<Integer> a) { } // ❌ error: name clash — both erase to f(List)
}
// ⑦ Không tạo được exception generic
// class MyException<T> extends Exception { } // ❌
// catch (MyException<String> e) // không phân biệt được lúc chạy
Vì sao Java chọn erasure — và cái giá phải trả
Năm 2004, đã có hàng triệu dòng Java 1.4 đang chạy trong production. Nếu generics đổi định dạng
.class, mọi thư viện cũ sẽ ngừng hoạt động. Erasure cho phép
tương thích hai chiều: code Java 5 chạy với thư viện cũ, và ngược lại.
Cái giá: mất thông tin kiểu lúc chạy, dẫn tới bảy hạn chế ở trên. C# (ra sau, không có gánh nặng tương
thích) chọn reified generics — giữ nguyên thông tin kiểu, nên
typeof(List<int>) khác typeof(List<string>) và
new T() hoạt động.
Dự án Valhalla của OpenJDK đang nghiên cứu đưa reified generics vào Java, nhưng đó là bài toán rất khó và chưa có mốc thời gian.
8. Raw type và cảnh báo "unchecked"
// RAW TYPE — generic class dùng mà không có tham số kiểu
List raw = new ArrayList(); // ⚠️ chỉ tồn tại vì tương thích ngược
raw.add("a");
raw.add(42); // không ai ngăn
String s = (String) raw.get(1); // 💥 ClassCastException
// Raw type LÂY LAN — vô hiệu hoá generics ở mọi nơi nó đi qua
List raw2 = new ArrayList<String>();
raw2.add(42); // ⚠️ biên dịch được, chỉ cảnh báo unchecked!
// ✅ Không biết kiểu gì? Dùng wildcard, KHÔNG dùng raw type
List<?> anToan = new ArrayList<String>();
// anToan.add(42); // ❌ bị chặn ✅
// --- @SuppressWarnings: dùng đúng cách ---
// ❌ Đặt trên cả method — che luôn cảnh báo khác
@SuppressWarnings("unchecked")
public void phuongThucDai() {
... 50 dòng ...
}
// ✅ Thu hẹp tới đúng dòng cần, kèm comment giải thích VÌ SAO an toàn
public <T> T[] toArray(T[] a) {
// An toàn: mảng được tạo từ chính component type của a
@SuppressWarnings("unchecked")
T[] kq = (T[]) Arrays.copyOf(mang, soLuong, a.getClass());
return kq;
}
Raw type chỉ tồn tại để code Java 1.4 vẫn biên dịch được. Dùng nó là tự nguyện từ bỏ toàn bộ lợi ích của generics, và tệ hơn — nó lây lan: một biến raw type khiến mọi thao tác trên nó mất kiểm tra kiểu, kể cả khi đối tượng gốc được tạo có generic đầy đủ.
Bật -Xlint:rawtypes,unchecked khi biên dịch để thấy hết cảnh báo, và coi chúng như lỗi.
9. Thực hành tốt
// ✅ ① Ưu tiên List<T> hơn mảng T[]
// Mảng hiệp biến + generic bất biến = không trộn được an toàn.
// List an toàn hơn, linh hoạt hơn, và có sẵn nhiều method.
// ✅ ② Dùng wildcard cho THAM SỐ, không dùng cho KIỂU TRẢ VỀ
static double tong(List<? extends Number> ds) { ... } // ✅ linh hoạt
static List<? extends Number> lay() { ... } // ❌ ép người gọi
// cũng phải dùng wildcard
// ✅ ③ Tên tham số kiểu theo quy ước: T, E, K, V, R
// ✅ ④ Generic method thay wildcard khi hai tham số phải CÙNG kiểu
static void doiCho(List<?> ds, int i, int j) {
// ds.set(i, ds.get(j)); // ❌ không ghi được vào List<?>
}
static <T> void doiCho(List<T> ds, int i, int j) { // ✅
T tam = ds.get(i);
ds.set(i, ds.get(j));
ds.set(j, tam);
}
// ✅ ⑤ Kiểu trả về generic tránh phải ép kiểu
static <T> List<T> loc(List<T> ds, Predicate<? super T> dk) {
List<T> kq = new ArrayList<>();
for (T x : ds) if (dk.test(x)) kq.add(x);
return kq;
}
List<String> dai = loc(List.of("a", "abc"), s -> s.length() > 1);
// ✅ ⑥ Bounded type khi cần gọi method của kiểu đó
static <T extends Comparable<? super T>> T max(List<? extends T> ds) { ... }
// ↑ Đây là chữ ký thật của Collections.max — PECS áp dụng ở CẢ HAI chỗ
| Java | TypeScript | Python | |
|---|---|---|---|
| Cú pháp | List<T> | Array<T> | list[T] |
| Tồn tại lúc chạy | ❌ Bị xoá | ❌ Bị xoá | ❌ Chỉ là chú thích |
| Kiểm tra | Lúc biên dịch | Lúc biên dịch | Chỉ khi chạy mypy |
| Giới hạn trên | <T extends X> | <T extends X> | TypeVar("T", bound=X) |
| Hiệp biến / nghịch biến | Wildcard tại nơi dùng | Suy ra theo cấu trúc | covariant=True tại nơi khai báo |
| Bắt buộc? | Không, nhưng nên | Không, nhưng nên | Hoàn toàn tuỳ chọn |
Điểm khác biệt lớn nhất: Java đặt wildcard ở nơi sử dụng (List<? extends T>
trong chữ ký method), còn Kotlin/C#/Python đặt ở nơi khai báo (out T /
in T). Cách của Java linh hoạt hơn nhưng dài dòng hơn — bạn phải viết PECS ở mọi chữ ký,
thay vì khai báo một lần trong định nghĩa kiểu.
10. Cheat sheet chương 9
| Tình huống | Cách viết |
|---|---|
| Generic class | class Hop<T> { } |
| Generic method | static <T> T f(List<T> ds) — <T> trước kiểu trả về |
| Giới hạn trên | <T extends Number> |
| Nhiều giới hạn | <T extends Number & Comparable<T>> — class đứng đầu |
| Tham số chỉ để đọc | List<? extends T> (Producer) |
| Tham số chỉ để ghi | List<? super T> (Consumer) |
| Vừa đọc vừa ghi | List<T> — không wildcard |
| Không quan tâm kiểu | List<?> |
| Không biết kiểu gì | List<?> — không bao giờ raw List |
Cần tạo instance của T | Truyền Supplier<T> hoặc Class<T> |
| Cần mảng generic | Dùng List<T>; bất đắc dĩ thì (T[]) new Object[n] |
| Kiểm tra kiểu lúc chạy | x instanceof List<?> — không thể sâu hơn |
| Ép kiểu unchecked | @SuppressWarnings("unchecked") ở hẹp nhất + comment |
| Kiểu trả về | Cụ thể (List<T>), không wildcard |
| Bật cảnh báo | javac -Xlint:unchecked,rawtypes |
Bài tập
Bộ sưu tập generic tự viết
Cài đặt ba cấu trúc generic từ đầu:
class NganXep<T> { void push(T x); T pop(); T peek(); boolean rong(); int size(); }
class HangDoi<T> { void offer(T x); T poll(); T peek(); boolean rong(); }
class Cay<T extends Comparable<T>> { void them(T x); boolean chua(T x); List<T> duyetGiua(); }
NganXepdùng mảng — giải quyết vấn đề không tạo đượcnew T[], giải thích cách bạn chọn.HangDoidùng đệm vòng (circular buffer), tự nới rộng.Caylà cây tìm kiếm nhị phân, cầnTso sánh được.- Cả ba đều
implements Iterable<T>. - Thêm generic method
static <T> NganXep<T> tuList(List<? extends T> ds)— chú ý wildcard.
Gợi ý
Hai cách xử lý mảng generic: (a) Object[] bên trong, ép kiểu khi lấy ra —
đây là cách ArrayList làm; (b) (T[]) new Object[n] —
ArrayDeque làm thế này. Cách (a) an toàn hơn vì phép ép kiểu nằm ở method
get, nơi trình biên dịch đã chèn sẵn.
Câu cuối minh hoạ vì sao dùng ? extends T: nhờ nó,
NganXep<Number> tạo được từ List<Integer>.
Thư viện tiện ích với PECS
Viết class TienIch với các generic method sau. Với mỗi method, quyết định wildcard và giải thích lý do:
static <T> void chepHet(Collection<___> nguon, Collection<___> dich);
static <T> T max(Collection<___> ds, Comparator<___> cmp);
static <T> List<T> loc(List<___> ds, Predicate<___> dk);
static <T, R> List<R> anhXa(List<___> ds, Function<___, ___> f);
static <T> void voiMoi(Iterable<___> ds, Consumer<___> hanhDong);
static <T> int dem(Collection<___> ds, Predicate<___> dk);
static <T> List<List<T>> chiaNhom(List<___> ds, int kichThuoc);
Sau đó viết test chứng minh tính linh hoạt: chepHet(List<Integer>, List<Number>) phải chạy được.
Gợi ý
Quy tắc: bất kỳ tham số nào bạn chỉ đọc từ → ? extends T; bất kỳ tham số
nào bạn chỉ ghi vào → ? super T.
Với Function<? super T, ? extends R>: nó tiêu thụ T (nên
super) và sản xuất R (nên extends). Đây chính xác là chữ ký
của Stream.map trong JDK — hãy mở mã nguồn ra đối chiếu.
Chứng minh type erasure
Thực hiện các thí nghiệm sau, ghi lại kết quả và giải thích:
- Chứng minh
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass(). - Biên dịch một class generic rồi chạy
javap -c. Chỉ ra chính xác chỗTbiến thànhObject, và chỗ trình biên dịch chèn phép ép kiểu. - Dùng phản chiếu để thêm
Integervào mộtList<String>. Chuyện gì xảy ra khi đọc ra? Ở đâu thìClassCastExceptionđược ném? - Viết hai method
void f(List<String>)vàvoid f(List<Integer>). Chép lại thông điệp lỗi và giải thích. - Dùng
getClass().getGenericSuperclass()trên một subclass ẩn danh (new TypeToken<List<String>>(){}) — vì sao ở đây thông tin kiểu vẫn còn?
Gợi ý
(3): List.class.getMethod("add", Object.class).invoke(ds, 42) — chạy trót lọt vì
lúc chạy chẳng còn kiểu nào. Ngoại lệ chỉ nổ ở dòng
String s = ds.get(0) — nơi trình biên dịch đã chèn
(String).
(5): thông tin kiểu của lớp cha được ghi vào metadata class file (thuộc tính
Signature) và không bị xoá. Đây chính là mẹo đứng sau
TypeToken của Guava và TypeReference của Jackson — cách duy nhất để
"lấy lại" thông tin generic lúc chạy.
Kho lưu trữ generic (mẫu Repository)
Thiết kế hệ thống lưu trữ generic dùng được cho mọi loại thực thể:
interface CoId<ID> { ID getId(); }
interface Kho<T extends CoId<ID>, ID> {
void luu(T x);
Optional<T> tim(ID id);
List<T> tatCa();
boolean xoa(ID id);
List<T> tim(Predicate<? super T> dk);
<R> List<R> chieu(Function<? super T, ? extends R> f);
}
- Cài đặt
KhoTrongBoNho<T, ID>dùngHashMap<ID, T>. - Dùng thử với hai thực thể:
record SinhVien(String getId, String ten)vàrecord DonHang(Long getId, long tongTien). - Thêm
KhoCoCache<T, ID>bọc mộtKhokhác — mẫu decorator, dùng composition chứ không kế thừa. - Thêm generic method
<T extends CoId<ID>, ID> void dongBo(Kho<T,ID> tu, Kho<T,ID> den).
Gợi ý
<T extends CoId<ID>, ID> là ví dụ về tham số kiểu phụ thuộc lẫn
nhau: T bị ràng buộc bởi ID. Đây là mẫu Spring Data JPA dùng cho
Repository<T, ID>.
Method chieu có tham số kiểu <R> riêng của method, độc
lập với T và ID của interface — minh hoạ phạm vi tham số kiểu.
Sửa code vi phạm nguyên tắc generics
Đoạn code sau mắc bảy vấn đề về generics. Tìm, sửa, và giải thích từng cái.
public class TienIch {
public static List loc(List ds, Object dieuKien) {
List kq = new ArrayList();
for (Object o : ds) {
if (((Predicate) dieuKien).test(o)) kq.add(o);
}
return kq;
}
@SuppressWarnings("unchecked")
public static void themTatCa(List<Object> dich, List<Object> nguon) {
for (Object o : nguon) dich.add(o);
}
public static <T> T[] taoMang(int n) {
return (T[]) new Object[n];
}
public static <T> T lonNhat(List<T> ds) {
T max = ds.get(0);
for (T x : ds) if (((Comparable) x).compareTo(max) > 0) max = x;
return max;
}
public static boolean laListChuoi(Object o) {
return o instanceof List<String>;
}
}
Gợi ý
Danh sách kiểm: (1) raw type List; (2) Object dieuKien thay vì
Predicate<? super T>; (3) ép kiểu raw (Predicate);
(4) themTatCa không nhận được List<Integer> — thiếu PECS;
(5) @SuppressWarnings đặt sai chỗ (không có cảnh báo nào để chặn);
(6) taoMang trả về Object[] đội lốt T[] — ném
ClassCastException ngay khi người gọi gán vào String[];
(7) lonNhat thiếu bound, và instanceof List<String> không biên
dịch được.
Quiz
Generics giải quyết vấn đề gì? Nêu lợi ích cụ thể.
Xem đáp án
Nó chuyển lỗi kiểu từ lúc chạy sang lúc biên dịch. Trước Java 5, collection chứa
Object: bạn có thể nhét bất cứ gì vào, và mỗi lần lấy ra phải ép kiểu bằng tay — sai
thì ClassCastException nổ ở máy khách hàng.
Ba lợi ích cụ thể:
- An toàn kiểu —
ds.add(42)vàoList<String>bị chặn ngay trong IDE. - Không phải ép kiểu —
String s = ds.get(0)thay vì(String) ds.get(0). Code sạch hơn và không có chỗ để sai. - Code dùng lại được — viết
NganXep<T>một lần thay vìNganXepString,NganXepInteger…
Một lợi ích ít người để ý: generics là tài liệu. Nhìn
Map<String, List<DonHang>> là biết ngay cấu trúc dữ liệu, không cần đọc
comment.
Type erasure là gì? Nêu ba hệ quả.
Xem đáp án
Type erasure là cách Java cài đặt generics: trình biên dịch kiểm tra kiểu rất kỹ, sau đó xoá sạch thông tin kiểu và tự chèn phép ép kiểu vào những chỗ cần. Bytecode sinh ra gần như giống hệt code Java 1.4.
List<String> → List
<T> void f(T x) → void f(Object x)
<T extends Number> → thay bằng Number
Hệ quả (chọn ba trong bảy):
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()→true.- Không dùng được
instanceof List<String>— chỉ tớiList<?>. - Không tạo được
new T()haynew T[n]. - Không nạp chồng
f(List<String>)vàf(List<Integer>)— cùng chữ ký sau khi xoá.
Lý do: tương thích ngược. Năm 2004 đã có hàng triệu dòng Java 1.4 đang chạy; erasure cho phép code mới và thư viện cũ dùng chung. C# ra sau nên chọn reified generics — giữ nguyên thông tin kiểu.
Vì sao List<String> không phải là con của List<Object>?
Xem đáp án
Vì generic trong Java bất biến (invariant) — và đó là quyết định đúng đắn. Giả sử nó được phép:
List<String> chuoi = new ArrayList<>();
List<Object> obj = chuoi; // nếu được phép...
obj.add(42); // hợp lệ với List<Object>
String s = chuoi.get(0); // 💥 Integer trong List<String>
Toàn bộ lời hứa an toàn kiểu sẽ tan vỡ. Nên Java cấm ở bước biên dịch.
Đối lập: mảng thì hiệp biến — Object[] m = new String[3] biên
dịch được, nhưng m[0] = 42 ném ArrayStoreException lúc chạy. Mảng có
trước generics (Java 1.0) và cần hiệp biến để viết được Arrays.sort(Object[]). Đây
cũng là lý do không nên trộn mảng với generic.
Cần linh hoạt? Dùng wildcard: List<? extends Object> nhận được
List<String> — nhưng đổi lại, bạn không ghi vào nó được.
PECS là gì? Cho ví dụ.
Xem đáp án
Producer Extends, Consumer Super — quy tắc chọn wildcard:
- Producer (bạn đọc dữ liệu ra) →
? extends T. Bạn chắc chắn mọi thứ lấy ra ít nhất làT, nhưng không ghi vào được vì không biết chính xác kiểu gì. - Consumer (bạn ghi dữ liệu vào) →
? super T. Bạn chắc chắn nó chứa đượcT, nhưng đọc ra thì chỉ chắc làObject. - Vừa đọc vừa ghi → không dùng wildcard, dùng
Tthẳng.
// Chữ ký thật trong JDK:
static <T> void copy(List<? super T> dich, // consumer
List<? extends T> nguon) // producer
void sort(Comparator<? super E> c); // comparator tiêu thụ E
<R> Stream<R> map(Function<? super T, ? extends R> f); // cả hai!
Lợi ích thực tế: nhờ PECS, Collections.copy(List<Object>, List<Integer>)
hợp lệ, và một Comparator<Object> dùng được để sắp
List<String>.
Vì sao không tạo được new T[10]?
Xem đáp án
Vì mảng giữ thông tin kiểu lúc chạy (để kiểm tra ArrayStoreException),
còn generic thì bị xoá. Hai cơ chế mâu thuẫn nhau: JVM không biết phải tạo mảng
kiểu gì khi T đã biến mất.
Ba cách xử lý, theo thứ tự nên dùng:
// ✅ 1. Dùng List<T> thay mảng — lựa chọn tốt nhất
List<T> ds = new ArrayList<>();
// ✅ 2. Object[] bên trong, ép kiểu khi lấy ra (ArrayList làm thế này)
private Object[] mang = new Object[10];
@SuppressWarnings("unchecked")
T get(int i) { return (T) mang[i]; }
// ⚠️ 3. Truyền Class<T> để tạo mảng đúng kiểu qua phản chiếu
@SuppressWarnings("unchecked")
T[] tao(Class<T> cls, int n) {
return (T[]) Array.newInstance(cls, n);
}
Cách (2) có một bẫy: (T[]) new Object[n] rồi trả về cho người gọi sẽ ném
ClassCastException ngay khi họ gán vào String[] — vì object thật vẫn là
Object[]. Chỉ an toàn khi mảng nằm hoàn toàn bên trong class.
Raw type là gì? Vì sao không nên dùng?
Xem đáp án
Raw type là dùng generic class mà không có tham số kiểu: List ds = new
ArrayList();. Nó chỉ tồn tại để code Java 1.4 vẫn biên dịch được.
Vì sao tránh:
- Mất hoàn toàn an toàn kiểu — nhét gì vào cũng được, lỗi nổ lúc chạy.
- Lây lan — đây là điểm nguy hiểm nhất: gán một
ArrayList<String>vào biến raw type rồi thao tác qua biến đó là mất hết kiểm tra, dù object gốc có generic đầy đủ. - Phải ép kiểu bằng tay ở mọi chỗ đọc.
List raw = new ArrayList<String>();
raw.add(42); // ⚠️ chỉ CẢNH BÁO, vẫn biên dịch
List<?> anToan = new ArrayList<String>();
// anToan.add(42); // ❌ bị chặn ✅
Thay thế: không biết kiểu gì thì dùng List<?> (wildcard), không
bao giờ dùng raw. Bật -Xlint:rawtypes,unchecked và coi cảnh báo như lỗi.
<T extends Number> và <? extends Number> khác nhau thế nào?
Xem đáp án
<T extends Number> là tham số kiểu có giới hạn — nó
đặt tên cho kiểu. Nhờ có tên, bạn dùng lại T ở nhiều chỗ và đảm bảo
chúng là cùng một kiểu. Ghi vào được.
<? extends Number> là wildcard — kiểu vô
danh. Không dùng lại được, và không ghi vào được (trừ null).
// Cần T để đảm bảo hai list CÙNG kiểu và để ghi vào
static <T extends Number> void chep(List<T> a, List<T> b) {
a.addAll(b); // ✅ ghi được
}
// Chỉ đọc → wildcard gọn hơn và linh hoạt hơn
static double tong(List<? extends Number> ds) {
double t = 0;
for (Number n : ds) t += n.doubleValue();
// ds.add(1); // ❌ không ghi được
return t;
}
Quy tắc chọn: nếu kiểu chỉ xuất hiện một lần trong chữ ký và bạn chỉ đọc → wildcard. Nếu cần dùng lại tên kiểu, hoặc cần ghi vào → tham số kiểu có tên.
Vì sao List<int> không hợp lệ?
Xem đáp án
Vì type erasure. Sau khi xoá, mọi tham số kiểu trở thành Object (hoặc
giới hạn của nó). Mà int là kiểu nguyên thủy — nó không phải object, không kế
thừa Object, nên không thể được thay bằng Object.
Giải pháp: dùng wrapper — List<Integer>. Java tự
autoboxing/unboxing nên viết vẫn tự nhiên.
Cái giá phải trả:
- Bộ nhớ —
List<Integer>tốn khoảng 4 lần so vớiint[](mỗi số là một object có header, cộng thêm một tham chiếu). - Tốc độ — chậm hơn 4-6 lần do phải truy cập qua con trỏ và tạo rác cho GC.
- Bẫy
NullPointerException—int x = map.get(k)nổ khi khoá không tồn tại.
Vì vậy với dữ liệu số lượng lớn, mảng nguyên thủy hoặc IntStream (chương 10) vẫn tốt
hơn. Dự án Valhalla đang nghiên cứu "generic đặc hoá" để cho phép List<int>
thật, nhưng chưa có mốc thời gian.
Tổng kết
Sau chương 9, bạn đã nắm:
- Mục đích: chuyển lỗi kiểu từ lúc chạy sang lúc biên dịch, bỏ ép kiểu thủ công.
- Generic class & method; quy ước tên
T,E,K,V,R. - Bounded type:
<T extends X>, nhiều giới hạn với&, giới hạn đệ quy. - Bất biến:
List<String>không phải con củaList<Object>— và vì sao điều đó đúng. - Wildcard:
?,? extends T(đọc),? super T(ghi). - PECS — Producer Extends, Consumer Super; chính JDK dùng nguyên tắc này.
- Type erasure và bảy hệ quả: không
instanceofsâu, khôngnew T(), không mảng generic, không nạp chồng theo tham số kiểu… - Raw type lây lan và vô hiệu hoá generics — không bao giờ dùng.
@SuppressWarningsđặt hẹp nhất có thể, kèm giải thích.- Đối chiếu với TypeScript (cũng xoá kiểu) và Python (chú thích tuỳ chọn).
Kết nối
- Chương 7-8 (Collections & Map) — toàn bộ framework là generic; giờ bạn hiểu vì sao
ArrayListdùngObject[]bên trong. - Chương 10 (Lambda & Stream) —
Function<T,R>,Predicate<T>,Stream<T>, và chữ ký PECS ở khắp nơi. - Chương 5 (OOP nâng cao) —
record Cap<A,B>,sealed interface KetQua<T>. - Chương 12 (Đa luồng) —
Future<T>,CompletableFuture<T>,Callable<V>. - Chương 13 (JDBC) — mẫu
Repository<T, ID>. - Chương 14 (Kiểm thử) —
assertThrows(Class<T>, …), matcher generic của Mockito. - JavaScript Chương 10 — đối chiếu: TypeScript cũng xoá kiểu, nhưng có kiểu điều kiện và mapped type mà Java không có.
- Python Chương 12 —
TypeVar,Generic[T], và cú pháp PEP 695 (Python 3.12) gần giống Java.