Hướng dẫn sử dụng Java Generics
Công ty Vĩnh Cửu tuyển dụng lập trình viên Java

1- Tại sao Java Generics?

Generics là một khái niệm được đưa vào Java từ phiên bản 5. Trước khi đưa ra khái niệm Generics là gì, chúng ta hãy xem một đoạn code của Java trước phiên bản 5.
Trong ví dụ này ArrayList là một danh sách, bạn có thể thêm, xóa, sửa trong danh sách, và truy cập vào các phần tử của danh sách.
BeforeJ5Example.java
package org.o7planning.tutorial.generics;

import java.util.ArrayList;

public class BeforeJ5Example {

   public static void main(String[] args) {

       // Tạo một đối tượng danh sách.
       // Mục đích chứa các tên người dùng.
       ArrayList userNames = new ArrayList();

       // Thêm String vào danh sách
       userNames.add("tom");
       userNames.add("jerry");

       // Bạn vô tình thêm một phần tử không phải String.
       // (Điều này hoàn toàn cho phép).
       userNames.add(new Integer(100));

       // Và lấy ra phần tử đầu tiên
       // Nó là một Object (Nhưng bạn biết nó là một String)
       // ==> tom
       Object obj1 = userNames.get(0);

       // Ép kiểu về String.
       String userName1 = (String) obj1;

       System.out.println("userName1 = " + userName1);

       // Lấy ra phần tử thứ 2.
       // (Bạn biết nó là String)
       // ==> jerry
       String userName2 = (String) userNames.get(1);

       System.out.println("userName2 = " + userName2);

       // Lấy ra phần tử thứ 3 (Thực tế nó là một Integer).
       // (Lỗi ép kiểu xẩy ra tại đây).
       String userName3 = (String) userNames.get(2);

       System.out.println("userName3 = " + userName3);
   }

}
Một tình huống trong Java trước phiên bản 5:

Bạn tạo ra một đối tượng ArrayList với mục đích chỉ chứa các phần tử có kiểu String, tuy nhiên tại nơi nào đó trong chương trình bạn thêm vào danh sách này một phần tử không phải String (Việc này hoàn toàn có thể), khi bạn lấy ra các phần tử đó và ép kiểu về String, một ngoại lệ sẽ bị ném ra.
  • TODO (Hinh minh hoa)
Java 5 đưa vào khái niệm Generics. Với sự trợ giúp của Generics, bạn có thể tạo ra một đối tượng ArrayList chỉ cho phép chứa các phần tử có kiểu String, và không cho phép chứa các phần tử có kiểu khác.
J5Example.java
package org.o7planning.tutorial.generics;

import java.util.ArrayList;

public class J5Example {

   public static void main(String[] args) {
       // Tạo một đối tượng danh sách.
       // Mục đích chứa các tên người dùng.
       ArrayList<String> userNames = new ArrayList<String>();

       // Thêm String vào danh sách
       userNames.add("tom");
       userNames.add("jerry");

       // Bạn không thể thêm một phần tử khác kiểu String
       userNames.add(new Integer(100));

       // Bạn không cần ép kiểu.
       String userName1 = userNames.get(0);

       System.out.println("userName1 = " + userName1);

   }

}
Khi bạn tạo một đối tượng ArrayList<String>, nó chỉ chứa các phần tử có kiểu String, trình biên dịch của Java không cho phép đối tượng này chứa các phần tử có kiểu khác String.

2- Kiểu Generic Class, Interface

2.1- Class Generics

Ví dụ dưới đây định nghĩa ra một class generics. KeyValue là một class generics nó chứa một cặp khóa và giá trị (key/value).
 
KeyValue.java
package org.o7planning.tutorial.generics.ci;

public class KeyValue<K, V> {

   private K key;
   private V value;

   public KeyValue(K key, V value) {
       this.key = key;
       this.value = value;
   }

   public K getKey() {
       return key;
   }

   public void setKey(K key) {
       this.key = key;
   }

   public V getValue() {
       return value;
   }

   public void setValue(V value) {
       this.value = value;
   }

}
K, V trong class KeyValue<K,V> được gọi là tham số generics nó là một kiểu tham chiếu nào đó. Khi sử dụng class này bạn phải xác định kiểu tham số cụ thể.

Hãy xem ví dụ sử dụng class KeyValue.
 
KeyValueDemo.java
package org.o7planning.tutorial.generics.ci;

public class KeyValueDemo {

