Collection và Generic – phần 2

phần trước, tôi đề cập đến generic cùng những hiểu biết về cài đặt trong Collections framework. Xét về khía cạnh sử dụng, chắc nó cũng không còn xa lạ với nhiều người. Ở bài viết 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 báo generic, dữ liệu sẽ tồn tại như thế nào trong object?

Tôi bổ sung thêm một hàm (setter) cho GenericExam như sau :

public void setT(T t){
     this.t = t;
     System.out.println("set data "+hashCode()+" ... "+t+" ... "+english);
  }

và đoạn code sử dụng generic sau:

GenericExam exp2 = new GenericExam("google", 8);
  exp2.setT(45);
   System.out.println(exp2);

Khi compile và chạy, mọi việc đều diễn ra hết sức bình thường, ở init, đối tượng t thuộc kiểu String nhưng sau khi setT với đầu vào là một số nguyên thì t lại thuộc kiểu Integer. Điều đó cho phép chúng ta kết luận rằng : mặc định, nếu không khai báo thì mọi generic sẽ là object, khi đó chúng ta mặc nhiên có thể đặt giá trị vào biến với các dữ liệu thuộc nhiều kiểu object khác nhau. Xin đề cập vấn đề này sâu hơn ở phần 3 của bài viết với reflection.

Nếu như bạn khai báo generic, dĩ nhiên khi exp.setT(45) thì sẽ không thể compile được bởi kiểu dữ liệu nhận vào của setter đó phải là String. Việc không khai báo generic thường không ảnh hưởng nhiều đến những cài đặt trừ phép toán ép kiểu, dĩ nhiên việc không khai báo generic sẽ làm nảy sinh sự trường hợp lập trình viên đưa vào dữ liệu sai kiểu, và khi JVM thực hiện một phép ép kiểu, chúng sẽ throw ra một exception trong runtime. Đây là có thể là lỗi nguy hiểm ở nhiều chương trình.

Đối với interface hoặc abstract, inner class, outer class, việc cài đặt generic cũng diễn ra tương tự như trên, chúng ta có những khai báo toàn cục theo ngữ pháp đặt tên, nằm trong cặp ngoặc nhọn đóng mở và kế cận ở tên class hoặc interface. Ví dụ sau là một minh họa cho việc khai báo generic ở interface.

public interface Listener<T> {
     public void execute(T t);
  }

và một cài đặt từ Listener:

public class SelectionListener implements Listener<String> {
     public void execute(String t){
       System.out.println(t);
    }
  }

Như vậy, kiểu String sẽ là kiểu dữ liệu được dùng xuyên suốt các cài đặt của Listener trong SelectionListener. Một thay đổi khác có thể là lỗi ngữ pháp, không cho phép biên dịch class. Nếu không khai báo generic thì mặc nhiên kiểu của generic sẽ là object. Ví dụ:

public class SelectionListener implements Listener {
    public void execute(Object t){
       System.out.println(t);
    }
  }

Trong trường hợp đó generic sẽ là Object, không chấp nhận các primary type (dữ liệu nguyên thủy) như int, double, long, char, byte,… Nếu tôi nhớ không nhầm thì hình như Apache cũng có cài đặt một Collection Framework hỗ trợ các primary type và có sử dụng generic, về cách thức cài đặt cụ thể như thế nào thì tôi chưa có dòm qua và tôi đang viết bài viết này ở nhà, nơi không có internet.

Việc extends hay implements một class hoặc interface có cài đặt generic cũng có tính chất bắc cầu. Trở lại với SelectionListener, chúng ta có một implements sau:

public class SelectionListener<E> implements Listener<E> {
     public void execute(E e){
       System.out.println(e);
    }
  }

Generic được khai báo cho SelectionListener và chúng thông qua cho Listener interface, khi tạo object chúng ta có thể viết:

SelectionListener<Integer> listener = new SelectionListener<Integer>();
   listener.execute(23);

Cũng có thể không cần pass generic của SelectionListener qua cho Interface,như đoạn code sau:

public class SelectionListener<E> implements Listener<String> {
    public void execute(String e){
       System.out.println(e);
    }
  }

  ...

 SelectionListener<Integer> listener = new SelectionListener<Integer>();
   listener.execute("string value");

Trong ví dụ trên, generic của SelectionListener không liên quan gì đến Listener mà generic của nó ở đây nó đã được khai báo là String.

Tôi thử cài đặt generic cho annotation và enum nhưng đều thất bại. Annotation là một dạng meta data (dữ liệu đặc tả chứ không nên dịch là siêu dữ liệu như một số cuốn sách tin học của Việt nam) thì chắc chắn không cài được generic là đúng rồi. Và enum cũng vậy, chúng là loại dữ liệu bất biến trong một JVM.

