前言
很高興遇見你~
在本系列的上一篇中为鳄,我們學(xué)習(xí)了 Kotlin 大部分知識點裳仆,體驗到了 Kotlin 語法的便捷,強大孤钦,以及高效的函數(shù)式編程歧斟。還沒有看過上一篇文章的朋友,建議先去閱讀 "Kotlin"系列: 一偏形、Kotlin入門静袖,接下來我們就進入 Kotlin 泛型的學(xué)習(xí),泛型在我看來是比較復(fù)雜的俊扭,同時也是面試中經(jīng)常問的队橙,很長一段時間,我對泛型的認識比較模糊,那么在使用的時候就更加疑惑了捐康,因此這篇文章希望能帶大家攻克這一知識點
問題
下面我拋出一系列問題仇矾,咱們帶著問題去學(xué)習(xí):
1、什么是泛型解总?
2贮匕、泛型有什么作用?
3倾鲫、怎么去定義和使用泛型粗合?
1、什么是泛型乌昔?
泛型通俗的理解就是:很多的類型隙疚,它通過使用參數(shù)化類型的概念,允許我們在不指定具體類型的情況下進行編程
2磕道、泛型有什么作用供屉?
泛型是 JDK 1.5 引入的安全機制,是一種給編譯器使用的技術(shù):
1溺蕉、提高了代碼的可重用性
2伶丐、將運行期的類型轉(zhuǎn)換異常提前到了編譯期,保證類型的安全疯特,避免類型轉(zhuǎn)換異常
3哗魂、怎么去定義和使用泛型?
我們可以給一個類漓雅,方法录别,或者接口指定泛型,在具體使用的地方指定具體的類型
一邻吞、Java 泛型
要學(xué)習(xí)好 Kotlin 泛型组题,我們先要對 Java 泛型足夠的了解,因為 Kotlin 泛型和 Java 泛型基本上是一樣的抱冷,只不過在 Kotlin 上有些東西換了新的寫法
1崔列、泛型的簡單使用
在 Java 中,我們可以給一個類旺遮,方法赵讯,或者接口指定泛型,在具體使用的地方指定具體的類型
1)耿眉、定義一個泛型類瘦癌,在類名的后面加上 <T>
這種語法結(jié)構(gòu)就是定義一個泛型類,泛型可以有任意多個
//定義一個泛型類
public class JavaGenericClass<T> {
private T a;
public JavaGenericClass(T a) {
this.a = a;
}
public T getA() {
return a;
}
public void setA(T a) {
this.a = a;
}
//泛型類使用
public static void main(String[] args) {
//編譯器可推斷泛型類型跷敬,因此 new 對象后面的泛型類型可省略
JavaGenericClass<String> javaGenericClass1 = new JavaGenericClass<String>("erdai");
JavaGenericClass<Integer> javaGenericClass2 = new JavaGenericClass<>(666);
System.out.println(javaGenericClass1.getA());
System.out.println(javaGenericClass2.getA());
}
}
//打印結(jié)果
erdai
666
2)讯私、定義一個泛型方法热押,在方法的返回值前面加上 <T>
這種語法結(jié)構(gòu)就是定義一個泛型方法,泛型可以有任意多個斤寇,泛型方法的泛型與它所在的類沒有任何關(guān)系
public class JavaGenericMethod {
public <T> void getName(T t){
System.out.println(t.getClass().getSimpleName());
}
public static void main(String[] args) {
JavaGenericMethod javaGenericMethod = new JavaGenericMethod();
//編譯器可推斷出泛型類型桶癣,因此這里的泛型類型也可省略
javaGenericMethod.<String>getName("erdai666");
}
}
//打印結(jié)果
String
3)、定義一個泛型接口
在接口名的后面加上 <T>
這種語法結(jié)構(gòu)就是定義一個泛型接口娘锁,泛型可以有任意多個
public interface JavaGenericInterface<T> {
T get();
}
class TestClass<T> implements JavaGenericInterface<T>{
private final T t;
public TestClass(T t) {
this.t = t;
}
@Override
public T get() {
return t;
}
}
class Client{
public static void main(String[] args) {
JavaGenericInterface<String> javaGenericInterface = new TestClass<>("erdai666");
System.out.println(javaGenericInterface.get());
}
}
//打印結(jié)果
erdai666
2牙寞、泛型擦除
1、泛型擦除是什么莫秆?
看下面這段代碼:
//使用了不同的泛型類型 結(jié)果得到了相同的數(shù)據(jù)類型
public class JavaGenericWipe {
public static void main(String[] args) {
Class a = new ArrayList<String>().getClass();
Class b = new ArrayList<Integer>().getClass();
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("a == b: " + (a == b));
}
}
//打印結(jié)果
a = class java.util.ArrayList
b = class java.util.ArrayList
a == b: true
為啥會出現(xiàn)這種情況呢间雀?
因為 Java 中的泛型是使用擦除技術(shù)來實現(xiàn)的:泛型擦除是指通過類型參數(shù)合并,將泛型類型實例關(guān)聯(lián)到同一份字節(jié)碼上镊屎。編譯器只為泛型類型生成一份字節(jié)碼惹挟,并將其實例關(guān)聯(lián)到這份字節(jié)碼上
之所以要使用泛型擦除是為了兼容 JDK 1.5 之前運行時的類加載器,避免因為引入泛型而導(dǎo)致運行時創(chuàng)建不必要的類
2缝驳、泛型擦除的具體步驟
1)连锯、擦除所有類型參數(shù)信息,如果類型參數(shù)是有界的用狱,則將每個參數(shù)替換為其第一個邊界运怖;如果類型參數(shù)是無界的,則將其替換為 Object類型擦除的規(guī)則:
<T>
擦除后變?yōu)?Object
<T extends A>
擦除后變?yōu)?A
<? extends A>
擦除后變?yōu)?A
<? super A>
擦除后變?yōu)镺bject
2)夏伊、(必要時)插入類型轉(zhuǎn)換摇展,以保持類型安全
3)、(必要時)生成橋接方法以在子類中保留多態(tài)性
//情況1: 擦除所有類型參數(shù)信息溺忧,如果類型參數(shù)是有界的咏连,則將每個參數(shù)替換為其第一個邊界;如果類型參數(shù)是無界的砸狞,則將其替換為 Object
class Paint {
void draw() {
System.out.println("Paint.draw() called");
}
}
//如果不給 T 設(shè)置邊界捻勉,那么 work 方法里面的 t 就調(diào)用不到 draw 方法
class Painter<T extends Paint> {
private T t;
public Painter(T t) {
this.t = t;
}
public void work() {
t.draw();
}
}
//情況2:(必要時)插入類型轉(zhuǎn)換镀梭,以保持類型安全
public class JavaGenericWipe {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("erdai");
stringList.add("666");
for (String s : stringList) {
System.out.println(s);
}
}
}
//編譯時生成的字節(jié)碼文件翻譯過來大致如下
public class JavaGenericWipe {
public JavaGenericWipe() {
}
public static void main(String[] args) {
List<String> stringList = new ArrayList();
stringList.add("erdai");
stringList.add("666");
Iterator var2 = stringList.iterator();
while(var2.hasNext()) {
//編譯器給我們做了強轉(zhuǎn)的工作
String s = (String)var2.next();
System.out.println(s);
}
}
}
//情況3 (必要時)生成橋接方法以在子類中保留多態(tài)性
class Node {
public Object data;
public Node(Object data) {
this.data = data;
}
public void setData(Object data) {
this.data = data;
}
}
class MyNode extends Node {
public MyNode(Integer data) {
super(data);
}
public void setData(Integer data) {
super.setData(data);
}
}
//編譯時生成的字節(jié)碼文件翻譯過來大致如下
class MyNode extends Node {
public MyNode(Integer data) {
super(data);
}
// 編譯器生成的橋接方法
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
3刀森、偽泛型
Java 中的泛型是一種特殊的語法糖,通過類型擦除實現(xiàn)报账,這種泛型稱為偽泛型研底,我們可以反射繞過編譯器泛型檢查,添加一個不同類型的參數(shù)
//反射繞過編譯器檢查
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("erdai");
stringList.add("666");
//使用反射增加一個新的元素
Class<? extends List> aClass = stringList.getClass();
try {
Method method = aClass.getMethod("add", Object.class);
method.invoke(stringList,123);
} catch (Exception e) {
e.printStackTrace();
}
Iterator iterator = stringList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
//打印結(jié)果
erdai
666
123
4透罢、泛型擦除進階
下面我拋出一個在工作中經(jīng)常會遇到的問題:
在進行網(wǎng)絡(luò)請求的時候榜晦,傳入一個泛型的實際類型,為啥能夠正確的獲取到該泛型類型羽圃,并利用 Gson 轉(zhuǎn)換為實際的對象乾胶?
答:是因為在運行期我們可以使用反射獲取到具體的泛型類型
What? 泛型不是在編譯的時候被擦除了嗎?為啥在運行時還能夠獲取到具體的泛型類型????
答:泛型中所謂的類型擦除识窿,其實只是擦除 Code 屬性中的泛型信息斩郎,在類常量池屬性(Signature 屬性、LocalVariableTypeTable 屬性)中其實還保留著泛型信息喻频,而類常量池中的屬性可以被 class 文件缩宜,字段表,方法表等攜帶甥温,這就使得我們聲明的泛型信息得以保留锻煌,這也是我們在運行時可以反射獲取泛型信息的根本依據(jù)
//這是反編譯后的 JavaGenericClass.class 文件,可以看到 T
public class JavaGenericClass<T> {
private T a;
public JavaGenericClass(T a) {
this.a = a;
}
public T getA() {
return a;
}
public void setA(T a) {
this.a = a;
}
//...
}
注意:Java 是在 JDK 1.5 引入的泛型姻蚓,為了彌補泛型擦除的不足宋梧,JVM 的 class 文件也做了相應(yīng)的修改,其中最重要的就是新增了 Signature 屬性表和 LocalVariableTypeTable 屬性表
我們看下下面這段代碼:
class ParentGeneric<T> {
}
class SubClass extends ParentGeneric<String>{
}
class SubClass2<T> extends ParentGeneric<T> {
}
public class GenericGet {
//獲取實際的泛型類型
public static <T> Type findGenericType(Class<T> cls) {
Type genType = cls.getGenericSuperclass();
Type finalNeedType = null;
if (genType instanceof ParameterizedType) {
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
finalNeedType = params[0];
}
return finalNeedType;
}
public static void main(String[] args) {
SubClass subClass = new SubClass();
SubClass2<Integer> subClass2 = new SubClass2<Integer>();
//打印 subClass 獲取的泛型
System.out.println("subClass: " + findNeedClass(subClass.getClass()));
//打印subClass2獲取的泛型
System.out.println("subClass2: " + findGenericType(subClass2.getClass()));
}
}
//運行這段代碼 打印結(jié)果如下
subClass: class java.lang.String
subClass2: T
上面代碼:
1史简、 SubClass 相當(dāng)于對 ParentGeneric<T> 做了賦值操作 T = String乃秀,我們通過反射獲取到了泛型類型為 String
2、SubClass2 對 ParentGeneric<T>沒有做賦值操作 圆兵,我們通過反射獲取到了泛型類型為 T
這里大家肯定會有很多疑問跺讯?
1、為啥 1 中沒有傳入任何泛型的信息卻能獲取到泛型類型呢殉农?
2刀脏、為啥 2 中我創(chuàng)建對象的時候傳入的泛型是 Integer ,獲取的時候變成了 T 呢超凳?
現(xiàn)在我們來仔細分析一波:
上面我講過愈污,類型擦除其實只是擦除 Code 屬性中的泛型信息,在類常量池屬性中還保留著泛型信息轮傍,因此上面的 SubClass 和SubClass2 在編譯的時候其實會保留各自的泛型到字節(jié)碼文件中暂雹,一個是 String,一個是 T 创夜。而 subClass 和 subClass2 是運行時動態(tài)創(chuàng)建的杭跪,這個時候你即使傳入了泛型類型,也會被擦除掉驰吓,因此才會出現(xiàn)上面的結(jié)果涧尿,到這里,大家是否明了了呢檬贰?
如果還有點模糊姑廉,我們再來看一個例子:
class ParentGeneric<T> {
}
public class GenericGet {
//獲取實際的泛型類型
public static <T> Type findGenericType(Class<T> cls) {
Type genType = cls.getGenericSuperclass();
Type finalNeedType = null;
if (genType instanceof ParameterizedType) {
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
finalNeedType = params[0];
}
return finalNeedType;
}
public static void main(String[] args) {
ParentGeneric<String> parentGeneric1 = new ParentGeneric<String>();
ParentGeneric<String> parentGeneric2 = new ParentGeneric<String>(){};
//打印 parentGeneric1 獲取的泛型
System.out.println("parentGeneric1: " + findGenericType(parentGeneric1.getClass()));
//打印 parentGeneric2 獲取的泛型
System.out.println("parentGeneric2: " + findGenericType(parentGeneric2.getClass()));
}
}
//運行這段代碼 打印結(jié)果如下
parentGeneric1: null
parentGeneric2: class java.lang.String
上述代碼 parentGeneric1 和 parentGeneric2 唯一的區(qū)別就是多了 {},獲取的結(jié)果卻截然不同翁涤,我們在來仔細分析一波:
1桥言、 ParentGeneric 聲明的泛型 T 在編譯的時候其實是保留在了字節(jié)碼文件中萌踱,parentGeneric1 是在運行時創(chuàng)建的,由于泛型擦除号阿,我們無法通過反射獲取其中的類型虫蝶,因此打印了 null
這個地方可能大家又會有個疑問了,你既然保留了泛型類型為 T倦西,那么我獲取的時候應(yīng)該為 T 才是能真,為啥打印的結(jié)果是 null 呢?
如果你心里有這個疑問扰柠,說明你思考的非常細致粉铐,要理解這個問題,我們首先要對 Java 類型(Type)系統(tǒng)有一定的了解卤档,這其實和我上面寫的那個獲取泛型類型的方法有關(guān):
//獲取實際的泛型類型
public static <T> Type findGenericType(Class<T> cls) {
//獲取當(dāng)前帶有泛型的父類
Type genType = cls.getGenericSuperclass();
Type finalNeedType = null;
//如果當(dāng)前 genType 是參數(shù)化類型則進入到條件體
if (genType instanceof ParameterizedType) {
//獲取參數(shù)類型 <> 里面的那些值,例如 Map<K,V> 那么就得到 [K,V]的一個數(shù)組
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
//將第一個泛型類型賦值給 finalNeedType
finalNeedType = params[0];
}
return finalNeedType;
}
上述代碼我們需要先獲取這個類的泛型父類蝙泼,如果是參數(shù)化類型則進入到條件體,獲取實際的泛型類型并返回劝枣。如果不是則直接返回 finalNeedType , 那么這個時候就為 null 了
在例1中:
SubClass1 subClass1 = new SubClass1();
SubClass2<Integer> subClass2 = new SubClass2<>();
System.out.println(subClass1.getClass().getGenericSuperclass());
System.out.println(subClass2.getClass().getGenericSuperclass());
//運行程序 打印結(jié)果如下
com.dream.java_generic.share.ParentGeneric<java.lang.String>
com.dream.java_generic.share.ParentGeneric<T>
可以看到獲取到了泛型父類汤踏,因此會走到條件體里面獲取到實際的泛型類型并返回
在例2中:
ParentGeneric<String> parentGeneric1 = new ParentGeneric<String>();
System.out.println(parentGeneric1.getClass().getGenericSuperclass());
//運行程序 打印結(jié)果如下
class java.lang.Object
可以看到獲取到的泛型父類是 Object,因此進不去條件體舔腾,所以就返回 null 了
2溪胶、parentGeneric2 在創(chuàng)建的時候后面加了 {},這就使得 parentGeneric2 成為了一個匿名內(nèi)部類稳诚,且父類就是 ParentGeneric哗脖,因為匿名內(nèi)部類是在編譯時創(chuàng)建的,那么在編譯的時候就會創(chuàng)建并攜帶具體的泛型信息扳还,因此 parentGeneric2 可以獲取其中的泛型類型
通過上面兩個例子我們可以得出結(jié)論:如果在編譯的時候就保存了泛型類型到字節(jié)碼中才避,那么在運行時我們就可以通過反射獲取到,如果在運行時傳入實際的泛型類型氨距,這個時候就會被擦除桑逝,反射獲取不到當(dāng)前傳入的泛型實際類型
例子1中我們指定了泛型的實際類型為 String,編譯的時候就將它存儲到了字節(jié)碼文件中俏让,因此我們獲取到了泛型類型楞遏。例子2中我們創(chuàng)建了一個匿名內(nèi)部類,同樣在編譯的時候會進行創(chuàng)建并保存了實際的泛型到字節(jié)碼中舆驶,因此我們可以獲取到橱健。而 parentGeneric1 是在運行時創(chuàng)建的而钞,雖然 ParentGeneric 聲明的泛型 T 在編譯時也保留在了字節(jié)碼文件中沙廉,但是它傳入的實際類型被擦除了,這種泛型也是無法通過反射獲取的臼节,記住上面這條結(jié)論撬陵,那么對于泛型類型的獲取你就得心應(yīng)手了
5珊皿、泛型獲取經(jīng)驗總結(jié)
其實通過上面兩個例子可以發(fā)現(xiàn),當(dāng)我們定義一個子類繼承一個泛型父類巨税,并給這個泛型一個類型蟋定,我們就可以獲取到這個泛型類型
//定義一個子類繼承泛型父類,并給這個泛型一個實際的類型
class SubClass extends ParentGeneric<String>{
}
//匿名內(nèi)部類草添,其實我們定義的這個匿名內(nèi)部類也是一個子類驶兜,它繼承了泛型父類,并給這個泛型一個實際的類型
ParentGeneric<String> parentGeneric2 = new ParentGeneric<String>(){};
因此如果我們想要獲取某個泛型類型远寸,我們可以通過子類的幫助去取出該泛型類型抄淑,一種良好的編程實踐就是把當(dāng)前需要獲取的泛型類用 abstract 聲明
3、邊界
邊界就是在泛型的參數(shù)上設(shè)置限制條件驰后,這樣可以強制泛型可以使用的類型肆资,更重要的是可以按照自己的邊界類型來調(diào)用方法
1)、Java 中設(shè)置邊界使用 extends 關(guān)鍵字灶芝,完整語法結(jié)構(gòu):<T extends Bound>
郑原,Bound 可以是類和接口,如果不指定邊界夜涕,默認邊界為 Object
2)犯犁、可以設(shè)置多個邊界,中間使用 & 連接女器,多個邊界中只能有一個邊界是類栖秕,且類必須放在最前面,類似這種語法結(jié)構(gòu)
<T extends ClassBound & InterfaceBound1 & InterfaceBound2>
下面我們來演示一下:
abstract class ClassBound{
public abstract void test1();
}
interface InterfaceBound1{
void test2();
}
interface InterfaceBound2{
void test3();
}
class ParentClass <T extends ClassBound & InterfaceBound1 & InterfaceBound2>{
private final T item;
public ParentClass(T item) {
this.item = item;
}
public void test1(){
item.test1();
}
public void test2(){
item.test2();
}
public void test3(){
item.test3();
}
}
class SubClass extends ClassBound implements InterfaceBound1,InterfaceBound2 {
@Override
public void test1() {
System.out.println("test1");
}
@Override
public void test2() {
System.out.println("test2");
}
@Override
public void test3() {
System.out.println("test3");
}
}
public class Bound {
public static void main(String[] args) {
SubClass subClass = new SubClass();
ParentClass<SubClass> parentClass = new ParentClass<SubClass>(subClass);
parentClass.test1();
parentClass.test2();
parentClass.test3();
}
}
//打印結(jié)果
test1
test2
test3
4晓避、通配符
1簇捍、泛型的協(xié)變,逆變和不變
思考一個問題俏拱,代碼如下:
Number number = new Integer(666);
ArrayList<Number> numberList = new ArrayList<Integer>();//編譯器報錯 type mismatch
上述代碼暑塑,為啥 Number 的對象可以由 Integer 實例化,而 ArrayList<Number>
的對象卻不能由 ArrayList<Integer>
實例化锅必?
要明白上面這個問題事格,我們首先要明白,什么是泛型的協(xié)變搞隐,逆變和不變
1)驹愚、泛型協(xié)變,假設(shè)我定義了一個
Class<T>
的泛型類劣纲,其中 A 是 B 的子類逢捺,同時Class<A>
也是Class<B>
的子類,那么我們說 Class 在 T 這個泛型上是協(xié)變的2)癞季、泛型逆變劫瞳,假設(shè)我定義了一個
Class<T>
的泛型類倘潜,其中A 是 B
的子類,同時Class<B>
也是Class<A>
的子類志于,那么我們說 Class 在 T 這個泛型上是逆變的3)涮因、泛型不變,假設(shè)我定義了一個
Class<T>
的泛型類伺绽,其中 A 是 B 的子類养泡,同時Class<B>
和Class<A>
沒有繼承關(guān)系,那么我們說 Class 在 T 這個泛型上是不變的
因此我們可以知道 ArrayList<Number>
的對象不能由 ArrayList<Integer>
實例化是因為 ArrayList 當(dāng)前的泛型是不變的奈应,我們要解決上面報錯的問題瓤荔,可以讓 ArrayList 當(dāng)前的泛型支持協(xié)變,如下:
Number number = new Integer(666);
ArrayList<? extends Number> numberList = new ArrayList<Integer>();
2钥组、泛型的上邊界通配符
1)输硝、泛型的上邊界通配符語法結(jié)構(gòu):<? extends Bound>
,使得泛型支持協(xié)變程梦,它限定的類型是當(dāng)前上邊界類或者其子類点把,如果是接口的話就是當(dāng)前上邊界接口或者實現(xiàn)類,使用上邊界通配符的變量只讀屿附,不可以寫郎逃,可以添加 null ,但是沒意義
public class WildCard {
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<Integer>();
List<Number> numberList = new ArrayList<Number>();
integerList.add(666);
numberList.add(123);
getNumberData(integerList);
getNumberData(numberList);
}
public static void getNumberData(List<? extends Number> data) {
System.out.println("Number data :" + data.get(0));
}
}
//打印結(jié)果
Number data: 666
Number data: 123
問題:為啥使用上邊界通配符的變量只讀挺份,而不能寫褒翰?
1、<? extends Bound>
,它限定的類型是當(dāng)前上邊界類或者其子類匀泊,它無法確定自己具體的類型优训,因此編譯器無法驗證類型的安全,所以不能寫
2各聘、假設(shè)可以寫揣非,我們向它里面添加若干個子類,然后用一個具體的子類去接收躲因,勢必會造成類型轉(zhuǎn)換異常
3早敬、泛型的下邊界通配符
1)、泛型的下邊界通配符語法結(jié)構(gòu):<? super Bound>
大脉,使得泛型支持逆變搞监,它限定的類型是當(dāng)前下邊界類或者其父類,如果是接口的話就是當(dāng)前下邊界接口或者其父接口镰矿,使用下邊界通配符的變量只寫琐驴,不建議讀
public class WildCard {
public static void main(String[] args) {
List<Number> numberList = new ArrayList<Number>();
List<Object> objectList = new ArrayList<Object>();
setNumberData(numberList);
setNumberData(objectList);
}
public static void setNumberData(List<? super Number> data) {
Number number = new Integer(666);
data.add(number);
}
}
問題:為啥使用下邊界通配符的變量可以寫,而不建議讀?
1棍矛、<? super Bound>
,它限定的類型是當(dāng)前下邊界類或者其父類抛杨,雖然它也無法確定自己具體的類型够委,但根據(jù)多態(tài),它能保證自己添加的元素是安全的怖现,因此可以寫
2茁帽、獲取值的時候,會返回一個 Object
類型的值屈嗤,而不能獲取實際類型參數(shù)代表的類型潘拨,因此建議不要去讀,如果你實在要去讀也行饶号,但是要注意類型轉(zhuǎn)換異常
4铁追、泛型的無邊界通配符
1)、無邊界通配符的語法結(jié)構(gòu):<?>
茫船,實際上它等價于 <? extends Object>
琅束,也就是說它的上邊界是 Object 或其子類,因此使用無界通配符的變量同樣只讀算谈,不能寫涩禀,可以添加 null ,但是沒意義
public class WildCard {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
List<Number> numberList = new ArrayList<Number>();
List<Integer> integerList = new ArrayList<Integer>();
stringList.add("erdai");
numberList.add(666);
integerList.add(123);
getData(stringList);
getData(numberList);
getData(integerList);
}
public static void getData(List<?> data) {
System.out.println("data: " + data.get(0));
}
}
//打印結(jié)果
data: erdai
data: 666
data: 123
5然眼、PECS 原則
泛型代碼的設(shè)計艾船,應(yīng)遵循PECS原則(Producer extends Consumer super):
1)、如果只需要獲取元素高每,使用 <? extends T>
2)屿岂、如果只需要存儲,使用 <? super T>
//這是 Collections.java 中 copy 方法的源碼
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
//...
}
這是一個很經(jīng)典的例子鲸匿,src 表示原始集合雁社,使用了 <? extends T>
,只能從中讀取元素晒骇,dest 表示目標集合霉撵,只能往里面寫元素,充分的體現(xiàn)了 PECS 原則
6洪囤、使用通配符總結(jié)
1)徒坡、當(dāng)你只想讀取值的時候,使用 <? extends T>
2)瘤缩、當(dāng)你只想寫入值的時候喇完,使用 <? super T>
3)、當(dāng)你既想讀取值又想寫入值的時候剥啤,就不要使用通配符
5锦溪、泛型的限制
1)不脯、泛型不能顯式地引用在運行時類型的操作里,如 instanceof 操作和 new 表達式刻诊,運行時類型只適用于原生類型
public class GenericLimitedClass<T> {
private void test(){
String str = "";
//編譯器不允許這種操作
if(str instanceof T){
}
//編譯器不允許這種操作
T t = new T();
}
}
2)防楷、不能創(chuàng)建泛型類型的數(shù)組,只可以聲明一個泛型類型的數(shù)組引用
public class GenericLimitedClass<T> {
private void test(){
GenericLimitedClass<Test>[] genericLimitedClasses;
//編譯器不允許
genericLimitedClasses = new GenericLimitedClass<Test>[10];
}
}
3)则涯、不能聲明類型為泛型的靜態(tài)字段
public class GenericLimitedClass<T> {
//編譯器不允許
private static T t;
}
4)复局、泛型類不可以直接或間接地繼承 Throwable
//編譯器不允許
public class GenericLimitedClass<T> extends Throwable {
}
5)、方法中不可以捕獲類型參數(shù)的實例粟判,但是可以在 throws 語句中使用類型參數(shù)
public class GenericLimitedClass<T> {
private <T extends Throwable> void test1() throws T{
try {
//編譯器不允許
}catch (T exception){
}
}
}
6)亿昏、一個類不可以重載在類型擦除后有同樣方法簽名的方法
public class GenericLimitedClass<T> {
//編譯器不允許
private void test2(List<String> stringList){
}
private void test2(List<Integer> integerList){
}
}
6、問題
1)档礁、類型邊界和通配符邊界有什么區(qū)別角钩?
類型邊界可以有多個,通配符邊界只能有一個
2)呻澜、List<?>
和 List<Object>
一樣嗎彤断?
不一樣
1、 List<Object>
可讀寫易迹,但是 List<?> 只讀
2宰衙、List<?>
可以有很多子類,但是 List<Object>
沒有
二睹欲、Kotlin 泛型
Kotlin 泛型和 Java 泛型基本上是一樣的供炼,只不過在 Kotlin 上有些東西換了新的寫法
1、泛型的基本用法
1)窘疮、在 Kotlin 中我們定義和使用泛型的方式如下:
//1袋哼、定義一個泛型類,在類名后面使用 <T> 這種語法結(jié)構(gòu)就是為這個類定義一個泛型
class MyClass<T>{
fun method(params: T) {
}
}
//泛型調(diào)用
val myClass = MyClass<Int>()
myClass.method(12)
//2闸衫、定義一個泛型方法涛贯,在方法名的前面加上 <T> 這種語法結(jié)構(gòu)就是為這個方法定義一個泛型
class MyClass{
fun <T> method(params: T){
}
}
//泛型調(diào)用
val myClass = MyClass()
myClass.method<Int>(12)
//根據(jù) Kotlin 類型推導(dǎo)機制,我們可以把泛型給省略
myClass.method(12)
//3蔚出、定義一個泛型接口弟翘,在接口名后面加上 <T> 這種語法結(jié)構(gòu)就是為這個接口定義一個泛型
interface MyInterface<T>{
fun interfaceMethod(params: T)
}
對比 Java 中定義泛型,我們可以發(fā)現(xiàn):在定義類和接口泛型上沒有任何區(qū)別骄酗,在定義方法泛型時稀余,Kotlin 是在方法名前面添加泛型,而 Java 是在返回值前面添加泛型
2趋翻、邊界
1)睛琳、為泛型指定邊界,我們可以使用 <T : Class>
這種語法結(jié)構(gòu),如果不指定泛型的邊界师骗,默認為 Any?
2)历等、如果有多個邊界,可以使用 where 關(guān)鍵字辟癌,中間使用 : 隔開寒屯,多個邊界中只能有一個邊界是類,且類必須放在最前面
//情況1 單個邊界
class MyClass1<T : Number> {
var data: T? = null
fun <T : Number> method(params: T) {
}
}
//情況2 多個邊界使用 where 關(guān)鍵字
open class Animal
interface Food
interface Food2
class MyClass2<T> where T : Animal, T : Food, T : Food2 {
fun <T> method(params: T) where T : Animal, T : Food, T : Food2 {
}
}
3愿待、泛型實化
泛型實化在 Java 中是不存在的浩螺,Kotlin 中之所以能實現(xiàn)泛型實化靴患,是因為使用的內(nèi)聯(lián)函數(shù)會對代碼進行替換仍侥,那么在內(nèi)聯(lián)函數(shù)中使用泛型,最終也會使用實際的類型進行替換
1)鸳君、使用內(nèi)聯(lián)函數(shù)配合 reified 關(guān)鍵字對泛型進行實化农渊,語法結(jié)構(gòu)如下:
inline fun <reified T> getGenericType() {
}
實操一下:
inline fun <reified T> getGenericType() = T::class.java
fun main() {
//泛型實化 這種情況在 Java 是會被類型擦除的
val result1 = getGenericType<String>()
val result2 = getGenericType<Number>()
println(result1)
println(result2)
}
//打印結(jié)果
class java.lang.String
class java.lang.Number
2)、實際應(yīng)用
在我們跳轉(zhuǎn) Activity 的時候通常會這么操作
val intent = Intent(mContext,TestActivity::class.java)
mContext.startActivity(intent)
有沒有感覺寫這種 TestActivity::class.java 的語法很難受或颊,反正我是覺得很難受砸紊,那么這個時候我們就可以使用泛型實化換一種寫法:
//定義一個頂層函數(shù)
inline fun <reified T> startActivity(mContext: Context){
val intent = Intent(mContext,T::class.java)
mContext.startActivity(intent)
}
//使用的時候
startActivity<TestActivity>(mContext)
這種寫法是不是清爽了很多,那么在我們跳轉(zhuǎn) Activity 的時候囱挑,可能會攜帶一些參數(shù)醉顽,如下:
val intent = Intent(mContext,TestActivity::class.java)
intent.putExtra("params1","erdai")
intent.putExtra("params2","666")
mContext.startActivity(intent)
這個時候我們可以增加一個函數(shù)類型的參數(shù)炬转,使用 Lambda 表達式去調(diào)用蹬昌,如下:
inline fun <reified T> startActivity(mContext: Context, block: Intent.() -> Unit){
val intent = Intent(mContext,T::class.java)
intent.block()
mContext.startActivity(intent)
}
//使用的時候
startActivity<SecondActivity>(mContext){
putExtra("params1","erdai")
putExtra("params2","666")
}
4、泛型協(xié)變秉馏,逆變和不變
1)通熄、泛型協(xié)變的語法規(guī)則:<out T>
類似于 Java 的 <? extends Bound>
唆涝,它限定的類型是當(dāng)前上邊界類或者其子類,如果是接口的話就是當(dāng)前上邊界接口或者實現(xiàn)類唇辨,協(xié)變的泛型變量只讀廊酣,不可以寫,可以添加 null 赏枚,但是沒意義
open class Person
class Student: Person()
class Teacher: Person()
class SimpleData<out T>{
}
fun main() {
val person: Person = Student()
val personGeneric: SimpleData<Person> = SimpleData<Student>()
val list1: ArrayList<out Person> = ArrayList<Student>()
}
2)亡驰、泛型逆變的語法規(guī)則:<in T>
類似于 Java 的 <? super Bound>
,它限定的類型是當(dāng)前下邊界類或者其父類饿幅,如果是接口的話就是當(dāng)前下邊界接口或者其父接口隐解,逆變的泛型變量只能寫,不建議讀
open class Person
class Student: Person()
class Teacher: Person()
class SimpleData<in T>{
}
fun main() {
val person1: Person = Student()
val personGeneric1: SimpleData<Student> = SimpleData<Person>()
val list2: ArrayList<in Person> = ArrayList<Any>()
}
5)诫睬、泛型不變和 Java 語法規(guī)則是一樣的
open class Person
class Student: Person()
class Teacher: Person()
class SimpleData<T>{
}
fun main() {
val person: Person = Student()
//編譯器不允許
val personGeneric: SimpleData<Person> = SimpleData<Student>()
}
6)煞茫、Kotlin 使用 <*> 這種語法結(jié)構(gòu)來表示無界通配符,它等價于 <out Any>
,類似于 Java 中的 <?>续徽,在定義一個類的時候你如果使用<out T : Number>
蚓曼,那么 * 就相當(dāng)于 <out Number>
class KotlinGeneric<out T: Number>{
}
//無界通配符 等價于 <out Any>,但是我這個類限制了泛型邊界為 Number钦扭,因此這里相當(dāng)于 <out Number>
fun main() {
val noBound: KotlinGeneric<*> = KotlinGeneric<Int>()
//根據(jù)協(xié)變規(guī)則 編譯器不允許這樣寫
val noBound: KotlinGeneric<*> = KotlinGeneric<Any>()
}
三纫版、泛型總結(jié)
要學(xué)好 Kotlin 泛型,就要先學(xué)習(xí)好 Java 泛型客情,最后總結(jié)一下這篇文章講的內(nèi)容:
1其弊、回答了一些關(guān)于泛型的問題
2、講解了 Java 泛型膀斋,其中我覺得泛型擦除和泛型的協(xié)變梭伐,逆變和不變是比較難理解的,因此大家可以多花點時間去理解這一塊
3仰担、講解了 Kotlin 泛型糊识,相對于 Java 泛型,Kotlin泛型就是在語法結(jié)構(gòu)上有些不一樣摔蓝,但功能是完全一樣的赂苗,另外 Kotlin 中的泛型實化是 Java 中所沒有的
好了,到這里贮尉,Kotlin 泛型就講完了拌滋,相信你如果從頭看到這里,收獲一定很多猜谚,如果覺得我寫得還不錯败砂,請給我點個贊吧??,如果有任何問題龄毡,歡迎評論區(qū)一起討論
感謝你閱讀這篇文章
全文到此吠卷,原創(chuàng)不易,歡迎點贊沦零,收藏祭隔,評論和轉(zhuǎn)發(fā),你的認可是我創(chuàng)作的動力