Java中泛型的詳細解析,深入分析泛型的使用方式

泛型的基本概念

  • 泛型: 參數化類型
    • 參數:
      • 定義方法時有形參
      • 調用方法時傳遞實參
    • 參數化類型: 將類型由原來的具體的類型參數化,類似方法中的變數參數
      • 類型定義成參數形式, 可以稱為類型形參
      • 在使用或者調用時傳入具體的類型,可以稱為類型實參
  • 泛型的本質是為了參數化類型
    • 在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型
    • 在泛型使用過程中,操作的數據類型被指定為一個參數,這種參數類型可以用在:
      • 類 – 泛型類
      • 介面 – 泛型介面
      • 方法 – 泛型方法
  • 泛型示例:
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
 
arrayList.forEach(i -> {
	String item = (String) arrayList.get(i);
 Log.d("泛型", "item = " + item);
});
  • 這樣的寫法會導致程式出現異常崩潰結束:
	java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
  • 這裡的ArrayList可以存放任意類型,添加了一個String類型,添加了一個Integer類型,再使用時都以String的方式使用,因此程式崩潰
  • 泛型就是解決這樣的問題
  • 再討論另一種情況,如果將第一行聲明初始的程式碼修改一下,那麼在編譯階段就能發現問題:
List arrayList = new ArrayList<String>();
arrayList.add("aaaa");
arrayList.add(100); // 這一步在編譯階段,編譯器就會報錯
 
 arrayList.forEach(i -> {
 	String item = (String) arrayList.get(i);
 	Log.d("泛型", "item = " + item);
 });
  • 泛型只在編譯階段有效:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if (classStringArrayList.equals(classIntegerArrayList)) {
	Log.d("泛型", "類型相同");
}

可以發現,在編譯過後,程式會採取去泛型化措施.也就是說,Java中的泛型,只在編譯階段有效.在編譯過程中,正確檢驗泛型結果後,會將泛型的相關資訊擦除,並且在對象進入和離開方法的邊界處添加類型檢查和類型轉換方法

  • 泛型類型在邏輯上可以看成多個不同的類型,實際上都是相同的基本類型

泛型的使用

  • 泛型有三種使用方式:
    • 泛型類
    • 泛型介面
    • 泛型方法

泛型類

  • 泛型類: 泛型類型用於類定義中
    • 通過泛型類可以完成對一組類的操作對外開發相同的介面
    • 最典型的就是各種容器類:
      • List
      • Set
      • Map
  • 泛型類的最基本寫法:
class 類名稱 <泛型標識: 標識號,標識指定的泛型的類型> {
	private 泛型標識 成員變數類型 成員變數名;
}
  • 示例:
/*
 * 這裡的T可以為任意標識,通常使用T,E,K,V等形式的參數表示泛型
 * 在實例化泛型時,必須指定T的具體類型
 */
 public class Generic<T> {
 	// key這個成員變數的類型為T,T的類型由外部指定
 	private T key;
	
	// 泛型構造方法形參key的類型也為T,T的類型由外部指定
	public Generic(T key) {
		this.key = key;
	}

	// 泛型構造方法getKey的返回值類型為T,T的類型由外部指定
	public T getKey() {
	}
 }
/*
 * 泛型的類型參數只可以是類類型,包括自定義類. 不能是簡單類型
 */
 // 傳入的實參類型需要與泛型類型的參數類型相同,即Integer
 Generic<Integer> genericInteger = new Generic<Integer>(123456);
 // 傳入的實參類型需要與泛型類型的參數類型相同,即String
 Generic<String> genericString = new Generic<String>("key_value");

 Log.d("泛型測試", "key is" + genericInteger.getKey());
 Log.d("泛型測試", "key is" + genericString.getKey());
泛型測試: key is 123456
泛型測試: key is key_value
  • 泛型類中不一定要傳入泛型類型的實參:
    • 如果傳入泛型實參,會根據傳入的泛型實參做相應的限制,此時泛型才會起到本應起到的限制作用
    • 如果不傳如泛型類型的實參,在泛型類中使用泛型的方法或者成員變數的定義可以為任何類型
    Generic genericString = new Generic("1111");
    Generic genericInteger = new Generic(5555);
    Generic genericBigDecimal = new Generic(66.66);
    Generic genericBoolean = new Generic(true);
    
    Log.d("泛型測試", "key is" + genericString.getKey());
    Log.d("泛型測試", "key is" + genericInteger.getKey());
    Log.d("泛型測試", "key is" + genericBigDecimal.getKey());
    Log.d("泛型測試", "key is" + genericBoolean.getKey());
    
    D/泛型測試: key is 1111
    D/泛型測試: key is 5555
    D/泛型測試: key is 66.66
    D/泛型測試: key is true
    
  • 泛型的類型參數只能是類類型,不能是簡單類型
  • 不能對確切的泛型類型使用instanceof操作,編譯時會出錯

