Kotlin系列 - 进阶深入泛型协变逆变从java到Kotlin(四)

1,837 阅读11分钟

Kotlin细节文章笔记整理更新进度:
Kotlin系列 - 基础类型结构细节小结(一)
Kotlin系列 - 函数与类相关细节小结(二)
Kotlin系列 - 高阶函数与标准库中的常用函数(三)
Kotlin系列 - 进阶深入泛型协变逆变从java到Kotlin(四)
Koltin系列 - 协程从认识到安卓中的使用(五)

前言

本篇文章从java开始讲泛型,后面再切换到kotlin,重点java的泛型掌握住,koltin的泛型就会很快掌握。(可自行选取节段食用,码字不易看完觉得还可以的,麻烦给赞,本人能力有限,有错误或者有问题在评论区留言,感激~~)

总结

  • 虚拟机没有泛型,只有普通方法和类。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成用于保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。

一、泛型基础

1. 定义

泛型,也称参数化类型。可以使代码应用多种类型。使用类型参数,用尖括号括住,放在类名后面。在使用该类的时候用实际的类型替换该类型参数。 示例:

//这里的参数T可以自由命名
ClassA<T>{}

2. 存在的意义

如果在程序中只能使用具体的类型、具体的基本类型,或者自定义的类,在编写多种类型的代码,这种限制会对代码有很大的约束。我们需要一种在可在运行是才确定类型的一种方法来实现代码更加通用。 示例:

public class PrintClass {
        static  public void printInt(Integer a, Integer b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }

        static   public void printFloat(Float a, Float b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }

        static   public void printDouble(Double a, Double b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }
    }

改成泛型函数:

public class PrintClass1 {
        static  public <T> void printMultiply(T a, T b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }
    }

使用:

    public static void main(String[] args) {
        PrintClass.printDouble(10.0,10.0);
        PrintClass.printInt(10,10);
        PrintClass.printFloat(10f,10f);

        PrintClass1.printMultiply(10.0,10.0);
        PrintClass1.printMultiply(10,10);
        PrintClass1.printMultiply(10f,10f);
        PrintClass1.printMultiply("100","100");
    }
-----------------------打印的Log---------------------------
参数a=10.0参数b=10.0
参数a=10参数b=10
参数a=10.0参数b=10.0

参数a=10.0参数b=10.0
参数a=10参数b=10
参数a=10.0参数b=10.0
参数a=100参数b=100

通过上面的展示,大家对泛型有个最基本的了解。

二、java的泛型使用

1. 接口泛型

  • 接口泛型定义:
interface Animal<T> {
    void name();
    void cry();
    void mysteryData(T t);
}
  • 接口泛型实现一
public class Cat implements Animal<String> {
    @Override
    public void name() {
        System.out.println("猫");
    }
    @Override
    public void cry() {
        System.out.println("喵喵");
    }
    @Override
    public void mysteryData(String s) {
        System.out.println("假设它拥有一种数据类型"+ s.getClass().getName());
    }
}
  • 接口泛型实现二
public class Dog<T> implements Animal<T> {
    @Override
    public void name() {
        System.out.println("狗");
    }
    @Override
    public void cry() {
        System.out.println("汪汪汪");
    }
    @Override
    public void mysteryData(T t) {
        System.out.println("假设它拥有一种数据类型"+t.getClass().getName());
    }
}

使用:

    public static void main(String[] args) {
        Dog<Integer> dog = new Dog();
        dog.name();
        dog.cry();
        dog.mysteryData(10);

        Cat cat =new Cat();
        dog.name();
        cat.cry();
        cat.mysteryData("String");
    }
------------------------------log日志
狗
汪汪汪
假设它拥有一种数据类型java.lang.Integer
狗
喵喵
假设它拥有一种数据类型java.lang.String

2. 类泛型

上面接口泛型实现二中就是用了类泛型的实现了。

3. 方法泛型

public class PrintClass1 {
        static  public <T> void printMultiply(T a, T b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }
    }

三、泛型类型变量的限定

有时候类、方法需要对类型变量进行约束。

例如:增加一个类,用于专门打印各种动物的叫声 (这里泛型虽然可以替换为Animal,但是这个演示案例我就使用泛型替代,这个不是重点)

class AnimalCry{
  public static <T> void cry(T a){
    a.cry();
    }
}

这里你单纯这样子写,它不会有识别到cry这个方法,因为这个方法是Animal接口的CatDog等持有的方法,所以我们要给它个类型变量的限定。

 class AnimalCry{
  public static <T extends Animal> void cry(T a){
    a.cry();
  }
}
--------------------调用
  public static void main(String[] args) {
            AnimalCry.cry(new Dog());
    }
--------------------打印的Log
汪汪汪

格式 <T extends BoundingType> extends后面可以跟接口或者类名T:表示为绑定类型的子类型。 多个限定类型用&进行多个限定,例如 <T extends BoundingType & BoundingType1 & BoundingType2 & ... >

四、泛型类型擦拭

