Java、Kotlin泛型全面解析
序言
相信大家对 Java、Kotlin 中的泛型都不陌生,那首先来问大家一个问题,下面在集合中查找最大元素并返回的max
方法的声明是否足够完善?
public static <T extends Comparable<T>> T max(Collection<T> coll);
上述声明至少存在两处可进一步完善的点,在具体回答之前,让我们先来快速回顾一下泛型的相关知识。
PS:后文会基于 Java 来进行介绍,Kotlin 中差异的部分会单独展开。
概述
泛型的定义
泛型,即参数化类型,它允许我们在定义类、接口、方法时将类型声明为参数,等到使用时再指定具体的类型。
泛型的好处
一句话描述,泛型能够有效地减少类型不安全的问题,增强代码的健壮性、可读性和通用性。
举个例子,如下有一个用于保存 String 对象的 List,在没有泛型时,我们没法显式地约束可以被添加到 List 中的对象的类型,一旦一不小心添加了非 String 类型的对象到 List 中,便可能会导致运行时异常:
// List of String, only contains String instance.
List list = new ArrayList();
list.add("hello");
list.add(1);
String s1 = (String) list.get(0);
String s1 = (String) list.get(1); // 运行时异常
相对的,在有泛型时,编译器会替我们进行类型检查和转换,减少手动类型转换的次数,保障类型的安全:
List<String> list = new ArrayList<>();
list.add("hello");
list.add(1); // 编译报错
String s = list.get(0);
同时也让我们能够设计出更加通用的类型、方法,比如开头处提到的从集合中查找最大的元素的max
方法:
public static <T extends Comparable<T>> T max(Collection<T> coll);
泛型的声明
泛型的声明可分为泛型类型的声明和泛型方法的声明。
泛型类型
泛型类型分为泛型类和泛型接口,它的声明格式是类型名后跟上由<>
包裹的一组类型参数,类型参数用于指代类型,通常使用一个大写字母表示,比如:
// Java
class Shop <T> {
List<T> buy(float money) { ... };
}
// Kotlin
class Shop <T> {
fun buy(money: Float): List<T> { ... }
}
在使用泛型时,需要传递与形式类型参数(type parameter) 对应的实际类型参数(type argument) 列表,这一动作被称作泛型的实例化。在如下所示的代码中,Shop<String>
是Shop<E>
的一个实例,又被称作一个参数化的类型(parameterized type) :
Shop<Apple> shop = new Shop<>();
Apple apple = shop.buy(5f);
泛型方法
泛型方法拥有自己的类型参数声明,在 Java 中放置在返回值之前, 在 Kotlin 中放置在方法名之前,如下所示:
// Test.java
public static <T> void copy(List<T> dest, List<T> src) { ... }
// Test.kt
fun <T> copy(dest: List<T>, src: List<T>) { ... }
泛型方法的实例化发生在调用时,当编译器能够通过方法实参的类型推断得到类型参数的实际值时,便可以省略掉类型实参:
List<String> src = Arrays.asList("1", "2", "3");
List<String> dest = new ArrayList<>();
Test.copy(dest, src);
当然我们也可以显示地指定类型实参:
Test.<String>copy(dest, src);
泛型约束
当我们需要限制类型实参的类型时,比如一个只卖水果的商店,在 Java 中可以在类型参数后通过 extends 关键字指定类型的上界,如下所示:
class Shop <T extends Fruit> {
......
public int getTotalWeight(List<T> list) {
int total = 0;
for (T t : list) {
total += t.getWeight();
}
return total;
}
}
class Fruit {
private int weight;
public Fruit(int weight) {
this.weight = weight;
}
public int getWeight() {
return weight;
}
}
class Apple extends Fruit {
public Apple(int weight) {
super(weight);
}
}
class Banana extends Fruit {
public Banana(int weight) {
super(weight);
}
}
此时 T 一定是 Fruit 类型或者其子类型,因此我们能够自由地调用 Furit 暴露的接口。
此外,我们还可以指定类型的多个上界,上界之间通过&
来分隔,表示 T 需要是指定的多个上界的子类型:
class Shop <T extends Fruit & Comparable<T>> {...}
其中,多个上界中只允许存在一个类类型,且需要第一个指定。
而在 Kotlin 中,使用:
关键字指明上界,当有多个上界时通过where
来表示:
// 单上界
class Shop<T: Fruit>
// 多上界
class Shop<T> where T : Fruit, T : Comparable<T> {}
协变 vs 逆变
在 Java/Kotlin 中,泛型类型是不可变(invariant) 的,意味着对于任意类型 Type1 和 Type2,List<Type1>
既不是List<Type2>
的子类型,也不是List<Type2>
的父类型,举例来讲就是List<Fruit>
并不是List<Object>
的子类型,听起来很反直觉,但其实这样设计是为了避免潜在的类型安全问题。
在下面的示例中,fruitList 指向一个 Fruit 的 List,假设List<Fruit>
是List<Object>
的子类型,fruitList 便能够成功赋值给类型是List<Object>
的变量 objList,从而绕过限制向其中添加非 Fruit 类型的对象,造成运行时异常:
List<Fruit> fruitList = new ArrayList<>();
List<Object> objList = fruitList; // 假设能够成功赋值,实际上会报编译错误
objList.add("123");
Fruit fruit = fruitList.get(0);
但在某些场景中,这一限制会带来一些不便。举个例子,现在需要给 Shop 类增加一个退款接口,假设如下进行声明:
public class Shop<T> {
....
float refund(List<T> list) { ... }
}
由于泛型不变性的限制,下面的代码将无法通过编译,refund
方法只能接收一个List<Fruit>
类型的参数:
Shop<Fruit> fruitShop = new Shop<>();
List<Apple> appleList = ...;
fruitShop.refund(appleList); // 编译报错,需要的是List<Fruit>类型
但在实际的场景中,我们确实会有类似的需求,因此为提升代码的灵活性,Java 提供了一个特殊的通配符: ? extends 来解决这一问题。
? extends
? extends 称作上界通配符,其后紧跟需要的上界类型 Type, ? extends Type 指代 Type 及其子类类型,Type 类型既可以是一个类,也可以是一个接口。一个List<? extends Fruit>
能够接受任意 Fruit 及其子类类型的 List 的赋值,比如List<Apple>
、List<Banana>
。通过 ? extends Type 我们便能打破 Java 中不允许把一个子类的泛型类型对象赋值给一个父类的泛型类型引用的限制,让泛型类型具有协变性(covariance)。
所以在上面的例子中,只需要将List<T>
替换成 List<? extends T>
即可:
public class Shop<T> {
......
float refund(List<? extends T> list) { ... }
}
回想一下前面的内容,Java/Kotlin 中的泛型之所以不允许将一个List<Apple>
赋值给一个List<Fruit>
是为了保证类型安全,避免添加一个非 Apple 及其子类类型的对象到集合中。
因此当我们使用上界通配符放宽这一限制时,为保证类型安全,编译器将不允许我们向其中写入对象(仅null除外)。以List<E>
接口为例,凡是参数类型带有 E 的方法,在List<? extends Type>
上的调用都会受限,比如 add
、addAll
方法:
public interface List<E> extends Collection<E> {
......
boolean add(E e);
boolean addAll(Collection<? extends E> c)
......
}
List<Integer> integers = new ArrayList<>();
List<? extends Number> numbers = integers;
numbers.add(1); // 编译报错
? super
有时我们需要将一个父类的泛型类型对象赋值给一个子类的泛型类型引用,这种性质被称作逆变(contravariance) ,Java 中提供了 ? super 通配符来实现。
? super 称作下界通配符,其后紧跟需要的下界类型 Type, ? super Type 指代 Type 及其父类类型,Type 类型既可以是一个类,也可以是一个接口。一个List<? super Apple>
能够接受任意 Apple 及其父类类型的 List 的赋值,比如List<Fruit>
、List<Object>
,和 ? extends Type 正好相反。
回想开头时提到的可以从集合中查找最大元素并返回的max
方法:
public static <T extends Comparable<T>> T max(Collection<T> coll)
假设现在想借助 max
方法在一批水果中找到最重的那一个,为保持通用,我们让 Fruit 类实现 Comparable 接口:
class Fruit implements Comparable<Fruit> { ... }
class Apple extends Fruit {}
但是下面的调用却无法通过编译:
List<Apple> appleList = ...;
Apple apple = max(appleList); // compile error
这是因为在max
方法的声明中,T extends Comparable<T>
限制了 T 需要是Comparable<T>
的子类型, 而 Apple 是Comparable<Fruit>
的子类型,不是Comparable<Apple>
的子类型,所以我们需要通过 ? super Type 放宽限制,如下所示:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
此外,下界通配符也同样存在限制,我们只能从中读取到 Object 类型的对象:
List<Number> numbers = new ArrayList<>();
List<? super Integer> integers = numbers;
integers.add(1);
Integer i = (Integer) integers.get(0); // 手动转换
PECS: producer-extends, consumer-super
总结一下,使用 ? extends Type 来实例化的泛型类型,比如List<? extends Fruit>
,只能读取不能写入对应类型的对象(仅null除外),是类型对象的生产者。
使用 ? super Type 来实例化的泛型类型,比如List<? super Apple>
,可以写入对应类型的对象,比如这里的 Apple,但只能读取到 Object 类型的对象,因此通常作为类型对象的消费者。
在 Effective Java 3rd Edition 中,作者总结了一条便于记忆、理解的准则,简称为 PECS:
如果参数化类型表示一个 T 生产者,就使用<? extends T>;如果它表示一个 T 消费者,就使用<? super T>。
再回头看max
方法的声明:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
参数 coll 明显是 T 的一个生产者,我们只会从中读取对象,因此可以使用<? extends T>
来进一步提升代码的灵活性:
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
List<Apple> appList = ...;
Fruit furit = Test.<Fruit>max(appleList); // ok
更进一步的解释可参考:Explanation of the Collections.max signature
Kotlin中的 in、out
在 Kotlin 中,提供了 in、out 两个关键字来支持泛型类型的逆变、协变。关键字 in 对应着 Java 中的 ? super,out 对应着 Java 中的 ? extends:
class Shop<T> { ... }
val appleShop = Shop<Apple>()
val shop1: Shop<out Fruit> = appleShop
val fruitShop = Shop<Fruit>()
val shop2: Shop<in Apple> = fruitShop
不同于 Java 的地方是,如果某个泛型类型确定只会是 T 的消费者或者生产者,为进一步简化, Kotlin 支持直接在类、接口声明处通过 in/out 关键字指定泛型类型支持逆变/协变,等到使用时就不需要在指定 in/out 了:
class Shop<out T> { ... }
val appleShop = Shop<Apple>()
val shop: Shop<Fruit> = appleShop
类型擦除
泛型是在 Java 5 中引入的,在此之前已经有大量基于老 Java 版本的代码在线上运行,设计者们为保证兼容性,需要让在没有泛型世界里运行的 Java 代码在泛型的世界里也能正常运行。举个例子,下面的代码在 Java 5 中必须能继续运行:
ArrayList list = new ArrayList();
list.add(Integer.valueOf(123));
list.add("123");
在平行地新增一套泛型化版本的新类型和直接将已有的类型泛型化之间,设计者们选择了后者。在兼容性的要求下,一个泛型化的类型必须允许被当作非泛型化的类型来使用,并且允许前者赋值给后者:
ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<String> stringList = new ArrayList<>();
ArrayList rawList;
rawList = integerList;
rawList = stringList;
而不带类型实参的泛型类型又被称作:原始类型( raw type) ,为支持 raw type,设计者们最终选择了最为直接的擦出法来实现 Java 中的泛型,更详细的解释可以看下 R大的回答:Java 不能实现真正泛型的原因是什么? - 知乎。
具体来讲,代码中的泛型信息在经过编译后便会被编译器删除,只保留原始的类型、方法。其中,编译器会将泛型类型、泛型方法中所有使用类型参数的地方替换为声明时指定的首个上界,如果没有显示指定,默认使用 Object 来替换,并且在必要的地方还会加入类型转换以保证类型安全。
举个例子:
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) {
Iterator<? extends T> i = coll.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}
在经过擦除后,方法上的泛型信息被删除,并且使用类型参数的地方都被 Comparable 替换:
public static Comparable max(Collection coll) {
Iterator i = coll.iterator();
Comparable candidate = (Comparable) i.next();
while (i.hasNext()) {
Comparable next = (Comparable) i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}
此外,当继承或实现某一泛型类型时,为不破坏多态性编译器还会根据需要生成对应的桥接方法:
public interface Comparable<T> {
public int compareTo(T o);
}
class Fruit implements Comparable<Fruit> {
......
@Override
public int compareTo(Fruit o) { ... }
}
擦除后 Comparable 接口中compareTo
方法的签名与 Furit 类中compareTo
方法的签名并不匹配,前者接收一个 Object 类型的对象,后者接收一个 Fruit 类型的对象:
public interface Comparable {
public int compareTo(Object o);
}
class Fruit implements Comparable {
......
public int compareTo(Fruit o) { ... }
}
因此为满足重写的要求,编译器会替我们生成一个桥接方法:
class Fruit implements Comparable {
......
// bridge method
public int compareTo(Object o) {
return compareTo((Fruit) o);
}
public int compareTo(Fruit o) { ... }
}
最后,我们来比较一下前面经过完善的max
方法和 java.util.Collections 中的max
方法:
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) { ... }
class Collections {
......
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) { ... }
......
}
相较于前者,Collections 中max
方法的泛型声明中多了一个 Object 类型的上界,这样做的目的是保证擦除后的方法签名和引入泛型前的方法签名一致,避免基于老 Java 版本的代码无法找到对应的max
方法,而引入泛型前 Collections 的max
方法签名如下:
public static Object max(Collection coll)
运行时获取类型信息
虽然代码中的泛型信息在经过编译后会被编译器删除,但类、接口、方法以及字段的泛型签名信息会被记录在对应的 Class 文件中,以支持运行时通过反射获取泛型信息,泛型签名由一个被称作 Signature 的属性来描述。
举个例子,有如下一个泛型类Shop<T>
,包含一个返回List<T>
的buy
方法:
public class Shop<T> {
public List<T> buy(float money) {
return new ArrayList<>();
}
}
其对应的字节码如下,Shop 类和buy
方法的泛型签名信息被保存在对应的 Signature 属性中:
我们可以利用这一特性,在运行时通过构造一个匿名内部类来获取泛型信息:
// 获取匿名内部类对应的Class对象
Class<?> cls = (new Shop<Fruit>() {}).getClass();
// 获取泛型父类,即Shop<Fruit>
Type type = cls.getGenericSuperclass();
System.out.println(type); // 输出:Shop<Fruit>
一些限制
在 Java 的擦除实现下,会给我们带来一些使用限制,下面列举一些常见的,更全的可以看这里:Restrictions on Generics 。
首先,不允许创建基础类型的泛型类型,需要使用相应的包装类型(这一点是泛型的设计者偷懒了):
List<int> intList1 = new ArrayList<>(); // 编译报错
List<Integer> intList2 = new ArrayList<>(); // ok
其次,由于代码中泛型信息的丢失,我们无法直接获取类型参数的类型,也无法创建类型参数的对象:
public static <E> void append(List<E> list) {
E elem = new E(); // 编译报错
list.add(elem);
}
一种 workaround 方法是通过反射来创建:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance();
list.add(elem);
}
再次,不允许强转为参数化的类型以及参数化的类型无法出现在 instanceof 中:
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译报错
if (li instanceof List<Integer>) { // 编译报错
...
}
最后,为避免类型安全问题,Java 中不允许创建泛型数组,而 Kotlin 中是可以的。
List<String>[] stringLists = new List<String>[2]; // 编译报错
val stringLists = arrayOf<List<String>>() // ok
这是因为在 Java 中,数组是协变的,而 Kotlin 中数组是不变的。在 Java 中我们可以将一个子类型的数组赋值给父类型的引用:
Object[] strings = new String[2]; // ok
假设允许创建泛型数组,便可以将一个 Integer 的 ArrayList 设置给一个List<String>
的数组,导致运行时错误:
Object[] stringLists = new List<String>[2];
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // 运行时报错
其他
通配符
在 Java 中,当我们不关注泛型类型的具体类型时,可以使用通配符?
来简化使用。
ArrayList<String> stringList = new ArrayList<>();
List<?> wildcardList = stringList;
List<?>
等价于List<? extends Object>
,可以接收任意类型的 List。由于类型未知,为保证类型安全,只能读取 Object 类型的对象,不允许写入。
而在 Kotlin 中,提供了*
来实现上述效果,称作星投影:
val stringList = ArrayList<String>()
val wildcardList: List<*> = stringList
reified 修饰符
在 Kotlin 中,inline 方法支持通过 reified 修饰类型参数,被修饰的类型参数基本上可以当作一个普通类型来使用,可以获取其对应的类型,可以出现在 is 表达式中:
inline fun <reified T> someMethod(t: T) {
val cls = T::class.java // ok
if (t is T) { // ok
// ...
}
}
参考
转载自:https://juejin.cn/post/7169109476933369886