发生这种情况是因为 Java 类型擦除。
为了回答这个问题,我需要解释一下无界通配符、有界通配符和类型擦除。如果您熟悉任何部分,请随意跳过。
这篇文章的内容是根据 java 文档汇编而成的。
1.无限通配符
无界通配符类型使用通配符 (?
), 例如,List<?>
。这称为未知类型列表。在两种情况下,无界通配符是一种有用的方法:
2. 有界通配符
考虑一个可以绘制矩形和圆形等形状的简单绘图应用程序。为了在程序中表示这些形状,您可以定义一个类层次结构,如下所示:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
这些类可以在画布上绘制:
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
任何绘图通常都会包含许多形状。假设它们被表示为一个列表,那么在 Canvas 中有一个方法来绘制它们将会很方便:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
现在,类型规则说drawAll()
只能在完全形状的列表上调用:例如,它不能在List<Circle>
。这是不幸的,因为该方法所做的只是从列表中读取形状,所以它也可以在List<Circle>
。我们真正想要的是该方法接受任何形状的列表:
public void drawAll(List<? extends Shape> shapes) {
...
}
这里有一个很小但非常重要的区别:我们替换了类型List<Shape>
with List<? extends Shape>
. Now drawAll()
将接受任何子类的列表Shape
,所以我们现在可以调用它List<Circle>
如果我们愿意的话。
List<? extends Shape>
是有界通配符的示例。这?
代表未知类型,但是,在这种情况下,我们知道这个未知类型实际上是 Shape 的子类型。 (注意:它可以是 Shape 本身,也可以是某个子类;它不需要从字面上扩展 Shape。)我们说 Shape 是上限通配符的。
类似地,语法? super T
是有界通配符,表示未知类型,是 T 的超类型。
例如,ArrayedHeap280 包括ArrayedHeap280<Integer>
, ArrayedHeap280<Number>
, and ArrayedHeap280<Object>
。
正如你在Integer 类的 java 文档 https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html,Integer 是 Number 的子类,而 Number 又是 Object 的子类。
整数类* java.lang.Object
* java.lang.Number
* java.lang.Integer
3. 键入擦除和ClassCastException
Java 语言中引入泛型是为了在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器将类型擦除应用于:
- 如果类型参数无界,则将泛型类型中的所有类型参数替换为其边界或对象。因此,生成的字节码仅包含普通的类、接口和方法。
- 如有必要,请插入类型转换以保持类型安全。
- 生成桥接方法以保留扩展泛型类型中的多态性。
在类型擦除过程中,Java 编译器会擦除所有类型参数,如果类型参数是有界的,则将其替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object。
考虑以下表示单链表中的节点的泛型类:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) }
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
```
>Because the type parameter T is unbounded, the Java compiler replaces it with Object:
```java
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
在以下示例中,泛型 Node 类使用有界类型参数:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
Java 编译器将有界类型参数 T 替换为第一个绑定类 Comparable:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
```
> Sometimes type erasure causes a situation that you may not have anticipated. The following example shows how this can occur.
>
> Given the following two classes:
```java
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
类型擦除后,Node
and MyNode
类变为:
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
考虑以下代码:
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = mn.data; // Causes a ClassCastException to be thrown.
类型擦除后,此代码变为:
MyNode mn = new MyNode(5);
Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.
执行代码时会发生以下情况:
-
n.setData("Hello");
导致方法setData(Object)
在类的对象上执行MyNode
. (The MyNode
类继承setData(Object)
from Node
.)
- 在体内
setData(Object)
,
the
data
所引用对象的字段n
被分配给一个String
.
- The
data
同一对象的字段,通过引用mn
,可以访问并且预计是Integer
(since mn
is a MyNode
这是一个Node<Integer>
.尝试分配一个String
to an Integer
导致ClassCastException
来自 Java 编译器在赋值时插入的强制转换。