首先,对Java8 以及 泛型 还不熟悉的同学建议是先去学习,再来阅读这篇文章可能对你收获更大哦,我们今天聊的是Java8新特性—泛型类型推断,可能有些同学会说这还不简单,泛型有啥可以讲得,在开发中一顿<>操作猛如虎,如果不行,那就两套<<>>
同学,我们先来康康这个例子,定义一个简单的泛型类 Some。
这个类很简单,他的值由一个 supplier 函数通过静态工厂方法提供,还有额外两个方法 peek 、 get ;注意 peek 返回的是 this ,这样方便链式调用。
上面这段代码一如预期的正常编译,并且打印结果 [a, b, c],下面我们做点小改动,链式调用 peek.将 System.out.println 作为 consumer 传入 peek 。
上面的代码将会产生出乎预料的结果,无法正常编译。
编译器报类型不兼容的原因,是因为Java泛型是 invariant (可以参见covariant VS invariant)。Java语言规范 4.10.Subtyping 章节有这么句清晰的描述:
子类型关系不会扩展到参数化类型,也即是如果 T 是 S 的子类型,不能推断出 C<T> 是 C<S> 的子类型。
那为什么上面第一个列子调用方式没有 peek 可以,peek 返回 this ,和 of返回结果一样,第二个例子调用却不可以。
这里涉及到Java8引入的泛型目标类型推断(JEP 101: Generalized Target-Type Inference),泛型推断让编译器能够利用上下文信息来推断出合理的类型。下面逐步分析。
笔者所用的 IDE 是 IntelliJ IDEA , 这里有个小技巧, Mac 下按住 Command ( Window 下应该是 Ctrl , 待验证) 键同时将光标移动到泛型方法上,既可以看出Java编译器根据上下文推断出的类型,如果想让浮动弹窗长期保持,按住 Command 的同时将光标移动到浮窗,点击下,这时即可松开 Command , 移走光标浮窗也会保持,若未点击浮窗,则松开 Command 键,浮窗就会消失。
从截图中可以看出编译器推断 类型变量 T 是 List<String>,peek 返回结果是 Some<List<String>>类型。和表达式左边的类型是不一样的。
这也就清楚为什么第二个例子会报类型不兼容了。因为 List<? extends CharSequence>与List<String> 是不兼容的,原因就是上面提到的 Java泛型是 是 invariant 。
再看看下面的例子,注意 of 是一个静态工厂方法。
第一个 of 推断出 T 是 List<String>,返回类型是 <Some<List<String> ,第二个of T是List<? extends CharSequence>,返回类型是List<? extends Object>
为什么编译器会推断出 T 同时是两个类型?
#注意方法声明 of 是一个静态泛型方法,实则前后两个 of 方法的类型变量 T 根本不是同一个,所以可以类型不同,最后赋值给表达式左边的是第二个 of 的返回值,而他们类型是一样的,所以可以正常编译。
实际第一个 of 中 T 无论是 List还是 List, 表达式都合法,因为最终返回类型由链路中最后一个方法决定,只要它的类型和表达式左边被赋值对象类型兼容就合法。
由于 peek 是实例方法,所以他的变量类型 T 其实早已由前面 of 构造出的对象决定了。那为什么 Some.of(() -> Arrays.asList("a", "b", "c")) 在两个表达式中会推断出不同类型?
这是因为 Some.of(() -> Arrays.asList("a", "b", "c")) 在两个语句中所处的上下文不一样,我们知道泛型目标类型推断,编译器需要结合上下文来推断更合理的具体类型或者是它的子类。
Some.of(() -> Arrays.asList("a", "b", "c")) 这个语句中,编译器可以推断出类型变量 T 可以为 List、 List 、 List , 但有多个候选类型时,编译器会选择更为具体、继承链中更接近的类型。下面 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>任一皆可),这时会取两者交集,所以最终推断出 T 是 List<? extends CharSequence>。
第二个截图中, of 所处上下文并不是表达式链路的最后,他的返回结果不需要赋值给表达式左边的变量,所以编译器类型推断时只需要考虑兼容入参类型,这时会选择三种候选类型中最具体的List<String>。而后面的 peek 方法实则是 of 构造方法返回对象的实例方法,它的类型已经确定是 List<String>,故不存在类型推断的过程。peek 的返回类型也就知道是 Some<List<String>>。
有两种方式可以解决图2调用 peek 的问题:
现在我们知道图2中的 of 是由于没有提供足够的上下文供编译器参考,所以可以通过显示指定具体泛型类型。
图2是由于 peek 的链式调用,导致前面的 of 失去了返回类型的上下文约束信息,所以可以不采用链式调用,给予编译器更多的上下文信息。
下面看道题,根据上面的分析,可以类推下面的使用方式是错误的,因为 new ArrayList<>() 所处的上下文没有提供任何类型约束,编译器只能推断 ArrayList 的类型变量是 Object , Iterator 自然无法和 Iterator兼容。
1Iterator<String> it = new ArrayList<>().iterator(); //
那有没有办法写出一个类型安全的赋值方式?
1这里 iter 方法所处的上下文,为了满足表达式左边的类型,编译器推断出 T 的类型是 String ,而 new ArrayList<>() 也是泛型,假设他的类型变量是 E ,为了满足 iter 入参的类型, 编译器推断 E 也是 String , 上面的错误实例中之所以 new ArrayList<>() 被推断为 Object , 是因为语句既没有作为返回类型受到赋值类型的约束,也没有作为入参受到方法声明的约束,缺失这些上下文约束提示,编译器只能推断出 Object 。
2根据前面的理论知识,你应该不难推测出,下面的写法仅仅是比上面少了结果赋值,那么这种情况由于 iter 缺失返回类型约束,入参本身也是泛型,也不能提供上下文约束条件,所以 iter 的推断结果是 Object , new ArrayList<>() 由于 iter 是 Object ,所以 它的类型变量推断结果也是 Object .
1iter( new ArrayList<>() );
下面通过 idea 的自动提取变量来验证我们的猜测。通过选中 1处语句,按下 idea 提取变量快捷键,就会补全成 2这样的语句,这个其实就是通过编译器的泛型目标类型推断来推断变量类型。
最后再看看泛型推断在泛型实例化中的作用
没有泛型推断前,我们必须按照@1来实例化泛型类,这种使用方式看起来很累赘,前后都要声明参数化类型。有了泛型推断,就可以用如@2简洁的写法,用一对空尖括号 <> 代替。但是如果你用如@3这种写法,将会得到一个编译器警告,因为 new HashMap() 表示一个 raw type HashMap (不需要泛型推断),无法确定元素具体类型,因此将HashMap类型赋值给Map<String, List<String>>
1Map<String, List<String>> myMap = new HashMap<String, List<String>>(); //@1
2Map<String, List<String>> myMap = new HashMap<>(); //@2
3Map<String, List<String>> myMap = new HashMap(); //@3 unchecked conversion warning
希望这一系列泛型主题相关的内容能让你更加深入、透彻的理解泛型、看到泛型、运用泛型。
本文原创,未经作者允许不可转载!
更多内容,欢迎关注作者微信公众号:橘松Java技术窝!
暂无评论
违反法律法规
侵犯个人权益
有害网站环境