Java 泛型

1 泛型的作用

在說明下面泛型的使用前,我們要知道泛型的作用:

  • 爲了能夠在編譯期就能夠檢測出使用類型是否正確,而不是在運行期

  • 使用時不用強制類型轉換,自動就能實現對應類型(讓泛型使用者直接使用細化具體的類型)

下面的泛型使用要時刻記住上面的兩點作用。

2 創建

2.1 泛型類型的創建

// T可以是任意字母或文字
public class Wrapper<T> {
	T instance;

	public T get() {
		return instance;
	}

	public void set(T instance) {
		instance = newInstance;
	}
}

使用:
// 因爲泛型聲明在類名後面,所以泛型類型使用也是寫在類名後面
Wrapper<String> wrapper = new Wrapper<String>();
在jdk 1.7有了類型推斷,也可以是:
Wrapper<String> wrapper = new Wrapper<>();

String instance = wrapper.get();
wrapper.set("instance");

擴展問題:
// 不能編譯通過,雖然Object是String的子類
// 因爲會類型擦除
Wrapper<Object> wrapper = new Wrapper<String>();

2.2 泛型接口的創建

public interface Shop<T> {
	T buy();
	float refund(T item);
}

// 因爲T是任意字母或文字,那麼如果它名爲String呢?
public interface Shop<String> {
	// 注意:因爲泛型類型名爲String,所以這裏返回的是泛型類型String,而不是字符串類型String
	String buy();
	float refund(T item);
}

3 繼承

// AppleShop是具體的實現,說明是Apple類型的Shop
public class AppleShop implements Shop<Apple> {
	public Apple buy() {
		return new Apple();
	}
	
	public float refund(Apple item) {
		return 0;
	}
}

使用:
Shop appleShop = new AppleShop();
Apple apple = appleShop.buy();
appleShop.refund(apple);

// RepairableShop想要擴展Shop
// RepairableShop<E>的E類型是聲明,後面Shop<E>的E是使用,Shop<E>的類型E是被前面RepairableShop<E>的類型E限定的
public interface RepairableShop<E> extends Shop<E> {
	void repair(E item);
}
// 上面對RepairableShop的擴展結果相當於
public interface RepairableShop<E> {
	E buy();
	float refund(E item);
	void repair(E item);
}

// RepairableShop<E>是類型聲明,Shop<E>是類型使用
// 所以對類型E做擴展限制時是在聲明設置,而不是在使用時設置,即
// RepairableShop<E extends xxx>,而不是Shop<E extends xxx>因爲這裏是使用類型了
public interface RepairableShop<E extends xxx> extends Shop<E> {}

// 錯誤,T extends Eater<F extends Food>的<F extends Food>已經是使用類型了
public interface EaterShop<F, T extends Eater<F extends Food>> extends Shop<T> {}

// 正確,在聲明中設置
public interface EaterShop<F extends Food, T extends Eater<F>> extends Shop<T> {}

4 多個類型參數

public interface CustomMap<K, V> {
	void put(K key, V value);
	V get(K key);
}

// SimShop<T, C>想要擴展Shop<T>
// SimShop<T>的T類型是規定後面的Shop<T>的T類型
public interface SimShop<T, C> extends Shop<T> {
	C getSim(String name, String id);
}
// 上面對SimShop的擴展結果相當於
public interface SimShop<T, C> extends Shop<T> {
	T buy(float money);
	float refund(T item);
	C getSim(String name, String id);
}

5 泛型方法

public class Test {
	// 泛型方法是可以單獨聲明的,不需要跟隨類是否有聲明泛型類型
	// 泛型方法的類型聲明<T>放在返回值的前面
	public <T> void merge(T item, List<T> list) {
		list.add(item);
	}

	// 限定返回的是View類型或View的子類型
	public <T extends View> T findViewById(int id) {
		return getWindow().findViewByd(id);
	}
}

擴展問題:
// public <T> void merge(T item, List<T> list)中的T類型 和 public class Test<T>中的T類型 是否爲同一種類型?

// 不是,因爲泛型方法是獨立的泛型類型,與類的泛型類型要區分
// 泛型方法的T類型<T>、T item、List<T> list都在方法的作用範圍,不超出方法之外
// test(T item)的T就是類聲明的泛型T類型
public class Test<T> {
	public <T> void merge(T item, List<T> list) {
		list.add(item);
	}

	void test(T item) {}