java的虚拟机中没有泛型类型对象,所有的对象都是普通类,无论何时定义一个泛型类型,都会自动体用一个对应的原始类型。原始类型的名字就是擦拭后的类型参数的类型名。如果没有限定的类型变量则用Object代替,如果有限定则以限定的为代替。

public class PrintClass1 {
        public static  <T> void printMultiply(T a, T b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }
    }
--------------编译后为
public class PrintClass1 {
        public static void printMultiply(Object a, Object  b) {
            System.out.println("参数a=" + a + "参数b=" + b);
        }
    }
class AnimalCry{
  public static <T extends Animal> void cry(T a){
    a.cry();
  }
}
--------------编译后为
class AnimalCry{
  public static void cry(Animal a){
    a.cry();
  }
}

五、约束与局限性

大多数的限制都是由类型擦拭带来的。

  1. 不能用基本类型实例化类型参数。 没有Pair<double>只有Pair<Double>,因为泛型擦拭后,Pair类含有Object类型的域,而Object不能存储double值。
  2. 运行时类型查询只适用于原始类型 虚拟机的对象总是一个非泛型类型。所以,所有的类型查询只产生原始类型。
Pair p = new Pair("str","str1");
Pair i = new Pair(10,20);
// illegal generic type for instanceof 无法使用instanceof关键字判断泛型类的类型
 if (p instanceof Pair<String,String>)
//比较结果都是true,因为两次调用都是返回Pair.class
if (p.getClass() == i.getClass()){} 

  1. 不能创建参数化类型的数组
Pair<String,String>[] pairs = new Pair[10];
pairs[0] = new Pair(10,20);
//虽然能赋值但是会导致类型错误。
  1. 不能实例化类型变量或者实例化泛型数组 不能使用类似 new T(...)new T[]T.class等表达式的变量
  2. 泛型类的静态上下文类型变量无效
public class Singleton<T>{
      private static T singleInstance;  //ERROR

      public static T getSingleInstance(){ //ERROR
          if(singleInstance == null) 
              return singleInstance;
      }
}
------------------------------------类型擦除后被替换成Object具体类
public class Singleton{
      private static Object singleInstance; 

      public static Obejct getSingleInstance(){
          if(singleInstance == null) 
              return singleInstance;
      }
}

----------------------------------------------调用的时候
错误,返回Object类型
AType a = Singleton.getSingleInstance(); 
错误,这种用法是不允许的,只能在调用方法或构造方法时传递泛型参数
AType a = Singleton<AType>.getSingleInstance();

----------------------------------但是
如果是 
      public static <T> T getSingleInstance(){ //YES 静态方法可以声明泛型参数
          if(singleInstance == null) 
              return singleInstance;
      }
  1. 不能继承Exception或者Throwable,不能抛出或捕获泛型类的实例,但可以将泛型限定的异常抛出
public class Pair<T,Q> extends Exception{} // 报错,不能继承Exception或者Throwable
public static <T extends Throwable> void doWork(Class<T> t){
  try{
  ....
  }catch(T e){ //报错 这里不能抛出泛型类型
  }
}
//正确。
public static <T extends Throwable> void doWork(T t) throws T{
  try{
  ....
  }catch(Throwable t){ 
  }
}

六、泛型类型继承规则

  1. 两个泛型参数是继承关系,但是对应的两个泛型没有一点关系!
interface Animal{}
public class Cat extends Animal{}
public class Dog extends Animal{}
public class Cry<T>{}
------------------------------------------------
Cry<Animal> 与 Cry<Cat> 不是继承关系,也没有什么关系。
  1. 泛型类可以扩展或实现其他的泛型类
public class ArrayList<E> extends AbstractList<E>

七、通配符类型(重点!!!)

小结:

协变: <? extends Class> 指定泛型类型的上限,只能读取不能修改(修改是指对泛型集合添加元素,如果是 remove(int index)以及 clear当然是可以的)

逆变: <? super Class> 指定泛型类型的下线,只能修改不能读取,(不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转也可以)

<?> 相当于< ? extends Object>指定没有限制的泛型类型

Demo图

以上面的图为例子:

1. 协变: <? extends Class> 指定泛型类型的上限

它的限定范围为上限本身(上限可以是接口)以及所有直接跟间接的子类。

  • 应用场景
Animal animal = new Dog();//  java的多态
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = dogs; //这里会报错 incompatible types: List<Dog> cannot be converted to List<Animal>

上面的例子因为发生了类型擦拭,为了保证类型安全所以不允许这样子赋值。 这个时候就可以使用协变的写法<? extends Class>限制参数类型的上界,也就是泛型类型必须满足这个 extends 的限制条件。

List<? extends Animal> animals = new ArrayList<Animal>(); // 本身
List<? extends Animal> animals = new ArrayList<Cat>(); //  直接子类
List<? extends Animal> animals = new ArrayList<ShamoDog>(); //  间接子类
  • 限制

只能够向外提供数据被消费,类似生产者

