Generics: một Repository dùng cho Order, Payment, Customer
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 đề
- Phải cast ở mọi chỗ gọi.
- Bỏ nhầm kiểu vào không ai báo; lỗi nổ lúc runtime, có khi trên production.
- Viết 3 class
OrderCache,PaymentCache,CustomerCachethì copy-paste.
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áo | Nhận vào | Đọc ra | Thêm vào |
|---|---|---|---|
List<Payable> | chỉ List<Payable> | Payable | mọi Payable |
List<? extends Payable> | List<Order>, List<Payment>… | Payable | không (trừ null) |
List<? super Order> | List<Order>, List<Payable>, List<Object> | Object | Order |
List<?> | mọi List | Object | không |
7. Khi nào nên dùng / không nên dùng
- Nên: container, repository, util (
groupBy), response wrapperApiResponse<T>. - Nên: wildcard ở tham số của API public để linh hoạt cho caller.
- Không nên: wildcard ở kiểu trả về (bắt caller xử lý
?); generic lồng 3 tầng khó đọc. - Không nên: raw type
List(không có<>), mất toàn bộ kiểm tra.
List<Order> vào tham số List<Payable> mà phải có ? extends?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>.
extends, khi nào dùng super?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:
- List cho ra dữ liệu để mình đọc (producer) →
? extends T. - List nhận vào dữ liệu mình ghi (consumer) →
? super T.
// đọ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 được | Lý do | Cách vòng |
|---|---|---|
new T() | runtime không biết T | truyền Supplier<T> hoặc Class<T> |
instanceof List<Order> | chỉ còn List | instanceof List<?> |
new T[10] | mảng cần biết kiểu lúc chạy | dùng List<T> |
List<int> | erasure về Object, primitive không phải Object | List<Integer> (autoboxing) |
overload f(List<Order>) và f(List<Customer>) | cùng chữ ký sau erasure | đặt tên method khác |
field static T | static dùng chung mọi T | khô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
ClassCastExceptionlúc chạy. Ví dụ mộtInMemoryCache<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
ClassCastExceptionat runtime. For example oneInMemoryCache<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ảiList<Payable>.? extends Tdùng khi chỉ đọc (producer),? super Tkhi 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 aList<Payable>. Use? extends Twhen you only read from it (producer) and? super Twhen 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ônginstanceof 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ềnClass<T>hoặc dùngTypeReference.
🇬🇧 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(), checkinstanceof List<Order>, create generic arrays, use primitives as type arguments, or overload methods that differ only by type argument. Workarounds are passing aClass<T>or using aTypeReference.
Cách nhớ ngắn gọn
- Generics = lỗi kiểu chuyển từ runtime sang compile time.
- Invariant:
List<Order>≠List<Payable>; mảng thì covariant. - PECS: đọc dùng extends, ghi dùng super.
- Erasure: runtime không biết T → truyền
Class<T>.
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.