Bạn đã dùng List<String>, Map<String, Integer> trong nhiều bài trước. Dấu <...> là generics — cơ chế Java cho phép class/method nhận kiểu làm tham số. Thay vì viết StringList, IntegerList, UserList riêng, bạn viết một List<E> duy nhất, compiler thay E bằng kiểu cụ thể ở call site.
Bài này giải thích tại sao generics cần thiết (pre-Java 5 không có), cú pháp class/method generic, bounded type parameter, và viết một Box<T> đơn giản để nắm cơ chế.
1. Trước Java 5 — thế giới không generics
// Java 4 style — khong generics
List list = new ArrayList();
list.add("hello");
list.add(42); // cast vo tu — compiler khong can
String s = (String) list.get(0); // phai cast moi lan
String broken = (String) list.get(1); // ClassCastException RUNTIME
Vấn đề:
- Compiler không biết list chứa gì → không check type at compile time.
- Add nhầm kiểu → ra runtime mới biết → bug tinh vi.
- Caller cast mọi lần → code nặng nề, dễ sai.
Java 5 (2004) giới thiệu generics để fix:
List<String> list = new ArrayList<>();
list.add("hello");
list.add(42); // COMPILE ERROR — int khong phai String
String s = list.get(0); // khong can cast
Compiler bắt kiểu tại compile time, bỏ cast runtime, type-safe từ đầu đến cuối.
2. Cú pháp generic class
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// Su dung:
Box<String> s = new Box<>();
s.set("hello");
String v = s.get(); // tu dong la String, khong cast
Box<Integer> i = new Box<>();
i.set(42);
int n = i.get(); // auto-unbox Integer -> int
<T>khai báo type parameter — chữ viết hoa đơn, conventionT,E,K,V,R.- Trong class body,
Tdùng như kiểu bình thường — compiler thay bằng kiểu cụ thể ở call site. new Box<>()— diamond operator (Java 7+), compiler suyString/Integertừ biến khai báo.
2.1 Convention tên type parameter
| Ký tự | Ý nghĩa |
|---|---|
T | Type (general) |
E | Element (collection: List, Set) |
K | Key (Map) |
V | Value (Map) |
R | Return type |
N | Number |
? | Wildcard (bài 3) |
Không bắt buộc theo — nhưng đọc quen mắt với JDK.
2.2 Nhiều type parameter
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
}
Pair<String, Integer> p = new Pair<>("Alice", 30);
3. Generic method
Không chỉ class — method cũng có thể generic riêng:
public class Utils {
public static <T> T firstOrDefault(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
}
}
String s = Utils.firstOrDefault(List.of("a", "b"), "default"); // "a"
Integer n = Utils.firstOrDefault(List.<Integer>of(), 0); // 0
Cú pháp: <T> khai trước return type. Compiler suy T từ argument.
// Compiler infer T = String
firstOrDefault(List.of("a"), "default");
// Explicit:
Utils.<String>firstOrDefault(List.of("a"), "default");
3.1 Generic method trong generic class
Class có type T, method có type riêng R:
public class Container<T> {
private T value;
public <R> R transform(Function<T, R> mapper) {
return mapper.apply(value);
}
}
Container<Integer> c = new Container<>();
// c.value = ...
String s = c.transform(n -> "value=" + n);
T là class-level, R là method-level.
4. Bounded type parameter — T extends X
Mặc định T có thể là bất kỳ kiểu nào. Nếu cần gọi method của kiểu cụ thể (vd compareTo), giới hạn T bằng extends:
public class SortedBox<T extends Comparable<T>> {
private final List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
Collections.sort(items); // can T co compareTo
}
}
SortedBox<Integer> ints = new SortedBox<>(); // OK — Integer implement Comparable
SortedBox<Book> books = new SortedBox<>(); // compile error neu Book khong Comparable
T extends Comparable<T> đọc là "T là một kiểu có thể compare với T". Trong body, compiler biết T có compareTo(T) → cho phép gọi.
4.1 Multi-bound
public class Task<T extends Runnable & AutoCloseable> {
private final T task;
public Task(T task) { this.task = task; }
public void runAndClose() {
task.run(); // co tu Runnable
task.close(); // co tu AutoCloseable
}
}
Cú pháp: T extends A & B & C. Class chỉ cho 1 (nếu có); interface nhiều. Throwable không được.
5. Ví dụ — tự viết Stack<E> đơn giản
public class Stack<E> {
private final List<E> items = new ArrayList<>();
public void push(E item) {
items.add(item);
}
public E pop() {
if (items.isEmpty()) throw new IllegalStateException("Stack is empty");
return items.remove(items.size() - 1);
}
public E peek() {
if (items.isEmpty()) throw new IllegalStateException("Stack is empty");
return items.get(items.size() - 1);
}
public int size() { return items.size(); }
public boolean isEmpty() { return items.isEmpty(); }
}
// Su dung:
Stack<Integer> s = new Stack<>();
s.push(1);
s.push(2);
int top = s.pop(); // 2
Viết 1 lần, dùng được với mọi kiểu E — không lặp IntStack, StringStack.
6. Generic interface
public interface Converter<FROM, TO> {
TO convert(FROM input);
}
public class StringToIntConverter implements Converter<String, Integer> {
@Override
public Integer convert(String input) {
return Integer.parseInt(input);
}
}
Interface generic dùng khắp JDK: Comparator<T>, Predicate<T>, Function<T, R>, Supplier<T>, Iterable<T>...
7. Kiểu raw — legacy, tránh
List list = new ArrayList(); // RAW TYPE — khong khai kieu
list.add("hello");
list.add(42); // compile ok, type-unsafe
Java cho phép dùng raw type để backward compat với code pre-Java 5. Compiler warning "unchecked" — đừng bỏ qua. Raw type bypass type checking.
Luôn khai rõ: List<String>, Map<String, Integer>. Nếu không biết kiểu chính xác, dùng wildcard List<?> (bài 3) — type-safe, rõ ý.
8. Generic không dùng được cho primitive
List<int> list = ...; // COMPILE ERROR
List<Integer> list = ...; // OK — wrapper
Generic chỉ chấp nhận reference type. Primitive (int, double...) phải dùng wrapper (Integer, Double). Auto-boxing tự động chuyển:
List<Integer> list = new ArrayList<>();
list.add(42); // auto-box int -> Integer
int x = list.get(0); // auto-unbox
Vấn đề perf: mỗi auto-box tạo Integer object trên heap — tốn memory + GC. Trong hot loop tính toán, dùng primitive stream (IntStream, LongStream, DoubleStream) hoặc mảng int[] thay vì List<Integer>.
Java 21 đang bàn Valhalla — generic trên primitive (như C#). Chưa stable.
9. Pitfall tổng hợp
❌ Nhầm 1: Dùng raw type trong code mới.
List list = new ArrayList(); // warning "unchecked"
✅ List<String> list = new ArrayList<>(); với diamond <>.
❌ Nhầm 2: Generic trên primitive.
Box<int> b = new Box<>(); // COMPILE ERROR
✅ Box<Integer>. Nhớ cost auto-box.
❌ Nhầm 3: new T() trong class generic — không được.
class Container<T> {
T createDefault() { return new T(); } // COMPILE ERROR
}
✅ Pass factory: Container<T> constructor nhận Supplier<T>.
❌ Nhầm 4: Static field type-generic — không được.
class Container<T> {
static T shared; // COMPILE ERROR
}
✅ Static thuộc class, không instance → không có type parameter. Dùng Object hoặc tách ra class non-generic.
❌ Nhầm 5: Exception không generic.
class MyException<T> extends Exception { } // COMPILE ERROR
✅ Exception không được generic (liên quan type erasure + catch clauses).
10. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- JLS §8.1.2 — Generic Classes and Type Parameters
- JLS §8.4.4 — Generic Methods
- JLS §4.4 — Type Variables
- JLS §4.5 — Parameterized Types
- Oracle Generics Tutorial — beginner-friendly.
- Effective Java Item 26: "Don't use raw types"; Item 29: "Favor generic types"; Item 30: "Favor generic methods".
Ghi chú: Generics được thêm vào Java năm 2004 với JDK 5 — một trong những thay đổi lớn nhất. Thiết kế dùng type erasure (bài 2) để backward compat với code pre-Java 5 — đây vừa là điểm mạnh (không phá code cũ) vừa là điểm yếu (không có runtime type info cho generic). Project Valhalla đang thử sửa: generics specialization cho primitive và value type.
11. Tóm tắt
- Generics = class/method nhận kiểu làm tham số.
- Trước Java 5: không generics → cast runtime, type-unsafe. Sau: compiler bắt kiểu tại compile time.
- Cú pháp class:
class Box<T> { T value; }. Cú pháp method:<T> T foo(T x). - Diamond
<>(Java 7+):new Box<>()— compiler suy kiểu từ context. - Convention:
Tgeneric,Eelement,Kkey,Vvalue,Rreturn. - Bounded:
T extends X— giới hạn kiểu, dùng method của X trong body. - Generic interface dùng khắp JDK (
Comparator,Predicate,Function). - Raw type (
Listkhông khai kiểu) — legacy, compiler warning, tránh. - Generic chỉ cho reference type. Primitive phải dùng wrapper (auto-box cost).
- Không được
new T(), không được static field generic, không được generic exception.
12. Tự kiểm tra
Q1Viết class Pair<A, B> có 2 field final + method swap() trả Pair<B, A>.▸
Pair<A, B> có 2 field final + method swap() trả Pair<B, A>.public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
public Pair<B, A> swap() {
return new Pair<>(second, first);
}
}
// Su dung:
Pair<String, Integer> p = new Pair<>("alice", 30);
Pair<Integer, String> swapped = p.swap();Điểm chú ý:
- Type parameter order trong
swap(): returnPair<B, A>— A và B đổi chỗ. Compiler kiểm tra type safety. - Diamond
<>:new Pair<>(second, first)— compiler suy<B, A>từ return type. - Với Java 16+, có thể viết gọn hơn bằng record:
public record Pair<A, B>(A first, B second) { public Pair<B, A> swap() { return new Pair<>(second, first); } }
Q2Đoạn sau compile không? Vì sao?public class Container<T> {
public T createDefault() {
return new T();
}
}
▸
public class Container<T> {
public T createDefault() {
return new T();
}
}Không compile. new T() không hợp lệ — Java không biết T có constructor nào (do type erasure, runtime T thành Object).
Workaround — pass factory qua argument:
public class Container<T> {
private final Supplier<T> factory;
public Container(Supplier<T> factory) { this.factory = factory; }
public T createDefault() { return factory.get(); }
}
// Su dung:
Container<User> c = new Container<>(User::new); // method reference
User u = c.createDefault();Hoặc nhận Class<T> rồi dùng reflection — phức tạp hơn, dễ fail runtime, chỉ dùng khi bắt buộc.
Q3Khi nào dùng bounded type parameter <T extends X>? Cho ví dụ.▸
<T extends X>? Cho ví dụ.Khi trong body cần gọi method của kiểu cụ thể mà T không đảm bảo có. Ví dụ:
// Khong bound -> khong goi duoc compareTo
public static <T> T max(List<T> list) {
// list.stream().sorted(...) -> KHONG biet T co compareTo khong
}
// Bound <T extends Comparable<T>>
public static <T extends Comparable<T>> T max(List<T> list) {
T maxVal = list.get(0);
for (T item : list) {
if (item.compareTo(maxVal) > 0) maxVal = item;
}
return maxVal;
}
max(List.of(3, 1, 4, 1, 5)); // OK — Integer implements Comparable<Integer>
max(List.of(new Object())); // COMPILE ERROR — Object khong ComparableBound thu hẹp tập kiểu T xuống những kiểu "có compareTo" → compiler cho phép gọi.
Các case khác:
<T extends Number>— T phải là số (Integer, Double, ...). CóintValue(),doubleValue().<T extends Closeable>— T cóclose().<T extends Runnable & AutoCloseable>— T phải có cả 2 (multi-bound).
Q4Vì sao List<int> không compile?▸
List<int> không compile?Java generics chỉ chấp nhận reference type, không chấp nhận primitive (int, double, boolean, v.v.).
Lý do: type erasure (bài 2) — runtime List<T> thành List (raw) với mọi element là Object. Primitive không phải Object, không wrap được trực tiếp.
Workaround: dùng wrapper class:
List<Integer> list = new ArrayList<>();
list.add(42); // auto-box: int -> Integer
int x = list.get(0); // auto-unbox: Integer -> intCost: mỗi Integer là object trên heap (~16 bytes overhead mỗi instance). Với 1M phần tử, List<Integer> tốn ~40MB, còn int[] chỉ ~4MB.
Cho hot path số học: dùng int[] / IntStream / LongStream / DoubleStream — primitive specialized, không auto-box.
Project Valhalla (đang phát triển) nhắm cho phép List<int> qua value type — hiện chưa production-ready.
Q5Đoạn sau compile không?public class Cache<T> {
private static List<T> items = new ArrayList<>();
public static void add(T item) { items.add(item); }
}
▸
public class Cache<T> {
private static List<T> items = new ArrayList<>();
public static void add(T item) { items.add(item); }
}Không. Static member không thấy type parameter class.
Lý do: T là parameter của instance — mỗi new Cache() chỉ định T khác nhau (Cache<String>, Cache<Integer>). Static field thuộc class, không instance — không biết T nào để dùng.
Compile error: "non-static type variable T cannot be referenced from a static context".
Fix (chọn theo ý đồ):
- Không cần generic, đổi static dùng
Object:static List<Object>items — bạn mất type safety. - Static generic method với type parameter riêng:
public static <U> void add(U item, List<U> list). - Bỏ static, biến thành instance:
private List<T> items. - Nếu muốn một cache toàn cục theo kiểu: dùng
Map<Class<?>, List<?>>(pattern heterogeneous map).
Bài tiếp theo: Type erasure — sự thật không dễ chịu của generics Java