上一节讲到通配符,已经把大部分的使用场景都梳理了一遍。当然,使用通配符还有一些需要注意的地方。
在某些特定场景,编译器会自动推断通配符的类型。比如,一个List,在使用它具有泛型类型的方法时,就会从代码中自动推断出特定的类型 。这种情况称为通配符捕获。
以下示例编译时将会报错:(参数不匹配)
List<?> foo = new ArrayList<>();
foo.add(0, foo.get(0)); // error
这是因为编译器无法去人要插入列表中对象的类型 ,这就意味着编译器认为我们正在把错误的类型分配给变量。
在上面示例中 ,假设我们已知代码正在执行安全操作,那么怎么样解决编译器的错误呢?这时候就可以通过编写捕获通配符的私有帮助方法来解决:
private static void foo(List<?> i) {
// i.add(i.get(0));
helpWildcards(i);
}
private static <T> void helpWildcards(List<T> t) {
t.add(0, t.get(0));
}
public static void main(String[] args) {
List<?> foo = new ArrayList<>();
foo(foo);
}
其中helpWildcards就是通配符帮助方法,编译器在调用中使用推断来确定T是捕获变量。这时就可正常编译。
现在考虑 一个比较复杂的示例:
static class WildcardError {
public static void main(String[] args) {
}
static void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number tmp = l1.get(0);
l1.set(0, l2.get(0)); // error
l2.set(0, tmp); // error
}
}
这时上面两行代码就会报错,这时为什么呢?由上可知,l1和l2的泛型都是Number的子类型,这就导致了有不安全操作的可能性,比如:
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double> ld = Arrays.asList(10.10, 20.20);
swapFirst(li, ld);
因为li和ld的类型不一致,导致类型不安全。
在学习使用泛型编程时,更令人困惑的方面之一是确定何时使用上限的通配符以及何时使用下限的通配符。此页面提供了一些在设计代码时要遵循的准则。
为了便于讨论,将变量视为提供以下两个功能之一将很有帮助:“输入”变量:输入变量将数据提供给代码。想象一个具有两个参数的复制方法: copy(src, dest) 。src参数提供要复制的数据,因此它是输入参数。
“输出”变量:输出变量保存要在其它地方使用的数据。在复制示例copy(src, dest) 中,dest参数接受数据,因此它是输出参数。当然,某些变量既用于“输入”又用于“输出”目的(准则中也解决了这种情况)。
在决定是否使用通配符以及哪种类型的通配符时通配符准则:,可以使用“输入”和“输出”原理。以下列表提供了要遵循的准则:
通配符准则:
- 使用上限通配符定义输入变量,使用 extends 关键字。
- 使用下限通配符定义输出变量,使用 super 关键字。
- 如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
- 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符
List<Number> le = new ArrayList<>();
List<? extends Number> ln = le;
// ln.add(1223); // compile-time error
以上代码将会编译失败,因为List extends Number>
是List
的一个子类型,所以可以将le赋值给ln。但不能用ln进行一系列使用泛型方法的操作。不过可以进行以下操作:
ln.add(null)
ln.add(0, ln.get(0))
类型擦除机制也是java泛型中的一个重点和难点。Java语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。 为了实现泛型,Java编译器将类型擦除应用于:
类型擦除可以确保不会为参数化类型创建新的类,这样,泛型就不会产生运行时的额外开销。
static class Response<T> {
T data;
public Response(T data) {
this.data = data;
}
}
以上代码中,由于T是无界的,所以编译器在进行擦除时,会将T替换为Object:
static class Response {
Object data;
public Response(Object data) {
this.data = data;
}
}
下面使用限定类型参数
static class Response2<T extends Number> {
T data;
public Response2(T data) {
this.data = data;
}
}
java编译器将绑定 类型参数T替换为第一个绑定类Number:
static class Response2 {
Number data;
public Response2(Number data) {
this.data = data;
}
}
编译器编译时也会擦除通用方法中的类型参数。比如:
<T> T test(T t) {
return t;
}
因为T是无界,所以编译器在擦除时会变成:
Object test(Object t) {
return t;
}
为了验证以上示例,我们打开字节码来对比一下:
// class version 52.0 (52)
// access flags 0x21
public class bytecode/Test {
// compiled from: Test.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lbytecode/Test; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x0
// signature (TT;)TT;
// declaration: T test(T)
test(Ljava/lang/Object;)Ljava/lang/Object;
// ........
}
看最下面那一行
test(Ljava/lang/Object;)Ljava/lang/Object;
可知test方法传入了一个Object,并返回了一个Object。
有时类型擦除可能导致无法预料的情况。下面的示例提现了这种情况的发生方式。展示了编译器有时如何创建一个综合方法(桥接方法),作为类型擦除过程的部分。
给定以下两个类 :
class Test1<T> {
private T data;
public Test1(T data) {
this.data = data;
}
public void setData(T data) {
this.data = data;
}
}
class MyTest extends Test1<Integer> {
public MyTest(Integer data) {
super(data);
}
public void setData(Integer data) {
System.out.println("MyTest.setData");
super.setData(data);
}
}
乍一看没啥问题,我们这样调用试试:
public static void main(String[] args) {
MyTest myTest = new MyTest(2);
Test1 t1 = myTest;
t1.setData("hello world");
Integer x = myTest.data;
System.out.println("x ======= " + x);
}
类型擦除后,代码变为:
MyTest myTest = new MyTest(2);
Test1 t1 = (MyTest) myTest;
t1.setData("Hello");
Integer x = (String) myTest.data;
这时就会在最后一行报错类型转换异常。为什么会这么转换呢?执行代码会发生什么情况呢?
t1.setData("Hello");
导致在MyTest类的 对象上执行setData(Object)方法。(MyTest继承了Test1类的setData()方法)。在编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,编译器可能需要创建一个称为桥接方法的综合方法。你通常不必担心桥接方法,但是如果其中一个出现在堆栈跟踪中,你可能会感到困惑。类型擦除后,Test1和MyTest类变为:
public class Test1 {
public Object data;
public Test1(Object data) { this.data = data; }
public void setData(Object data) { this.data = data; }
}
public class MyTest extends Test1 {
public MyTest(Integer data) { super(data); }
public void setData(Integer data) { super.setData(data); }
}
类型擦除后 ,方法签名不匹配。Test变成了setData(object), 而MyTest变成了setData(integer)。所以,MyTest的setData()方法不会覆盖Test1的setData()方法。为了解决这个问题,斌仔类型擦除后保留泛型类型的多态性,Java编译器生成了一个桥接方法来确保子类型能按照预期工作。对于MyTest类来说,编译器为setData生成了以下桥接方法:
class MyTest extends Test1 {
/**
* 这里就是生成的桥接方法
*/
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
super.setData(data);
}
}
可以看到,在类型擦除后 ,MyTest类的桥接方法setData(Object data)与Node类的setData(Object data)方法具有相同的签名,它会委托给原来的setData(Integer data)方法。
具体化类型是其类型信息在运行时完全可用的类型。这包括基本类型,非通用类型,原始(raw)类型以及未绑定通配符的调用。
非具体化类型是指在编译时通过类型擦除法删除了信息的类型(对通用类型的调用没有被定义为非绑定通配符)。非具体化类型在运行时并不具备所有的信息。非具体化类型的例子是 List 和 List ;JVM在运行时无法区分这些类型。正如对通用类型的限制中所示,在某些情况下,非具体化类型不能使用:例如,在instanceof 表达式中,或者作为数组中的元素。
这里不懂没关系,先继续往下看。
当参数化类型的变量引用的对象不是该参数化类型的对象时,就会发生堆污染。如果程序执行某些操作会在编译时产生未经检查的警告,则会发生这种情况。如果在编译时(在编译时类型检查规则的范围内)或在运行时,无法确定涉及参数化类型的操作(例如,强制转换或方法调用)的正确性,则会生成未经检查的警告。例如,当混合原始(raw)类型和参数化类型时,或者执行未经检查的强制转换时,就会发生堆污染。
在正常情况下,当同时编译所有代码时,编译器会发出未经检查的警告,以引起你对潜在堆污染的注意。如果分别编译代码部分,则很难检测到堆污染的潜在风险。如果确保代码在没有警告的情况下进行编译,则不会发生堆污染。
我们把具体化类型和堆污染放在一起说。要注意一下,具有不可具体化形式参数的Varagrs方法可能会导致堆污染 。
比如:
private static class ArrayListBuilder {
public static <T> void addToList(List<T> list, T... ele) {
for (T x : ele) {
list.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l;
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0);
System.out.println(s);
}
}
public static void main(String[] args) {
List<String> stringListA = new ArrayList<>();
List<String> stringListB = new ArrayList<>();
ArrayListBuilder.addToList(stringListA, "7", "8", "9");
ArrayListBuilder.addToList(stringListB, "10", "11", "12");
List<List<String>> listOfStringLists = new ArrayList<>();
ArrayListBuilder.addToList(listOfStringLists, stringListA, stringListB);
ArrayListBuilder.faultyMethod(
Arrays.asList("hello"), Arrays.asList("world")
);
}
当编译到faultyMethod方法时,它将可变长参数转换为数组。但是java不循序创建参数化类型的数组。在方法 ArrayBuilder.addToList 中,编译器将varargs形式参数 T … 元素转换为形式参数 T[] 元素,即数组。但是,由于类型擦除,编译器将varargs形式参数转换为 Object[] 元素。因此,存在堆污染的可能性。
以下语句将varargs形式参数分配给 对象数组objectArgs:
Object[] objectArray = l;
该语句可能会导致堆污染。可以将与varargs形式参数l的参数化类型匹配的值分配给变量objectArray,从而可以将其分配给l。但是,编译器不会在此语句上生成未经检查的警告。当编译器将varargs形式参数 List… l 转换为形式参数 List[] l 时,已经生成了警告。此声明有效;变量l具有类型 List[] ,它是 Object[] 的子类型。因此,如果将任何类型的List对象分配各objectArray数组的时,编译器不会发出警告或错误 。如以下语句所示:
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
运行时,jvm在一下语句中引发ClassCastException:
String s = l[0].get[0]; // ClassCastException
class Pair<K,V> {
//...
}
创建对象是,不能使用基本类型替换参数K,V
Pair<int, double> p = new Pair<>();// error
只能使用基本类型的封装类:
Pair<Integer, Double> p = new Pair<>();
这样使用时,java编译器会自动装箱为具体的值。
比如:
public static <E> void append(List<E> list) {
E element = new E(); // error
list.add(element);
}
如有必要,可以通过反射创建类型参数的具体对象:
public static <E> void append(List<E> list,Class<E> cls) {
E element = cls.newInstance();
list.add(element);
}
public static void main(String[] args) {
List<String> l = new ArrayList<>();
append(l, String.class);
}
类的静态字段是该类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段。
比如:
public class Test<T> {
private static T t; // error
}
不能使用静态修饰符修饰泛型类型,否则将会被混淆:
// 反面示例
Test<String> phone = new Test<>
Test<Integer> pager = new Test<>();
Test<Double> pc = new Test<>();
因为静态字段t由String、Integer和Double共享,所以t的实际类型是什么?它不能同时是
String、Integer和Double。因此,你无法创建类型参数的静态字段。
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
传递给rtti方法的参数化类型的集合:
S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
运行时不跟踪类型参数,因此无法区分 ArrayList 和 ArrayList 之间的区别。你最多可以做的是使用无界通配符来验证列表是否为ArrayList:
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<?>) {
// ...
}
}
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.
如果你对通用列表尝试相同的操作,则会出现问题:
Object[] stringLists = new List<String>[]; // compiler error, but pretend
it's allowed
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should
be thrown,
// but the runtime can't detect
it.xxxxxxxxxx Object[] stringLists = new List<String>[]; // compiler error, but pretendit's allowedstringLists[0] = new ArrayList(); // OKstringLists[1] = new ArrayList(); // An ArrayStoreException shouldbe thrown,// but the runtime can't detectit.java
如果允许参数化列表的数组,那么前面的代码将无法抛出所需的 ArrayStoreException 。
泛型类不能直接或间接扩展 Throwable 类。例如,以下类将无法编译
class Test<T> extends Exception { /* ... */ } // error
方法无法捕获类型参数的实例:
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
但是,我们可以在throws子句 中使用类型参数:
class Test<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}
一个类不能有两个重载的方法,这些方法在类型擦除后将具有相同的签名。
// 错误示例
public class Example P{
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
重载将共享相同的类文件表示形式,并且将生成编译时错误。