
Java Generic
Đó là điểm mới về mặt ngôn ngữ từ java 5. Mặc dù đã sử dụng Generic khá thành thạo từ lâu
nhưng đến nay tôi mới có dịp đề cập về nó một cách chi tiết hơn.
Một ưu điểm mà người ta thường nhắc tới chính là dùng Generic có thể hạn chế được các lỗi
trong ép kiểu. Không biết nên bắt đầu từ đâu nhỉ ? Có lẽ nên bắt đầu từ Collections
Framework, một trong những gói được cài đặt generic nhiều nhất ở Java 5.
Container với Generic.
Bạn hay dùng List, cụ thể nhất với những người mới làm quen với java là Vector. Vector giống
như một mảng động được cài đặc các functions từ List interface. Tuy nhiên, Vector có
synchronized, do đó vận dụng Vector một các bừa bãi có thể làm cho code java chạy chậm
hơn rất nhiều, sử dụng người anh em của nó là ArrayList và LinkedList trong những trường
hợp không cần synchronized sẽ nhanh hơn. Nhưng xin được cài đặt thử vài dòng với Vector vì
nó khá quen với nhiều người.
Vector là một mảng động, một mảng các Object (Collections Framework trong JDK của Sun
không hỗ trợ primary type). Do đó, nếu khai báo như sau thì bạn có thể chứa bất cứ kiểu dữ
liệu nào được extends từ Object :
Vector exam = new Vector();
Trong lập trình, chúng ta thường chứa một mảng dữ liệu cùng kiểu, chẳng hạn một Vector
chứa các String hoặc integer. Ví dụ :
exam.add(new Integer(1));
Do đó khi lấy lại dữ liệu chúng ta phải thực hiện việc ép kiểu, ví dụ:
Integer intg = (Integer)exam.get(0);
Ở những phiên bản trước, lập trình viên bắt buộc phải ép kiểu dữ liệu trả về từ một
collection, điều trên thực sự là hơi bất tiện và không cần thiết với một ngôn ngữ. Hơn nữa
việc ép kiểu có thể ném ra lỗi bởi dữ liệu trả về không thuộc kiểu dữ liệu mà bạn cần lấy.
Chẳng hạn exam.add(“4”); và lấy lại dữ liệu Integer intg2 = (Integer)exam.get(1);. Khi đó,
một lỗi runtime xảy ra. Trong lập trình, lỗi có thể xảy ra ở thời điểm compile (chủ yếu là lỗi về
cú pháp) và lỗi xảy ra lúc runtime. Những lỗi xảy ra ở thời điểm chạy chương trình thường là
những lỗi khó debug, NullPointerException là một ví dụ và ClassCastException cũng rất
thường gặp. Trong một vài cài đặt, chúng trở thành những lỗi cứng đầu nhất với lập trình
viên.

Generic đã khắc phục được điểm yếu đó, dĩ nhiên 100% thì không thể khẳng định được
nhưng việc cài đặt Generic có thể hạn chế được tối đa các lỗi ClassCastException. Chẳng hạn,
bây giờ bạn có thể viết như sau:
Vector<Integer> exam = new Vector<Integer>();
exam.add(1);
Khai báo một vector dùng để chứa các giá trị kiểu Integer. Khi đó, ở bất cứ đâu nếu tôi đưa
dữ liệu dưới dạng khác, chẳng hạn exam.add(“4”); thì compiler sẽ báo lỗi ngay ở lúc biên
dịch chương trình và coi đó như một lỗi cú pháp. Bạn đừng ngạc nhiên khi tôi viết
exam.add(1);, thực ra thì số 1 thuộc kiểu int, một primary type chứ không phải là Integer –
một kiểu dữ liệu đối tượng. Tuy nhiên trong java 5 có cài đặt Boxing - Unboxing nên chúng ta
có thể mặc nhiên sử dụng ở một số hoàn cảnh các giá trị kiểu int giống như là Integer và
ngược lại.
Nếu dữ liệu ở nhiều kiểu đối tượng khác nhau thì generic có thể khai báo ở dạng cha, tức là
kiểu đối tượng chung nhất mà chúng extend. Chẳng hạn, nếu muốn tạo một vector chứa dữ
liệu ở dạng string hoặc integer tôi có thể dùng Object, khai báo như sau:
Vector<Object> exam = new Vector<Object>();
Nhưng khi muốn lấy lại chính xác dữ liệu thuộc kiểu String hoặc integer bạn vẫn phải dùng ép
kiểu, do đó, có thể không cần khai báo generic : Vector exam = new Vector(). Khi biên dịch,
bạn sẽ nhận được một thông báo về việc sự an toàn khi thực hiện các phép toán trên dữ liệu.
Nếu không muốn nhận lại sự thông báo này có thể khai báo một annotation là
@SuppressWarnings("unchecked"). Cụ thể một hàm main nhỏ sẽ viết như sau:
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Vector exam = new Vector();
exam.add(1);
}
tìm ra kiểu chung gần nhất để khai báo generic cho những loại dữ liệu có thể được lưu trữ
trong collection là một cách nhằm giảm bớt lỗi runtime ngay từ lúc compile, chẳng hạn nếu
muốn khai báo một vector chứa dữ liệu kiểu Integer hoặc Long tôi có thể chọn kiểu cho
generic là Number bởi cả Long và Integer đều được extend từ lớp này. Cụ thể như sau :
Vector<Number> exam = new Vector<Number>();
exam.add(1);
Khi đó, nếu ở đâu đó, tôi có vô tình add một số dưới dạng string thì một lỗi ngữ pháp sẽ
thông báo cho tôi biết ngay sau khi tôi compile class đó.
Nếu phải cài đặt container, tốt nhất là nên cài đặt generic, điều này sẽ giúp container của bạn
được linh hoạt và có thể tránh được các lỗi về ép kiểu dữ liệu trên các phép toán. Chẳng hạn,
tôi cài một stack như sau:

