这是由于Java类型擦除造成的。要回答这个问题,我需要解释无界通配符、有界通配符和类型擦除。如果您对此已经熟悉,可以随意跳过任何部分。
本文内容来自Java文档。
1. 无界通配符
无界通配符类型使用通配符字符(
?
)指定,例如
List<?>
。这被称为未知类型的列表。有两种情况下,无界通配符是一种有用的方法:
- 如果您正在编写一个可以使用Object类提供的功能实现的方法。
- 当代码在使用不依赖于类型参数的泛型类中的方法时。例如,List.size或List.clear。事实上,Class<?>经常被使用,因为Class<T>中的大多数方法不依赖于T。
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()
只能在完全是Shape的列表上调用:例如,它不能在
List<Circle>
上调用。这很不幸,因为该方法只是从列表中读取形状,所以它同样可以在
List<Circle>
上调用。我们真正想要的是该方法接受任何类型的形状列表。
public void drawAll(List<? extends Shape> shapes) {
...
}
这里有一个很小但非常重要的区别:我们将类型
List<Shape>
替换为
List<? extends Shape>
。现在,
drawAll()
将接受任何
Shape
子类的列表,因此我们现在可以在
List<Circle>
上调用它,如果需要的话。
List<? extends Shape>
是有界通配符的一个示例。
?
代表未知类型,但在这种情况下,我们知道这个未知类型实际上是
Shape
的子类型。 (注意:它可以是
Shape
本身,或者一些子类;它不一定是
Shape
的直接子类。)我们说
Shape
是通配符的上限。
同样地,语法
? super T
表示有一个未知类型,它是T的超类型,这被称为有界通配符。
例如,ArrayedHeap280包含
ArrayedHeap280<Integer>
,
ArrayedHeap280<Number>
和
ArrayedHeap280<Object>
。
正如你在
java documentation for Integer class中看到的那样,Integer是Number的子类,而Number又是Object的子类。
3. 类型擦除和ClassCastException
泛型是引入到Java语言中用于提供更严格的类型检查并支持泛型编程的。为了实现泛型,Java编译器对以下内容进行类型擦除:
- 将泛型类型中的所有类型参数替换为它们的边界或Object(如果类型参数没有边界)。因此,生成的字节码仅包含普通类、接口和方法。
- 必要时插入类型转换以保留类型安全性。
- 生成桥接方法以保留扩展泛型类型中的多态性。
在类型擦除过程中,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
和
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
n.setData("Hello")
Integer x = mn.data
在类型擦除后,此代码变为:
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") 导致对类MyNode的对象执行setData(Object)方法。(MyNode类从Node继承了setData(Object)方法。)
- 在setData(Object)方法的方法体中,
- 引用n所指向的对象的data字段被赋值为一个字符串。
以下是更多内容:
- 相同对象(通过mn引用),也可以访问该对象的data字段,该字段应该是一个整数(因为mn是个MyNode,它是一个Node)。试图给Integer类型的变量赋一个字符串会导致Java编译器在赋值时插入一个转换,最终导致ClassCastException。
heap = (K[]) new Comparable[capacity];
- user2692465Element<K, V>[] heap = (Element<K, V>[]) new Element[size];
吗? - sgbj