Java范型那些事(四)

之前写的Java范型相关文章:

Java范型那些事(一)

Java范型那些事(二)

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™ 教程(类型擦除)

Java 理论与实践:使用通配符简化泛型使用
Java中泛型区别以及泛型擦除详解
Java™ 教程(泛型的限制)

发布了82 篇原创文章 · 获赞 86 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/102053961