public class Stack<T> {
private Node<T> stack = null;
public T pop( ) {
T result = stack.value;
stack = stack.next;
return result;
}
public boolean hasNext(){
return stack != null;
}
public void push(T v) { stack = new Node<T>(v, stack); }
private class Node<T> {
public T value;
public Node<T> next;
Node(T v) {
value = v;
}
Node(T v, Node<T> n) {
value = v;
next = n;
}
}
}
Một stack chứa một chuỗi các node được gắn kết với nhau. Các node này chứa dữ liệu thực
là một kiểu dữ liệu sẽ được khai báo khi dùng stack. Trong cài đặt Generic mặc định qui ước
là <T> (không nhất thiết phải đặt là T, bạn có thể đặt A, B, C, E,... bất kì). Nó tượng trưng
cho dữ liệu thực được đồng nhất ở các phép toán. <T> khai báo trên Stack đồng nhất với
Node, trong node chứa giá trị thực của một đối tượng thuộc T nào đó mà ta chưa biết: value.
Các phép toán trên stack cũng đồng nhất các kiểu dữ liệu truyền vào và lấy ra là T. Thực chất
T là một tượng trưng sẽ được cụ thể hóa khi sử dụng.
Khi đó, tôi có thể dùng stack này để chứa nhiều kiểu dữ liệu khác nhau với sự linh hoạt và an
toàn khi khai báo generic. Ví dụ:
Stack<Integer> stackInt = new Stack<Integer>();
stackInt.push(4);
stackInt.push(new Integer(3));
while(stackInt.hasNext()){
System.out.println("integer value :"+stackInt.pop());
}
Stack<String> stackStr = new Stack<String>();
stackStr.push("4");
stackStr.push(new Integer(3).toString());

