Kotlin学习历程——泛型

Kotlin学习历程——泛型,第1张

Kotlin语言中文站

简单回顾Java泛型 泛型是什么

JavaJDK5中引入了泛型机制。 它的意思可以理解为把具体的类型 参数化,编码时用符号代替类型,实际使用的时候再传入确定的类型,可以用在类、接口或者方法上面。

那么泛型的作用是什么呢?先看一段代码。

public final class Main {
    public static void main(String[] args) {
        /**
         * 案例场景
         *
         * 同事1:
         * 定义了一个集合,想着是用来存放字符串数据的
         */
        List dataList = new ArrayList();
        dataList.add("a");
        dataList.add("b");

        /**
         * 迭代无数次,此处省略无数代码
         */

        /**
         * 同事1离职,同事2接锅
         * 由于对业务逻辑的生疏,没有理清代码逻辑,贸然存入了其他类型的数据
         */
        dataList.add(888);


        /**
         * 同时又理所当然的取用数据,
         * 好了,完了,错误就出现了:
         *	 Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
         * 		at Main.main(Main.java:**)
         */
        int data0 = (int) dataList.get(0);
        int data1 = (int) dataList.get(1);
        int data2 = (int) dataList.get(2);
    }
}

从上面的例子可以知道,List集合并没有指定存储的数据类型,这种情况下默认可以添加任意类型的数据,编译器不会做类型检查, 这种做法在取数据的时候就很容易出现ClassCastException异常错误。而泛型的出现,就解决了这种类型安全的问题。

我们用泛型优化下上面的代码:

public final class Main {
    public static void main(String[] args) {
        /**
         * 案例场景
         *
         * 同事1:
         * 定义了一个集合,想着是用来存放字符串数据的
         */
        //定义一个集合,泛型类型是String
        List<String> dataList = new ArrayList();
        dataList.add("a");
        dataList.add("b");

        /**
         * 迭代无数次,此处省略无数代码
         */

        /**
         * 同事1离职,同事2接锅
         * 由于对业务逻辑的生疏,没有理清代码逻辑,贸然存入了其他类型的数据
         *
         * 类型检查错误:error,无法编译通过
         */
        dataList.add(888);

        /**
         * 不需要强转
         */
        String data = dataList.get(0);
    }
}

强行存入其他数据,编译器类型检查报错:

类型通配符

父类:Animal, 直接子类:FlyAnimal, 间接子类:Bird。

  • 上界通配符:, 泛型类型就只能是FlyAnimal的子类,比如Bird
  • 下界通配符:,泛型类型就只能是BirdBird的父类,比如FlyAnimal / Animal / Object
  • 无界通配符:的简写 任意类型。

类型通配符有什么作用?先来看个代码场景:

public final class Main {
    public static void main(String[] args) {
        //Java多态
        FlyAnimal flyAnimal = new Bird();

        //创建Bird的一个集合
        List<Bird> birds = new ArrayList<>();
        //赋值给FlyAnimal集合: Error:Java本身不可型变
        List<FlyAnimal> flyAnimals = birds;
        //错误: 不兼容的类型: List无法转换为List, List flyAnimals = birds;
    }
}

如上代码所示,Bird继承自FlyAnimal,由于Java的多态,赋值是成立的。 但由于Java中的泛型是不型变的,也就意味着List并不是List的子类型, 所以List flyAnimals = birds赋值不成立。

但需求总是有的,怎么使List flyAnimals = birds赋值成立呢? Java提供了类型通配符来解决这个问题。

public final class Main {
    public static void main(String[] args) {
        //Java多态
        FlyAnimal flyAnimal = new Bird();

        List<Bird> birds = new ArrayList<>();
        //正常赋值,不会发生错误。 使Java泛型具有了协变性
        List<? extends FlyAnimal> flyAnimals = birds;
    }
}

所以类型通配符的一大作用就是突破Java泛型是不型变的限制。


疑问

结合上面的知识点,我们或许会产生如下疑问🤔️?

  • Java泛型为什么不型变?
  • 型变、协变、逆变、不变是什么?
  • 类型通配符为什么可以保证类型安全?

Java泛型为什么 不型变?

我们先看个例子:

public final class Main {
    public static void main(String[] args) {
        List<Bird> birds = new ArrayList<>();
        //如果Java泛型不是 不型变,那么由于Java多态的特征,此赋值就会成立
        List<FlyAnimal> flyAnimals = birds;
    }
}

如上,我们创建一个泛型类型是Bird的集合,如果泛型不是 不型变,List就可以赋值给List,那么我们就可以往集合中添加其他泛型类型的元素,比如Butterfly,那么就违反了泛型类型安全的原则:我明明限制了只能存入Bird,你却存入了Butterfly! 所以Java禁止这样的事情发生!因此Java泛型是不型变的。


型变、协变、逆变、不变是什么?

型变分为协变和逆变,与不变对应,用来描述泛型类型转换后的继承关系。

  • 协变(covariant): BirdFlyAnimal子类,同时满足条件ListList的子类时,称为协变。Java使用上界通配符 如List表示协变。 由于协变,我们可以成功的把List赋值给List

  • 逆变(contravariant):BirdFlyAnimal子类,同时满足条件ListList的子类时,称为逆变。

  • 不变(invariant):BirdFlyAnimal子类, 协变、逆变都不成立,即ListList相互之间没有继承关系,称为不变。 Java中的泛型是不变的。

类型通配符为什么可以保证类型安全?

我们用协变举个例子:

public final class Main {
    public static void main(String[] args) {
        List<Bird> birds = new ArrayList<>();
        /**
         * 协变
         */
        List<? extends FlyAnimal> flyAnimals = birds;

        //!!!报错Error?????????????
        flyAnimals.add(new Bird());

        //get出来的对象肯定是FlyAnimal的子类,根据多态,是可以赋值给FlyAnimal的。
        if (flyAnimals.size() > 0) {
            FlyAnimal flyAnimal = flyAnimals.get(0);
        }
    }
}

