无限制通配符
无限通配符即: <?>
,主要在不确定或不关心实际参数类型时使用,如:
1 | public boolean removeAll(Collection<?> c){ |
由于它不确定具体类型,所以不能将任何元素(Null
除外)放入,即它是只读的,但在很多情况下需要放入对象,因此一种比较常见的方法是使用 类型参数 作为辅助函数
1 | public static void swap(List<?> list, int i, int j){ |
那么 List<?>
和 List<Object>
有什么区别呢?
List<Object>
已经指定了类型的参数,而泛型具有不变性,所以它只能传入参数类型为Object
1
2
3
4
5
6test(new ArrayList<Object>()); // 正确
test(new ArrayList<String>()); // 错误
void test(List<Object> list){
System.out.println(list);
}而
List<?>
是无限制通配符类型,它可以表示为任意的实际的类型参数1
2
3
4
5
6test(new ArrayList<Object>()); // 正确
test(new ArrayList<String>()); // 正确
void test(List<?> list){
System.out.println(list);
}List<Object>
的类型参数已经确定,所以可以对其中的元素进行诸如get
,add
、remove
等操作1
2
3
4
5void test(List<Object> list){
list.add("str");
list.add(1);
list.remove("str");
}而
List<?>
是只读的,不能add
,只能get
,
remove
操作(当然可以使用上述的辅助函数实现add
),且返回元素都是Object
类型的1
2
3
4void test(List<?> list){
list.get(0);
list.remove("str");
}
所以一般情况下,使用无限制通配符的优先级大于 Object
作为类型参数
有限制通配符
java
泛型有两种有限制通配符,<? extends E>
和 <? super E>
,那么它们的作用是什么呢?这要从协变和逆变说起。
协变(Covariance)和逆变(Contravariance)
逆变与协变用来描述类型转换(type transformation)后的继承关系
- 协变:具有子类型关系之间的类型经过“类型转换”后,所构造出更复杂的类型之间仍保持着子类型关系。
- 逆变:具有子类型关系之间的类型经过“类型转换”后,所构造的更复杂的类型之间建立了逆向子类型关系。
- 不变:具有子类型关系之间的类型经过“类型转换”后,所构造的更复杂的类型之间没有任何关系
这里指的类型转换代表指的是从一种类型构造为另一种新的类型,如 String
到 String[]
,String
到 List<String>
。在 java
中,泛型具有不变性,如
1 | Number number = new Integer(0); // True |
那么问题的答案就出现了:有限制通配符是为了实现泛型的协变与逆变
<? extends E>
实现了泛型的协变:1
ArrayList<? extends Number> arrayList = new ArrayList<Integer>();
<? super E>
实现了泛型的逆变:1
ArrayList<? super Number> arrayList = new ArrayList<Object>();
extends 与 super
有限制通配符有它的局限性,看一个例子:
1 | List<? extends Number> list = new ArrayList<Integer>(); |
我们通过 extends
通配符构建了对象,但是却不能插入 Integer
类型的元素,这看起来很不合理。其实这是可以理解的,首先看一些 List
类的 add
方法接口
1 | public interface List<E> extends Collection<E> { |
在调用 add
方法时,泛型 E
自动变成了 <? extends Number>
,也就是说其类型是 Number
的子类中的一个(不含 Number
),因此 add
一个 Integer
类型对象是错误的。如果要实现 add
一个 Interger
对象,可以使用 super
关键字
1 | List<? super Number> list = new ArrayList<Object>(); |
<? super Number>
代表其持有的类型是 Number
的父类,那么 add
一个 Integer
类型对象是正确的。所以我们又可以总结出:
<? extends Number>
是只读的
<? super Number>
是只写的
应用
那么究竟什么时候用 extends
,什么时候用 super
呢?其实很简单,遵循 PECS
原则
PECS: producer-extends, consumer-super. 换句话说:
- 如果要从泛型类取数据时,用
extends
- 如果要往泛型类写数据时,用
super
举几个例子,首先看 java.util.AbstractList
的 addAll
方法
1 | public boolean addAll(int index, Collection<? extends E> c) { |
addAll
方法需要将传入的泛型类中的所有元素保存到当前集合中,因此将从泛型类读取所有元素,所以使用 extends
。又如 java.util.Collections
的 copy
方法
1 | public static <T> void copy(List<? super T> dest, List<? extends T> src) { |
copy
方法将一个集合中的元素拷贝到另一个集合,完美的诠释了有限制通配符的使用。
拓展
考虑以下代码:
1 | public static <T extends Comparable<T>> T max(Collection<T> coll){ |
它的作用是对集合中的元素进行排序,那么我们需要对传入的集合中的元素进行限定,它需要能够进行比较,即实现 Comparable
接口,<T extends Comparable<T>>
的作用就是如此。但在继承关系中,上述声明会出现错误,考虑以下情况:
1 | class Fruit implements Comparable<Fruit> { |
Apple
类继承了 Fruit
类,但是它没有实现 Comparable<Apple>
,而是实现了 Comparable<Fruit>
,因此它不符合 <T extends Comparable<T>>
要求,因此不能对 Apple
集合使用
1 | List<Apple> list = new ArrayList<>(); |
因此,为了能够对这种情况予以支持,需要使用如下声明:
1 | public static <T extends Comparable<? super T>> T max(Collection<T> coll) |
<T extends Comparable<? super T>>
的限定含义是:
T implements Comparable<T>
T implements Comparable<X>
,其中X
是T
的父类
其实以之前的
PECS
原则也能很好的解释,无论是Comparable
还是Comparator
,它们的方法都需要写数据,即向泛型类写数据,所以需要使用<? super T>
,所以使用Comparator
的声明为:
1public static <T> T max(Collection<T> coll, Comparator<? super T> c)
其实到这里为止,这个 API
已经能够支持大部分情况了。但是假设我们需要传入的泛型集合是 T
的子类,将会仍然编译错误
1 | Algorithm.<Fruit>max(list); // T为Fruit,传入的是List<Apple>,编译错误 |
这就是之前说的泛型的不变性问题,因此,最灵活的声明是:
1 | public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) |
也许你不太明白我们这样使用它的意义,为什么一定要强制加一个 <Fruit>
呢?看起来没什么必要,其实这是在模拟一种情况,上述的声明等同于如下:
1 | class Algorithm<T extends Comparable<? super T>>{ |
这样的调用是不是就比较常见了
1 | List<Apple> list = new ArrayList<>(); |
其实 java.util.Collections
的 max
方法可以看到类似的声明:
1 | public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) { |
其中使用 Object &
主要是因为泛型擦除,编译生成的 Java
字节码中是不包含泛型中的类型信息,泛型类型会被 Object
所代替(无限制通配符也用 Object
),而有限制通配符则会被第一个边界的类型变量来替换,如上面的声明会被 Comparable
所代替,使用了 Object &
后将被 Object
所代替,参看 Why is T bounded by Object in the Collections.max() signature?
泛型单例类
实现一个泛型单例类,使得它能够对于传入任意类型的 Class
对象都创建一个单例对象
1 | public class Singleton { |
创建 Persion
类
1 | public class Person { |
将它传入泛型单例类,每次 getInstance
可以得到相同的对象
1 | System.out.println(Singleton.getInstance(Person.class) == Singleton.getInstance(Person.class)); // True |
最佳实践
1. 不应使用原生态类型
原生态类型即不带实际类型参数的泛型名称,如 List<E>
的原生态类型为 List
。它逃避了泛型检查,当你不小心插入了类型错误的对象,在运行时转换对象会出现 ClassCastException
。因此应该摈弃这样的做法,取而代之使用泛型,优点有以下两点:
- 在编译期间进行类型检查
- 获取对象不需要手动转换类型
例子如下:
1 | // 原生态类型,不推荐 |
2. 不要使用通配符类型作为返回类型
使用通配符类型作为返回类型将会强制在客户端代码中使用通配符类型,如:
1 | // 使用通配符类型作为返回类型,不推荐 |
3. 泛型无法使用instanceof
由于泛型擦除,编译期间会擦除类型参数,所以不能使用 instanceof
1 | List<Apple> list = new ArrayList<>(); |
而对于无限制通配符是可以使用 instanceof
的
1 | List<?> list = new ArrayList<>(); |
当然尖括号和 ?
有些多余,可以直接判断
1 | list instanceof List |