💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
in和out是一个对立面,其中**in代表泛型参数类型逆变,out代表泛型参数类型协变**。从字面意思上也可以理解,**in代表着输入,而out代表着输出。但同时它们又与泛型不变相对立,统称为型变,而且它们可以用不同方式使用**。比如: ``` public interface List<out E> : Collection<E> {} ``` 这种方式是在声明处型变,另外还可以在使用处型变,比如上面例子中sortWith方法。在了解了泛型变形的原理后,我们来看一下泛型变形到底在什么地方发挥了它最大的用处。 假设现在有个场景,需要将数据从一个Double数组拷贝到另一个Double数组,我们该怎么实现呢? 一开始我们可能会这么做: ``` fun copy(dest: Array<Double>, src: Array<Double>) { if (dest.size < src.size) { throw IndexOutOfBoundsException() } else { src.forEachIndexed{index, value -> dest[index] = src[index]} } } var dest = arrayOfNulls<Double>(3) val src = arrayOf<Double>(1.0,2.0,3.0) copy(dest, src) ``` 这很直观也很简单,但是学过泛型后的你一定不会这么做了,**因为假如替换成Int类型的列表,是不是又得写一个copy方法**?所以我们可以对其进一步抽象: ``` fun <T> copy(dest: Array<T>, src: Array<T>) { if (dest.size < src.size) { throw IndexOutOfBoundsException() } else { src.forEachIndexed{index, value -> dest[index] = src[index]} } } var destDouble = arrayOfNulls<Double>(3) val srcDouble = arrayOf<Double>(1.0,2.0,3.0) copy(destDouble, srcDouble) var destInt = arrayOfNulls<Int>(3) val srcInt = arrayOf<Int>(1,2,3) copy(destInt, srcInt) ``` **通过实现一个泛型的copy,可以支持任意类型的List拷贝**。那么这种方式有没有什么局限呢?我们发现,**使用copy方法必须是同一种类型,那么假如我们想把`Array<Double>`拷贝到`Array<Number>`中将不允许,这时候我们就可以利用上面所说的泛型变形了**。这种场景下是用协变还是逆变呢? ``` //in版本 fun <T> copyIn(dest: Array<in T>, src: Array<T>) { if (dest.size < src.size) { throw IndexOutOfBoundsException() } else { src.forEachIndexed{index, value -> dest[index] = src[index]} } } //out版本 fun <T> copyOut(dest: Array<T>, src: Array<out T>) { if (dest.size < src.size) { throw IndexOutOfBoundsException() } else { src.forEachIndexed{index, value -> dest[index] = src[index]} } } var dest = arrayOfNulls<Number>(3) val src = arrayOf<Double>(1.0,2.0,3.0) copyIn(dest, src) //允许 copyOut(dest, src) //允许 ``` 到这里你可能迷糊了,为什么两种方式都允许?其实细看便能发现不同,**in是声明在dest数组上,而out是声明在src数组上,所以dest可以接收T类型的父类型的Array,out可以接收T类型的子类型的Array**。当然这里的T要到编译的时候才能确定。比如: * in版本,T是Double类型,所以dest可以接收Double类型的父类型Array,比如`Array<Number>`。 * out版本,T是Number类型,所以src可以接收Number类型的子类型Array,比如`Array<Double>`。 所以in和out的使用是非常灵活的。当然上面我们也提到了使用了它们就会有相应的限制,这里我们就对泛型参数类型不同情况的特性及实现方式进行一下总结,如表所示。 :-: 表Kotlin与Java的型变比较 ![](https://img.kancloud.cn/f3/08/f308f952981728ed77c0416d07589c25_533x94.png) 前面我们所说的泛型变形或者不变都是在一种前提下的讨论,那就是你需要知道泛型参数是什么类型或者哪一类类型,比如它是String或者是Number及其子类型的。**如果你对泛型参数的类型不感兴趣,那么你可以使用类型通配符来代替泛型参数。前面已经接触过Java中的泛型类型通配符“? ”,而在Kotlin中则用“*”来表示类型通配符**。比如: ``` val list: MutableList<*> = mutableListOf(1, "kotlin") list.add(2.0) //出错 ``` 这个列表竟然不能添加,不是说好是通配吗?按道理应该可以添加任意元素。**其实不然,`MutableList<*>`与`MutableList<Any? >`不是同一种列表,后者可以添加任意元素,而前者只是通配某一种类型,但是编译器却不知道这是一种什么类型,所以它不允许向这个列表中添加元素,因为这样会导致类型不安全**。不过细心的读者应该发现前面所说的**协变也是不能添加元素**,那么它们**两者之间有什么关系呢?其实通配符只是一种语法糖,背后上也是用协变来实现的。所以`MutableList<*>`本质上就是`MutableList<out Any? >`,使用通配符与协变有着一样的特性**。 当前泛型变形的另一大用处是体现于高阶函数,比如Java 8中新增的Stream中就有其应用: ``` <R> Stream<R> map(Function<? super T, ? extends R> mapper); ``` 另外,**在责任链模式也会涉及高阶函数对于泛型变形的应用**。