Generic là một tính năng cho phép chúng ta truyền các kiểu dữ liệu cao cấp như lớp và interface vào phương thức, lưu ý là kiểu truyền này khác với việc truyền đối tượng lớp hoặc interface vào phương thức mà chúng ta thường dùng.
Generic có các đặc điểm sau:
- Hỗ trợ trình biên dịch kiểm tra tính chặt chẽ của kiểu dữ liệu
- Loại bỏ việc ép kiểu dữ liệu
- Cho phép lập trình viên viết các thuật toán generic
Ví dụ
Chúng ta định nghĩa lớp Box
như sau:
public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } }
Lớp này có một thuộc tính kiểu Object
, 2 phương thức get()
và set()
để gán và lấy đối tượng Object
đó.
Do kiểu dữ liệu ở đây là Object
nên chúng ta có thể truyền vào bất cứ kiểu dữ liệu nào cũng được vì Object
là lớp gốc trong cây phân lớp, ngoại trừ các kiểu dữ liệu cơ bản như int
, float
, double
…. Nhưng khi chúng ta muốn lấy ra đúng kiểu dữ liệu mà chúng ta đã truyền vào thì chúng ta phải ép kiểu, ví dụ:
Object obj = new Object(); Integer n = new Integer(); obj.set(n); Integer m = (Integer)obj.get();
Và giả sử ở một đoạn code khác chúng ta lại truyền vào một đối tượng khác như String
, Float
..v.v và nếu nhầm lẫn, chúng ta có thể ép kiểu sai và gây ra lỗi exception.
Để giải quyết thì chúng ta có thể viết lại lớp Box
theo kiểu Generic.
Cú pháp Generic
Cú pháp của Generic như sau:
class name<T1, T2, ..., Tn> { }
Chúng ta định nghĩa lớp như bình thường, và theo sau tên lớp là cặp dấu <>
, rồi đến các tham số biến là các kí tự T1
, T2
,… Tn.
Bên trong khai báo lớp, tất cả các kiểu dữ liệu đều có tên là T1
, T2
… Tn
chứ không còn là các tên cụ thể như Object
nữa.
Lớp Box
được viết lại như sau:
public class Box<T> { private T object; public T get() { return this.object; } public void set(T object) { this.object = object; } }
Tham số biến ở đây có thể hiểu là tham số cho các kiểu dữ liệu (trừ các kiểu cơ bản như int
, float
, char
…), và khi khởi tạo một đối tượng thì chúng ta thay tế “T
“ bằng tên các lớp cụ thể như sau:
Box<Integer> integerBox = new Box<Integer>();
Và như thế thì thuộc tính object bên trong lớp Box
giờ đây sẽ có kiểu dữ liệu là Integer
, và khi gọi phương thức get()
thì chúng ta không cần phải ép kiểu nữa.
Một số lưu ý:
- Có thể sử dụng bất kì kí tự nào cho tham số biến và có thể viết hoa hay viết thường tùy ý, không nhất thiết phải là
T.
- Kể từ phiên bản Java SE 7 trở lên, chúng ta có thể không truyền vào tham số biến khi khởi tạo , tức là chỉ ghi dấu
<>
trống không, ví dụnew Box<>(),
và trình biên dịch sẽ tự suy ra kiểu dữ liệu phù hợp.
Sử dụng nhiều tham số biến
Trong cú pháp ở trên chúng ta thấy là có thể sử dụng nhiều tham số biến khi khai báo (T1
, T2
, … Tn
). Để ví dụ thì chúng ta xem đoạn code sau:
public class MyPair<K, V> { private K key; private V value; public MyPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
Để khai báo nhiều tham số biến thì chúng ta chỉ cần ghi tên chúng cách nhau bởi dấu phẩy. Khi sử dụng thì chúng ta cũng khai báo danh sách các tên lớp hoặc interface cách nhau bởi dấu phẩy.
MyPair<String, Integer> myPair1 = new MyPair<String, Integer>("PhoCode", 2017); MyPair<String, String> myPair2 = new MyPair<String, String>("PhoCode", "Blog");
Như đã lưu ý ở trên, chúng ta có thể không điền tên lớp trong đoạn code khởi tạo:
MyPair<String, Integer> myPair1 = new MyPair<>("PhoCode", 2017); MyPair<String, String> myPair2 = new MyPair<>("PhoCode", "Blog");
Kiểu nguyên thủy
Khi chúng ta không khai báo tham số biến cho lớp thì lớp đó bây giờ sẽ được gọi là “kiểu nguyên thủy”, chẳng hạn như chúng ta có đoạn code định nghĩa lớp Box
như sau:
public class Box<T> { // ... }
Như bình thường thì chúng ta sẽ truyền tên lớp khi khởi tạo lớp Box
:
Box<Integer> intBox = new Box<>();
Tuy nhiên chúng ta có thể không khai báo cả tên lớp và lúc này lớp Box
được gọi là kiểu nguyên thủy.
Box rawBox = new Box();
Chúng ta có thể tham chiếu đối tượng kiểu nguyên thủy tới đối tượng có khai báo rõ ràng tên lớp như sau:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; // OK
Nếu tham chiếu theo chiều ngược lại thì Java sẽ in câu cảnh báo:
Box rawBox = new Box(); Box<Integer> intBox = rawBox; // warning: unchecked conversion
Và đôi khi sẽ là lỗi biên dịch luôn nếu việc chuyển đổi kiểu không phù hợp. Để ẩn câu cảnh báo thì chúng ta có thể dùng annotation @SuppressWarnings("unchecked")
, tuy nhiên chúng ta cũng không nên dùng cách này mà nên làm mọi thứ minh bạch thì hơn.