泛型是什么
在我們的實際工作中 泛型(Generics) 是無處不在的募书,我們也寫過不少莹捡,看到的更多扣甲,如,源碼涣脚、開源框架... 隨處可見寥茫,但是纱耻,我們真正理解泛型嗎?理解多少呢玖喘?例如:Box 累奈、Box<Object> 急但、Box<?> 羊始、Box<T> 突委、Box<? extends T> 、Box<? super T> 之間的區(qū)別是什么缘缚?
將類型由原來的具體的類型參數(shù)化桥滨,類似于方法中的變量參數(shù)弛车,此時類型也定義成參數(shù)形式(可以稱之為類型形 參)纷跛,然后在使用/調(diào)用時傳入具體的類型(類型實參)贫奠。
泛型的本質(zhì)是為了參數(shù)化類型(在不創(chuàng)建新的類型的情況下望蜡,通過泛型指定的不同類型來控制形參具體限制的類型)脖律。也就是說 在泛型使用過程中小泉,操作的數(shù)據(jù)類型被指定為一個參數(shù)兜挨,這種參數(shù)類型可以用在類拌汇、接口和方法中,分別被稱為泛型類魁淳、泛型接口界逛、 泛型方法纺座。
為什么我們需要泛型
示例一:如果我們需要一個功能净响,如果參數(shù)類型每改變一次馋贤,就要再重新寫一遍方法,那么代碼就會產(chǎn)生冗余仿滔。
public class NonGeneric {
public int addInt(int a, int b) {
return a + b;
}
public Float addFloat(float a, float b) {
return a + b;
}
public static void main(String[] args) {
NonGeneric nonGeneric = new NonGeneric();
System.out.println(nonGeneric.addInt(1, 2));
System.out.println(nonGeneric.addFloat(1f, 2f));
}
}
示例二:
ArrayList list = new ArrayList();
list.add("cycy");
list.add(1);
for (int i=0;i<list.size();i++) {
String name = list.get(i);
System.out.println(name);
}
這里程序會報錯:兼容的類型: Object無法轉(zhuǎn)換為String崎页。list默認(rèn)的類型會是Object類型实昨,如果不會String類型荒给,那么轉(zhuǎn)為String類型就會報錯刁卜。
那么泛型就能解決如上兩種情況的問題蛔趴,使用泛型可以使多種數(shù)據(jù)類型使用同一段代碼孝情,泛型能在編譯期校驗出不合法的類型。
為什么要使用泛型
- 消除類型轉(zhuǎn)換
- 在編譯時進(jìn)行更強(qiáng)的類型檢查
- 增加代碼的復(fù)用性
泛型類魁亦,泛型接口洁奈,泛型方法的定義
定義泛型類利术,如T就能代表一個任意的類型印叁。
class NormalGeneric<T> {
private T data;
public NormalGeneric(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
NormalGeneric normalGeneric = new NormalGeneric<String>("data");
System.out.println(normalGeneric.getData());
}
}
泛型類型個數(shù)可以不定:
/**
* 多類型參數(shù)
*/
interface MultipleGeneric<K, V> {
public K getKey();
public V getValue();
}
public class ImplMultipleGeneric<K,V> implements MultipleGeneric<K,V> {
private K key;
private V value;
public ImplMultipleGeneric(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
public static void main(String[] args) {
MultipleGeneric<String, Integer> m1 = new ImplMultipleGeneric<>("lili", 18);
System.out.println("key:" + m1.getKey() + ", value:" + m1.getValue());
MultipleGeneric<String, String> m2 = new ImplMultipleGeneric<>("name", "sign");
System.out.println("key:" + m2.getKey() + ", value:" + m2.getValue());
}
}
泛型類型的繼承規(guī)則
定義泛型接口轮蜕,這里再使用泛型類去實現(xiàn)肠虽,對于泛型類型的繼承玛追,可以繼續(xù)使用泛型類型痊剖,也可以在繼承中去指定具體類型陆馁。
interface Generitor<T> {
T next();
}
public class Generitorimpl<T> implements Generitor<T> {
@Override
public T next() {
return null;
}
}
public class Generitorimpl<T> implements Generitor<String> {
@Override
public String next() {
return null;
}
}
定義泛型方法并使用:
class GenericMethod {
public <T> T genericMethod(T ...a) {
return a[a.length-1];
}
public static void main(String[] args) {
//聲明調(diào)用genericMethod方法的泛型為String類型
String name = new GenericMethod().<String>genericMethod("xiaoming", "xiaohong", "xiaowang");
int num = new GenericMethod().genericMethod(1, 2, 3);
System.out.println(name);
System.out.println(num);
}
}
泛型類里定義泛型方法
/**
* 泛型類
* @param <T>
*/
public class ShowClass <T> {
/**
* 定義普通方法
*/
public void show1(T t) {
System.out.println(t.toString());
}
/**
* 定義泛型方法
* @param e
* @param <E>
*/
public <E> void show2(E e) {
System.out.println(e.toString());
}
public <T> void show3(T t) {
System.out.println(t.toString());
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
ShowClass<Fruit> showClass = new ShowClass<>();
showClass.show1(apple);
showClass.show1(person); //編譯器會報錯击狮,因為 ShowClass<Fruit> 已經(jīng)限定類型
showClass.show2(apple); // 可以放入彪蓬,泛型方法 <E> 可以是任何非基本類型
showClass.show2(person); // 可以放入,泛型方法 <E> 可以是任何非基本類型
showClass.show3(apple); //可以放入膘茎,泛型方法 <T> 和泛型類中的 <T> 不是同一條 T披坏,可以是任何非基本類型
showClass.show3(person); //可以放入棒拂,泛型方法 <T> 和泛型類中的 <T> 不是同一條 T娘扩,可以是任何非基本類型
}
}
在泛型類中定義泛型方法時琐旁,需要注意灰殴,泛型類里的泛型參數(shù) <T> 和泛型方法里的泛型參數(shù) <T> 不是同一個。
限定類型變量
在泛型中伟阔,我們要求泛型類型有一定的特征皱炉,我們就要對泛型類型做某種限定:
限定類型參數(shù)的泛型類:
public class BoundedClass <T extends Comparable> {
private T t;
public void setT(T t) {
this.t = t;
}
public T min(T outter) {
if (this.t.compareTo(outter) > 0) {
return outter;
} else {
return this.t;
}
}
public static void main(String[] args) {
BoundedClass<String> boundedClass = new BoundedClass<>(); //只能傳入實現(xiàn)了 Comparable 接口的類型
boundedClass.setT("IOS");
System.out.println(boundedClass.min("Android"));
}
}
限定類型參數(shù)的泛型方法:
public class BoundedGeneric {
public static <T extends Comparable> T min(T a, T b) {
if (a.compareTo(b) < 0) {
return a;
} else {
return b;
}
}
public static void main(String[] args) {
System.out.println(BoundedGeneric.min(66,666));
}
}
多重限定
多個限定參數(shù),如果其中有類灾部,類必須放在第一個位置惯退,例如:
interface A {}
interface B {}
class C {}
public class MultipleBounds <T extends C & A & B>{
}
通配符類型
常用的通配符有常用的 T,E,K锁蠕,V夷野,?
T (type) 表示具體的一個java類型
K V (key value) 分別代表java鍵值中的Key Value
E (element) 代表Element
匿沛? 表示不確定的 java 類型
上界通配符 < ? extends E>
在類型參數(shù)中使用 extends 表示這個泛型中的參數(shù)必須是 E 或者 E 的子類扫责。
下界通配符 < ? super E>
在類型參數(shù)中使用 super 表示這個泛型中的參數(shù)必須是 E 或者 E 的父類。
通配符和子類型
例如Orange 是 Fruit 的子類逃呼,但是,F(xiàn)ruitPlateGen<Orange> 不是 FruitPlateGen<Fruit> 的子類者娱。
但是,你可以使用通配符在泛型類或接口之間創(chuàng)建關(guān)系黄鳍。
代碼如下:
List<Apple> apples = new ArrayList<>();
List<Fruit> fruits = apples;
編譯不過推姻,因為,List<Apple> 不是 List<Fruit> 的子類框沟,實際上這兩者無關(guān)藏古,那么,它們的關(guān)系是什么忍燥?
List<Apple> 和 List<Fruit> 的公共父級是 List<?>拧晕。
我們可以使用上下限通配符,在這些類之間創(chuàng)建關(guān)系梅垄,可以使承接通過厂捞,如下:
List<Apple> apples = new ArrayList<>();
List<? extends Fruit> fruits1 = apples; //第一種方式承接
List<? super Apple> fruits2 = apples; //第二種方式承接
?和 T 的區(qū)別
队丝?和 T 都表示不確定的類型靡馁,區(qū)別在于我們可以對 T 進(jìn)行操作,但是對 机久?不行臭墨,比如如下這種 :
// 可以
T t = operate();
// 不可以
?car = operate();
泛型中的約束和局限性
如下3種使用泛型方式都是錯誤的:
class GenericLimit<T> {
private T data;
public GenericLimit() {
data = new T(); //1.泛型類型不能直接創(chuàng)建對象
}
private static T instance; //2.靜態(tài)變量不能用泛型類型聲明
private void createInstance() {
GenericLimit<double> instance = new GenericLimit<double>(); //3.基本數(shù)據(jù)類型不能用作泛型變量
}
}
-
虛擬機(jī)是如何實現(xiàn)泛型的
泛型本質(zhì)是將數(shù)據(jù)類型參數(shù)化膘盖,它通過擦除的方式來實現(xiàn)胧弛。聲明了泛型的 .java 源代碼,在編譯生成 .class 文件之后衔憨,泛型相關(guān)的信息就消失了叶圃。可以認(rèn)為践图,源代碼中泛型相關(guān)的信息掺冠,就是提供給編譯器用的。泛型信息對 Java 編譯器可以見,對 Java 虛擬機(jī)不可見德崭。
Java 編譯器通過如下方式實現(xiàn)擦除:
用 Object 或者界定類型替代泛型斥黑,產(chǎn)生的字節(jié)碼中只包含了原始的類,接口和方法眉厨;
在恰當(dāng)?shù)奈恢貌迦霃?qiáng)制轉(zhuǎn)換代碼來確保類型安全锌奴;
在繼承了泛型類或接口的類中插入橋接方法來保留多態(tài)性。
定義了一個無界泛型類
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) { this.data = data;
this.next = next;
}
public T getData() { return data; }
...
}
由于類型參數(shù) T 是無界的憾股,因此鹿蜀,Java 編譯器將其替換為 Object
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) { this.data = data;
this.next = next;
}
public Object getData() { return data; }
...
}
定義一個有界的泛型類
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) { this.data = data;
this.next = next;
}
public T getData() { return data; }
...
}
Java 編譯器其替換為第一個邊界 Comparable
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) { this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
...
}
-
泛型中類型擦除是什么
泛型信息只存在于代碼編譯階段,在進(jìn)入 JVM 之前服球,與泛型相關(guān)的信息會被擦除掉茴恰,專業(yè)術(shù)語叫做類型擦除。通俗地講斩熊,泛型類和普通類在 java 虛擬機(jī)內(nèi)是沒有什么特別的地方往枣。
舉例:
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
打印的結(jié)果為 true 是因為 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。
協(xié)變
我們假設(shè)有三個類粉渠,動物分冈、貓、狗霸株。父類是動物Animal雕沉,有兩個子類貓Cat和狗Dog。
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
那么由多態(tài)性可以有如下寫法:
Animal animalOne = new Cat();
Animal animalTwo = new Dog();
如果一只貓是一只動物淳衙,那一群貓是一群動物嗎蘑秽?一群狗是一群動物嗎?Java數(shù)組認(rèn)為是的箫攀。
//編譯通過
Animal[] animals = new Cat[10];
我往這一群動物中添加一只貓肠牲、一只狗,它還是一群動物吧:
Animal[] animals = new Cat[10];
animals[0] = new Cat();
// 下面這行代碼會拋運(yùn)行時異常
animals[1] = new Dog();
Animal animal = animals[0];
編譯沒有任何問題靴跛。但是一運(yùn)行缀雳,會拋出一個運(yùn)行時異常:ArrayStoreException。這個異常頭頂?shù)淖⑨屢呀?jīng)寫得很明顯了梢睛,如果你往數(shù)組中添加一個類型不對的對象肥印,就會拋這個異常。animals雖然門面上是一個Animal數(shù)組绝葡,但是它運(yùn)行時的本質(zhì)還是一個Cat數(shù)組啊深碱,一個Cat數(shù)組怎么能添加一個Dog呢?
如果Cat是Animal的子類型藏畅,那么Cat[]也是Animal[]的子類型敷硅,我們稱這種性質(zhì)為協(xié)變(covariance)。Java中,數(shù)組是協(xié)變的绞蹦。
- 泛型的不變性
而Java中的泛型是不變(invariance)的力奋,也就是說,List<Cat>并不是List<Animal>的子類型幽七。
List<Cat> cats = new LinkedList<>();
// 編譯器報錯
List<Animal> animals = cats;
//編譯通過
List<? extends Animal> animals2 = cats;
雖然因為泛型的不變性景殷,List<Cat>并不是List<Animal>的子類型,但Java通過其它方式來支持了泛型的協(xié)變澡屡,List<Cat>是List<? extends Animal>的子類型猿挚。與此同時,Java在編譯器層面通過禁止寫入的方式挪蹭,保證了協(xié)變下的安全性亭饵。
協(xié)變不可添加元素:
List<? extends Animal> animals = new LinkedList<Cat>();
// 以下四行代碼都不能編譯通過
// animals.add(new Dog());
// animals.add(new Cat());
// animals.add(new Animal());
// animals.add(new Object());
// 可以添加null,但沒意義
animals.add(null);
// 可以安全地取出來
Animal animal = animals.get(0);
逆變
// 下面這行代碼編譯不通過
// List<? super Animal> animals = new LinkedList<Cat>();
// 下面都是OK的寫法
// List<? super Animal> animals = new LinkedList<Object>();
// List<? super Animal> animals = new LinkedList<Animal>();
// 等價于上面一行的寫法
List<? super Animal> animals = new LinkedList<>();
animals.add(new Cat());
animals.add(new Dog());
// 取出來一定是Object
Object object = animals.get(0);
// 這樣寫是OK的
List<? super Cat> cats = new LinkedList<Animal>();
逆變(contravariance)梁厉,也稱逆協(xié)變,從名字可以看出來踏兜,它與協(xié)變的性質(zhì)是相反的词顾。也就是說,List<Animal>是List<? super Cat>的子類型碱妆。
參考:
https://segmentfault.com/a/1190000023379970
https://juejin.cn/post/6911302681583681544