	// 靜態泛型方法和非靜態泛型方法是沒有區別的
	// 但是和類聲明的泛型類型就有區別了
	// 泛型方法會在調用這個方法的時候將泛型實例化(爲具體類型)
	// 類泛型是在類對象被創建的時候將泛型實例化(爲具體類型)
	static <E> E getViewById(int id) {
		.....
	}
]

使用:
Test test = new Test();
test.<String>merge("add item", new ArrayList<String>());
因爲有了類型推斷,可以簡寫爲:
test.merge("add item", new ArrayList<>());

6 泛型類型限制

6.1 T、?

T 的作用:

  • 寫在類名(接口名)右邊的括號裏,表示 Type Parameter 的聲明 [我要創建一個代號]

  • 寫在類裏的其他地方,表示 [這個類型就是我那個代號的類型]

  • 只在這個類裏有用,出了類就沒用了(比如泛型方法,類型的作用範圍只在方法)

? 的作用:只能寫在泛型聲明的地方(例如變量 List<? extends View> views),表示 [這個類型是什麼都行,只要不超過 ? extends? super 的限制]

6.2 <\T extends xxx> 、 <? extends xxx>

<T extends xxx> 是限制類型範圍。只有這種方式,沒有 <T super xxx> 的情況。

<? extends xxx> 是放寬類型範圍,一般用在方法上。在變量或方法參數、返回值使用時:

  • 作爲變量,它可以是任意類型,只要是 xxx 類型或者 xxx 子類類型即可

  • 只能讀數據,不能寫數據

// 限制T的類型並且擴展Shop接口
// 類型T只能是Apple類型或者Apple的子類類型,Shop中的T類型也一樣限制
public interface FruitShop<T extends Apple> extends Shop<T> {}

// 放寬了範圍,ArrayList<T>的T類型可以是Fruit或者Fruit的子類類型
// 相比起List<Fruit> fruitList = new ArrayList<Fruit>();類型放寬了
List<? extends Fruit> fruitList = new ArrayList<Apple>();

// 都能正常編譯通過
List<Fruit> fruitList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
List<? extends Fruit> list = new ArrayList<>();

totalWeight(fruitList);
totalWeight(appleList);
totalWeight(list);

// 提示錯誤
list.add(new Fruit());
list.add(new Apple());

// 放寬了限制,方法參數不侷限於一種類型List<Fruit>,可以是它的子類類型
float totalWeight(List<? extends Fruit> fruits) {
	float weight = 0;
	for (Fruit  fruit : fruits) {
		weight += fruit.getWeight();
	}
	return weight;
}

6.3 <? super xxx>

<? super xxx> 是放寬類型範圍,一般使用在方法上。在變量或方法參數、返回值使用時:

  • 作爲變量,它可以是任意類型,只要是 xxx 類型或者 xxx 父類類型即可

  • 可以讀寫數據

List<? super Fruit> list = new ArrayList<>();
list.add(new Fruit());
list.add(new Apple());

public void addMeToList(List<? super Apple> list) {
	list.add(this);
}

6.4 Kotlin中的out、in

Kotlin的泛型和Java基本一樣,out 對應Java的 ? extendsin 對應Java的 ? super

val fruitList: List<out Fruit> = ArrayList<>()
等價於
List<? extends Fruit> fruitList = new ArrayList<>();

val appleList: List<in Apple> = ArrayList<>()
等價於
List<? super Apple> appleList = new ArrayList<>();

// val fruitShop: List<out Fruit> = FruitShop<>()
val fruitShop: List<Fruit> = FruitShop<>()
// 這裏的out表示在變量類型聲明時不用寫List<out Fruit>,直接使用List<Fruit>
// 因爲我確定了FruitShop只有它的返回值用到了T
class FruitShop<out T : Fruit> {
	// T類型都是在返回值位置
	fun getResult(): T {}
	...
}

// val fruitShop: List<in Fruit> = FruitShop<>()
val fruitShop: List<Fruit> = FruitShop<>()
// 這裏的in表示在變量類型聲明時不用寫List<in Fruit>,直接使用List<Fruit>
// 因爲我確定了FruitShop只有它的方法參數用到了T
class FruitShop<in T : Fruit> {
	// T類型都是在方法參數位置
	fun getResult(param: T) {}
	...
}

