Java OO & Functional/Generics cơ bản — class và method nhận kiểu làm tham số
15/33
Bài 15 / 33~18 phútGenerics & CollectionsMiễn phí lượt xem

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

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

Đặt 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