如上,add() *** 作是不被编译器允许的,以此保证了运行时类型安全,报错原因我们可以这么理解:

  • List由于类型未知,可能是Bird,也可能是Butterfly等等。
  • 如果是类型是Bird,显然我们如果执行add(new Butterfly());是不可以的,因为违反了类型安全的原则。
  • 这样一来,编译器根本没法确定到底是什么类型,就报错了。

所以编译器为了保证类型安全,是不能向List中添加任何类型元素。

那么逆变呢?道理也差不多,我们同样举个例子:

public final class Main {
    public static void main(String[] args) {
        List<FlyAnimal> flyAnimals = new ArrayList<>();
        /**
         * 逆变
         */
        List<? super Bird> birds = flyAnimals;

        //这里不能add(new FlyAnimal());如果?是Bird, 就违背类型安全原则了
        birds.add(new Bird());

        //Error:编译器不知道取出的元素到底是什么类型
        if (birds.size() > 0) {
            FlyAnimal flyAnimal = birds.get(0);
        }
    }
}

Bird对象一定是这个未知类型的子类,根据多态的特性,是可以添加Bird对象的。 但是取出的元素就无法确定类型了,有可能是BirdFlyAnimalAnimalObject,所以编译器不允许这样的事情发生,就报错了。


小结

根据上面的知识点回顾,小结如下:

  • Java的泛型是不变的(不支持协变和逆变)。
  • 可使用上界通配符使泛型支持协变。由于上界通配符的限制,我们仅能对集合进行取元素,而不能添加元素——只能读取不能修改。
  • 可使用下界通配符使泛型支持逆变。由于下界通配符的限制,我们仅能对集合添加元素,而不能读取元素(备注说明下:不能读取元素,是说不能读取泛型类型的元素,你用Object接收当然没问题)——只能修改不能读取。

上面代码中我们都是用List来举例,我们自己定义个泛型类来加深对 只能读取不能修改 / 只能修改不能读取的理解:

/**
 * 泛型类GenericA
 * @param 
 */ 
public class GenericA<T> {
    private T mType;

    public T getType() {
        return mType;
    }
    
    public void setType(T type) {
        this.mType = type;
    }
}


public final class Main {
    public static void main(String[] args) {
        /**
         * 协变(只能读取不能修改)
         */
        GenericA<Bird> genericBird = new GenericA<>();
        genericBird.setType(new Bird());

        GenericA<? extends FlyAnimal> genericA = genericBird;
        //!!!Error: 违背类型安全原则,不能赋值
        genericA.setType(new Bird());
        //getType返回的类型肯定是FlyAnimal的子类,根据多态,是可以赋值给FlyAnimal的
        FlyAnimal flyAnimal = genericA.getType(); 
        
    
        /**
         * 逆变(只能修改不能读取)
         */
        GenericA<? super Bird> genericB = new GenericA<FlyAnimal>();
        //只能添加Bird
        genericB.setType(new Bird());
        //Error: 编译器无法确定取出的类型。 当然你如果用Object接收,那也是可以的,但没有意义。
        //FlyAnimal bird = genericB.getType();
        Object o = genericB.getType();
    }
}

Kotlin泛型

Kotlin中泛型类,接口,方法的写法和Java没啥区别,如下所示:

/**
 * 泛型类
 */
class Shape<T>(var data : T)

/**
 * 泛型接口
 */
interface DownloadLister<T> {}


/**
 * 泛型方法
 */
fun <T> getFirstElement(list : List<T>) : T? {
    if(list.isNotEmpty()) {
        return list[0]
    }
    return null
}

声明处型变outin

Java泛型一样,Kotlin泛型也是不型变的。out/in修饰符称为型变注解,一般在声明类型参数的地方使用,所以也称为声明处型变。(Java是使用处型变,也就是在使用的地方用类型通配符进行型变)它们的作用如下:

  • 使用修饰符out来支持协变,类似于java中的上界通配符
  • 使用修饰符in来支持逆变,类似Java中的下界通配符

具体使用如下:

/**
 * 泛型类GenericA(泛型类型T, 具有协变性)
 * 变量data只能用val修饰,因为out修饰,只能读取不能修改
 */
class GenericA<out T>(val data: T)
fun main(args: Array<String>) {
    val genericA : GenericA<Bird> = GenericA(Bird())
    //协变
    val genericB : GenericA<FlyAnimal> = genericA
}

感觉就是换了个写法,作用是一样的,out表示类型变量只用来读取,不能修改;in表示只用来修改,不能读取。

class GenericA<in T> {
    fun setData(data : T) {
    }
}

那就有同学说了,我硬是要读取呢? 好吧,编译器是不会“放过”你的,直接报错,如下图:


星投影(*)

Kotlin<*>相当于java中的无界通配符的简写,在Kotlin中<*>的简写。

fun main(args: Array<String>) {
    val genericA : GenericA<Bird> = GenericA(Bird())
    //协变: Any是所有类的超类,所以协变成立
    val genericB : GenericA<*> = genericA
}

where关键字

Java中我们给泛型类型加边界的写法如下:

/**
 * entends关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口
 * @param 
 */
public class GenericB<T extends Animal & AnimalAction>{
}

Kotlin中就得换种写法了:

class GenericA<T> where T : Animal, T : AnimalAction

备注说明下,是泛型类型边界,是类型通配符,是两个不同的概念,注意区分。


上一篇:Kotlin学习历程——扩展

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/786211.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-05
下一篇 2022-05-05

发表评论

登录后才能评论

评论列表(0条)

保存