Java Generics là một trong những tính năng quan trọng nhất của ngôn ngữ Java. Ý tưởng đằng sau generics khá đơn giản, tuy nhiên, nó đôi khi trông phức tạp vì sự dịch chuyển từ cú pháp thông thường liên quan đến nó.
Mục đích của hướng dẫn này là giới thiệu cho bạn khái niệm hữu ích này của generics theo cách dễ hiểu.
Trước khi đi sâu vào generics bản thân, hãy tìm ra tại sao Java generics lại được cần từ đầu.
Mục Đích Của Java Generics
Trước khi giới thiệu generics trong Java 5, bạn có thể viết và compile đoạn code như sau mà không ném error hoặc warning:
List list = new ArrayList();
list.add("hey");
list.add(new Object());
Bạn có thể thêm giá trị bất kỳ kiểu vào list hoặc Java Collection khác mà không phải khai báo kiểu dữ liệu nó lưu trữ. Nhưng khi bạn lấy giá trị từ list, bạn phải ép kiểu (cast) nó tường minh sang một kiểu nhất định.
Xem xét lặp qua list trên.
for (int i=0; i < list.size(); i++) {
String value = (String) list.get(i); // ClassCastException khi i=1
}
Việc cho phép tạo list không khai báo trước kiểu dữ liệu lưu trữ, như chúng ta làm, có thể dẫn đến lập trình viên mắc lỗi như trên ném ClassCastExceptions tại runtime.
Generics được giới thiệu để ngăn chặn lập trình viên mắc những lỗi như vậy.
Với generics, bạn có thể khai báo tường minh kiểu dữ liệu sẽ được lưu khi tạo Java Collection như ví dụ sau:
Lưu ý: Bạn vẫn có thể tạo Java collection object không chỉ định kiểu dữ liệu lưu trữ nhưng không khuyến khích.
List stringList = new ArrayList<>();
Bây giờ, bạn không thể nhầm lẫn lưu Integer vào list kiểu String mà không ném compile-time error. Điều này đảm bảo chương trình của bạn không gặp runtime error.
stringList.add(new Integer(4)); // Compile time Error
Mục đích chính của việc giới thiệu generics vào Java là tránh gặp ClassCastExceptions tại runtime.
Tạo Java Generics
Bạn có thể dùng generics để tạo Java class và method. Hãy xem ví dụ cách tạo generics cho từng loại.
Generic Class
Khi tạo generic class, type parameter cho class được thêm ở cuối tên class trong ngoặc vuông <>.
public class GenericClass {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return this.item;
}
}
Ở đây, T là type parameter (tham số kiểu dữ liệu). T, N, và E là một số chữ cái dùng cho type parameter theo convention của Java.
Trong ví dụ trên, bạn có thể truyền một kiểu dữ liệu cụ thể khi tạo object GenericClass.
public static void main(String[] args) {
GenericClass gc1 = new GenericClass<>();
gc1.setItem("hello");
String item1 = gc1.getItem(); // "hello"
gc1.setItem(new Object()); // Error
GenericClass gc2 = new GenericClass<>();
gc2.setItem(new Integer(1));
Integer item2 = gc2.getItem(); // 1
gc2.setItem("hello"); // Error
}
Bạn không thể truyền primitive data type cho type parameter khi tạo generic class object. Chỉ kiểu dữ liệu extends Object type mới có thể truyền làm type parameter.
Ví dụ:
GenericClass gc3 = new GenericClass<>(); // Error
Generic Methods
Tạo generic method tuân theo pattern tương tự tạo generic class. Bạn có thể triển khai generic method bên trong generic class cũng như non-generic class.
public class GenericMethodClass {
public static void printItems(T[] arr){
for (int i=0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static void main(String[] args) {
String[] arr1 = {"Cat", "Dog", "Mouse"};
Integer[] arr2 = {1, 2, 3};
GenericMethodClass.printItems(arr1); // "Cat", "Dog", "Mouse"
GenericMethodClass.printItems(arr2); // 1, 2, 3
}
}
Ở đây, bạn có thể truyền mảng kiểu cụ thể để parameterize method. Generic method printItems() lặp qua mảng được truyền và in các mục lưu trữ giống như method Java bình thường.
Bounded Type Parameters (Tham Số Kiểu Có Ranh Giới)
Cho đến nay, generic class và method chúng ta tạo ở trên có thể parameterize thành bất kỳ kiểu dữ liệu nào ngoại trừ primitive type. Nhưng nếu chúng ta muốn giới hạn kiểu dữ liệu có thể truyền cho generics? Đây là lúc bounded type parameters xuất hiện.
Bạn có thể bound (ràng buộc) kiểu dữ liệu được chấp nhận bởi generic class hoặc method bằng cách chỉ định nó phải là subclass của kiểu dữ liệu khác.
Ví dụ:
// chỉ chấp nhận subclass của List
public class UpperBoundedClass{
// chỉ chấp nhận subclass của List
public void UpperBoundedMethod(T[] arr) {
}
}
Ở đây, UpperBoundedClass và UpperBoundedMethod chỉ có thể parameterize dùng subtype của kiểu dữ liệu List.
Kiểu dữ liệu List đóng vai trò upper bound (ranh giới trên) cho type parameter. Nếu bạn cố dùng kiểu dữ liệu không phải subtype của List, nó sẽ ném compile-time error.
Ranh giới (bounds) không giới hạn chỉ ở class. Bạn có thể truyền interface cũng được. Extends interface nghĩa là, trong trường hợp này, implement interface.
Một parameter cũng có thể có nhiều bounds như ví dụ này cho thấy.
// chỉ chấp nhận subclass của cả Mammal và Animal
public class MultipleBoundedClass{
// chỉ chấp nhận subclass của cả Mammal và Animal
public void MultipleBoundedMethod(T[] arr){
}
}
Kiểu dữ liệu chấp nhận phải là subclass của cả Animal và Mammal class. Nếu một trong các bounds này là class, nó phải đứng đầu trong khai báo bound.
Trong ví dụ trên, nếu Mammal là class và Animal là interface, Mammal phải đứng đầu như trên. Nếu không, code ném compile-time error.
Java Generics Wildcards
Wildcards được dùng để truyền tham số kiểu generic cho method. Khác với generic method, ở đây, generic parameter được truyền cho parameter được chấp nhận bởi method, khác với type parameter chúng ta bàn ở trên. Wildcard được biểu diễn bởi ký hiệu ?.
public void printItems(List> list) {
for (int i=0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
Method printItems() trên chấp nhận list của bất kỳ kiểu dữ liệu nào làm parameter. Điều này ngăn chặn lập trình viên phải lặp lại code cho list các kiểu dữ liệu khác nhau, trường hợp sẽ xảy ra nếu không có generics.
Upper Bounded Wildcards
Nếu chúng ta muốn giới hạn kiểu dữ liệu lưu trong list được chấp nhận bởi method, chúng ta có thể dùng bounded wildcards.
Ví dụ:
public void printSubTypes(List extends Color> list) {
for (int i=0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
Method printSubTypes() chỉ chấp nhận list lưu subtype của Color. Nó chấp nhận list object RedColor hoặc BlueColor, nhưng không chấp nhận list object Animal. Điều này vì Animal không phải subtype của Color. Đây là ví dụ của upper-bounded wildcard.
Lower Bounded Wildcards
Tương tự, nếu chúng ta có:
public void printSuperTypes(List super Dog> list) {
for (int i=0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
Thì method printSuperTypes() chỉ chấp nhận list lưu super type của class Dog. Nó sẽ chấp nhận list object Mammal hoặc Animal nhưng không chấp nhận list object LabDog vì LabDog không phải superclass của Dog, mà là subclass. Đây là ví dụ của lower-bounded wildcard.
Kết Luận
Java Generics đã trở thành tính năng mà lập trình viên không thể sống thiếu kể từ khi giới thiệu.
Sự phổ biến này đến từ tác động của nó làm cho cuộc sống lập trình viên dễ dàng hơn. Ngoài việc ngăn chặn họ mắc lỗi code, việc dùng generics làm code ít lặp lại hơn. Bạn có nhận ra nó làm thế nào để generalize class và method để tránh phải lặp code cho các kiểu dữ liệu khác nhau?
Nắm vững generics quan trọng để thành chuyên gia trong ngôn ngữ. Vì vậy, áp dụng những gì bạn học trong hướng dẫn này vào code thực tế là đường đi tiến về phía trước.