序文

相信大家对 JavaKotlin 中的泛型都不陌生,那首要来问大家一个问题,下面在调集中查找最大元素并回来的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>上的调用都会受限,比方 addaddAll办法:

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 中,供给了 inout 两个关键字来支撑泛型类型的逆变、协变。关键字 in 对应着 Java 中的 ? superout 对应着 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 属性中:

Java、Kotlin泛型全面解析

咱们能够运用这一特性,在运转时经过结构一个匿名内部类来获取泛型信息:

// 获取匿名内部类对应的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
        // ...
    }
} 

参阅

  • Lesson: Generics
  • Generics: in, out, where | Kotlin
  • Effective Java
  • 换个姿态,十分钟拿下Java/Kotlin泛型 –
  • Kotlin 的泛型
  • Covariance and Contravariance in Generics
  • Java 不能完成真实泛型的原因是什么? – 知乎
  • Java Generics FAQs – Frequently Asked Questions
  • Explanation of the Collections.max signature
  • Chapter 4. The class File Format