while(stackStr.hasNext()){
System.out.println("string value :"+stackStr.pop());
}
Trong các class của Collection framework cũng được cài đặt generic tương tự, chẳng hạn kiểu
generic của lớp ArrayList sẽ được nhận vào từ class viết như sau:
public class ArrayList<T> implements List<T>{
T [] values ;
...
public ArrayList(int initialCapacity) {
super();
values = (T[])new Object[initialCapacity];
}
...
}
Khi tạo mới mảng dữ liệu chúng ta không thể trực tiếp viết: T[] values = new
T[initialCapacity]; mà phải tạo một mảng object rồi ép kiểu chúng lại thành mảng T – mảng
có kiểu giá trị thực sự sẽ được khai báo khi sử dụng. Việc ép kiểu trong compiler của IBM có
thể viết như sau T[] values = T[].class.cast(new Object[initialCapacity]); cách này có vẻ
chuyên nghiệp hơn nhưng khi compile bằng javac của Sun thì gặp phải lỗi cú pháp “cannot
select from a type variable” mặc dù trong runtime thì JVM có thể chạy được. Khi đó, có thể
ép theo kiểu T[] values = (T[])new Object[initialCapacity];. Vấn đề về không được phép new
T[initialCapacity] hay new T(); là do kiểu dữ liệu thực chưa được biết chính xác chúng có
những contructor nào (bao nhiêu và kiểu dữ liệu của thông số truyền vào khi tạo object).Do
đó chúng ta không thể new trực tiếp được các generic type. Tuy nhiên, nếu đã có class thì có
thể tạo object được, bởi lẽ khi đó chúng ta có thể dùng reflection lấy ra được contructor của
class truyền vào. Method sau là một ví dụ :
public T createInstance(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
việc new Object[initialCapacity] thì không liên gì đến contructor của generic type (của T),
chúng chỉ việc cấp pháp vùng nhớ lưu địa chỉ con trỏ trỏ đến một mảng object nào đó mà T
cũng là một dạng object. Do đó, khởi tạo một mảng địa chỉ, sau đó ép mảng địa chỉ này trỏ
về dạng T là phép toán hợp lệ. Trong method tạo instance trên, class của kiểu dữ liệu thực sẽ
phải tồn tại một contructor rỗng với quyền truy cập là public. Nếu không chúng ta phải dùng
reflection, mặc nhiên generic type bắt buộc phải tồn tại một contructor nào đó đã biết trước
số lượng và kiểu dữ liệu của các thông số truyền vào. Nếu không lỗi về
IllegalArgumentException, NoSuchMethodException hoặc SecurityException có thể xảy ra.
Ở phần này, xin được trình bày về các thức cài đặt generic trong code cụ thể.
Ai đã từng làm việc với C++ đều có thể kết luận rằng generic trong java cũng có điểm tương
đồng như template trong C++, nhưng ở java thì generic có sự đơn giản và phức tạp xét trên

một vài tiêu chí nào đó. Generic giống như một khai báo biến tượng trưng. Điều đó đồng
nghĩa với việc có generic toàn cục và có generic cục bộ. Đối với class, interface hay các
abstract đều có thể cài đặt generic, chúng được bao bọc bởi cặp đóng - mở của ngoặc nhọn,
biểu thị bằng một hoặc nhiều chữ cái và có thể khai báo nhiều generic trong class đó. Class
sau đây minh họa việc khai báo generic.
public class GenericExam<t,g, T, EN, A,y,ku_> {
T t;
EN english;
public GenericExam(T t, EN en){
this.t = t;
this.english = en;
System.out.println("init "+hashCode()+" ... "+t+" ... "+english);
}
}
Như chúng ta đã thấy, generic cũng tuân thủ các quy tắc đặt tên thông thường trong Java.
Tuy nhiên, trong cài đặt, việc khai báo generic thường được dùng bằng một chữ cái hoa cho
dễ nhận biết. Và bằng kinh nghiệm bản thân, tôi cũng xin khuyến cáo thiết kế một class
không nên vượt quá 3 khai báo generic, mặc dù chưa có những khẳng định chính xác về
những khuyết điểm của việc cài đặt quá nhiều generic, nhưng một điều chắc chắn rằng, nếu
dùng quá nhiều generic sẽ gây nên tình trạng rối code cho bản thân cũng như những người
sử dụng các class của bạn. Ở những cuốn sách hay các tài liệu liên quan đến gerneric, người
ta thường tập trung chủ yếu vào việc minh họa và khai thác các ý tưởng thiết kế ở collection.
Generic đã phản ánh tối đa tác dụng của nó trên các dạng framework đó. Tuy nhiên, mở rộng
vấn đề thì generic có thể cài đặt ở bất cứ đâu phụ thuộc vào ý tưởng thiết kế một hay nhiều
class của bạn. Nếu như tác dụng đầu tiên của generic là tránh được các lỗi ép kiểu thì qua
kinh nghiệm code với eXo Portal 2.0 (tên mã là WebOS) đã cho tôi thấy, generic còn tạo ra sự
linh hoạt, gọn nhẹ, tường minh,... mặc dù chúng thực chất là những trừu tượng hóa dữ liệu.
Xét ở ví dụ trên, tôi khai báo tới bảy generic khác nhau, class chứa hai biến toàn cục thuộc
dạng T và EN. Chú ý: các biến static sẽ không được khai báo kiểu là generic. Khi tạo mới
Object của GenericExam, coder có thể phải cung cấp đủ 7 khai báo cho đối tượng được tạo
ra từ class đó nếu khai báo generic. Đoạn code sau là một ví dụ minh họa :
GenericExam<String, Integer, String, Double, List<Long>, JFrame, Window> exp;
exp = new GenericExam<String, Integer, String, Double, List<Long>, JFrame,
Window>("yahoo", new Double(3));
System.out.println(exp);
GenericExam exp2 = new GenericExam("google", 8);
System.out.println(exp2);
với exp, khi tạo object, chúng ta có đầy đủ 7 kiểu dữ liệu, contructor nhận vào hai đối tượng
thuộc kiểu thứ nhất và kiểu thứ tư là String và Double. Nếu bạn truyền vào một đối số khác,
chắc chắn bạn sẽ không thể biên dịch nổi. Ở exp2, tôi không khai báo generic, về compile và
run, ngoài một cảnh báo kiểu dữ liệu truyền vào thì instance exp2 vẫn được tạo ra một cách
bình thường. Tuy nhiên, chúng ta có thể lấy thêm một ví dụ nữa để xem xét việc không khai

