Generics: một Repository dùng cho Order, Payment, Customer

5 câu hỏi~25 phútTừ khóa: type parameter, invariance, wildcard, ? extends, ? super, PECS, type erasure, bounded type, generic method

Tiếp đi: Generics.

shop-backend có 3 entity: Order, Payment, Customer. Mỗi cái cần một cache nhỏ trong bộ nhớ và các hàm tiện ích như "tính tổng tiền". Bài này cho thấy generics giúp viết một lần, dùng cho mọi kiểu, mà vẫn type-safe.

1. Bài toán: cache dùng Object

public class SimpleCache {
    private final Map<String, Object> map = new HashMap<>();
    public void put(String id, Object value) { map.put(id, value); }
    public Object get(String id) { return map.get(id); }
}

Order o = (Order) cache.get("c-1");    // thực ra là Customer → ClassCastException lúc chạy

2. Vấn đề

3. Giải pháp: type parameter

Khai báo <T> như một "biến kiểu". Compiler kiểm tra kiểu lúc compile và tự chèn cast. Lỗi chuyển từ runtime sang compile time.

4. Code từng bước

Bước 1: Generic class với bounded type

public interface Identifiable { String getId(); }

public class InMemoryCache<T extends Identifiable> {      // bounded: T phải có getId()
    private final Map<String, T> map = new ConcurrentHashMap<>();
    public void put(T item) { map.put(item.getId(), item); }
    public Optional<T> get(String id) { return Optional.ofNullable(map.get(id)); }
}

InMemoryCache<Order> orders = new InMemoryCache<>();
orders.put(customer);       // ❌ compile error: Customer không phải Order
Order o = orders.get("o-1").orElseThrow();   // không cần cast

Bước 2: Generic method

public static <T, K> Map<K, List<T>> groupBy(List<T> items, Function<T, K> key) {
    Map<K, List<T>> result = new HashMap<>();
    for (T item : items) result.computeIfAbsent(key.apply(item), k -> new ArrayList<>()).add(item);
    return result;
}

Map<OrderStatus, List<Order>> byStatus = groupBy(orders, Order::getStatus);  // T, K tự suy ra

Bước 3: Wildcard cho hàm tính tổng

public interface Payable { BigDecimal amount(); }
// Order và Payment đều implement Payable

