通配符 ?
在泛型中,问号 ? 叫做通配符,它表示了未知的类型。在使用上,通配符可以用来定义参数类,字段或本地变量,有时也可以作为方法返回类型。
有了类型T,为何要引入通配符 ?
类型 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
遇到与前两个定义方式一样的问题。
-
总结:
- 使用范围不同:T 用来声明类、接口、方法的类型参数。通配符 ? 用来声明方法的参数类型、字段类型、局部变量类型等。
- 使用限制不同:T 表示为某个特定的类型。通配符可以被限定为某个特定类型 T 的子类型或父类型。
- 使用场景不同:T通常用于表示泛型类型的实际类型。通配符通常用于表示泛型类型的任意类型。
通配符的边界
通配符也可以分为 有界通配符 和 无界通配符 两种类型。
- 有界通配符,同样使用到关键字
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<Object>
作为方法的参数,即代码:
static void printListSize(List<Object> list) {
System.out.println("size: " + list.size());
}
这个方法定义,只能传入 List<Object>
类型的列表,不能用于打印 List<Integer>
List<String>
等类型列表的大小。要适用于所有类型列表大小的打印需求,可以将参数类型定义为 List<?>
,使用无界通配符作为参数 List 的类型参数。
static void printListSize(List<?> list) {
System.out.println("size: " + list.size());
}
上述两个方法定义的类型 List<Object>
与 List<?>
类型不同。List<Object>
列表中可以放入任意 Object 及其子类型对象。而List<?>
列表中只能放入 null
。
通配符和子类型
在前面的文章中有写过,有继承关系的两个类,在作为类型参数用以定义列表类型时,两个列表没有任何关系。
class A { // ... }
class B extends A { // ... }
上述代码,B 继承自 A,在若将这两个类型作为 List
的类型参数,List<A>
与 List<B>
确是没有任何的关系。
使用通配符定义,可以将 List<A>
定义修改为 List<? extends A>
,这样 List<B>
与 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与通配符的区别
-
泛型类型参数 T 是一种确定性的类型,表示在使用泛型类或方法时,指定了具体的类型参数。例如,
List<String>
中的 T 被明确地指定为String
类型,因此在编译时可以知道使用的是具体的类型。通配符表示不确定的类型,用于在泛型类型中接受多种可能的类型。通配符使用?表示,例如List<?>,表示可以接受任意类型的List。
-
泛型类型参数 T 可以用于读取和写入操作。可以根据具体的类型参数执行各种操作,例如读取、修改和写入元素。
通配符通常用于读取操作,可以从具有通配符类型的对象中读取元素,但无法添加或修改元素。这是因为编译器无法确定通配符的具体类型,无法保证类型安全性。
通配符使用准则
通配符在泛型的使用过程中有时会让人觉得困惑,下面可以看下几个通配符使用中的准则。
先了解下什么时"in","out"变量。
- in 变量主要用于数据的输入,只能被读取,不能被修改。
- out 变量可用于他处,数据被修改。
- 既是 in,又是 out 变量。
使用的注意:
- 一个 “in” 变量的定义使用上界通配符,即使用
? extends T
。 - 一个 “out” 变量的定义使用下界通配符,即使用
?super T
。 - 可使用
Object
类中方法访问的 “in” 变量的情况下,使用无界通配符。 - 代码中需要访问的变量既作 “in” 又作 “out” 变量,不使用通配符。
上述几条使用注意项不适用于方法返回值。假设方法的返回类型定义使用了通配符,那么程序中的其他位置需要手动处理通配符的问题。