由Kotlin 中关键字out和in联想到Java中的泛型
最近在学习kotlin语法,发现了kotlin中的关键字out和in,感觉甚是新颖,就细细琢磨了一下,发现这两个关键字和Java中的泛型边界有着千丝万缕的联系。那么接下来我们就先谈谈Java中的泛型,顺便复习一下泛型知识,在逐步谈谈kotlin中的out和in关键字。
- Java中的泛型
- Java中的泛型擦除
- Java中的泛型边界
- kotlin中关键字out和in
Java中的泛型
1、泛型的由来
Java是单继承,这会使程序受限太多。如果方法的参数是一个接口,而不是一个类,这种限制就放松了许多。因此,接口允许我们快捷实现类继承,也使我们有机会创建一个新类来做到这一点。在Java 5中出现了“泛型”概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。“泛型”顾名思义是:适用于许多许多的类型。泛型在编程语言总出现时,其最初的目的希望类或方法具备最广泛的表达能力。但Java泛型对比与其他语言(c++)局限就非常大,这不是本篇的重点,可以自行查询。
2、泛型分类
泛型可以分为:简单泛型、泛型接口、泛型方法
2.1
简单泛型
就是为了创造容器类,就是存放使用的对象的地方。数组也是如此,不过与简单的数组相比,容器类更加灵活,具备更多不同的功能。事实上,所有的程序,在运行时都要求你持有以答对对象,所以容器类算得上是最具有重用功能的类库之一。
public class Holder<T>{
private T t;
public Holder(T t){this.t = t;}
public T get(){return t;}
}
2.2
泛型接口
泛型可以应用于接口,接口使用泛型与类使用泛型没有区别,例如:
public interface Generator<T> { T next();}
2.3
泛型方法
在类中可以包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。泛型方法使得该方法能够独立于类而产生变化。一个基本原则是:无论何时,只要你能够做到,你就应该尽量使用泛型方法。也就是说,如果泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
public class GenericMethods{
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
}
Java中的泛型擦除
Java中的泛型是不型变的
,这就意味着 List<String>
并不是List<Object>
的子类型。为什么会这样?如果 List 不是不型变的
,它就没比Java数组好到哪去,如下代码会通过编译然后导致运行时异常:
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串
因此,残酷的现实是:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
Java泛型是使用擦除来实现的,意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此List<String>
和List<Object>
在运行上事实上是相同的类型。这两种形式都被擦除成它们的“原生”类型,即List
Java中的泛型边界
即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。因为擦除在方法中移除了类型信息,所以在运行时的问题就是边界
:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
正如我们看到的,擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作。那么,为弥补这些,就出现了边界技术。边界使得你可以在用于泛型的参数类型上设置限制条件。尽管使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。为了执行这种限制,Java泛型重用了extends
关键字。对你来说有一点很重要,即来理解extends
关键字在泛型边界上下文和普通情况下所具有的意义是完全不同的。
例如,考虑Collection
接口中addAll()
方法。该方法的签名应该是什么?估计大多数人会认为:
interface Collection<E> …… {
void addAll(Collection<E> items);
}
但随后,我们将无法做到以下简单的事情(这是完全安全):
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !!!对于这种简单声明的 addAll 将不能编译:
// Collection<String> 不是 Collection<Object> 的子类型
}
这就是为什么 addAll() 的实际签名是以下这样:
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
通配符类型参数? extends E
表示此方法接受E
或者E的一些子类型
对象的集合,而不只是E
自身。这意味着我们可以安全地从其中(该集合中的元素是E的子类型的实例)读取E
,但是不能写入,因为我们不知道什么对象符合那个未知的E
的子类型。反过来,该限制可以让Collection<String>
表示Collection<? extends Object>
的子类型。简而言之,带 extends
限定(上界)的通配符类型使得类型是协变的(covariant)
。
理解为什么这个技巧能够工作的关键相当简单:如果只能从集合中获取项目,那么使用 String
集合,并且从其中读取Object
也没问题。反过来,如果只能向集合中写入
,就可以用Object
集合并向其中放入String
:在Java中有List<? super String>
是List<Object>
的一个超类。
后者称为逆变性(contravariance)
,并且对于 List<? super String>
你只能调用接受String作为参数的方法(例如,你可以调用add(String)
或者 set(int,String)
),当然如果调用函数返回List<T>
中的T
,你得到的并非一个String
而是一个Object
。
kotlin中关键字out和in
那么在Java中,在 Source 类型的变量中存储 Source 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>
,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
所以在Kotlin中,有一种方法向编译器解释这种情况。这称为声明处型变
:我们可以标注Source
的参数类型T
来确保它仅从Source<T>
成员中返回(只读取,相当于Java中? extends T
)。为此,kotlin提供out
修饰符。
//kotlin
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
一般原则是:当一个类C
的类型参数T
被声明为out
时,它就只能出现在C
的成员的输出位置,但回报是C<Base>
可以安全的作为C<Derived>
的超类。
另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变
:只可以被写入而不可以被读取(相当于Java中 ? super T
)。逆变类型的一个很好的例子是 Comparable
:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
简单的总结,方便以后查阅。