Generics cơ bản — class và method nhận kiểu làm tham số
Vì sao cần generics, cú pháp class/method generic, type parameter T, bounded type parameter (T extends Comparable), và ví dụ tự viết generic collection đơn giản.
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.
Chạy hai đoạn code trên cạnh nhau thì thấy điều đáng nói không phải là generics cho bạn viết được thêm gì, mà là dòng add(42) sai kia dừng lại ở đâu:

Cả hai cột đều chứa đúng cùng một lỗi. Khác nhau ở chỗ cột trái để lỗi đi hết quãng đường tới máy người dùng rồi mới nổ, còn cột phải chặn nó lại trên máy bạn — đổi lại bạn phải khai kiểu ra cho compiler đọc.
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
Để giữ cho mã nguồn dễ đọc và nhất quán, Java quy ước sử dụng các ký tự đơn, viết hoa để đại diện cho tham số kiểu:
| Ký tự | Tên viết tắt đầy đủ | Ý nghĩa thực tế |
|---|---|---|
T | Type | Kiểu dữ liệu chung (General Type parameter). |
E | Element | Phần tử trong một Collection (ví dụ: List<E>, Set<E>). |
K | Key | Khóa của bản đồ (thường đi cặp trong Map<K, V>). |
V | Value | Giá trị đi kèm với khóa trong Map<K, V>. |
R | Return / Result | Kiểu dữ liệu trả về của một hàm (Functional Interface). |
N | Number | Giới hạn kiểu số (thường dùng làm bound T extends Number). |
? | Wildcard | Kiểu không xác định (sẽ được tìm hiểu sâu ở 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.
3.2 Target Type Inference (Java 8+, JEP 101)
Trong các phiên bản Java ban đầu (Java 5 đến 7), khả năng tự động suy luận kiểu (Type Inference) của compiler còn rất sơ khai. Đối với các phương thức generic, dev thường phải khai báo Type Witness tường minh ở call site khi compiler gặp khó khăn trong việc đoán kiểu:
// Cu phap nap kieu tuong minh (Explicit Type Witness) truoc Java 8:
String s = Utils.<String>firstOrDefault(Arrays.asList("a", "b"), "default");
Từ Java 8 (JEP 101 - Generalized Target-Type Inference), JDK đã cải tiến vượt bậc công cụ suy luận kiểu của trình biên dịch. Compiler giờ đây không chỉ nhìn vào các đối số truyền vào phương thức, mà còn phân tích Target Type (Kiểu đích) từ ngữ cảnh xung quanh, bao gồm:
- Kiểu biến được gán (Variable Assignment Target):
// Compiler tu luan T = Integer dua vao kieu nhan cua bien n (Integer) Integer n = Utils.firstOrDefault(Collections.emptyList(), 0); - Kiểu đối số truyền vào phương thức lồng nhau (Nested Method Calls Context):
// Compiler tự biết Collections.emptyList() cần trả về List<String> // để tương thích với signature nhận vào của firstOrDefault String s = Utils.firstOrDefault(Collections.emptyList(), "default");
Nhờ JEP 101, cú pháp <Kiểu> (Type Witness) hầu như không còn cần thiết trong code hàng ngày, giúp code trở nên tinh gọn, tự nhiên mà vẫn đảm bảo tính an toàn kiểu ở mức tối đa tại thời điểm compile.
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.
Trong ví dụ trên, lớp Stack<E> được thiết kế bằng cơ chế Composition (Ủy thác - Delegation) bằng cách bao bọc một List<E> bên trong. Đây là một tư duy thiết kế cấu trúc dữ liệu mới chuẩn mực, khắc phục hoàn toàn những sai lầm nghiêm trọng của lớp legacy java.util.Stack trong bộ JDK gốc:
- Lỗi kế thừa sai lầm (Inheritance Anti-Pattern):
java.util.Stackcủa JDK kế thừa trực tiếp từjava.util.Vector(một cấu trúc mảng động cổ xưa). Do kế thừa (extends),java.util.Stackkế thừa luôn tất cả các phương thức củaVectornhưinsertElementAt(obj, index),removeElementAt(index),clear()... Điều này vi phạm nghiêm trọng nguyên lý đóng gói (Encapsulation) và nguyên lý thay thế Liskov (Liskov Substitution Principle - LSP), cho phép client can thiệp trực tiếp vào giữa Stack vốn chỉ được phép truy xuất dạng LIFO (push/pop). - Hiệu năng kém do lạm dụng đồng bộ (Synchronized Overhead):
Vectorđược đồng bộ hóa mọi phương thức để chạy an toàn đa luồng. Do đó,java.util.Stackcũng bị khóa (synchronized) ở mọi thao tác. Điều này dẫn đến sự sụt giảm hiệu năng nặng nề trong các ứng dụng đơn luồng hoặc khi sử dụng các giải pháp concurrency hiện đại.
Định hình tư duy thiết kế: Khi cần cấu trúc Stack trong thực tế:
- Hãy ưu tiên sử dụng
Deque<E> stack = new ArrayDeque<>()của JDK (không bị synchronized và kế thừa đúng đặc tả). - Hoặc tự viết một lớp bọc (Composition) như
Stack<E>ở trên để bảo vệ hoàn toàn tính chất LIFO của cấu trúc dữ liệu.
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
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 + methodswap()trảPair<B, A>. - Q2Đoạn sau compile không? Vì sao?
public class Container<T> { public T createDefault() { return new T(); } } - Q3Khi nào dùng bounded type parameter
<T extends X>? Cho ví dụ. - Q4Vì sao
List<int>không compile? - 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); } }
Bài tiếp theo: Type erasure — sự thật không dễ chịu của generics Java
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?
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