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

攻城獅Chova發表於2021-08-09

泛型的基本概念

  • 泛型: 引數化型別
    • 引數:
      • 定義方法時有形參
      • 呼叫方法時傳遞實參
    • 引數化型別: 將型別由原來的具體的型別引數化,類似方法中的變數引數
      • 型別定義成引數形式, 可以稱為型別形參
      • 在使用或者呼叫時傳入具體的型別,可以稱為型別實參
  • 泛型的本質是為了引數化型別
    • 在不建立新的型別的情況下,通過泛型指定的不同型別來控制形參具體限制的型別
    • 在泛型使用過程中,操作的資料型別被指定為一個引數,這種引數型別可以用在:
      • 類 - 泛型類
      • 介面 - 泛型介面
      • 方法 - 泛型方法
  • 泛型示例:
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); // 可以這樣取出資料

相關文章