   public static void main(String[] args) {

       // Tạo một đối tượng KeyValue
       // Integer: Số điện thoại  (K = Integer)
       // String: Tên người dùng.  (V = String).
       KeyValue<Integer, String> entry = new KeyValue<Integer, String>(12000111, "Tom");
       
       // Java hiểu kiểu trả về là Integer (K = Integer).
       Integer phone = entry.getKey();
       
       // Java hiểu kiểu trả về là String (V = String).
       String name = entry.getValue();
       
       System.out.println("Phone = "+ phone+" / name = "+ name);
   }

}
Chạy ví dụ:

2.2- Thừa kế class Generics

Một class mở rộng từ một class generics, nó có thể chỉ định rõ kiểu cho tham số generics, giữ nguyên các tham số generics hoặc thêm các tham số generics.

Ví dụ 1:

PhoneNameEntry.java
package org.o7planning.tutorial.generics.ci;

// Class này mở rộng từ class KeyValue<K,V>
// Và chỉ định rõ K,V
// K = Integer  (Số điện thoại).
// V = String   (Tên người dùng).
public class PhoneNameEntry extends KeyValue<Integer,String> {

   public PhoneNameEntry(Integer key, String value) {
       super(key, value);
   }

}
Ví dụ sử dụng PhoneNameEntry:
PhoneNameEntryDemo.java
package org.o7planning.tutorial.generics.ci;

public class PhoneNameEntryDemo {

   public static void main(String[] args) {

       PhoneNameEntry entry = new PhoneNameEntry(12000111, "Tom");

       // Java hiểu kiểu trả về là Integer.
       Integer phone = entry.getKey();

       // Java hiểu kiểu trả về là String.
       String name = entry.getValue();

       System.out.println("Phone = " + phone + " / name = " + name);

   }

}

Ví dụ 2:

StringAndValueEntry.java
package org.o7planning.tutorial.generics.ci;

// Class này mở rộng class KeyValue<K,V>
// Xác định rõ kiểu tham số K là String.
// Vẫn giữ kiểu tham số generic V.
public class StringAndValueEntry<V> extends KeyValue<String, V> {

   public StringAndValueEntry(String key, V value) {
       super(key, value);
   }

}
Ví dụ sử dụng StringAndValueEntry class:
StringAndValueEntryDemo.java
package org.o7planning.tutorial.generics.ci;

public class StringAndValueEntryDemo {

    public static void main(String[] args) {

        // (Mã nhân viên, Tên nhân viên).
        // V = String (Tên nhân viên)
        StringAndValueEntry<String> entry = new StringAndValueEntry<String>("E001", "Tom");

        String empNumber = entry.getKey();

        String empName = entry.getValue();

        System.out.println("Emp Number = " + empNumber);
        System.out.println("Emp Name = " + empName);

    }

}

Ví dụ 3:

KeyValueInfo.java
package org.o7planning.tutorial.generics.ci;

// Class này mở rộng class KeyValue<K,V>
// Nó có thêm một tham số generics I.
public class KeyValueInfo<K, V, I> extends KeyValue<K, V> {

   private I info;

   public KeyValueInfo(K key, V value) {
       super(key, value);
   }

   public KeyValueInfo(K key, V value, I info) {
       super(key, value);
       this.info = info;
   }

   public I getInfo() {
       return info;
   }

   public void setInfo(I info) {
       this.info = info;
   }

}

2.3- Interface Generics

Một Interface có tham số Generics:
GenericInterface.java
package org.o7planning.tutorial.generics.ci;

public interface GenericInterface<G> {

 
  public G doSomething();
 
}
Ví dụ một class thi hành Interface:
GenericInterfaceImpl.java
package org.o7planning.tutorial.generics.ci;

public class GenericInterfaceImpl<G> implements GenericInterface<G>{

   private G something;
   
   @Override
   public G doSomething() {
       return something;
   }

}

2.4- Java không hỗ trợ generic Throwable

Bạn không thể tạo một class generic là hậu duệ của Throwable, java không hỗ trợ tạo một class như vậy.
Thông báo lỗi của trình biên dịch:
- The generic class MyException<E> may not subclass java.lang.Throwable
Việc Java không hỗ trợ tạo một class Throwable generic, vì điều đó là không mang lại lợi ích gì. Nguyên nhân là thông tin Generic chỉ sử dụng cho trình biên dịch kiểm soát code của người lập trình. Trong thời điểm chạy Java thông tin Generic không hề tồn tại, một đối tượng của Mistake<Account> hoặc Mistake<User> đều là một kiểu đối tượng của Mistake.
} catch( Mistake<Account> ea) {
    // Nếu ngoại lệ Mistake xẩy ra, khối này sẽ được thực thi
    ...
} catch( Mistake<User> eu) {
     // Khối này không bao giờ được thực thi
    ...
}