static BigDecimal total(List<? extends Payable> items) {      // nhận List<Order>, List<Payment>
    return items.stream().map(Payable::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
}

5. Cách hoạt động: type erasure

Source:   InMemoryCache<Order>     Order o = cache.get(id).orElseThrow();
             │ compile
Bytecode: InMemoryCache (T → Identifiable)   Order o = (Order) cache.get(id)...
             │
Runtime:  JVM không biết T là Order. List<Order> và List<Customer> cùng class List.

6. So sánh

Khai báoNhận vàoĐọc raThêm vào
List<Payable>chỉ List<Payable>Payablemọi Payable
List<? extends Payable>List<Order>, List<Payment>…Payablekhông (trừ null)
List<? super Order>List<Order>, List<Payable>, List<Object>ObjectOrder
List<?>mọi ListObjectkhông

7. Khi nào nên dùng / không nên dùng

Order là Payable, vậy sao List<Order> lại không phải List<Payable>? Mình truyền vào thì bị compile error.

Đúng là trực giác bảo "phải được", nhưng nếu cho phép thì sẽ phá type-safety. Generics trong Java là invariant:

List<Order> orders = new ArrayList<>();
List<Payable> payables = orders;      // giả sử được phép
payables.add(new Payment(...));       // hợp lệ với List<Payable>
Order o = orders.get(0);              // 💥 thực ra là Payment

Mảng thì ngược lại, covariant: Payable[] arr = new Order[1]; compile được, nhưng arr[0] = new Payment() ném ArrayStoreException lúc chạy. Generics chọn chặn từ lúc compile.

Wildcard là cách nói "mình chỉ đọc": List<? extends Payable> nhận List<Order> nhưng cấm add, nên không thể xảy ra tình huống trên. Tương tự List<Object> không nhận List<String>.

Mình hay bị lẫn ? extends với ? super. Có quy tắc nào dễ nhớ không?

Có: PECS = Producer Extends, Consumer Super. Nhìn tham số từ góc của method:

// đọc từ src, ghi vào dst: đúng chữ ký của Collections.copy
static <T> void copy(List<? super T> dst, List<? extends T> src) {
    for (T item : src) dst.add(item);
}

List<Order> paidOrders = ...;
List<Payable> toReconcile = new ArrayList<>();
copy(toReconcile, paidOrders);         // T = Order

// Comparator là consumer: nhận T để so sánh
orders.sort(Comparator.comparing(Payable::amount));   // sort(Comparator<? super Order>)

Vì sort nhận Comparator<? super E>, một comparator viết cho Payable dùng chung cho cả list Order và list Payment.

Bạn nói runtime không biết T. Vậy mình viết new T() hay if (x instanceof List<Order>) được không?

Không được, cả hai đều là hệ quả của type erasure:

Không làm đượcLý doCách vòng
new T()runtime không biết Ttruyền Supplier<T> hoặc Class<T>
instanceof List<Order>chỉ còn Listinstanceof List<?>
new T[10]mảng cần biết kiểu lúc chạydùng List<T>
List<int>erasure về Object, primitive không phải ObjectList<Integer> (autoboxing)
overload f(List<Order>) và f(List<Customer>)cùng chữ ký sau erasuređặt tên method khác
field static Tstatic dùng chung mọi Tkhông có
public <T> T fromJson(String json, Class<T> type) {      // truyền Class để biết kiểu lúc chạy
    return objectMapper.readValue(json, type);
}
// List<Order> thì Class không đủ → Jackson dùng TypeReference
List<Order> list = objectMapper.readValue(json, new TypeReference<List<Order>>() {});

TypeReference là anonymous subclass, và kiểu generic của superclass được lưu trong bytecode, nên đọc lại được bằng reflection. Spring dùng cùng mẹo này với ParameterizedTypeReference để biết bạn inject List<PaymentGateway>.

Phỏng vấn hỏi generics, wildcard, type erasure thì trả lời sao?

Câu 1: Generics để làm gì?

🇻🇳 Tiếng Việt:

Generics cho phép tham số hóa kiểu để viết code tái sử dụng mà vẫn type-safe: compiler kiểm tra kiểu lúc compile, bỏ cast thủ công và chặn ClassCastException lúc chạy. Ví dụ một InMemoryCache<T extends Identifiable> dùng chung cho Order, Payment, Customer.

🇬🇧 English:

Generics parameterize types so code can be reused while staying type-safe: the compiler checks types at compile time, removes manual casts and prevents ClassCastException at runtime. For example one InMemoryCache<T extends Identifiable> serves orders, payments and customers.

Câu 2: ? extends và ? super khác gì?

🇻🇳 Tiếng Việt:

Generics là invariant nên List<Order> không phải List<Payable>. ? extends T dùng khi chỉ đọc (producer), ? super T khi ghi vào (consumer): quy tắc PECS. Ví dụ Collections.copy(List<? super T>, List<? extends T>).

🇬🇧 English:

Generics are invariant, so List<Order> is not a List<Payable>. Use ? extends T when you only read from it (producer) and ? super T when you write into it (consumer): PECS. Collections.copy(List<? super T>, List<? extends T>) is the classic example.

Câu 3: Type erasure là gì, hệ quả?

🇻🇳 Tiếng Việt:

Compiler xóa thông tin kiểu generic, thay bằng bound (hoặc Object) và chèn cast, để tương thích code trước Java 5. Hệ quả: không new T(), không instanceof List<Order>, không mảng generic, không primitive làm type argument, không overload chỉ khác type argument. Cách vòng: truyền Class<T> hoặc dùng TypeReference.

🇬🇧 English:

The compiler erases generic type information, replacing it with the bound or Object and inserting casts, for backward compatibility with pre-Java 5 code. As a result you can't call new T(), check instanceof List<Order>, create generic arrays, use primitives as type arguments, or overload methods that differ only by type argument. Workarounds are passing a Class<T> or using a TypeReference.

Cách nhớ ngắn gọn


Tự kiểm tra

Vì sao không add được vào List<? extends Payable>?

Compiler không biết list thật là List<Order> hay List<Payment>, thêm gì cũng có thể sai kiểu.

<T extends Identifiable> cho mình gì?

Gọi được method của Identifiable (getId) trên T, và giới hạn T chỉ nhận kiểu có id.

Vì sao không có List<int>?

Erasure biến T thành Object; primitive không phải Object nên phải dùng wrapper Integer.

Jackson đọc JSON thành List<Order> thế nào khi có erasure?

Dùng TypeReference<List<Order>>: kiểu generic của superclass được giữ trong bytecode, đọc lại bằng reflection.

Mảng covariant gây lỗi gì?

ArrayStoreException lúc chạy khi gán phần tử sai kiểu vào mảng được tham chiếu bằng kiểu cha.