Đó 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.

(Collection và Generic – Phần 1)

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.
(còn tiếp)