3- Phương thức generics

Một phương thức trong class hoặc Interface có thể được generic hóa (generify).
MyUtils.java
package org.o7planning.tutorial.generics.m;

import java.util.ArrayList;

import org.o7planning.tutorial.generics.ci.KeyValue;

public class MyUtils {

   // <K,V> : Nói rằng phương thức này có 2 kiểu tham số K,V
   // Phương thức trả về kiểu K.
   public static <K, V> K getKey(KeyValue<K, V> entry) {
       K key = entry.getKey();
       return key;
   }

   // <K,V> : Nói rằng phương thức này có 2 kiểu tham số K,V
   // Phương thức trả về kiểu V.
   public static <K, V> V getValue(KeyValue<K, V> entry) {
       V value = entry.getValue();
       return value;
   }

   // ArrayList<E>: Danh sách chứa các phần tử kiểu E
   // Phương thức trả về kiểu E.
   public static <E> E getFirstElement(ArrayList<E> list) {
       if (list == null || list.isEmpty()) {
           return null;
       }
       E first = list.get(0);
       return first;
   }

}
Ví dụ sử dụng phương thức generics:
MyUtilsDemo.java
package org.o7planning.tutorial.generics.m;

import java.util.ArrayList;

import org.o7planning.tutorial.generics.ci.KeyValue;

public class MyUtilsDemo {

   public static void main(String[] args) {

       // K = Integer: Phone
       // V = String: Name
       KeyValue<Integer, String> entry1 = new KeyValue<Integer, String>(12000111, "Tom");
       KeyValue<Integer, String> entry2 = new KeyValue<Integer, String>(12000112, "Jerry");

       // (K = Integer).
       Integer phone = MyUtils.getKey(entry1);
       System.out.println("Phone = " + phone);

       // Một danh sách chứa các phần tử kiểu KeyValue<Integer,String>.
       ArrayList<KeyValue<Integer, String>> list = new ArrayList<KeyValue<Integer, String>>();

       // Thêm phần tử vào danh sách.
       list.add(entry1);
       list.add(entry2);

       KeyValue<Integer, String> firstEntry = MyUtils.getFirstElement(list);

       System.out.println("Value = " + firstEntry.getValue());
   }

}

4- Khởi tạo đối tượng Generic

Đôi khi bạn muốn khởi tạo một đối tượng Generic:
// Khởi tạo đối tượng Generic

T t = new T(); // Error
Việc khởi tạo một đối tượng generic như trên là không được phép, vì <T> không hề tồn tại ở thời điểm chạy của Java. Nó chỉ có ý nghĩa với trình biên dịch kiểm soát code của người lập trình. Mọi kiểu <T> đều như nhau nó được hiểu là Object tại thời điểm chạy của Java.

Muốn khởi tạo đối tượng generic <T> bạn cần cung cấp cho Java đối tượng Class<T>, Java sẽ tạo đối tượng <T> tại thời điểm runtime bằng Java Reflection.
Bar.class
package org.o7planning.tutorial.generics.o;

import java.util.Date;

public class Bar {

   // Class này phải có cấu tử mặc định
   public Bar() {

   }

   public void currentDate() {
       System.out.println("Now is: " + new Date());
   }

}
MyGeneric.java
package org.o7planning.tutorial.generics.o;

public class MyGeneric<T> {

   private T tobject;

   public MyGeneric(Class<T> tclass)
           throws InstantiationException, IllegalAccessException {
       
       this.tobject = (T) tclass.newInstance();
       
   }

   public T getTObject() {
       return this.tobject;
   }
}
MyGenericDemo.java
package org.o7planning.tutorial.generics.o;

public class MyGenericDemo {

   public static void main(String[] args) throws Exception {

       MyGeneric<Bar> mg = new MyGeneric<Bar>(Bar.class);

       Bar bar = mg.getTObject();

       bar.currentDate();
   }
}

5- Mảng Generic

Bạn có thể khai báo một mảng generic, nhưng bạn không thể khởi tạo một mảng generic.
// Khai báo một mảng generic

T[] myarray;

// Nhưng không thể khởi tạo mảng generic.
// (Điều này không được phép).