泛型介面

  • 泛型介面與泛型類的定義及使用基本相同
  • 泛型介面常常被用在各種類的生產器中
  • 示例:
// 定義一個泛型介面
public interface Generator<T> {
	public T next();
}
  • 當實現泛型介面的類,未傳入泛型實參時:
/**
 * 未傳入泛型實參時,與泛型類的定義相同,在聲明類的時候,需將泛型的聲明也一起加到類中:
 * 		即 class FruitGenerator<T> implements Generator<T> {}
 * 		如果不聲明泛型,比如: class FruitGenerator implements Generator<T>. 此時編譯器會報錯 - Unknown class
 */
 class FruitGenerator<T> implements Generator<T> {
 	@Override
 	public T next() {
 		return null;
 	}
 }
  • 當實現泛型介面的類,傳入泛型實參時:
/**
 * 傳入泛型實參時:
 * 		定義一個生產器實現這個介面
 * 		儘管只創建了一個泛型介面Generator<T>,但是可以為T傳入無數個實參,形成無數種類型的Generator介面
 * 		在實現類實現泛型介面時,如果已經將泛型類型傳入實參類型,則所有使用泛型的地方動搖替換成傳入的實參類型
 * 			即: Generator<T>, public T next(); 這裡的T都要替換成傳入的String類型
 */
 public class FruitGenerator implements Generator<String> {
 	private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

	@Override
	public String next() {
		Random rand = new Random();
		return fruits[rand.nextInt(3)];
	}
 }

泛型通配符

  • Integernumber的一個子類 ,Generic< Integer >Generic< number > 實際上是相同的一種類型
  • 由此,產生如下問題:
    • 在使用Generic< number > 作為形參的方法中,能否使用Generic< Integer > 的實例傳入?
    • 在邏輯上類似於Generic< number >和Generic< Integer >是否可以看成是具有父子關係的泛型類型呢?
  • Generic< T >泛型類示例:
