Wildcards and Subtyping
(通配符和子类型)
如泛型,继承和子类型中所述,泛型类或接口不仅仅因为它们的类型之间存在关系而相关。但是,你可
以使用通配符在通用类或接口之间创建关系。
给定以下两个常规(非泛型)类:
class
A
{
/* ... */
}
编写以下代码是合理的:
B b = new B();
A a = b;
此示例显示常规类的继承遵循此子类型规则:如果
B
扩展了
A
,则类
B
是类
A
的子类型。此规则不适用于
通用类型:
List
<
B
>
lb
=
new
ArrayList
<>
();
List
<
A
>
la
=
lb
;
// compile-time error
假定
Integer
是
Number
的子类型,则
List
和
List
之间是什么关系?
![](https://i-blog.csdnimg.cn/blog_migrate/892b5391f05eba302e407c85d761b1d1.png)
尽管
Integer
是
Number
的子类型,但
List
不是
List
的子类型,实际上,这两种类型无关。
List
和
List
的公共父级是
List
。
为了在这些类之间创建关系,以便代码可以通过
List
的元素访问
Number
的方法,请使用上限通配
符:
List
<?
extends
Integer
>
intList
=
new
ArrayList
<>
();
List
<?
extends
Number
>
numList
=
intList
;
// OK. List<? extends Integer>
is a subtype of List<? extends Number>
由于
Integer
是
Number
的子类型,并且
numList
是
Number
对象的列表,因此
intList
(一个
Integer
对象的列表)和
numList
之间现在存在关系。下图显示了使用上下限通配符声明的几个
List
类之间的
关系。
![](https://i-blog.csdnimg.cn/blog_migrate/ae431b0a6d8e93ea7c8df2855c192113.png)
通配符使用准则
部分提供了有关使用上下限通配符的后果的更多信息。
Wildcard Capture and Helper Methods
(通配符捕获和帮助方
法)
在某些情况下,编译器会推断通配符的类型。例如,可以将列表定义为
List
,但是在评估表达式时,
编译器会从代码中推断出特定类型。这种情况称为通配符捕获。
在大多数情况下,你无需担心通配符捕获,除非你看到包含短语
“capture of”
的错误消息。
WildcardError
示例在编译时产生捕获错误:
import
java
.
util
.
List
;
public class
WildcardError
{
void
foo
(
List
<?>
i
) {
i
.
set
(
0
,
i
.
get
(
0
));
}
}
在此示例中,编译器将
i
输入参数处理为
Object
类型。当
foo
方法调用
List.set(int, E)
时,编译器
无法确认要插入列表中的对象的类型,并产生错误。当发生这种类型的错误时,通常意味着编译器认为
你正在将错误的类型分配给变量。为此,将泛型添加到
Java
语言中(以便在编译时强制类型安全)。
由
Oracle
的
JDK 7 javac
实现编译时,
WildcardError
示例将生成以下错误:
![](https://i-blog.csdnimg.cn/blog_migrate/008e2a71206a40a39e8caf9b11454684.png)
在此示例中,代码正在尝试执行安全操作,那么如何解决编译器错误?你可以通过编写捕获通配符的私
有帮助器方法来修复它。在这种情况下,你可以通过创建私有帮助器方法
fooHelper
来解决此问题,
如
WildcardFixed
中所示:
由于使用了辅助方法,编译器在调用中使用推断来确定
T
是
CAP
#
1
(捕获变量)。该示例现在可以成功
编译。
按照约定,辅助方法通常命名为
originalMethodNameHelper
。
现在考虑一个更复杂的示例
WildcardErrorBad
:
![](https://i-blog.csdnimg.cn/blog_migrate/65062d4fec3c6a2149ee0ff88a6e13e4.png)
在此的示例代码正在尝试不安全的操作。例如,考虑对
swapFirst
方法的以下调用:
List
<
Integer
>
li
=
Arrays
.
asList
(
1
,
2
,
3
);
List
<
Double
>
ld
=
Arrays
.
asList
(
10.10
,
20.20
,
30.30
);
swapFirst
(
li
,
ld
);
虽然
List
和
List
都满足了
List
的条件,但从
Integer
值列表中提取一个项并试图将其放入
Double
值列表中显然是不正确的。
使用
Oracle
的
JDK javac
编译器编译代码会产生以下错误:
![](https://i-blog.csdnimg.cn/blog_migrate/b5a4de297f2cd2760e5446337aa38954.png)
![](https://i-blog.csdnimg.cn/blog_migrate/894f2a72fa025cb4c0eefefac2ac6674.png)
没有解决此问题的辅助方法,因为代码根本上是错误的。
Guidelines for Wildcard Use
(通配符使用准则)
在学习使用泛型编程时,更令人困惑的方面之一是确定何时使用上限的通配符以及何时使用下限的通配
符。此页面提供了一些在设计代码时要遵循的准则。
为了便于讨论,将变量视为提供以下两个功能之一将很有帮助:
“
输入
”
变量
:输入变量将数据提供给代码。想象一个具有两个参数的复制方法:
copy(src, dest)
。
src
参数提供要复制的数据,因此它是输入参数。
“
输出
”
变量
:输出变量保存要在其它地方使用的数据。在复制示例
copy(src, dest)
中,
dest
参数接
受数据,因此它是输出参数。
当然,某些变量既用于
“
输入
”
又用于
“
输出
”
目的(准则中也解决了这种情况)。
在决定是否使用通配符以及哪种类型的通配符时,可以使用
“
输入
”
和
“
输出
”
原理。以下列表提供了要遵
循的准则:
通配符准则:
- 使用上限通配符定义输入变量,使用 extends 关键字。
- 使用下限通配符定义输出变量,使用 super 关键字。
- 如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
- 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为这会迫使程序员使用代码
来处理通配符。
由
List
定义的列表可以被非正式地认为是只读的,但这并不是一个严格的保证。假设你有以下两个
类。
![](https://i-blog.csdnimg.cn/blog_migrate/adacd8cb5a82f36e7a01190057c2a661.png)
考虑以下代码:
List
<
EvenNumber
>
le
=
new
ArrayList
<>
();
List
<?
extends
NaturalNumber
>
ln
=
le
;
ln
.
add
(
new
NaturalNumber
(
35
));
// compile-time error
因为
List
是
List
的一个子类型,所以可以将
le
赋给
ln
。但不能用
ln
将自然数添加到偶数列表中。可以
对该列表进行以下操作。
可以添加
null
。
可以调用
clear
。
可以获取迭代器(
iterator
)和调用
remove
。
可以捕获通配符和写入从列表中读取的元素。
你可以看到由
List
定义的列表不是最严格意义上的只读,但你可能会这样想,因为你不能在列表中存
储一个新的元素或改变一个现有的元素。
Type Erasure
(类型擦除)
Java
语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。 为了实现泛型,
Java
编译器
将类型擦除应用于:
如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界(上下限)或
Object
。因
此,产生的字节码仅包含普通的类,接口和方法。
必要时插入类型转换,以保持类型安全。
生成桥接方法以在扩展的泛型类型中保留多态。
类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
Erasure of Generic Types
(泛型类型的擦除)
在类型擦除过程中,
Java
编译器将擦除所有类型参数,如果类型参数是有界的,则将每个参数替换为其
第一个边界;如果类型参数是无界的,则将其替换为
Object
。
考虑以下表示单个链接列表中的节点的通用类:
![](https://i-blog.csdnimg.cn/blog_migrate/8a7598f334b43c448cd46e0f994060e6.png)
由于类型参数
T
是无界的,因此
Java
编译器将其替换为
Object
:
![](https://i-blog.csdnimg.cn/blog_migrate/603fd0d1aa3c26979bd4148ce876cfa3.png)
在下面的示例中,通用
Node
类使用限定类型参数:
![](https://i-blog.csdnimg.cn/blog_migrate/2de7c6fef7f893b683f88779b4ed783f.png)
Java
编译器将绑定类型参数
T
替换为第一个绑定类
Comparable
:
![](https://i-blog.csdnimg.cn/blog_migrate/af6b8dadecca61d1939a379c75f0648d.png)
Erasure of Generic Methods
(通用方法的擦除)
Java
编译器还会擦除通用方法参数中的类型参数。考虑以下通用方法:
![](https://i-blog.csdnimg.cn/blog_migrate/09f14ac565277d3017337928101ceeba.png)
假设定义了以下类:
class
Shape
{
/* ... */
}
class
Circle
extends
Shape
{
/* ... */
}
class
Rectangle
extends
Shape
{
/* ... */
}
你可以编写一个通用方法来绘制不同的形状:
public static
<
T
extends
Shape
>
void
draw
(
T shape
) {
/* ... */
}
Java
编译器用
Shape
替换
T
:
public static
void
draw
(
Shape shape
) {
/* ... */
}
Effffects of Type Erasure and Bridge Methods
(类型擦除和桥接
方法的影响)
有时类型擦除会导致可能无法预料的情况。以下示例显示了这种情况的发生方式。该示例(在
“
桥接方
法
”
中进行了介绍)展示了编译器有时如何创建一个综合方法,称为桥接方法,作为类型擦除过程的一
部分。
给定以下两类
![](https://i-blog.csdnimg.cn/blog_migrate/d516eb06684b03f400278b1939649b05.png)
考虑以下代码:
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")
;
导致在
MyNode
类的对象上执行
setData(Object)
方法(
MyNode
类继承
了
Node
的
setData(Object)
)。
在
setData(Object)
的主体中,将
n
引用的对象的数据字段分配给
String
。
可以访问通过
mn
引用的同一对象的数据字段,并且该字段应该是整数(因为
mn
是
MyNode
,它
是
Node
)。
尝试将
String
分配给
Integer
会导致
Java
编译器在分配时插入的强制转换导致
ClassCastException
。
Bridge Methods
(桥接方法)
在编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,编译器可能需要创
建一个称为桥接方法的综合方法。你通常不必担心桥接方法,但是如果其中一个出现在堆栈跟踪中,你
可能会感到困惑。
类型擦除后,
Node
和
MyNode
类变为:
![](https://i-blog.csdnimg.cn/blog_migrate/7ee0542ba64b151896272998209ab112.png)
类型擦除后,方法签名不匹配。
Node
方法变为
setData(Object)
,而
MyNode
方法变为
setData(Integer)
。因此,
MyNode
的
setData
方法不会覆盖
Node
的
setData
方法。
为了解决此问题并在类型擦除后保留泛型类型的多态性,
Java
编译器生成了一个桥接方法来确保子类型
能够按预期工作。对于
MyNode
类,编译器为
setData
生成以下桥接方法:
![](https://i-blog.csdnimg.cn/blog_migrate/9f9597100594d29ef8b1a3e1b480c9ec.png)
可以看到,在类型擦除后,
MyNode
类的桥接方法(
setData(Object)
)与
Node
类的
setData(Object)
方法具有相同的方法签名,它委托给原来的
setData(Integer)
方法。
Non-Reififiable Types
(不可具体化类型)
“
类型擦除
”
部分讨论了编译器删除与类型参数和类型参数有关的信息的过程。类型擦除的结果与变量参
数(也称为
varargs
)方法有关,这些方法的
varargs
形式参数具有不可更改的类型。有关
varargs
方法的
更多信息,请参见
将信息传递给方法或构造方法
中的
任意参数数目
。
此页面涵盖以下主题:
不可具体化类型。
堆污染。
具有不可具体化形式参数的
Varargs
方法的潜在漏洞。
防止使用不可具体化形式参数的
Varargs
方法发出警告。
Non-Reififiable Types
(不可具体化类型)
具体化类型是其类型信息在运行时完全可用的类型。这包括基本类型,非通用类型,原始(
raw
)类型
以及未绑定通配符的调用。
非具体化类型是指在编译时通过类型擦除法删除了信息的类型(对通用类型的调用没有被定义为非绑定
通配符)。非具体化类型在运行时并不具备所有的信息。非具体化类型的例子是
List
和
List
;
JVM
在
运行时无法区分这些类型。正如
对通用类型的限制
中所示,在某些情况下,非具体化类型不能使用:例
如,在
instanceof
表达式中,或者作为数组中的元素。
Heap Pollution
(堆污染)
当参数化类型的变量引用的对象不是该参数化类型的对象时,就会发生堆污染。如果程序执行某些操作
会在编译时产生未经检查的警告,则会发生这种情况。如果在编译时(在编译时类型检查规则的范围
内)或在运行时,无法确定涉及参数化类型的操作(例如,强制转换或方法调用)的正确性,则会生成
未经检查的警告。例如,当混合原始(
raw
)类型和参数化类型时,或者执行未经检查的强制转换时,
就会发生堆污染。
在正常情况下,当同时编译所有代码时,编译器会发出未经检查的警告,以引起你对潜在堆污染的注
意。如果分别编译代码部分,则很难检测到堆污染的潜在风险。如果确保代码在没有警告的情况下进行
编译,则不会发生堆污染。
Potential Vulnerabilities of Varargs Methods with Non-Reififiable Formal
Parameters
(具有不可具体化形式参数的
Varargs
方法的潜在漏洞)
包含
vararg
输入参数的泛型方法可能导致堆污染。
考虑以下
ArrayBuilder
类:
![](https://i-blog.csdnimg.cn/blog_migrate/55542fea2a653eac14b8d21ea52eee3f.png)
以下示例
HeapPollutionExample
使用
ArrayBuiler
类:
![](https://i-blog.csdnimg.cn/blog_migrate/1924ea89ee768742cbe6a72823a12e4a.png)
编译后,
ArrayBuilder.addToList
方法的定义会产生以下警告:
warning
: [
varargs
]
Possible heap pollution from parameterized vararg type T
当编译器遇到
varargs
方法时,它将
varargs
形式参数转换为数组。但是,
Java
编程语言不允许创建参数
化类型的数组。在方法
ArrayBuilder.addToList
中,编译器将
varargs
形式参数
T ...
元素转换为形
式参数
T[]
元素,即数组。但是,由于类型擦除,编译器将
varargs
形式参数转换为
Object[]
元素。
因此,存在堆污染的可能性。
以下语句将
varargs
形式参数
l
分配给对象数组
objectArgs
:
Object
[]
objectArray
=
l
;
该语句可能会导致堆污染。可以将与
varargs
形式参数
l
的参数化类型匹配的值分配给变量
objectArray
,
从而可以将其分配给
l
。但是,编译器不会在此语句上生成未经检查的警告。当编译器将
varargs
形式参
数
List... l
转换为形式参数
List[] l
时,已经生成了警告。此声明有效;变量
l
具有类型
List[]
,
它是
Object[]
的子类型。
因此,如果将任何类型的
List
对象分配给
objectArray
数组的任何数组组件,则编译器不会发出警告或
错误,如以下语句所示:
objectArray
[
0
]
=
Arrays
.
asList
(
42
);
该语句将
List
对象分配给
objectArray
数组的第一个数组组件,该
List
对象包含一个
Integer
类型的
对象。
假设你使用以下语句调用
ArrayBuilder.faultyMethod
:
ArrayBuilder
.
faultyMethod
(
Arrays
.
asList
(
"Hello!"
),
Arrays
.
asList
(
"World!"
));
在运行时,
JVM
在以下语句中引发
ClassCastException
:
// ClassCastException thrown here
String
s
=
l
[
0
].
get
(
0
);
存储在变量
l
的第一个数组组件中的对象的类型为
List
,但是此语句期望使用类型为
List
的对象。
Prevent Warnings from Varargs Methods with Non-Reififiable Formal
Parameters
(防止使用不可具体化形式参数的
Varargs
方法发出警告)
如果你声明具有参数化类型参数的
varargs
方法,并确保由于对
varargs
形式参数的处理不当,该方法的
主体不会引发
ClassCastException
或其它类似的异常,则可以避免警告编译器通过为静态和非构造方
法声明添加以下注解,为此类
varargs
方法生成:
@SafeVarargs
@SafeVarargs
注解是该方法契约的书面部分;该注解断言该方法的实现不会不适当地处理
varargs
形
式参数。
尽管不太理想,但也可以通过在方法声明中添加以下内容来抑制此类警告:
@SuppressWarnings
({
"unchecked"
,
"varargs"
})
但是,这种方法不能抑制从该方法的调用站点生成的警告。如果你不熟悉
@SuppressWarnings
语法,
请参阅
注解
。
Restrictions on Generics
(对泛型的限制)
为了有效地使用
Java
泛型,必须考虑以下限制:
无法实例化具有基本类型的泛型类型。
无法创建类型参数的实例。
无法声明类型为类型参数的静态字段。
无法将
Casts
或
instanceof
与参数化类型一起使用。
无法创建参数化类型的数组。
无法创建,捕获或抛出参数化类型的对象。
无法重载每个重载的形式参数类型都擦除为相同原始(
raw
)类型的方法。
Cannot Instantiate Generic Types with Primitive Types
(无法实例化具有基
本类型的泛型类型)
考虑以下参数化类型:
![](https://i-blog.csdnimg.cn/blog_migrate/0d20774aa6b39e3ace5711ff851cca79.png)
创建对对象时,不能用基本类型替换类型参数
K
或
V
:
Pair
<
int
,
char
>
p
=
new
Pair
<>
(
8
,
'a'
);
// compile-time error
你只能将非基本类型替换为类型参数
K
和
V
:
Pair
<
Integer
,
Character
>
p
=
new
Pair
<>
(
8
,
'a'
);
请注意,
Java
编译器自动将
8
装箱为
Integer.valueOf(8)
,将
'a'
装箱为
Character('a')
:
Pair
<
Integer
,
Character
>
p
=
new
Pair
<>
(
Integer
.
valueOf
(
8
),
new
Character
(
'a'
));
有关自动装箱的更多信息,请参阅
数字和字符串
课程中的
自动装箱和拆箱
。
Cannot Create Instances of Type Parameters
(无法创建类型参数的实例)
你不能创建类型参数的实例。例如,以下代码会导致编译时错误:
public static
<
E
>
void
append
(
List
<
E
>
list
) {
E elem
=
new
E
();
// compile-time error
list
.
add
(
elem
);
}
解决方法是,可以通过反射创建类型参数的对象:
public static
<
E
>
void
append
(
List
<
E
>
list
,
Class
<
E
>
cls
)
throws
Exception
{
E elem
=
cls
.
newInstance
();
// OK
list
.
add
(
elem
);
}
你可以按以下方式调用
append
方法:
List
<
String
>
ls
=
new
ArrayList
<>
();
append
(
ls
,
String
.
class
);
Cannot Declare Static Fields Whose Types are Type Parameters
(无法声明
类型为类型参数的静态字段)
类的静态字段是该类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段。考
虑以下类别:
public class
MobileDevice
<
T
>
{
private static
T os
;
// ...
}
如果允许使用类型参数的静态字段,那么以下代码将被混淆:
MobileDevice
<
Smartphone
>
phone
=
new
MobileDevice
<>
();
MobileDevice
<
Pager
>
pager
=
new
MobileDevice
<>
();
MobileDevice
<
TabletPC
>
pc
=
new
MobileDevice
<>
();
因为静态字段
os
由
Smartphone
、
Pager
和
TabletPC
共享,所以
os
的实际类型是什么?它不能同时是
Smartphone
、
Pager
和
TabletPC
。因此,你无法创建类型参数的静态字段。
Cannot Use Casts or instanceof With Parameterized Types
(无法将
Casts
或
instanceof
与参数化类型一起使用)
因为
Java
编译器会擦除通用代码中的所有类型参数,所以你无法验证在运行时使用的是通用类型的参数
化类型:
![](https://i-blog.csdnimg.cn/blog_migrate/20d6d6d951d1881059283ac6c025940a.png)
传递给
rtti
方法的参数化类型的集合是:
S
=
{
ArrayList
<
Integer
>
,
ArrayList
<
String
>
LinkedList
<
Character
>
, ... }
运行时不跟踪类型参数,因此无法区分
ArrayList
和
ArrayList
之间的区别。你最多可以做的是使用
无界通配符来验证列表是否为
ArrayList
:
![](https://i-blog.csdnimg.cn/blog_migrate/8e62fcb3d92bef255feea7d721d9443d.png)
通常,除非使用不受限制的通配符对其进行参数化,否则无法将其转换为参数化类型。例如:
List
<
Integer
>
li
=
new
ArrayList
<>
();
但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:
List
<
String
>
l1
=
...;
ArrayList
<
String
>
l2
=
(
ArrayList
<
String
>
)
l1
;
// OK
Cannot Create Arrays of Parameterized Types
(无法创建参数化类型的数
组)
你不能创建参数化类型的数组。例如,以下代码无法编译:
List
<
Integer
>
[]
arrayOfLists
=
new
List
<
Integer
>
[
2
];
// compile-time error
以下代码说明了将不同类型插入到数组中时发生的情况:
Object
[]
strings
=
new
String
[
2
];
strings
[
0
]
=
"hi"
;
// OK
strings
[
1
]
=
100
;
// An ArrayStoreException is thrown.
如果你对通用列表尝试相同的操作,则会出现问题:
![](https://i-blog.csdnimg.cn/blog_migrate/87a6ad00cf5d564c5f8b99a51605eea2.png)
如果允许参数化列表的数组,那么前面的代码将无法抛出所需的
ArrayStoreException
。
Cannot Create, Catch, or Throw Objects of Parameterized Types
(无法创
建,捕获或抛出参数化类型的对象)
泛型类不能直接或间接扩展
Throwable
类。例如,以下类将无法编译:
![](https://i-blog.csdnimg.cn/blog_migrate/01a1794b29b33b5a07edca310ce7a6d8.png)
方法无法捕获类型参数的实例:
![](https://i-blog.csdnimg.cn/blog_migrate/fe39532a2ff9e95d4157966bc9f5ff6d.png)
但是,你可以在
throws
子句中使用类型参数:
![](https://i-blog.csdnimg.cn/blog_migrate/67acaa6b12e67e81f3c03afa6c6c6d50.png)
Cannot Overload a Method Where the Formal Parameter Types of Each
Overload Erase to the Same Raw Type
(无法重载每个重载的形式参数类型都
擦除为相同原始(
raw
)类型的方法)
一个类不能有两个重载的方法,这些方法在类型擦除后将具有相同的签名。
public class
Example
{
public
void
print
(
Set
<
String
>
strSet
) { }
public
void
print
(
Set
<
Integer
>
intSet
) { }
}
重载将共享相同的类文件表示形式,并且将生成编译时错误。