T[] myarray = new T[5];  // Error!
Ví dụ:
GenericArray.java
package org.o7planning.tutorial.generics.a;

public class GenericArray<T> {

   private T[] array;

   // Contructor.
   public GenericArray(T[] array) {
       this.array = array;
   }

   public T[] getArray() {
       return array;
   }

   // Trả về phần tử cuối cùng của mảng.
   public T getLastElement() {
       if (this.array == null || this.array.length == 0) {
           return null;
       }
       return this.array[this.array.length - 1];
   }

}
GenericArrayDemo.java
package org.o7planning.tutorial.generics.a;

public class GenericArrayDemo {

  public static void main(String[] args) {

   
      String[] names = new String[] { "Tom", "Jerry" };

      GenericArray<String> gArray = new GenericArray<String>(names);

      String last = gArray.getLastElement();
     
      System.out.println("Last Element = " + last);
  }

}
Quay trở lại với vấn đề tại sao Java không hỗ trợ khởi tạo một mảng Generic:
// Tại sao Java không hỗ trợ khởi tạo mảng Generic?

T[] genericArray = new T[10]; // Error!
Lý do là kiểu generic không hề tồn tại tại thời điểm chạy, List<String> hoặc List<Integer> đều là List. Generic chỉ có tác dụng với trình biên dịch để kiểm soát code của người lập trình. Điều đó có nghĩa là trình biên dịch của Java cần biết rõ <T> là cái gì mới có thể biên dịch new T[10];. Nếu không biết rõ nó mặc định coi T là Object. Khi đó:
 
// Giải sử Java cho phép khởi tạo mảng Generic:

T[]  tarray = new T[10];


// Tại thời điểm biên dịch, trình biên dịch sẽ coi T là Object.
// Câu lệnh trên tương đương với.

T[] tarray  = new Object[10];

// Tại thời điểm chạy của ứng dụng nếu T được xác định là String.
// Nghĩa là:

String[] tarray = new Object[10];

// Điều trên không được phép. Nguyên nhân:
// Type mismatch: cannot convert from Object[] to String[]
Nếu muốn khởi tạo mảng Generic bạn cần phải truyền cho Java đối tượng Class<T>, giúp Java có thể khởi tạo mảng generic tại thời điểm runtime bằng cách sử dụng Java Reflection. Hãy xem ví dụ minh họa:
GArray.java
package org.o7planning.tutorial.generics.a;

import java.lang.reflect.Array;

public class GArray<T> {

  private Class<T> tclass;

  private T[] myArray;

  public GArray(Class<T> tclass) {
      this.tclass = tclass;

      final int size = 10;
      myArray = (T[]) Array.newInstance(tclass, size);
  }

  public T[] getMyArray() {
      return this.myArray;
  }

}
GArrayDemo.java
package org.o7planning.tutorial.generics.a;

public class GArrayDemo {

   public static void main(String[] args) {

       GArray<Integer> garray = new GArray<Integer>(Integer.class);

       Integer[] myArray = garray.getMyArray();

       myArray[0] = 1;
       myArray[2] = 0;
   }

}

6- Generics với ký tự đại diện

Trong mã Generic, dấu chấm hỏi (?), được gọi là một đại diện (wildcard), nó đại diện cho một loại không rõ. Một kiểu tham số đại diện (wildcard parameterized type) là một trường hợp của kiểu Generic, nơi mà ít nhất một kiểu tham số là wildcard. Ví dụ của tham số đại diện (wildcard parameterized) là Collection<?>, List<? extends Number>, Comparator<? super String>Pair<String,?>. Các ký tự đại diện có thể được sử dụng trong một loạt các tình huống: như kiểu của một tham số, trường (field), hoặc biến địa phương; đôi khi như một kiểu trả về (Sẽ được nói rõ hơn trong các ví dụ thực hành). Các đại diện là không bao giờ được sử dụng như là một đối số cho lời gọi một phương thức Generic, khởi tạo đối tượng class generic, hoặc kiểu cha (supertype).
Các ký hiệu đại diện nằm ở các vị trí khác nhau có ý nghĩa khác nhau:
  • Collection<?> mô tả một tập hợp chấp nhận tất cả các loại đối số (chứa mọi kiểu đối tượng).
  • List<? extends Number> mô tả một danh sách, nơi mà các phần tử là kiểu Number hoặc kiểu con của Number.
  • Comparator<? super String> Mô tả một bộ so sánh (Comparator) mà thông số phải là String hoặc cha của String.

