简单回顾Java泛型 泛型是什么Kotlin语言中文站
Java
在JDK5
中引入了泛型机制。 它的意思可以理解为把具体的类型 参数化,编码时用符号代替类型,实际使用的时候再传入确定的类型,可以用在类、接口或者方法上面。
那么泛型的作用是什么呢?先看一段代码。
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。
- 上界通配符:
extends FlyAnimal>
, 泛型类型就只能是FlyAnimal
的子类,比如Bird
。 - 下界通配符:
super Bird>
,泛型类型就只能是Bird
或Bird
的父类,比如FlyAnimal / Animal / Object
。 - 无界通配符:
>, extends 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
赋值不成立。
但需求总是有的,怎么使List
赋值成立呢? 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):
Bird
是FlyAnimal
子类,同时满足条件List
是List
的子类时,称为协变。Java
使用上界通配符 如List extends T>
表示协变。 由于协变,我们可以成功的把List
赋值给List
。 -
逆变(contravariant):
Bird
是FlyAnimal
子类,同时满足条件List
是List
的子类时,称为逆变。 -
不变(invariant):
Bird
是FlyAnimal
子类, 协变、逆变都不成立,即List
与List
相互之间没有继承关系,称为不变。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 extends FlyAnimal>
由于类型未知,可能是Bird
,也可能是Butterfly
等等。- 如果是类型是
Bird
,显然我们如果执行add(new Butterfly());
是不可以的,因为违反了类型安全的原则。 - 这样一来,编译器根本没法确定到底是什么类型,就报错了。
所以编译器为了保证类型安全,是不能向List extends FlyAnimal>
中添加任何类型元素。
那么逆变呢?道理也差不多,我们同样举个例子:
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
对象的。 但是取出的元素就无法确定类型了,有可能是Bird
、FlyAnimal
、Animal
、Object
,所以编译器不允许这样的事情发生,就报错了。
小结
根据上面的知识点回顾,小结如下:
- Java的泛型是不变的(不支持协变和逆变)。
- 可使用上界通配符
extends T>
使泛型支持协变。由于上界通配符的限制,我们仅能对集合进行取元素,而不能添加元素——只能读取不能修改。 - 可使用下界通配符
super T>
使泛型支持逆变。由于下界通配符的限制,我们仅能对集合添加元素,而不能读取元素(备注说明下:不能读取元素,是说不能读取泛型类型的元素,你用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
}
声明处型变
out
、in
与Java
泛型一样,Kotlin
泛型也是不型变的。out/in
修饰符称为型变注解,一般在声明类型参数的地方使用,所以也称为声明处型变。(Java是使用处型变,也就是在使用的地方用类型通配符进行型变)它们的作用如下:
- 使用修饰符
out
来支持协变,类似于java
中的上界通配符extends T>
。 - 使用修饰符
in
来支持逆变,类似Java
中的下界通配符super T>
。
具体使用如下:
/**
* 泛型类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
中的无界通配符>
。>
是 extends Object>
的简写,在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
备注说明下,
是泛型类型边界, extends Animal>
是类型通配符,是两个不同的概念,注意区分。
上一篇:Kotlin学习历程——扩展
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)