和
是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
- :是指 “上界通配符(Upper Bounds Wildcards)”
- :是指 “下界通配符(Lower Bounds Wildcards)”
一、为什么要用通配符和边界?–泛型不是协变的
开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收 List
虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。
比如我们有Fruit类,和它的派生类Apple
class Fruit {} class Apple extends Fruit {}
然后有一个最简单的容器:Plate类,盘子里可以放一个泛型的”东西”
我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。
class Plate
{ private T item; public Plate(T t){item=t;} public void set(T t){item=t;} public T get(){return item;} }
现定义一个“水果盘”,逻辑上水果盘当然可以装苹果。
Plate
p=new Plate
(new Apple());
但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。
error: incompatible types: Plate
cannot be converted to Plate
实际上,编译器认定的逻辑是这样的:
- 苹果 IS-A 水果
- 装苹果的盘子 NOT-IS-A 装水果的盘子
所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系。
所以我们不可以把Plate
。
泛型不是协变的
在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Number,那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number是 Integer 的超类型,那么 Number[] 也是 Integer[]的超类型)。
您也许认为这一原理同样适用于泛型类型 —— List< Number> 是 List< Integer> 的超类型,那么可以在需要 List< Number> 的地方传递 List< Integer>。不幸的是,情况并非如此。为啥呢?这么做将破坏要提供的类型安全泛型。
类型擦除
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List
public class Test { public static void main(String[] args) { List
strList = new ArrayList<>(); List
intList = new ArrayList<>(); System.out.println(strList.getClass().getName()); System.out.println(intList.getClass().getName()); } }
上面这一段代码,运行后输出如下,可知在运行时获取的类型信息是不带具体类型的:
java.util.ArrayList java.util.ArrayList
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
- 泛型类并没有自己独有的 Class 类对象。比如并不存在 List
。
.class 或是 List
.class,而只有 List.class,因此在运行时无法获得泛型的真实类型信息
- 静态变量是被泛型类的所有实例所共享的。对于声明为 MyClass
的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过 new MyClass
还是 new MyClass
创建的对象,都是共享一个静态变量。
- 泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型 MyException
和 MyException
的。对于 JVM 来说,它们都是 MyException 类型的。也就无法执行与异常对应的 catch 语句。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get();List
就变成了 List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:
class MyString implements Comparable
{ public int compareTo(String str) { return 0; } }
当类型信息被擦除之后,上述类的声明变成了 class MyString implements Comparable。但是这样的话,类 MyString 就会有编译错误,因为没有实现接口 Comparable 声明的 int compareTo(Object) 方法。这个时候就由编译器来动态生成这个方法。
实例分析
了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的 List
public void inspect(List
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/225515.html原文链接:https://javaforall.net
