Ở đây mình sử dụng cú pháp trong lập trình Java để làm ví dụ mong nó sẽ sáng sủa và dễ hiểu cho mọi người.
Với lập trình, kế thừa không còn là khái niệm xa lạ. Generic cũng vậy, là một trừu tượng dữ liệu, bản thân chúng cũng có thể kế thừa hoặc được kế thừa từ một kiểu dữ liệu khác.
Trở lại ví dụ bài viết đầu tiên, chúng ta có một Vector và kiểu khai báo là Number, chúng được dùng để lưu trữ tất cả các đối tượng được extends từ Number. Xem xét một ví dụ sau :
public <E extends Number> List<E> toList(E...e) {
Vector<E> values = new Vector<E>();
for(E ele : e){
values.add(ele);
}
return values;
}
Hàm toList nhận đầu vào là một mảng thuộc kiểu E, kiểu E được định nghĩa với khai báo <E extends Number>, E là một kiểu extends từ Number. Hàm toList sẽ tạo ra một Vector và lưu trữ các phần tử từ mảng đầu vào, Vector values là giá trị được trả về. Trong các hoàn cảnh cài đặt được đề cập ở bài viết thứ hai, chúng ta có thể hoàn toàn định nghĩa một generic được thừa kế từ một kiểu dữ liệu nào đó. Theo tiếng Anh thì chúng được gọi là subtyping, tạm dịch là kiểu dữ liệu con.
WildCards
Dịch là gì nhỉ ? WildCard – ký tự đại diện. Vâng, chúng sẽ được ký hiệu là dấu hỏi. Wildcard có thể đứng một mình trong cài đặt, cũng có thể là khai báo theo dạng kế thừa hoặc được kết thừa. Lần lượt xét đến từng ví dụ sau.
Wildcard trong khai báo generic : List<?> list. Wildcard trong trường hợp này như một khai báo generic chưa biết chắc, mặc định chúng ta tạm hiểu ngầm là khai báo list chứa các object. Kiểu generic sẽ được tường minh khi khởi tạo list, chẳng hạn như ví dụ sau:
List<?> list = new ArrayList<Number>();
//...
list = new ArrayList<Integer>();
//...
list = new Vector<String>();
Như chúng ta thấy, đầu tiên là list được gán cho một đối tượng ArrayList với kiểu generic là Number, lúc sau list được gán là một đối tượng ArrayList với kiểu generic là Integer, cuối cùng là vector với kiểu String. Wildcard như một “lối thoát” cho sự linh hoạt của đối tượng khai báo generic, nghĩa là chúng sẽ tường minh khi được gán đối tượng. Chúng ta không thể tạo một đối tượng với khai báo generic dạng wildcard, chẳng hạn khai báo sau sẽ gặp lỗi ngữ pháp.
List<?> list = new ArrayList<?>();
Trong cài đặt wildcard thường được dùng để ép kiểu, wildcard cũng có thể extends hoặc khai báo được với kiểu được thừa kế. Các mẩu code sau sẽ là minh họa.
List<? extends Number> list = new ArrayList<Long>();
và
List<? super Integer> list = new ArrayList<Number>();
Với generic thông thường thì chúng ta không thể khai báo kiểu như List<E super Integer> nhưng với wildcard thì có thể. Wildcard cũng là generic nhưng là dạng khai báo trừu tượng bất tường minh.
Bây giờ chúng ta lần lượt xét đến việc sử dụng wildcard như thế nào ?
Với Put và Get, không phải lúc nào chúng ta cũng có thể sử dụng tùy ý trong các kiểu generic dạng wildcard. Với Get, xét một ví dụ nhỏ sau :
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(3);
arrayList.add(45);
List<? extends Number> list = arrayList;
for(Number ele : list){
System.out.println(ele);
}
Có thể lấy dữ liệu từ một list khai báo wildcard, dữ liệu lấy ra khai báo dạng object hoặc Number là kiểu được wildcard thừa kế.
Nhưng trong trường hợp Put, đặt dữ liệu thì lại có phần khó khăn hơn, chẳng hạn chúng ta không thể add dữ liệu như đoạn code sau:
ArrayList<? extends Number> arrayList = new ArrayList<Integer>();
arrayList.add(3); // code lỗi.
Như chúng ta lại có thể đặt null value vào trong list:
ArrayList<? extends Number> arrayList = new ArrayList<Integer>();
arrayList.add(null);
Tuy nhiên với remove thì hoàn toàn có thể : arrayList.remove(4); là code hợp lệ.
Với các khai báo extends cho wildcard thì không thể đặt dữ liệu nhưng với những khai báo super thì lại hoàn toàn có thể:
ArrayList<? super Integer> arrayList = new ArrayList<Integer>();
arrayList.add(45);
Mọi phép toán như set, remove, … đều có thể làm việc được với các dạng wildcard này.
Wildcard là một dạng generic, nó không phải là cách thức độc quyền cho collection. Ví dụ sau được cài đặt với class.
public interface Yahoo {
...
}
public class Mail implements Yahoo {
...
}
...
public static void main(String[] args) throws Exception {
Class<? extends Yahoo> clazz = Mail.class;
System.out.println(clazz.newInstance());
}
Việc hạn chế Put cũng vậy, chúng không phải chỉ được giới hạn trên collection, code sau là một minh họa:
public static interface Yahoo {
}
public static class Mail<T> implements Yahoo {
public void setT(T t) {
System.out.println(t.getClass());
}
}
Và code sử dụng :
Class<? extends Yahoo> clazz = Mail.class;
Mail<? extends Number> mail = (Mail<? extends Number>)clazz.newInstance();
mail.setT(45);// compile-time error
mail.setT("hello");// compile-time error
Các set đều không hợp lệ, lỗi compile.
Mặc dù wildcard là một cách thức trợ giúp cho sự linh động trong khai báo generic nhưng những hạn chế trên đã khiến cho nhiều cài đặt đắn đo trước khi quyết định mô hình của wildcard. Nói tóm lại, chúng rất ít khi được lập trình viên sử dụng trong những thiết kế của mình, ít nhất là với tầm hiểu biết của tôi.
Generic của Generic.
Đúng vậy, nếu chúng ta coi việc thể định nghĩa nhiều generic cho một class hoặc method có thể tạo thành độ rộng thì việc dùng generic cho generic giống như độ sâu trong sử dụng. Tuy nhiên, generic là một dạng trừu tượng dữ liệu, do đó trước khi chúng một định danh cụ thể được cung cấp thì chúng ta không thể trừu tượng cái trừu tượng, đó là nguyên nhân gây ra lỗi compile của cài đặt sau:
public class Generic2 <T<H<B>>> {
public Generic2(){
}
}
Nhưng, nhưng đã là định danh cụ thể thì điều đó đồng nghĩa với việc chúng ta có thể dùng generic, những dòng code dưới đây là minh chứng cụ thể :
List<List<List<List<Class<String>>>>> list ;
list = new ArrayList<List<List<List<Class<String>>>>>();
khai báo và khởi tạo trên là hợp lệ bởi chúng là các định danh cụ thể, tuy nhiên level của generic quá sâu và cũng chẳng ai dại gì mà tự làm khó mình bằng cài đặt như vậy.
Generic với reflection.
Reflection hiện nay không còn xa lạ gì với nhiều người bởi lẽ vì sự linh động trong design cũng như sự lười biếng của lập trình viên mà nó được cài đặt quá nhiều. Reflection có thể ví von như một dạng ánh xạ hay ảnh. Từ java 5 trở đi, ngoài collection framework thì bản thân tôi cho rằng reflection framework là ứng cử viên thứ hai cho tần xuất cài đặt generic. Xét về mật độ chúng ta có thể thấy khái nhiều các định nghĩa trong Reflection cài đặt generic: class, constructor, method, field,… và đoạn code sau mà một minh họa cho việc sử dụng generic với reflection.
Class<String> clazz = String.class;
Constructor<String> constructor = clazz.getConstructor(new Class[]{String.class});
String strObj = constructor.newInstance("yahoo hee");
System.out.println(strObj +" is instanceOf "+strObj.getClass());
Strong một vài thiết kế, nếu như chúng ta không thể gọi toán tử new thì việc kết hợp reflection với generic có thể tạo ra object khá hiệu quả. Code sau là một minh họa:
public static <T> T createObjects(Class<T> clazz, Class [] parameterTypes, Object...objs) throws Exception {
Constructor<T> constructor = clazz.getConstructor(parameterTypes);
constructor.setAccessible(true);
return constructor.newInstance(objs);
}
và code sử dụng :
String str = Generic2.createObjects(String.class, new Class[]{String.class}, "hi");
System.out.println(str);
int number = Generic2.createObjects(Integer.class, new Class[]{String.class}, "2");
System.out.println(number);
Những mô hình như trên có thể giả lập các cài đặt closure thông qua reflection, một điểm mới sẽ được giới thiệu ở bản JDK 7.0. Tuy nhiên, reflection là một dạng ánh xạ, do đó, chúng chạy khá chậm mặc dù có thể giảm code cài đặt cho lập trình viên cũng như sự linh hoạt. Tôi đã gặp những bài học đắt giá với việc cài đặt reflection và cuối cùng thì rút ra được kinh nghiệm cho bản thân là cân nhắc giữa hiệu quả mang lại giữa một design đơn thuần với việc lựa chọn reflection. Đây cũng là nguyên nhân khiến hibernate càng ngày càng trở nên nặng nề, chậm chạp với mô hình object data mapping của nó. Với JSR 170, eXo đã quyết định loại bỏ dần hibernate ra khỏi những module trong flatform và portal. Điều mà chúng tôi cần mang lại là một kiến trúc khỏe và linh hoạt hơn là những thiết kế phức tạp nặng nề đến mức không cần thiết khi phải căn ke những khả năng của lập trình viên. Có lẽ xin được dừng việc đề cập reflection với generic tại đây bởi lẽ cũng không cần thiết đến mức phải trình bày lan man.
Bình luận