之前写的Java范型相关文章:
1. 通配符捕获
在某些情况下,编译器会推断出通配符的类型,例如,列表可以定义为List<?>,但是在评估表达式时,编译器会从代码中推断出特定类型
,此场景称为通配符捕获
。
看以下两个方法,其中test1方法中,将i中的一个元素取出后,再放入,由于编译器的类型推断机制,i.get(0)
被推断为Object类型,
报错信息如下:
对于一个在其类型中含有通配符?
的变量,比如这里的test1函数的参数list,编译器会认为存在一些类型T,使得对这些 T 而言 list是 List<T>。它不知道 T 代表什么类型,但它可以为该类型创建一个占位符来指代 T 的类型
。占位符被称为这个特殊通配符的捕获(capture)
。这种情况下,编译器将名称 “capture#1”
分配给T。
报错信息中说明,共?
,说明类型实参Object和通配符的捕获(即占位符capture#1)都是?
类型,无法区分,所以编译报错。
⚠️注意:每个变量声明中每出现一个通配符都将获得一个不同的捕获,因此在泛型声明 foo(Pair<?,?> x, Pair<?,?> y) 中,编译器将给每四个通配符的捕获分配一个不同的名称,因为任意未知的类型参数之间没有关系
对于一些产生了通配符捕获的场景,可以通过一个内部的Helper类来捕获通配符?
,将其捕获成T
,如:
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> list) {
list.set(0, list.get(0));
}
}
这时在fooHelper方法中,由于参数list被申明为List<T>
类型,所以list.get(0)
返回值不再是Object类型,而是T
类型,这时候当然可以把确定的T
类型值(list.get(0)
)插入到list(List<T>
类型)中。
其实也就是对原来未知的通配符类型命名,或称作对原来不相容的边界incompatible bounds进行相容处理
。
我们再来看一下test2方法:
报错如下:
这里提示捕获的<? extends java.lang.Number>
和<? super java.lang.Number>
,编译器为其设置的占位符名称为capture#2、capture#3,
以共? extends java.lang.Number
为例,即类型实参(Integer和Float)和编译器自动捕获的类型形参“capture#2”
、都是? extends java.lang.Number
类型,但是又无法确切的判断出到底是哪个类型(是Integer?还是Float?),所以报找不到合适的方法。
再提供一个试图使得list1.add() 能不报错的Helper方法:
从报错信息中可以看出,不存在一个能够使得Integer类型能够确定捕获住? extends java.lang.Number
的对象。
总结:
对于存在通配符的类型变量,编译器会自动结合后续传入的类型实参对通配符类型进行类型推断,这个场景就是通配符捕获,但是编译器捕获的类型有时候无法使得结果满足预期,需要我们自己再定义一个中转的Helper方法,将不确定的捕获类型capture#XXX
转为确定的类型T
,使得满足预期效果;但不是所有的通配符捕获后报错的场景都能通过Helper中转方法解决,因为本来这样的代码逻辑就不对。
2. 范型擦除
泛型被引入到Java语言中,以便在编译时提供更严格的类型检查并支持通用编程,为了实现泛型,Java编译器将类型擦除应用于:
- 如果类型参数是无界的,则用它们的边界或Object替换泛型类型中的所有类型参数,因此,生成的字节码仅包含普通的类、接口和方法。
- 如有必要,插入类型转换以保持类型安全。
- 生成桥接方法以保留扩展泛型类型中的多态性。
类型擦除确保不为参数化类型创建新类,因此,泛型不会产生运行时开销。
在类型擦除过程中,Java编译器将擦除所有类型参数,并在类型参数有界时将其每一个替换为第一个边界,如果类型参数为无界,则替换为Object。
那为什么Java编译器要进行类型擦除呢?只是为了不为参数化类型创建新类来不产生运行时开销吗?
由来:一开始java并没有泛型,后来1.5加入了泛型,为了能向前兼容(旧版本的jvm能解释运行新版本的.class文件)所以就采用了伪泛型——“泛型擦除”
,并一直保留了下来。
原理:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉
,擦除后会变成原始类型(去掉 <T>,将方法内的T擦除成Object)例如Generic<T>会被擦除成Generic。还需要注意的是,不同的通配符的擦除的方式也有不同:
口诀:【存入:取下界;取出:取上界】—or—【存下,取上】
-
当泛型作为方法的传入参数的时候,此时替换成通配泛型的下界,例如add方法
-
当泛型作为方法的返回参数的时候,此时替换成通配泛型的上界,例如get方法
List<? extends Integer> list1 = new ArrayList<Integer>();
list1.add(null); // 此时传入取<? extends Integer> 下界————无 所以只能传null,否则报错
Integer integer1 = list1.get(0); // // 此时返回取<? extends Integer> 上界————Integer
List<? super Integer> list2 = new ArrayList<Integer>();
list2.add(111); // 此时传入取<? super Integer> 下界——————Integer
Integer integer2 = (Integer) list2.get(0); // // 此时返回取<? super Integer> 上界————Object
参考:
官网关于通配符捕获的文章:Wildcard Capture and Helper Methods
对应的一个中文翻译:Java™ 教程(泛型通配符捕获和Helper方法)
官网关于类型擦除的介绍:Type Erasure
对应的一个中文翻译:Java™ 教程(类型擦除)