先看一段例子,定义一个简单的泛型类 Some<T>

    1. import java.util.function.Consumer;
    2. import java.util.function.Supplier;
    3. public final class Some<T> {
    4. private final T value;
    5. private Some(final T t) {
    6. this.value = t;
    7. }
    8. static <T> Some<T> of(final Supplier<T> supplier) {
    9. return new Some<>(supplier.get());
    10. }
    11. public Some<T> peek(final Consumer<T> consumer) {
    12. consumer.accept(value);
    13. return this;
    14. }
    15. public T get() {
    16. return value;
    17. }
    18. }

    这个类很简单,他的值由一个 supplier 函数通过静态工厂方法提供,还有额外两个方法 peekget .
    注意 peek 返回的是 this ,这样方便链式调用。

    1. public static void main(String[] args) {
    2. Some<List<? extends CharSequence>> some =
    3. Some.of(() -> Arrays.asList("a", "b", "c"));
    4. System.out.println(some.get());
    5. }

    上面这段代码一如预期的正常编译,并且打印结果 [a, b, c] . 下面我们做点小改动,链式调用 peek .
    System.out.println 作为 consumer 传入 peek

    1. public static void main(String[] args) {
    2. //❌编译错误
    3. Some<List<? extends CharSequence>> some =
    4. Some.of(() -> Arrays.asList("a", "b", "c")).peek(System.out::println);
    5. }

    上面的代码将会产生出乎预料的结果,无法正常编译。

    image.png

    编译器报类型不兼容的原因,是因为Java泛型是 invariant (可以参见covariant VS invariant)。
    Java语言规范 4.10.Subtyping 章节有这么句清晰的描述:

    :::info 子类型关系不会扩展到参数化类型,也即是如果 TS 的子类型,不能推断出 C<T>C<S> 的子类型。 :::

    那为什么上面第一个列子调用方式没有 peek 可以, peek 返回 this ,和 of 返回结果一样,第二个例子调用切不可以。

    这里涉及到Java8引入的泛型目标类型推断(JEP 101: Generalized Target-Type Inference),泛型推断让编译器能够利用上下文信息来推断出合理的类型。下面逐步分析。

    笔者所用的 IDEIntelliJ IDEA , 这里有个小技巧, Mac 下按住 Command ( Window 下应该是 Ctrl , 待验证) 键同时将光标移动到泛型方法上,既可以看出Java编译器根据上下文推断出的类型,如果想让浮动弹窗长期保持,按住 Command 的同时将光标移动到浮窗,点击下,这时即可松开 Command , 移走光标浮窗也会保持,若未点击浮窗,则松开 Command 键,浮窗就会消失。

    image.png

    从截图中可以看出编译器推断 类型变量 TList<? extends CharSequence>of 返回类型是 Some<List<? extends CharSequence>> ,和表达式左边的类型是一致的。

    image.png

    image.png

    可以看出编译器推断出类型变量 TList<String>peek 返回结果是 Some<List<String>> 类型。和表达式左边的类型是不一样的。

    这也就清楚为什么第二个例子会报类型不兼容了。因为 List<? extends CharSequence>List<String> 是不兼容的,原因就是上面提到的 Java泛型是 invariant 。想要更加深入的了解invariant 、泛型兼容 相关概念,可以移步 《covariant VS invariant》《为什么不建议使用raw type类型》

    再看看下面的例子,注意 of 是一个静态工厂方法。

    1. //✅正常编译
    2. Some<List<? extends CharSequence>> some =
    3. Some.of(() -> Arrays.asList("a", "b", "c"))
    4. .of(() -> Arrays.asList("a", "b", "c"));

    image.png

    image.png

    第一个 of 推断出 TList<String> ,返回类型是 Some<List<String>> ,第二个 of TList<? extends CharSequence> ,返回类型是 Some<List<? extends CharSequence>>

    为什么编译器会推断出 T 同时是两个类型?

    注意方法声明 of 是一个静态泛型方法,实则前后两个 of 方法的类型变量 T 根本不是同一个,所以可以类型不同,最后赋值给表达式左边的是第二个 of 的返回值,而他们类型是一样的,所以可以正常编译。

    :::info 实际第一个 ofT 无论是 List<String> 还是 List<Integer> , 表达式都合法,因为最终返回类型由链路中最后一个方法决定,只要它的类型和表达式左边被赋值对象类型兼容就合法。 :::

    由于 peek 是实例方法,所以他的变量类型 T 其实早已由前面 of 构造出的对象决定了。

    那为什么 Some.of(() -> Arrays.asList("a", "b", "c")) 在两个表达式中会推断出不同类型?

    image.png
    图 1
    image.png
    图 2
    这是因为 Some.of(() -> Arrays.asList("a", "b", "c")) 在两个语句中所处的上下文不一样,我们知道泛型目标类型推断,编译器需要结合上下文来推断更合理的具体类型或者是它的子类。

    Some.of(() -> Arrays.asList("a", "b", "c")) 这个语句中,编译器可以推断出类型变量 T 可以为 List<String>List<? extends CharSequence>List<? extends Object> , 但有多个候选类型时,编译器会选择更为具体、继承链中更接近的类型。下面 JLS 关于类型推断的一句描述可以佐证。

    Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.

    第一个截图中, of 所处上下文,编译器既要让 of 的结果类型满足表达式左边的类型(针对类型变量 T ) List<? extends CharSequence>又 要满足入参的类型(List<String>List<? extends CharSequence>List<? extends Object> 任一皆可),这时会取两者交集,所以最终推断出 TList<? extends CharSequence>

    第二个截图中, of 所处上下文并不是表达式链路的最后,他的返回结果不需要赋值给表达式左边的变量,所以编译器类型推断时只需要考虑兼容入参类型,这时会选择三种候选类型中最具体的List<String> 。而后面的 peek 方法实则是 of 构造方法返回对象的实例方法,它的类型已经确定是 List<String> ,故不存在类型推断的过程。 peek 的返回类型也就知道是 Some<List<String>

    有两种方式可以解决图2调用 peek 的问题:

    1. 现在我们知道图2中的 of 是由于没有提供足够的上下文供编译器参考,所以可以通过显示指定具体泛型类型。
    1. public static void main(String[] args) {
    2. Some<List<? extends CharSequence>> some =
    3. Some.<List<? extends CharSequence>>of(() -> Arrays.asList("a", "b", "c"))
    4. .peek(System.out::println); //✅
    5. }
    1. 图2是由于 peek 的链式调用,导致前面的 of 失去了返回类型的上下文约束信息,所以可以不采用链式调用,给予编译器更多的上下文信息。
    1. public static void main(String[] args) {
    2. //✅
    3. Some<List<? extends CharSequence>> some = Some.of(() -> Arrays.asList("a", "b", "c"));
    4. some.peek(System.out::println);
    5. }

    下面看道题,根据上面的分析,可以类推下面的使用方式是错误的,因为 new ArrayList<>() 所处的上下文没有提供任何类型约束,编译器只能推断 ArrayList 的类型变量是 Object , Iterator<Object> 自然无法和 Iterator<String> 兼容。

    1. Iterator<String> it = new ArrayList<>().iterator(); //❌

    那有没有办法写出一个类型安全的赋值方式?

    1. public static <T> Iterator<T> iter(Iterable<T> i)
    2. {
    3. return i.iterator();
    4. }
    5. public static void main(String[] args)
    6. {
    7. Iterator<String> it = iter( new ArrayList<>() ); //✅
    8. \____________________________/
    9. }

    这里 iter 方法所处的上下文,为了满足表达式左边的类型,编译器推断出 T 的类型是 String ,而 new ArrayList<>() 也是泛型,假设他的类型变量是 E ,为了满足 iter 入参的类型, 编译器推断 E 也是 String , 上面的错误实例中之所以 new ArrayList<>() 被推断为 Object , 是因为语句既没有作为返回类型受到赋值类型的约束,也没有作为入参受到方法声明的约束,缺失这些上下文约束提示,编译器只能推断出 Object

    根据前面的理论知识,你应该不难推测出,下面的写法仅仅是比上面少了结果赋值,那么这种情况由于 iter 缺失返回类型约束,入参本身也是泛型,也不能提供上下文约束条件,所以 iter 的推断结果是 Object , new ArrayList<>() 由于 iterObject ,所以 它的类型变量推断结果也是 Object .

    1. iter( new ArrayList<>() );

    下面通过 idea 的自动提取变量来验证我们的猜测。通过选中 1处语句,按下 idea 提取变量快捷键,就会补全成 2这样的语句,这个其实就是通过编译器的泛型目标类型推断来推断变量类型。

    1. iter( new ArrayList<>() ); // 1
    2. Iterator<Object> iter = iter(new ArrayList<>()); // 2

    image.png

    最后再看看泛型推断在泛型实例化中的作用

    没有泛型推断前,我们必须按照@1来实例化泛型类,这种使用方式看起来很累赘,前后都要声明参数化类型。有了泛型推断,就可以用如@2简洁的写法,用一对空尖括号 <> 代替。但是如果你用如@3这种写法,将会得到一个编译器警告⚠️,因为 new HashMap() 表示一个 raw type HashMap (不需要泛型推断),无法确定元素具体类型,因此将 HashMap 类型赋值给 Map<String, List<String>> 会产生警告。

    1. Map<String, List<String>> myMap = new HashMap<String, List<String>>(); //@1
    2. Map<String, List<String>> myMap = new HashMap<>(); //@2
    3. Map<String, List<String>> myMap = new HashMap(); //@3 ⚠️ unchecked conversion warning

    希望这一系列泛型主题相关的内容能让你更加深入、透彻的理解泛型、看到泛型、运用泛型。

    1. covariant VS invariant
    2. 为什么不建议使用raw type类型