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.
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
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
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