public void showKeyValue1(Generic<Number> obj) {
	Log.d("泛型測試", "key value is" + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);
showKeyValue這個方法編譯器會報錯:
	Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number> showKeyValue(gInteger);

由此可以看到Generic< Integer >不能看作是Generic< Number >的子類.

  • 由此可見:
    • 同一種泛型可以對應多個版本,因為參數類型是不確定的
    • 不同版本的泛型類型實例是不兼容的
  • 為了解決這樣的問題,又不能為了定義一個新的方法來處理Generic< Integer >,這與Java中多態的理念違背.因此,需要一個在邏輯上可以表示同時是Generic< Integer >和Generic< Number >父類的引用類型.這樣的類型就是類型通配符:
  • 使用通配符表示泛型:
public void showKeyValueWildcard(Generic<?> obj) {
	Log.d("泛型測試", "key value is" + obj.getKey());
}
  • 類型通配符一般使用 ? 代替具體的類型實參:
    • 此處的 ?類型實參, 而不是類型形參.
    • 和Number,String,Integer一樣,都是一種實際的類型
    • 可以把 ? 看作是所有類型的父類,是一種真實的類型
  • 類型通配符的使用場景:
    • 當具體類型不確定的時候,這個通配符就是 ?
    • 當操作類型時,不需要使用類型的具體功能,只使用Object類中的功能,那麼可以使用 ? 通配符來表示未知的類型

泛型方法

  • 泛型類: 在實例化類的時候指明泛型的具體類型
  • 泛型方法: 在調用方法的時候指明泛型的具體類型
/**
 * 泛型方法:
 * 		1. public 和 返回值中間的 <T> 非常重要,可以理解為聲明此方法為泛型方法
 * 		2. 只有聲明了 <T> 的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法
 * 		3. <T> 表示該方法將使用泛型類型T,此時才可以在方法中使用泛型類型T
 * 		4. 與泛型類的定義一樣,此處的T可以為任意標識,常見的比如: T, E, K, V等形式的參數常用於表示泛型
 * 
 * @param tClass 傳入的泛型實參
 * @return T 返回值為T類型
 */
 public <T> T genericMethod(Class<T> tClass) throws InstanttiationException, IllegalAccessException {
 	T instance = tClass.newInstance();
 	return instance;
 }
Object obj = genericMethod(Class.forName("com.oxford.test"));
泛型方法的基本用法
  • 泛型方法使用示例:
public class GenericTest {
	/* 
	 * 下面這個類是一個泛型類
	 */
	 public class Generic<T> {
	 	private T key;

		public Generic(T key) {
			this.key = key;
		}

		/*
		 * 這個方法雖然在方法使用了泛型,但是這不是一個泛型方法
		 * 這只是類中一個普通的成員方法,只不過返回值是在聲明泛型類已經聲明過的泛型
		 * 所以在這個方法中才可以繼續使用T這個泛型
		 */
		 public T getKey() {
		 	return key;
		 }

		/*
		 * 下面的這個方法顯然是有問題的,在編譯器中就會提示錯誤"cannot resolve symbol E"
		 * 因為在類的聲明中並未聲明泛型E,所以在使用E做形參和返回值類型時,編譯器會無法識別
		 *  
		 * public E setKey(E key) {
		 * 	this.key = key
		 * }
		 */
	 } 
	  
	 /*
	  * 下面這個方法是一個泛型方法:
	  * 	首先在public與返回值之間的<T>必不可少,這個表明這是一個泛型方法,並且聲明了一個泛型T
	  * 	這個T可以出現在這個泛型方法的任意位置
	  * 	泛型的數量也可以為任意多個
	  */
	  public <T> T showKeyName(Generic<T> container) {
	  	System.out.println("container key:" + container.getKey());
	  	T test = container.getKey();
	  	return test;
	  }
	  
	  /*
	   * 下面這個方法也不是一個泛型方法
	   * 這就是一個普通的方法,只是使用了Generic<Number>這個泛型類做形參
	   */
	   public void showKeyValue1(Generic<Number> obj) {
	   	Log.d("泛型測試", "key value is " + obj.getKey());
	   }

	  /*
	   * 下面這個方法也不是一個泛型方法
	   * 這也是一個普通方法,只是使用了泛型通配符 ?
	   * 從這裡可以驗證: 泛型通配符 ? 是一種類型實參,可以看作是所有類的父類
	   */
	   public void showKeyValue2(Generic<?> obj) {
	   	Log.d("泛型測試", "key value is" + obj.getKey());
	   }

	  /*
	   * 下面這個方法是有問題的,在編譯器中就會提示錯誤資訊:"Unknown class 'E'"
	   * 	雖然聲明了 <T>, 也表明這是一個可以處理泛型類型的泛型方法
	   * 	但是只聲明了泛型類型T,並未聲明泛型類型E,因此編譯器不知道如何處理E這個類型
	   * 
	   * public <T> T showKeyName(Generic<E> container) {
	   * 	...
	   * }
	   */	

	  /*
	   * 下面這個方法也是有問題的,在編譯器中就會提示錯誤資訊:"Unknown class 'T'"
	   * 	對於編譯器來說 T 這個類型並未在項目中聲明過,因此編譯器也不知道該如何編譯這個類
	   * 	所以這也不是一個正確的泛型方法聲明
	   *  
	   * public void showKey(T genericObj) {
	   * 	...
	   * }
	   */

		public void main(String[] args) {
		}	 
}
類中的泛型方法
  • 泛型方法可以出現在任何地方任何場景中進行使用
  • 但是,當泛型方法出現在泛型類中時,情況比較特殊:
public class GenericFruit {
	class Fruit {
		@Override
		public String toString() {
			return "fruit";
		}
	}

	class Apple extends Fruit {
		@Override
		public String toString() {
			retrun "apple";
		}
	}

	class Person {
		@Override
		public String toString() {
			return "Person";
		}
	}

	class GenerateTest<T> {
		public void show_1(T t) {
			System.out.println(t.toString());
		}

		/*
		 * 在泛型類中聲明一個泛型方法,使用泛型T
		 * 注意這個T是一種全新的類型,可以與泛型類中聲明的T不是同一個類型
		 */
		 public <T> void show_2(T t) {
		 	System.out.println(t.toString());
		 }
		 
		/* 
		 * 在泛型類中聲明一個泛型方法,使用泛型E. 這種泛型E可以為任意類型,可以與類型T相同
		 * 由於泛型方法在聲明的時候會聲明泛型 <E>,因此即使在泛型類中並未聲明泛型,編譯器也能夠正確識別泛型方法中識別的泛型
		 */
		 public <E> void show_3(E t) {
		 	System.out.println(t.toString());
		 }	
	} 

	public void main(String[] args) {
		Apple apple = new Apple();
		Person person = new Person();

		GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
		// apple是Fruit的子類,所以這裡可以
		generateTest.show_1(apple);

		/* 
		 * 編譯器會報錯,因為泛型類型實參指定的是Fruit,而傳入的實參類是Person
		 *  
		 * generateTest.show_1(person);
		 */

		/*
		 * 使用兩個參數都能成功
		 */
		 generateTest.show_2(apple);
		 generateTest.show_2(person);

		/*
		 * 使用兩個參數也都能成功
		 */
		 generateTest.show_3(apple);
		 generateTest.show_3(person);
	}
}
泛型方法與可變參數
  • 泛型方法與可變參數:
public <T> void printMsg(T... args) {
	for (T t : args) {
		Log.d("泛型測試", "t is" + t);
	}
}
靜態方法與泛型
  • 注意在類中的靜態方法使用泛型:
    • 靜態方法無法訪問類上定義的泛型
    • 如果靜態方法操作的引用數據類型不確定的時候,必須要將泛型定義在方法上
  • 如果靜態方法要使用泛型的話,必須將靜態方法定義成泛型方法:
public class StaticGenerator<T> {
	...
	...
	/*
	 * 如果在類中定義使用泛型的靜態方法,需要添加額外的泛型聲明 - 將這個方法定義成泛型方法
	 * 否則會報錯: StaticGenerator cannot be refrenced from static context
	 */
	 public static <T> void show(T t) {
	 }
}
泛型方法總結
  • 泛型方法能使方法獨立於類而產生變化,使用原則:
    • 無論何時,如果能做到,就盡量使用泛型方法
    • 如果使用泛型方法將整個類泛型話,就應該使用泛型方法
    • 對於一個static方法,無法訪問泛型類型的參數.如果static方法要使用泛型,就必須使之成為泛型方法

泛型的上下邊界

  • 在使用泛型的時候,可以為傳入的泛型類型實參進行上下邊界的限制:
    • 比如: 類型的實參只准傳入某種類型的父類或者某種類型的子類
  • 為泛型方法添加上邊界,即傳入的類型實參必須是指定類型的子類型:
public void showKeyValue1(Generic<? extends Number> obj) {
	Log.d("泛型測試", "key value is" + obj.getKey());
}

Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

/*
 * 這一行在編譯的時候就會報錯,因為String類型並不是Number類型的子類
 *  
 * showKeyValue1(generic1);
 */
 showKeyValue2(generic2);
 showKeyValue3(generic3);
 showKeyValue4(generic4);
  • 為泛型類添加上邊界,即類中泛型必須是指定類型的子類型:
public class Generic<T extends Number> {
	private T key;

	public Generic(T key) {
		this.key = key;
	}

	public T getKey() {
		return key;
	}
}

/*
 * 這一行程式碼在編譯的時候會報錯,因為String的類型不是Number的子類
 */
 Generic<String> generic1 = new Generic<String>("1111");
  • 在泛型方法中添加上下邊界限制時,必須在許可權聲明與返回值之間的< T >上添加上下邊界:
/*
 * 如果使用:
 * 		public <T> showKeyName(Generic<T extends Number> container);
 * 編譯器會報錯.
 */
 public <T extends Number> T showKeyName(Generic<T> container) {
 	System.out.println("container key:" + container.getKey());
 	T test = container.getKey();
 	return test;
 }
  • 從上面可以看出 : 泛型的上下邊界添加,必須與泛型的聲明在一起

泛型數組

  • 在Java中,不能創建一個確切的泛型類型的數組
/*
 * 這個數組創建的方式是不允許的
 * List<String>[] ls = new ArrayList<String>[10];
 */
 
 // 使用通配符創建泛型數組是可以的
 List<?>[] ls = new ArrayList<?>[10];

 // 下面的這個方法也是可以的
 List<String> ls = new ArrayList[10];
  • 示例:
List<String>[] lsa = new List<String>[10]; //不允許這樣定義
Object o = lsa;
Object[] oa = (Object) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3)); 
oa[1] = li; // 不建議這樣使用,但是可以通過運行時檢查
String s = lsa[1].get(0); // 運行時報錯,類型轉換異常
  • 由於JVM的擦除機制,在運行時JVM是不知道泛型資訊的:
    • 所有可以給oa[1] 賦值一個ArrayList卻不會出現異常
    • 但是在取出數據的時候要做一次類型轉換,就會出現ClassCastException
    • 如果可以進行泛型數組的聲明,那麼上面的這種情況在編譯期將不會出現任何警告和錯誤,只有在運行時才會報錯
  • 通過對泛型數組的聲明進行限制,對於這樣的情況,可以在編譯期提示程式碼有類型安全問題
  • 數組的類型不可以是類型變數,除非是採用通配符的方式: 因為對於通配符的方式,最後取出數據是要做顯式的類型轉換的
List<?>[] lsa= new List<?>[10]; // 可以這樣定義為泛型數組
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // 可以這樣賦值
Integer i = (Integer) lsa[1].get(0); // 可以這樣取出數據