简单回顾Java泛型 泛型是什么Kotlin语言中文站
Java在JDK5中引入了泛型机制。 它的意思可以理解为把具体的类型 参数化,编码时用符号代替类型,实际使用的时候再传入确定的类型,可以用在类、接口或者方法上面。
那么泛型的作用是什么呢?先看一段代码。
public final class Main {
public static void main(String[] args) {
List dataList = new ArrayList();
dataList.add("a");
dataList.add("b");
dataList.add(888);
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) {
//定义一个集合,泛型类型是String
List dataList = new ArrayList();
dataList.add("a");
dataList.add("b");
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 birds = new ArrayList<>();
//赋值给FlyAnimal集合: Error:Java本身不可型变
List flyAnimals = birds;
//错误: 不兼容的类型: List无法转换为List, List flyAnimals = birds;
}
}
如上代码所示,Bird继承自FlyAnimal,由于Java的多态,赋值是成立的。 但由于Java中的泛型是不型变的,也就意味着List
但需求总是有的,怎么使List
public final class Main {
public static void main(String[] args) {
//Java多态
FlyAnimal flyAnimal = new Bird();
List birds = new ArrayList<>();
//正常赋值,不会发生错误。 使Java泛型具有了协变性
List extends FlyAnimal> flyAnimals = birds;
}
}
所以类型通配符的一大作用就是突破Java泛型是不型变的限制。
疑问
结合上面的知识点,我们或许会产生如下疑问樂️?
- Java泛型为什么不型变?
- 型变、协变、逆变、不变是什么?
- 类型通配符为什么可以保证类型安全?
Java泛型为什么 不型变?
我们先看个例子:
public final class Main {
public static void main(String[] args) {
List birds = new ArrayList<>();
//如果Java泛型不是 不型变,那么由于Java多态的特征,此赋值就会成立
List flyAnimals = birds;
}
}
如上,我们创建一个泛型类型是Bird的集合,如果泛型不是 不型变,List
型变、协变、逆变、不变是什么?
型变分为协变和逆变,与不变对应,用来描述泛型类型转换后的继承关系。
-
协变(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 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 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来举例,我们自己定义个泛型类来加深对 只能读取不能修改 / 只能修改不能读取的理解:
public class GenericA{ 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 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 (); //只能添加Bird genericB.setType(new Bird()); //Error: 编译器无法确定取出的类型。 当然你如果用Object接收,那也是可以的,但没有意义。 //FlyAnimal bird = genericB.getType(); Object o = genericB.getType(); } }
Kotlin泛型
在Kotlin中泛型类,接口,方法的写法和Java没啥区别,如下所示:
class Shape(var data : T) interface DownloadLister {} fun getFirstElement(list : List ) : T? { if(list.isNotEmpty()) { return list[0] } return null }
声明处型变out、in
与Java泛型一样,Kotlin泛型也是不型变的。out/in修饰符称为型变注解,一般在声明类型参数的地方使用,所以也称为声明处型变。(Java是使用处型变,也就是在使用的地方用类型通配符进行型变)它们的作用如下:
- 使用修饰符out来支持协变,类似于java中的上界通配符 extends T>。
- 使用修饰符in来支持逆变,类似Java中的下界通配符 super T>。
具体使用如下:
class GenericA(val data: T)
fun main(args: Array) { val genericA : GenericA = GenericA(Bird()) //协变 val genericB : GenericA = genericA }
感觉就是换了个写法,作用是一样的,out表示类型变量只用来读取,不能修改;in表示只用来修改,不能读取。
class GenericA{ fun setData(data : T) { } }
那就有同学说了,我硬是要读取呢? 好吧,编译器是不会“放过”你的,直接报错,如下图:
星投影(*)
Kotlin中 <*>相当于java中的无界通配符>。>是 extends Object>的简写,在Kotlin中<*>是
fun main(args: Array) { val genericA : GenericA = GenericA(Bird()) //协变: Any是所有类的超类,所以协变成立 val genericB : GenericA<*> = genericA }
where关键字
在Java中我们给泛型类型加边界的写法如下:
public class GenericB{ }
在Kotlin中就得换种写法了:
class GenericAwhere T : Animal, T : AnimalAction
备注说明下,
上一篇:Kotlin学习历程——扩展