7 TypeParameter、TypeArgument

  • TypeParameter:public class Shop<T> 裏面的那個 <T> 就是 Type Parameter表示我要創建一個 Shop 類,它的內部會用到一個統一的類型,這個類型姑且稱它爲 T

  • TypeArgument:其他地方尖括號裏的全是 Type Argument,比如 Shop<Apple> appleShop 的類型 Apple表示那個統一代號 T,在這裏的類型我決定是這個 Apple

8 泛型的意義

泛型的意義在於:泛型的創建者讓泛型的使用者可以在使用時(實例化時)細化類型信息,從而可以觸及到使用者所細化的子類的API(簡單理解就是,泛型是 [有遠見的創造者] 創造的 [方便使用者] 的工具)。

  • 泛型參數要麼至少是一個方法的返回值類型:
public class Shop<T> {
	T buy();
}
  • 要麼是放在一個接口的參數裏,等着實現類去寫出不同的實現:
public interface Comparable<T> {
	int compareTo(T o);	
}
  • 不過,泛型由於語法自身特性,所以也有一個延伸用途,用於限制方法的參數類型或參數關係:
public <E extends Runnable & Serializable> void method(E param);

public <T> void merge(T item, List<T> list) {
	list.add(item);
}

9 類型擦除

java要做類型擦除的原因主要有兩點:

  • 兼容舊版本沒有泛型使用時的原生類型代碼(比如 List

  • 減少體積

出現了類型擦除,在使用泛型時需要留意的事項:

  • 運行時,所有的 T 以及尖括號裏的東西都會被擦除
List、List<String>、List<Integer>在運行時都是同一個類型List,因爲類型已經被擦除
  • 所有代碼中聲明瞭泛型類型的變量或參數或類或接口,在運行時可以通過反射獲取到泛型信息(泛型信息會寫進 .class 字節碼文件)

  • 如果是在運行時創建的對象,在運行時通過反射也獲取不到泛型信息(因爲 .class 字節碼文件裏面沒有);但有一個解決方案就是創建一個子類(哪怕是匿名類也行),用這個子類來生成對象,這樣由於子類在 .class 文件裏就有,所以可以通過反射拿到運行時創建的對象的泛型信息。比如 GsonTypeToken 也是使用同樣的方式獲取到類型信息

// 這個類在編譯時會被創建出來將泛型類型寫進字節碼
// 即使運行時類型被擦除了也可以通過反射獲取到類型
public class Test<T extends String> {
	private T value;

	public T getValue() {
		return value;
	}

	public void setValue(T newValue) {
		value = newValue;
	}
}

// 運行時創建的對象類型會被擦除,通過反射也無法讀取到泛型類型(因爲泛型類型沒有寫進.class字節碼文件裏)
List<Fruit> fruits = new ArrayList<Fruit>();
List<? extends Fruit> fruits = new ArrayList<>();
// 需要讀取到類型給它一個子類,然後通過反射就可以獲取
// (它沒有類就提供給它一個類,讓它在編譯時將類型信息寫入.class字節碼文件)
List<Fruit> fruits = new ArrayList<Fruit>(){};
List<? extends Fruit> fruits = new ArrayList<>(){};

// 注意後面的{},在編譯的時候它會創建匿名類將信息寫入.class字節碼文件
// 類型擦除只是在運行時擦除,但是類型信息還是保留在.class字節碼文件
TypeToken<List<String>> listStringTypeToken = new TypeToken<List<String>>(){};

10 數組是否有泛型

public class AppleShop<T> implements Shop<Apple> {
	// 錯誤
	// 因爲運行時會類型擦除,不知道T是什麼類型
	// 數組也一樣,不知道T的類型也就無法確定要分配多少空間,畢竟數組因爲不同類型分配的空間也不同
	T instance = new T();
	T[] instances = new T[10];
}

// 允許
List<Shop<Apple>> appleShops = new ArrayList<Shop<Apple>>();

// 錯誤,因爲數組是協變的
// 不允許創建一個指明瞭泛型類型的泛型數組,因爲程序會出錯
Shop<Apple>[] appleShops = new Shop<Apple>[10];

// 錯誤,不具備,因爲存在類型擦除
List<Object> objects = new ArrayList<String>();

// 允許,因爲數組是協變的
// 協變即類型的繼承狀態可以沿襲到泛型中也適用
// 比如Apple是Object的子類,因爲是協變的,所以Apple[]數組也是Object[]數組的子類
Object[] objects = new Apple[10];

根據上面的分析,我們應該儘量避免數組和泛型一起使用,因爲很容易出現各種各樣的問題。

發佈了199 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章