Java — Từ Zero đến Senior/Generics & Collections/Generics cơ bản — class và method nhận kiểu làm tham số
1/5
~18 phútGenerics & Collections

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 <...>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, convention T, E, K, V, R.
  • Trong class body, T dù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 suy String/Integer từ biến khai báo.

2.1 Convention tên type parameter

Ký tựÝ nghĩa
TType (general)
EElement (collection: List, Set)
KKey (Map)
VValue (Map)
RReturn type
NNumber
?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 TcompareTo(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:

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: T generic, E element, K key, V value, R return.
  • 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 (List khô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

Tự kiểm tra
Q1
Viết class 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(): return Pair<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();
    }
}

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.

Q3
Khi nào dùng bounded type parameter <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 Comparable

Bound 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).
Q4
Vì sao 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 -> int

Cost: 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); }
}

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