7. 通配符
通配符,即 "?",用来表示未知类型。
通配符可用作各种情况:作为参数,字段或局部变量的类型;有时也作为返回类型;通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。
7.1 上限有界的通配符
使用上限通配符来放宽对变量的限制。
声明上限通配符的语法:<? extends 上限>
举个例子:
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
这里 上线通配符为 <? extends Number>,表明可以匹配任何 Number 类及其子类型。该方法返回列表中数字的总和。
7.2 无界通配符
使用通配符 "?" 指定无界通配符类型,例如 List<?>,称为未知类型的列表。
无界通配符有两种适用场景:
- 当前正在编写可以借由 Object 类中的方法来实现的方法。
- 使用泛型类中不依赖于类型参数的方法时,如 List.size()、list.clear() 等;实际上,经常使用 Class<?>,因为 Class<T> 中的大多数方法都不依赖于 T
考虑如下方法:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
如果只是希望打印 Object 列表,那这个方法可行;但是如果目标是打印任何类型的列表,那这个方法就不行了,它无法输出 List<\Integer>、List<\Double> 等等,因为这些都不是 List<\Object> 的子类型。
解决上述问题,就需要使用 List<?>:
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
这样,对于任何具体类型 A,List<A> 都是 List<?> 的子类型,于是可以用该方法打印任何类型的列表。
7.3 下限有界通配符
上限有界通配符将未知类型限制为该类型的特定类型或子类型,并使用 extends 关键字表示;类似的,下限有界通配符将位置类型限制为该类型的特定类型或超类型;
下限有界通配符的语法:<? super 下限>
注意:上限有界 和 下限有界 不能同时指定。
举个例子:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
在这个例子中,下限有界通配符是 <? super Integer>,表示可以匹配任何 Integer 的超类型列表。该方法用来将数字 1 ~ 10 添加到列表的末尾。
7.4 通配符和子类型
在 5.1 泛型类和子类型 小节中,我们讲到泛型类或接口并不仅仅因为它们的类型之间存在关系而相互关联。
比如 Integer 是 Number 的子类型,但是 List<Integer> 和 List<Number> 却没什么关系,二者不过是有一个公共符类 List<?>:
如果需要通过 List<Integer> 的元素访问 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 和 numList 之间存在关联了。下图展示了使用上限和下限有界通配符声明的多个 List 类之间的关系:(箭头指向的是父类型)
7.5 通配符捕获和帮助方法
某些情况下,编译期会推断出通配符的类型。例如,列表定义为 List<?>,但是检查一个表达式的时候,编译期会从代码中推断出特定类型,这就叫做通配符捕获。
多数情况下,不必担心通配符捕获的问题,除非遇到包含 "capture of"的错误消息。
如下例子会报错:
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 添加泛型机制用来保证编译时类型安全。
那么发生错误时,如果解决编译器错误呢?通常通过编写捕获通配符的私有的 Helper 方法,如下所示:
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
由于辅助方法,编译器使用推断来确定 T 是调用中的捕获的变量
7.6 通配符使用指南
实际开发中,通常关于何时使用上限有界通配符以及何时使用下限有界通配符存在很大疑惑。本节就介绍设计代码时要遵循的一些原则。
首先假设两种变量:
- "in" 变量:向代码提供数据。
- "out" 变量:保存数据供其他地方使用。
通配符指南
- 用上限通配符定义“in”变量。(使用
extends
关键字) - 用下限通配符定义“out”变量。(使用
super
关键字) - 在可以使用
Object
类中定义的方法访问“in”变量的情况下,使用无界通配符。 - 在代码需要通过“in”和“out”变量访问其他变量的情况下,不要使用通配符。
避免使用通配符作为返回类型。
8. 类型擦除
Java 中并没有实现真正的泛型。为了实现泛型,Java 编译器将类型擦除应用于:
- 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或
对象
。 因此,生成的字节码仅包含普通的类,接口和方法。 - 如有必要,插入类型强制转换以保持类型安全。
- 生成桥接方法以保留扩展泛型类型中的多态性。
8.1 泛型类的擦除
在类型擦除过程中,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; }
// ...
}
因为 T 无界,所以Java 编译器会用 Object 替换它:
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; }
// ...
}
再举个上限有界通配符的例子:
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 编译器会用第一个边界 Comparable 替换有界类型参数 T:
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; }
// ...
}
8.2 泛型方法的擦除
Java 编译器还会擦除泛型方法形参中的类型参数
1.举一个无界类型参数的例子
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
由于T无界,因此 Java 编译器会用 Object 替换之:
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
2.再举一个有界类型参数的例子:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
public static <T extends Shape> void draw(T shape) { /* ... */ }
上面编写了一个绘制不同形状的泛型方法,Java 编译器会将 T 替换为 Shape:
public static void draw(Shape shape) { /* ... */ }
8.3 不可具体化类型
8.3.1 什么是不可具体化类型
可具体化类型是指:类型信息在运行时完全可用,这v澳阔基元、非泛型类、原生类型和为绑定通配符的引用。
不可具体化的类型是指:运行时没有提供所有信息,会通过类型擦除删除部分编译时的信息。不可具体化类型的示例是 List<String> 和 List<Number>,运行时 JVM 无法区分这两个类型,它们都会编程 List 原生类型。
8.3.2 堆污染
当参数化类型的变量引用不是该参数化类型的对象时,会发生堆污染 。 如果程序执行某些操作,在编译时产生未经检查的警告,则会出现这种情况。 如果在编译时(在编译时类型检查规则的限制内)或在运行时,生成涉及参数化类型(例如,强制转换或方法调用)的操作的正确性,则会生成未经检查的警告。验证。 例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。
在正常情况下,当所有代码同时编译时,编译器会发出未经检查的警告,以引起您对潜在堆污染的注意。 如果单独编译代码的各个部分,则很难检测到堆污染的潜在风险。 如果确保代码在没有警告的情况下编译,则不会发生堆污染。
8.3.3 可变参数的潜在漏洞
带有可变参数的泛型方法可能导致堆污染。
考虑以下例子:
public class ArrayBuilder {
public static <T> void addToList (List<T> listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l; // Valid
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0); // ClassCastException thrown here
}
}
下面的类使用 ArrayBuilder:
public class HeapPollutionExample {
public static void main(String[] args) {
List<String> stringListA = new ArrayList<String>();
List<String> stringListB = new ArrayList<String>();
ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
List<List<String>> listOfStringLists =
new ArrayList<List<String>>();
ArrayBuilder.addToList(listOfStringLists,
stringListA, stringListB);
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
}
}
当编译器遇到可变参数的方法时,它会将可变参数转换为数组。 但是,Java编程语言不允许创建参数化类型的数组。在方法ArrayBuilder.addToList
,编译器将可变参数T... elements
转换为形式参数T[] elements
,即数组。 但是,由于类型擦除,编译器会将可变参数转换为Object[] elements
。 因此,存在堆污染的可能性。
9. 泛型的限制
-
无法使用基本数据类型实例化泛型类
class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // ... }
调用以下语句会报错:
Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
应该如下调用:
Pair<Integer, Character> p = new Pair<>(8, 'a');
传入的基本类型参数如8 会自动装箱。
-
*无法创建类型参数的实例
//以下代码错误 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);
-
无法声明类型为类型参数的静态字段
//不允许将 静态字段类型设置为类型参数的类型 public class MobileDevice<T> { private static T os; // ... }
-
无法强制转换或使用 instanceof
因为Java编译器会擦除通用代码中的所有类型参数,所以无法验证在运行时使用泛型类型的参数化类型。
-
特殊的,可以通过无界通配符验证是否属于某个基类型
public static void rtti(List<?> list) { if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type // ... } }
-
更特殊的,某些情况下,编译器知道类型参数始终有效并允许强制转换:
List<String> l1 = ...; ArrayList<String> l2 = (ArrayList<String>)l1; // OK
无法创建参数化类型的数组
-
无法直接或间接扩展 Throwable 类,无法捕获类型参数的实例
// Extends Throwable indirectly class MathException<T> extends Exception { /* ... */ } // compile-time error // Extends Throwable directly class QueueFullException<T> extends Throwable { /* ... */ // compile-time error public static <T extends Exception, J> void execute(List<J> jobs) { try { for (J job : jobs) // ... } catch (T e) { // compile-time error // ... } }
-
类型擦除到原生类型的方法无法重载
public class Example { public void print(Set<String> strSet) { } public void print(Set<Integer> intSet) { } }
以上方法在类型擦除后具有相同的签名,会在编译时报错。