List<? extends Animal> animals = new ArrayList<Dog>();
Animal animal= animals.get(0); //get 出来的是 Animal 类型的
animals.add(textView);//报错,no suitable method found for add(TextView)

2. 逆变: <? super Class> 指定泛型类型的下限

它的限定范围为下限本身(下限可以是接口)以及所有直接跟间接的父类。

  • 应用场景
List<? super ShamoDog> shamoDogs= new ArrayList<shamoDogs>(); // 本身
List<? super ShamoDog> shamoDogs= new ArrayList<WailaiDog>();//直接接父类
List<? super ShamoDog> shamoDogs= new ArrayList<Animal>();//间接父类
  • 限制

只能读取到Object对象,通常也只拿它来添加数据,也就是消费已有的 List<? super ShamoDog>,往里面添加 Dog,因此这种泛型类型声明相对协变 可以称为消费者

List<? super ShamoDog> shamoDogs = new ArrayList<Animal>();
Object object = shamoDogs.get(0); // get 出来的是 Object 类型
Dog dog = ...
shamoDogs.add(dog); // add 操作是可以的

八、Kotlin的泛型

1. 格式

java泛型一样的格式

interface AnimalKot<T> { } //接口泛型
class DogKot<Q> { } //类泛型
fun <T>TestLooperManager(t:T): Unit { }//方法泛型

2. 关键字outin*where

  • out:协变、与java的上限通配符<? extends BoundType>对应
  • in:逆变,与java的下限通配符<? super BoundType>对应
  • *: 与 java<?>,不过java的是<? extends Object>,kotlin的是<out Any>
  • where : 与java<T extends Animal & Person >&符号对应
//where的示例
//java中多个限定泛型定义
public class Cry <T extends Animal & Person>{ }
//kotlin的对应写法
class Cry<T> where T:Animal,T:Person{ }

重点: kotlin提供另外一种附加功能,在声明类的时候,给泛型类型加上in关键字,表明泛型参数 T 只会用来输入,在使用的时候就不用额外加 in 。对应out,则是表明泛型参数T只会用来输出,使用时不需要额外加out

例子

//koltin的 List
public interface List<out E> : Collection<E> {
}

var animalKot:List<Animal<String>> = ArrayList<Dog<String>>()// 不报错
var animalKot:List<out Animal<String>> = ArrayList<Dog<String>>()//写了out,不报错
var animalKot:List<in Dog<String>> = ArrayList<Animal<String>>()//报错,不能写in

//定义一个 in泛型类型
class All<in T>{
    fun p(t:T){
    }
}

var all:All<Dog<String>> = All()// 不报错
var all:All<in Dog<String>> = All()//写了in,不报错
var all:All<out Dog<String>> = All()//报错,不能写in

3. 关键字reified

关于java泛型存在擦拭的情况下,在上面五、约束性与局限性中第二点中提到的 运行时类型查询只适用于原始类型

<T> void println(Object obj) {
    if (obj instanceof T) { // IDE提示错误,illegal generic type for instanceof
    }
}
kotlin也是如此---------------------------------------------------------
fun <T> println(any: Any) {
    if (any is T) { // IDE提示错误,Cannot check for instance of erased type: T
    }
}

java的解决方法:额外传递一个 Class<T>类型的参数,然后通过 Class#isInstance 方法来检查

<T> void println(Object obj, Class<T> type) {
    if (type.isInstance(obj )) { 
    }
}

Kotlin的解决方法,就是reified关键字,但是 reified只能在inline表示的方法中使用,所以,要使用inline方法。

inline fun <reified T> println(any: Any) {
    if (any is T) {
        println(item)
    }
}

inline内联函数,当方法在编译时会拆解方法的调用为语句的调用,进而减少创建不必要的对象。在kotlin中一个inline可以被具体化reified,这意味着我们可以得到使用泛型类型的Class

  • 项目中使用的自定义扩展Gson
//定义`Gson`的扩展函数
inline fun <reified T> Gson.fromJson(json:String):T = fromJson(json,T::class.java)

4. 注解 @UnsafeVariance

public interface List<out E> : Collection<E> {
    // Query Operations

    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    // Bulk Operations
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    ....

上面是kotlinList源码,可以看到定义类泛型限定的时候为<out E>为协变,只用来输出,但是这里面的方法override fun contains(element: @UnsafeVariance E): Boolean为了支持输入,则使用@UnsafeVariance避免IDE的检查

5. Kotlin 泛型与 Java 泛型不一致的地方

  • Java 里的数组是支持协变的,而 Kotlin 中的数组 Array 不支持协变。 Kotlin中数组是用 Array 类来表示的,这个 Array 类使用泛型就和集合类一样,所以不支持协变。

  • Java 中的 List 接口不支持协变,而 Kotlin 中的 List 接口支持协变。 在 Kotlin 中,实际上 MutableList 接口才相当于 JavaListKotlin 中的 List 接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。

感谢

Kotlin 的泛型
Java核心技术 卷一