在泛型中,问号 ? 叫做通配符,它表示了未知的类型。在使用上,通配符可以用来定义参数类,字段或本地变量,有时也可以作为方法返回类型。
类型 T 表示的是任意类型,表示的是某个具体的类型。通配符 ? 表示的是未知类型。我们可以从类/接口定义,变量,方法的不同角度去看下具体的区别。
泛型类型 T 表示泛型参数的类型。下面 Box 类是一个泛型类的定义。
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
也可以使用有界的定义方式限制泛型参数的类型范围。
// 1. 上界类型定义
class Box<T extends Number> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
// 2. 下界类型定义,下面的代码会报编译错误
class Box<T super Number> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
上面的代码 2 的定义会导致编译器报错,T super Number
表示 T 必须是 Number 类型或是 Number 的父类型。而泛型类在 Java编译器编译期间被类型擦除 (后续会有文章说明),变成 Object 类型,这时 Box 类就变成了可以包含任意类型的类,这与泛型设计的目的相悖,因为泛型类应该限制其所能存储的对象类型。
泛型的功能:
- 提高代码的安全性:泛型可以限制变量或方法的类型,从而避免类型转换错误。
- 提高代码的可读性:泛型可以使代码更加清晰和简洁。
- 提高代码的灵活性:泛型可以提高代码的复用性。
使用通配符的类型定义。
// 这个类的定义是错误的。Unexpected wildcard
class Box<? extends Number> {}
通配符 ? 表示了未知类型,在泛型类 Box 定义中无法表示具体的类型,不能用来表示类型进行使用。如下:
class Box<? extends Number> {
private ? t;
// ...
}
这个写法肯定是错误的。
类型 T 在泛型方法定义中使用。
<T> void processList(List<T> list) {
System.out.println("list size:" + (list == null ? 0: list.size()));
}
通配符 ? 在方法定义中的使用。方法涉及参数,返回值,方法体中的操作。
参数类型的限制:通配符可以用于限制方法参数的类型,从而提高代码的安全性和可读性。
以下代码定义了一个方法,参数类型是List extends Number>
,表示参数可以是List
的子类,并且参数中的元素可以是Number的子类:
public static <T> void printList(List<? extends T> list) {
for (T t : list) {
System.out.println(t);
}
}
返回类型的限制:通配符可以用于限制方法返回类型的类型,从而提高代码的灵活性。
以下代码定义了一个方法,返回类型是List extends T>
,表示返回值可以是List
的子类,并且返回值中的元素可以是T的子类:
public static <T> List<? extends T> getList() {
return new ArrayList<>();
}
方法体中的操作:通配符可以用于方法体中的操作,从而提高代码的可读性和复用性。
以下代码定义了一个方法,用于比较两个对象的大小:
public static <T extends Comparable<T>> int compare(T o1, T o2) {
return o1.compareTo(o2);
}
通配符使用不限于泛型方法,如下的代码定义了数字类型列表的和。
public static double sumOfList(List<? extends Number> list) { // 非泛型方法
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
public static void main(String[] args) {
System.out.println("sum=" + sumOfList(Arrays.asList(1,2,3)));
}
泛型类型 T 在定义变量的过程中,需要注意的有类型兼容性。
T t;
这样给出了声明变量的方式,但 T 并不是 Java 中的具体类型,因此在使用上一般会与类型定义一起使用。
或者使用类型 T 定义方法的参数类型。
通配符 ? 的变量声明。
无界的情况下。无界通配符?
表示变量可以存储任何类型。
List<?> list;
这样的声明方式是符合 Java 语法定义的,但是在实际定义使用中,会有问题。如下声明:
List<?> list = new ArrayList<>(); // 正确
list.add("123"); // capture of ?
这里 list
的实际类型是 ArrayList
,尝试向其中加入字符串 123
,Java 编译器会提示 通配符捕获 的错误提示,后面将具体介绍。 因为泛型的一个作用就是限制类型范围,而列表 list
中可以加入任意类型的对象,这与泛型的初衷相悖的。
上界通配符。上界通配符? extends T
表示变量可以存储类型 T 的子类。
List<? extends Number> list = new ArrayList<>(); // 正确
list.add(3);
list.add(6.5f); // capture of ? extends Number
同样,定义 list
是符合 Java 语法的,但尝试向 list
中添加元素时,Java 编译器就抛出了 通配符捕获 异常。
下界通配符。下界通配符? super T
表示变量可以存储类型 T 的父类。
List<? super String> list = new ArrayList<>(); // 正确
list.add("123"); // 正确
list.add(new Object()); // capture of ? super String
遇到与前两个定义方式一样的问题。
总结:
通配符也可以分为 有界通配符 和 无界通配符 两种类型。
extends
和 super
,写法上是 ? extends T
或 ? super T
,其中 T
表示某个类型。?
表示泛型类型。上界通配符(Upper Bounded Wildcards)使用 ? extends T
表示 T 的子类。上界通配符主要使用在方法定义中限制参数类型,返回值类型及变量声明中。
限制方法的参数类型。
下列代码中参数中可以传入任意 Number 类型及其子类型的列表。
// 方法参数类型
public static void printList(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
限制方法的返回类型。
// 方法定义的返回类型被限制成 CharSequence 及其父类型的列表。
List<? super CharSequence> processStringList(List<?> list) {
String elem1 = "789";
List<? super CharSequence> v = new ArrayList<>();
System.out.println("list size:" + (list == null ? 0: list.size()));
return v;
}
声明变量。
List<? extends Number> list = new ArrayList<>();
适用无界通配符的两个场景:
Object
中的定义功能可以实现的方法定义。List.size()
方法等。假设要定义一个打印列表大小的方法,不管类型参数。若使用 List
作为方法的参数,即代码:
static void printListSize(List<Object> list) {
System.out.println("size: " + list.size());
}
这个方法定义,只能传入 List
类型的列表,不能用于打印 List
List
等类型列表的大小。要适用于所有类型列表大小的打印需求,可以将参数类型定义为 List>
,使用无界通配符作为参数 List 的类型参数。
static void printListSize(List<?> list) {
System.out.println("size: " + list.size());
}
上述两个方法定义的类型 List
与 List>
类型不同。List
列表中可以放入任意 Object 及其子类型对象。而List>
列表中只能放入 null
。
在前面的文章中有写过,有继承关系的两个类,在作为类型参数用以定义列表类型时,两个列表没有任何关系。
class A { // ... }
class B extends A { // ... }
上述代码,B 继承自 A,在若将这两个类型作为 List
的类型参数,List
与 List
确是没有任何的关系。
使用通配符定义,可以将 List
定义修改为 List extends A>
,这样 List
与 List extends A>
存在继承关系。
使用通配符定义泛型类型时,在编译器推导实际类型时,会遇到通配符捕捉问题。
例如,定义一个 List>
类型类型变量,但实际操作时,添加如 String
类型或其他引用类型对象是,Java 编译器会报编译错误,即通配符捕捉提示,通常含提示文字 “capture of”。
看下列一个代码:
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0)); // Java编译器会提示 "capture of ?"
}
}
这段代码中,Java 编译器会提示 通配符捕捉错误,进而编译失败。编译时 i.get(0)
获取到的结果被 Java 编译器认为是 Object
类型的,使用 List>.set()
方法将获取的对象设置到列表中,但是 Java 编译器无法判断要添加到 i
的对象是什么具体类型,对于可能由于类型不同而导致的类型安全问题,Java 编译器给出错误提示,禁止编译通过。
要解决这种编译错误,需要定一个帮助方法,让编译器任务对象的类型时确定的。
public class WildcardError {
void foo(List<?> i) {
fooHelper(i);
}
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
泛型类型参数 T 是一种确定性的类型,表示在使用泛型类或方法时,指定了具体的类型参数。例如,List
中的 T 被明确地指定为 String
类型,因此在编译时可以知道使用的是具体的类型。
通配符表示不确定的类型,用于在泛型类型中接受多种可能的类型。通配符使用?表示,例如List>,表示可以接受任意类型的List。
泛型类型参数 T 可以用于读取和写入操作。可以根据具体的类型参数执行各种操作,例如读取、修改和写入元素。
通配符通常用于读取操作,可以从具有通配符类型的对象中读取元素,但无法添加或修改元素。这是因为编译器无法确定通配符的具体类型,无法保证类型安全性。
通配符在泛型的使用过程中有时会让人觉得困惑,下面可以看下几个通配符使用中的准则。
先了解下什么时"in","out"变量。
使用的注意:
? extends T
。?super T
。Object
类中方法访问的 “in” 变量的情况下,使用无界通配符。上述几条使用注意项不适用于方法返回值。假设方法的返回类型定义使用了通配符,那么程序中的其他位置需要手动处理通配符的问题。