Bây giờ chúng ta đề cập đến như khai báo cục bộ của generic, một method được viết như sau.

public <M> void setData(M m) {
    System.out.println("print "+m + " instanceOf "+m.getClass());
  }
và code sử dụng :
exp.setData("nhuthuan.blogspot.com");
  exp.setData(3.02);
Kết quả in ra sẽ là:
print nhuthuan.blogspot.com instanceOf class java.lang.String
  print 3.02 instanceOf class java.lang.Double

Kiểu generic M được dùng cho method setData, chúng tuân thủ quy tắc đặt tên và cú pháp, generic được khai báo trước kiểu trả về của method bởi lẽ đôi khi chúng ta lấy kết quả trả về cũng là một đối tượng thuộc kiểu dữ liệu của generic đã khai báo. Ví dụ sau là một minh họa:

public <R> R createInstance(Class<R> clazz) throws Exception {
    return clazz.newInstance();
  }
Và code sử dụng :
try {
    StringBuilder builder ;
    builder = exp.<StringBuilder>createInstance(StringBuilder.class);
    builder.append("vietspider");
    System.out.println("print "+builder + " instanceOf "+builder.getClass());
  }catch(Exception err){
    err.printStackTrace();
  }
Kết quả in ra là :
init 17523401 ... class java.lang.String ... 3.0
  print vietspider instanceOf class java.lang.StringBuilder

Với các static method, cài đặt trên là tương tự. Constructor cũng là một method đặc biệt, do đó, chúng có cú pháp cài đặt generic như cài method. Chúng ta xét một ví dụ sau:

public class GenericExam<T, E> {

    T t;
    public <G> GenericExam(G g){
      t = (T)g;
      System.out.println("value t "+t+" ... "+t.getClass());
    }

    public T getT() { return t; }
  }

Có tới 3 kiểu generic, hai được khai báo trên class trong đó biến t là biến toàn cục sử dụng generic thứ nhất làm kiểu dữ liệu. Khi tạo instance thì G được truyền vào, nó sẽ được ép kiểu trở về kiểu của T. Liệu code trên sẽ có thể chạy đúng như ta đã suy nghĩ hay không?

Xem xét cài đặt sau:

GenericExam<Integer, Boolean> exp4;
  exp4 = new <Float> GenericExam<Integer, Boolean>(new Float(2.3));
  int a = exp4.getT();

T lúc này là Integer và G là Float. Nếu theo logic suy luận của tôi thì t có giá trị là 2. Trước tiên, chúng ta hãy xem generic được truyền vào contructor, chúng khai báo giữa toán tử new và tên Class new <Float> GenericExam… tiếp sau đó mới là generic của class. Truyền vào một object đúng kiểu của khai báo trong   constructor là new Float(2.3).

Khi chạy code cài đặt trên, chúng ta sẽ ra kết quả sau:

value t 2.3 ... class java.lang.Float
  Exception in thread "main" java.lang.ClassCastException: java.lang.Float at GenericExam.main(GenericExam.java:72)

Ngạc nhiên chưa, giá trị của t vẫn là Float, đúng kiểu của constructor, như vậy phép toán t = (T)g; đã không thực hiện đúng yêu cầu và cũng không throw ra lỗi. Tuy nhiên, nếu ta thực hiện get là exp4.getT() thì ClassCastException sẽ được ném ra. Điều này cho thấy phép ép kiểu t = (T)g đôi khi không thực hiện đúng yêu cầu và những giao tiếp ra bên ngoài mới thực sự ép kiểu generic, lúc đó, lỗi có thể được throw ra.

Static field thì chúng ta không dùng được generic nhưng ở static method, chúng ta hoàn toàn có thể cài đặt generic và sử dụng bình thường. Mẩu code sau từ một bài trả lời của tôi trên JVN là một ví dụ:

class abstract CommonSetting {

    public static Class ResourcesClass = ResourceAdapter.class;

  }

  public class Resources{
    public static <T extends CommonSetting> String getImagePath(String fileName) {
      System.out.println(T.ResourcesClass.getName());
      return T.ResourcesClass.getResource("").getPath() ;
    }
  }
và khi sử dụng :
System.out.println(Resources.<Setting>getImagePath("name"));
  Setting set = new Setting();
  System.out.println(Resources.<Setting>getImagePath("name"));

Với static code, đặt ngoài method và trong class thì hiện tại tôi chưa có cách nào để khai báo generic. Điều này có thể khắc phục bằng cách khai báo trên class, điều đó có nghĩa là một dạng vay mượn từ khai báo toàn cục, tuy nhiên chúng có thể chỉ được dùng trong một đoạn static code nào đó mà thôi.