共计 2256 个字符,预计需要花费 6 分钟才能阅读完成。
问题
在 v2 遇到有同学反馈了个问题, 第一眼的感觉应该是泛型擦除 (Type Erasure) 和类型推断 (Type Inference) 导致的. 但当我尝试去彻底解释这个问题的时候, 才发现关键原因是: 如果在调用方法时有 unchecked conversion, 那么方法返回的是定义中返回类型经过擦除 (erasure) 后的结果.
具体问题是这个样子的:
public static List<String> methodA(Collection<String> stringCollection) {
List<String> stringList = new ArrayList<>();
for (String s : stringCollection) {
stringList.add(s);
}
return stringList;
}
public static void methodB(String s) {}
public static void main(String args[]) {
// ok
methodA((Collection<String>) new ArrayList<String>()).stream().forEach(p -> methodB(p));
// compile error
// Question.java:29: 错误: 不兼容的类型: Object 无法转换为 String
// methodA((Collection) map.get(“A”)).stream().forEach(p -> methodB(p));
// ^
methodA((Collection) new ArrayList<String>()).stream().forEach(p -> methodB(p));
}
猜测过程
如果对 type erasure, unchecked warning 不太熟悉, 可以先阅读后几节.
依我的理解, lambda 中 p 的类型应该被推断为 String. 考虑 stream 和 forEach 的定义为 Stream<E> Collection<E>.stream(), void Stream<T>.forEach(Consumer<? super T> action). 整个类型推断的过程应该如下:
List<String> stringList = methodA(…);
Stream<String> stringStream = stringList.stream();
stringStream.forEach(p -> methodB(p));
但从实际编译的结果来看, methodA((Collection<String>) new ArrayList<String>()) 版本是符合预期的, 而 methodA((Collection) new ArrayList<String>()) 版本中, p 的类型被推断为 Object.
有了两个版本的对比, 很容易就会去猜测是不是 mehtodA 返回的结果因为入参类型的不同而不同. 但 tm 我就走歪了, 因为映像中没有什么情况下方法返回类型会和定义不一致, 所以我先去排查了一遍泛型擦除, 类型推断, Java 对 Lambda 的处理方式等. 直到最后走头无路,才尝试去看返回类型是否有猫腻.
最终发现报错版本返回的结果类型为 raw type List, 而不是预期的 parameterzied type List<String>. 验证的代码如下:
Collection rawCollection = new ArrayList<String>();
// methodA 返回的是 raw type List, 此处赋值会因为 parameterzied type 指向 raw type 而导致 unchecked warning
List<String> stringList = methodA(rawCollection);
官方的解释 是:
Otherwise, if unchecked conversion was necessary for the method to be applicable, then the result type is the erasure (§4.6) of the method’s declared return type.
嗯, 报错版本的传入参数类型是 Collection, 而 methodA 定义的参数类型为 Collection<String>, 调用触发 unchecked conversion.
泛型擦除
Generic type 是泛型的定义, 由 Java 传统的类型结合 type parameter 组成. 通过提供具体的 type argument, generic type 实例化成为 parameterized type.
引用 Java Generic FAQs 中的内容, 泛型擦除指的是: 在编译过程中通过消除 type parameter 和 type argument 来将同一 generic type 对应的不同实例映射成同一个类的过程. 具体可以分为两个部分:
Parameterized type 的 type parameters 被删除.
Generic type 中的 type arguments 被替换为具体的类型.
在泛型擦除的过程中, 编译器可能按需加入额外的方法和类型转换来保证编译结果的正确性.
unchecked warning
uncheked warning 在编译期间产生, 表示编译器无法保证完全的类型安全, 当然也不代表一定类型不安全. 产生的几种场景基本都和泛型有关. 和本文关联的场景是: 将 parameterized type 指向 raw type, 比如 List<String> stringList = new ArrayList()