Một kiểu tham số ký tự đại diện không phải là một loại cụ thể để có thể xuất hiện trong một toán tử new. Nó chỉ là gợi ý các quy tắc thực thi bởi generics java rằng những loại có giá trị trong bất kỳ tình huống cụ thể mà các kí hiệu đại diện đã được sử dụng.
  • Ví dụ:
Collection<?> coll = new ArrayList<String>();

// Một tập hợp chỉ chứa kiểu Number hoặc kiểu con của Number
List<? extends Number> list = new ArrayList<Long>();

// Một đối tượng tham số đại diện.
Pair<String,?> pair = new Pair<String,Integer>();
Một số khai báo không hợp lệ.
// String không phải là con của Number, vì vậy lỗi.
List<? extends Number> list = new ArrayList<String>();  

// String không phải là cha của Integer vì vậy lỗi
ArrayList<? super String> cmp = new ArrayList<Integer>();

6.1- Ví dụ với kiểu đại diện (wildcard)

WildCardExample1.java
package org.o7planning.tutorial.generics.w;

import java.util.ArrayList;

public class WildCardExample1 {

  public static void main(String[] args) {

      // Một danh sách chứa các phần tử kiểu String.
      ArrayList<String> listString = new ArrayList<String>();
     
      listString.add("Tom");
      listString.add("Jerry");

      // Một danh sách chứa các phần tử kiểu Integer
      ArrayList<Integer> listInteger = new ArrayList<Integer>();
     
      listInteger.add(100);

      // Bạn không thể khai báo:
      // ArrayList<Object> list = listString;

      // Một đối tượng kiểu tham số đại diện.
      ArrayList<? extends Object> list;

      // Nhưng bạn có thể khai báo:
      list = listString;

      // Hoặc
      list = listInteger;  
     
  }

}
WildCardExample2.java
package org.o7planning.tutorial.generics.w;

import java.util.ArrayList;
import java.util.List;

public class WildCardExample2 {

   public static void printElement(List<?> list) {
       for (Object e : list) {
           System.out.println(e);
       }
   }

   public static void main(String[] args) {

       List<String> names = new ArrayList<String>();
       names.add("Tom");
       names.add("Jerry");
       names.add("Donald");

       List<Integer> values = new ArrayList<Integer>();

       values.add(100);
       values.add(120);

       System.out.println("--- Names --");

       printElement(names);

       System.out.println("-- Values --");

       printElement(values);

   }

}

6.2- Đối tượng đại diện không thể sử dụng phương thức generic

ValidWildcard1.java
package org.o7planning.tutorial.generics.w;

import java.util.ArrayList;

public class ValidWildcard1 {

   public static void main(String[] args) {

       // ArrayList<E>
       // Một danh sách chứa các phần tử kiểu String.
       ArrayList<String> listString
                           = new ArrayList<String>();

       // Sử dụng phương thức generic: add(E)
       // Thêm một phần tử khác null vào danh sách
       listString.add("Tom");

       listString.add("Jerry");

       // Thêm phần tử null vào danh sách.
       listString.add(null);
   }

}
InvalidWildcard1.java
package org.o7planning.tutorial.generics.w;

import java.util.ArrayList;

public class InvalidWildcard1 {

  public static void main(String[] args) {

      // ArrayList<E>
      // Một danh sách chứa các phần tử kiểu String.
      ArrayList<String> listString = new ArrayList<String>();

      // Một đối tượng kiểu tham số đại diện:
      ArrayList<? extends Object> listWildcard = listString;

      // Sử dụng phương thức generic: add(E)
      // Đối tượng kiểu tham số đại diện không thể sử dụng
      // phương thức generic, với tham số generic khác null.
      listWildcard.add("Tom"); // Error!

      listWildcard.add("Jerry"); // Error!
     
      // Nhưng có thể sử dụng phương thức generic
      // với tham số null.
      listWildcard.add(null);

  }

}

6.3- Wildcard không thể tham gia trong toán tử new

Một kiểu tham số ký tự đại diện không phải là một loại cụ thể để có thể xuất hiện trong một toán tử new. Nó chỉ là gợi ý các quy tắc thực thi bởi generics java rằng những loại có giá trị trong bất kỳ tình huống cụ thể mà các kí hiệu đại diện đã được sử dụng.
// Tham số đại diện (Wildcard) không thể tham gian trong toán tử new
List<? extends Object> list= new ArrayList<? extends Object>();