openplanning

Hướng dẫn và ví dụ Java Generics

  1. Tại sao Java Generics?
  2. Kiểu Generic cho Class & Interface
  3. Phương thức generics
  4. Khởi tạo đối tượng Generic
  5. Mảng Generic
  6. Generics với ký tự đại diện

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 ArrayList (Một danh sách).
		// Để chứa tên các người dùng.
		ArrayList userNames = new ArrayList();

		// Thêm các 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 kiểu String vào danh sách.
		// (Đ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 (cast) thành 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 và ép kiểu (cast) nó thành một String.
		// (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 ArrayList (Một danh sách)
		// Danh sách này chỉ cho phép chứa các phần tử kiểu String.
		ArrayList<String> userNames = new ArrayList<String>();

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

		// Bạn không thể thêm các phần tử không phải String vào danh sách.
		// (Lỗi khi biên dịch).
		userNames.add(new Integer(100)); // Compile Error!

		// Bạn không cần phải ép kiểu (cast) của phần tử.
		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 cho Class & Interface

Generics Class
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ụ:
Phone = 12000111 / name = Tom
Thừa kế lớp 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;

// Lớp này mở rộng (extends) từ lớp 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;

// Lớp này mở rộng (extends) từ lớp 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;

// Lớp này mở rộng (extends) từ lớp 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;
	}

}
Generics Interface
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 trên:
GenericInterfaceImpl.java
package org.o7planning.tutorial.generics.ci;

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

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

}
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ề một đối tượng 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ề một đối tượng 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ề một đối tượng 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.java
package org.o7planning.tutorial.generics.o;

import java.util.Date;

public class Bar {

	// Lớp này phải có một Constructor 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.
// Bạn có thể 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) {

		// Một mảng các String.
		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 (compile) new T[10];. Nếu không biết rõ nó mặc định coi T là Object. Khi đó:
// Giả sử Java cho phép khởi tạo một mảng Generic:
T[]  tarray = new T[10];

// Tại thời điểm biên dịch (Compile-time)
// trình biên dịch sẽ coi <T> là một Object.
// Câu lệnh ở trên tương đương với:
T[] tarray  = new Object[10];

// Nếu tại thời điểm chạy của ứng dụng bạn chỉ định <T> 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õ ràng. 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 có kiểu tham số đại diện.
// (A wildcard parameterized type)
Pair<String,?> pair = new Pair<String,Integer>();
Một số khai báo không hợp lệ.
// String không phải là kiểu con của Number, vì vậy lỗi.
List<? extends Number> list = new ArrayList<String>();  

// String không phải là kiểu cha của Integer vì vậy lỗi
ArrayList<? super String> cmp = new ArrayList<Integer>();
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> list1 = listString; // ==> Error!

		// Một đối tượng kiểu tham số đại diện.
		// (wildcard parameterized object).
		ArrayList<? extends Object> list2;

		// Bạn có thể khai báo:
		list2 = listString;

		// Hoặc
		list2 = 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);

   }

}
Đố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) {

		// 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) {
 
		// Một danh sách với kiểu tham số đại diện.
		// (wildcard parameterized type).
		ArrayList<? extends Object> listWildcard = listString;

		// Bạn không thể sử dụng phương thức add(E)
		// với tham số có giá trị khác null.
		listWildcard.add("Tom"); // ==> Error!

		listWildcard.add("Jerry"); // ==> Error!

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

	}

}
Wildcard không thể tham gia trong toán tử new
Một kiểu tham số ký tự đại diện (wildcard parameterized type) không phải là một loại cụ thể, và nó không thể xuất hiện trong một toán tử new.
// Tham số Wildcard không thể tham gian trong toán tử new.
List<? extends Object> list= new ArrayList<? extends Object>();

Java cơ bản

Show More