本文主要是介绍类比学习——java 泛型 kotlin 泛型中的 in out where,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
在学习 kotlin 泛型的时候,经常会遇到 in out 这两个词,一会用in 一会用out,为啥这里要用 out ?为啥哪里用 in ?啥什么用 out 啥时候用in ?对应上面问题以前我是晕乎乎的,不是很明白,于是打算写这篇文章梳理一下,搞清楚怎么回事。
Java 泛型
在学kotlin 泛型之前,先回顾一下Java中的泛型
为了方便说明引入下面几个类
具体代码
public class Animal { }public class Dog extends Animal{ }public class Cat extends Animal{ }public class Corgi extends Dog { }public class Result<T>{private T data;public Result() { }public Result(T data) { this.data = data; }public void setData(T data) { this.data = data; }public T getData() { return data; }
}
1.不可型变
Result<Dog> dogResult = new Result<>();
Result< Animal> animalResult = dogResult; // 编译错误
虽然 Dog 是 Animal 的子类,但是Java 泛型是不可以型变的,Result<Dog>
对象不能赋值给 Result<Animal>
, 他们之间没有关系。
如果 Java 泛型不是这样设计的就容易造成运行时异常,例如
Result<Dog> dogResult = new Result<>();Result<Animal> animalResult = dogResult; // 编译器错误 ❌
//假设👆上面这句代码可以编译通过
//那么我们就可以调用set方法设置一个Animal对象animalResult.setData(new Animal());
//但是我们用dogResult的get方法取的时候,本以为是Dog 但实际是Animal 这样就会出现类型转换异常 ClassCastExceptionDog dog = dogResult.getData();
所以 Java 这样设计是为了安全考虑。
为了安全,这样的限制显然会失去一些API的灵活性。于是 Java 提供有限制通配符 ? extends X
、? super X
、<?>
来提升API 的灵活性。
2.型变性通配符 —— ? extends
Result<Dog> dogResult = new Result<>(new Dog());//泛型类型可以是Animal 或者Animal的子类
Result<? extends Animal> animalResult = dogResult; // 编译通过
// animalResult=new Result<Object>(new Object());//父类不行,编译错误 ❌//Result<? extends Animal> 修改data数据,保证了数据的安全性,不会让dogResult.getData()发生数据转换异常
// animalResult.setData(new Animal());// 编译错误//Result<? extends Animal> 泛型限制保证 animalResult.getData()一定是Animal对象,
// 所以可以通过animalResult获取数据是没问题的Animal animal = animalResult.getData();
<? extends X>
可以表示泛型是 X 也可以是 X 的子类,所以上面代码中 Result<? extends Animal> animalResult = dogResult;
是可以的,但是为了安全考虑,animalResult
只可以读数据,不可用写入数据,防止出现类型异常。
<? extends X>
可以安全的使用读取数据(返回值为 X 的函数),但不能写入数据(参数为 X 的函数)因为参数需要的是X 还是 X 的那个子类不确定,就像上面的例子 animalResult
指向的类型是 Result 所以setData 应该放入一个Dog对象,但是 animalResult
也可以指向一个 Result<Cat>
对象,我们只能确定 animalResult
指向的对象泛型是Animal 或者是它的子类,但无法确认它到底是具体哪一个类型(是 Dog 还是Cat 还是……),所以setData的时候我们无法确认到底放那个对象。为了安全考虑,写入数据在这种通配符的情况是不被允许的。
总结 ? extends 通配符
- 可以协变,如上 Cat、Dog、Corgi 都是 Animal 的子类,所以
Result<Cat>
、Result<Dog>
、Result<Corgi>
都可以用Result<? extends Animal>
表示。? extends 通配符 限定了上界,泛型的类型可以是 Animal 或它的子类,但不能超过它,即不能是它的父类。 - 此通配符,只可以读不可用写,这种对象通常称为消费者。
3.逆变型通配符 —— ?super
Result<Dog> dogResult = new Result<>(new Dog());Result<Object> objResult = new Result<>(new Object());//泛型类型可以是Animal 或者Animal的父类Result<? super Animal> animalResult = objResult; // 编译通过// 编译错误,Dog是Animal的子类
// animalResult=dogResult ❌//可以写,使用set方法animalResult.setData(new Animal());//如果读的话,返回值是Object,无法确认具体类型Object data = animalResult.getData();
<? super X>
可以表示泛型是 X 也可以是 X 的父类,所以上面代码 Result<? super Animal> animalResult = objResult;
是可以的,但 animalResult=dogResult
不可用。此通配符限制了泛型是 X 也可以是 X 的父类,所以通配符,是可以安全的写入数据的(参数为 X 的函数) 但传入的类型必须是 X 或者他的子类,因为 ? super X
可以保证泛型是X 或它的父类,根据类的多态特性,可以使用子类代替父类。如果读的话,无法确认具体的类型,因为只知道是 X 或 它的父类,但具体那个不知道,所有返回的类型是他们顶层父类 Object。
总结 ? super 通配符
- 可以逆变 。? super 通配符 限定了下界,泛型的类型可以是 Animal 或它的父类,但不能低于它,即不能是它的子类。
- 此通配符,可以写,但读无法确定类型,这种对象通常称为生产者。
4.使用通配符限定参数
? extends X 作为参数
例如 Java 中集合框架中的 addAll 方法
public interface Collection<E> extends Iterable<E> {boolean addAll(Collection<? extends E> items);
}-----------------------分割线-------------------------//假设声明一个ArrayList<Animal> 那么它的元素可以是Animal 或者它的子类
ArrayList<Animal> list = new ArrayList<>();
// 0️⃣
list.addAll(new ArrayList<Dog>());
list.addAll(new ArrayList<Cat>());
// 1️⃣
// list.addAll(new ArrayList<Object>()); 编译错误❌
声明的集合类型是 ArrayList 所以此时 addAll 的形参类型相当于是 Collection<? extends Animal>
这样就限定了集合泛型必须是 Animal 或者它的子类,于是就保证了通过addAll 方式添加到 list 集合中的元素一定是 Animal 或者它的子类的对象,保证了数据的正确性。
? super X 作为参数
public void forEach(Consumer<? super E> action) {……for(int i = 0; this.modCount == expectedModCount && i < size; ++i) {//? super E 通配符可以调用写入方法(参数有 E 的函数)action.accept(elementAt(es, i));}……
}-----------------------分割线-------------------------ArrayList<Animal> list = new ArrayList<>();
// 0️⃣ list.forEach(new Consumer<Animal>() {@Overridepublic void accept(Animal data) {System.out.println(data);}});// 0️⃣ list.forEach(new Consumer<Object>() {@Overridepublic void accept(Object data) {System.out.println(data);}});
在上面代码中 forEach 的形参类型是 Consumer<? super Animal>
, 所以泛型可以值 Animal 或者它的父类,所以不管是 Consumer<Animal>
还是 Consumer<Object>
都是可以的。
kotlin 泛型
kotlin 泛型和 Java 类似,虽然没有 ? extends 和 ?super 这样的通配符 ,但是有类似功能的修饰符 in out
可以简单理解 ?extends 对应 out,? super 对应 in
1.不可型变
val dogResult = Result<Dog>()
val animalResult: Result<Animal> = dogResult // 编译错误
和 Java 一致
2.型变修饰符 —— out
val dogResult = Result(Dog())//泛型类型可以是Animal 或者Animal的子类val animalResult: Result<out Animal> = dogResult // 编译通过
// animalResult=new Result<Object>(new Object())//父类不行,编译错误 ❌// animalResult.data=Animal()// 编译错误val animal = animalResult.data
和 Java ?extends 一致,具有型变行,可读不可写
3.型变修饰符 —— int
val dogResult = Result(Dog())val objResult = Result<Any>(Any())//泛型类型可以是Animal 或者Animal的父类val animalResult: Result<in Animal> = objResult // 编译通过// 编译错误,Dog是Animal的子类// animalResult=dogResult ❌//可以写,使用set方法animalResult.data = Animal()//如果读的话,返回值是Any?,无法确认具体类型val data:Any? = animalResult.data
和 Java ?super 一致,具有可逆变性,可写不可读
Kotlin 泛型之 —— 声明处型变
在Java 泛型中我们列举了使用通配符限定参数的方式,看了 java.util.ArrayList
的Api 是如何利用泛型通配符提升Api灵活性的同时,保证数据安全的。 kotlin 也可以按照 ?extends 替换 out,? super 替换 in 的方式限定参数也是可以达到和Java一样的效果。但是如果你看 kotlin.collections.ArrayList
的addAll 的方法并不是我们想的那样。java.util.ArrayList
的 addAll 方法
public boolean addAll(Collection<? extends E> item)
按照上面的分析,我们想的 kotlin.collections.ArrayList
的addAll 大概是这样的
fun addAll(elements: Collection<out E>): Boolean
但实际源码是这样写的
override fun addAll(elements: Collection<E>): Boolean
参数类型是 Collection<E>
而不是 Collection<out E>
虽然没有 out 但参数仍然可以起到限制作用。
val list = mutableListOf<Animal>() //public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()list.addAll(mutableListOf<Dog>())
// list.addAll(mutableListOf<Any>()) 编译错误
// list.addAll(mutableListOf<String>()) 编译错误
查看 Collection 你会发现它在定义的时候加上了out
public interface Collection<out E> : Iterable<E> {public val size: Int……
}
这种在类或接口定义处指定 out ,称之为**声明处型变,**这样声明的接口或类中,只能提供读的方法,不能提供写入的方法,此类型我们打算让他变成具有型变性的。例如我把上面的 Result 改成声明处型变
//原版 java to kotlin
class Result<T>(var data: T? = null)
我们加上out 发现报错了,因为 out 只读不能写,所以需要吧var 改成 val
这样我们在使用Result 就是协变的了
val dogResult:Result<Dog> = Result(Dog())//默认就是协变的了,所以 Result<Dog> 对象赋值给 Result<Animal> 类型变量val animalResult: Result<Animal> = dogResult // 编译通过val animal = animalResult.data
声明类型的时候 out 就可以不用写了,你要写了人家还提示你多余
说完 out,in 也是同理,我们也可以在类或接口上指定
class Result<in T>{private var data: T? = nullfun setData(data: T?){ this.data=data }编译报错,只写,不能读// fun getData(): T? =data
}
这样我们在使用Result 就是逆变性的
val objResult: Result<Any> = Result()val animalResult: Result< Animal> = objResult // 编译通过// 编译错误,Dog是Animal的子类// animalResult=Result<Dog>() //❌//可以写,使用set方法animalResult.setData(Animal())
kotlin 中的where
看kotlin的wherect之前,还是看看Java中类似的语法
泛型函数
//定义 在方法返回类型前用 <T> 声明泛型
public static <T> void test(T data) { }-----------------------分割线-------------------------
//使用 也放任意类型
test("");
test(new Animal());
加限制的泛型函数
//定义
public static <T extends Animal> void test(T data) { }
-----------------------分割线-------------------------
//使用 只可以传入 Animal或它的子类
test(new Dog());
test(new Animal());
加多条限制的泛型函数
//定义 多个限定用 & 连接 只能有一个类且必须放在首位,可以有多个接口
public static <T extends Animal & Serializable & Closeable> void test(T data) {}static class Data extends Animal implements Serializable,Closeable{@Overridepublic void close() throws IOException { }}public static void main(String[] args) {//接收的泛型必须是继承Animal 实现 Serializable,Closeable接口的类型test(new Data());
// test(Animal());//编译错误}
Kotlin 中的 where 就是用来实现 Java 中 多条限制的泛型函数
fun <T> test(data: T) where T : Serializable, T : Animal, T : Closeable {}class Data : Animal(), Serializable, Closeable {override fun close() {}
}fun main() {test(Data())
// test(Animal())//编译错误
}
参考文档
泛型:in、out、where - Kotlin 语言中文站
这篇关于类比学习——java 泛型 kotlin 泛型中的 in out where的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!