泛型进阶

无限制通配符

无限通配符即: <?>,主要在不确定或不关心实际参数类型时使用,如:

1
2
3
public boolean removeAll(Collection<?> c){
...
}

由于它不确定具体类型,所以不能将任何元素(Null 除外)放入,即它是只读的,但在很多情况下需要放入对象,因此一种比较常见的方法是使用 类型参数 作为辅助函数

1
2
3
4
5
6
7
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}

public static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.get(j));
}

那么 List<?>List<Object> 有什么区别呢?

  1. List<Object> 已经指定了类型的参数,而泛型具有不变性,所以它只能传入参数类型为 Object

    1
    2
    3
    4
    5
    6
    test(new ArrayList<Object>());  // 正确
    test(new ArrayList<String>()); // 错误

    void test(List<Object> list){
    System.out.println(list);
    }

    List<?> 是无限制通配符类型,它可以表示为任意的实际的类型参数

    1
    2
    3
    4
    5
    6
    test(new ArrayList<Object>()); // 正确
    test(new ArrayList<String>()); // 正确

    void test(List<?> list){
    System.out.println(list);
    }
  2. List<Object> 的类型参数已经确定,所以可以对其中的元素进行诸如 get , addremove 等操作

    1
    2
    3
    4
    5
    void test(List<Object> list){
    list.add("str");
    list.add(1);
    list.remove("str");
    }

    List<?> 是只读的,不能 add ,只能 get , remove 操作(当然可以使用上述的辅助函数实现 add),且返回元素都是 Object 类型的

    1
    2
    3
    4
    void test(List<?> list){
    list.get(0);
    list.remove("str");
    }

所以一般情况下,使用无限制通配符的优先级大于 Object 作为类型参数

有限制通配符

java 泛型有两种有限制通配符,<? extends E><? super E>,那么它们的作用是什么呢?这要从协变和逆变说起。

协变(Covariance)和逆变(Contravariance)

逆变与协变用来描述类型转换(type transformation)后的继承关系

  • 协变:具有子类型关系之间的类型经过“类型转换”后,所构造出更复杂的类型之间仍保持着子类型关系。
  • 逆变:具有子类型关系之间的类型经过“类型转换”后,所构造的更复杂的类型之间建立了逆向子类型关系。
  • 不变:具有子类型关系之间的类型经过“类型转换”后,所构造的更复杂的类型之间没有任何关系

这里指的类型转换代表指的是从一种类型构造为另一种新的类型,如 StringString[]StringList<String>。在 java 中,泛型具有不变性,如

1
2
3
4
Number number = new Integer(0);  // True
ArrayList<Number> arrayList = new ArrayList<Number>(); // True
ArrayList<Number> arrayList1 = new ArrayList<Integer>(); // 编译错误
ArrayList<Number> arrayList1 = new ArrayList<Object>(); // 编译错误

那么问题的答案就出现了:有限制通配符是为了实现泛型的协变与逆变

  • <? extends E> 实现了泛型的协变:

    1
    ArrayList<? extends Number> arrayList = new ArrayList<Integer>();
  • <? super E> 实现了泛型的逆变:

    1
    ArrayList<? super Number> arrayList = new ArrayList<Object>();

extends 与 super

有限制通配符有它的局限性,看一个例子:

1
2
List<? extends Number> list = new ArrayList<Integer>();
list.add(new Integer(1));

我们通过 extends 通配符构建了对象,但是却不能插入 Integer 类型的元素,这看起来很不合理。其实这是可以理解的,首先看一些 List 类的 add 方法接口

1
2
3
public interface List<E> extends Collection<E> {
boolean add(E e);
}

在调用 add 方法时,泛型 E 自动变成了 <? extends Number>,也就是说其类型是 Number 的子类中的一个(不含 Number),因此 add 一个 Integer 类型对象是错误的。如果要实现 add 一个 Interger 对象,可以使用 super 关键字

1
2
List<? super Number> list = new ArrayList<Object>();
list.add(new Integer(1));

<? super Number> 代表其持有的类型是 Number 的父类,那么 add 一个 Integer 类型对象是正确的。所以我们又可以总结出:

  • <? extends Number> 是只读的
  • <? super Number> 是只写的

应用

那么究竟什么时候用 extends,什么时候用 super 呢?其实很简单,遵循 PECS 原则

PECS: producer-extends, consumer-super. 换句话说:

  • 如果要从泛型类取数据时,用 extends
  • 如果要往泛型类写数据时,用 super

举几个例子,首先看 java.util.AbstractListaddAll 方法

1
2
3
4
5
6
7
8
9
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) { // 注意这里,从泛型类中获取对象!!
add(index++, e);
modified = true;
}
return modified;
}

addAll 方法需要将传入的泛型类中的所有元素保存到当前集合中,因此将从泛型类读取所有元素,所以使用 extends。又如 java.util.Collectionscopy 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i)); // 从src泛型类读取数据,写入dest泛型类!!!
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}

copy 方法将一个集合中的元素拷贝到另一个集合,完美的诠释了有限制通配符的使用。

拓展

考虑以下代码:

1
2
3
4
5
6
7
8
9
public static <T extends Comparable<T>> T max(Collection<T> coll){
T max = coll.iterator().next();

for (T elm : coll) {
if (max.compareTo(elm) < 0)
max = elm;
}
return max;
}

它的作用是对集合中的元素进行排序,那么我们需要对传入的集合中的元素进行限定,它需要能够进行比较,即实现 Comparable 接口,<T extends Comparable<T>> 的作用就是如此。但在继承关系中,上述声明会出现错误,考虑以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Fruit implements Comparable<Fruit> {
private String name;
private int size;

public Fruit(String name, int size) {
this.name = name;
this.size = size;
}

@Override
public int compareTo(Fruit that) {
if (size < that.size)
return -1;
else if (size == that.size)
return 0;
else
return 1;
}
}

class Apple extends Fruit {
public Apple(int size) {
super("Apple", size);
}
}

Apple 类继承了 Fruit 类,但是它没有实现 Comparable<Apple>,而是实现了 Comparable<Fruit>,因此它不符合 <T extends Comparable<T>> 要求,因此不能对 Apple 集合使用

1
2
3
4
5
List<Apple> list = new ArrayList<>();
list.add(new Apple(10));
list.add(new Apple(20));

Algorithm.<Apple>max(list); // 编译错误

因此,为了能够对这种情况予以支持,需要使用如下声明:

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>,其中 XT 的父类

其实以之前的 PECS 原则也能很好的解释,无论是 Comparable 还是 Comparator,它们的方法都需要写数据,即向泛型类写数据,所以需要使用 <? super T> ,所以使用 Comparator 的声明为:

1
public 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
2
3
4
5
6
7
8
9
10
11
12
class Algorithm<T extends Comparable<? super T>>{

public T max(Collection<? extends T> coll) {
T max = coll.iterator().next();

for (T elm : coll) {
if (max.compareTo(elm) < 0)
max = elm;
}
return max;
}
}

这样的调用是不是就比较常见了

1
2
3
4
5
6
List<Apple> list = new ArrayList<>();
list.add(new Apple(10));
list.add(new Apple(20));

Algorithm<Fruit> fruitAlgorithm = new Algorithm<>();
fruitAlgorithm.max(list);

其实 java.util.Collectionsmax 方法可以看到类似的声明:

1
2
3
4
5
6
7
8
9
10
11
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
Iterator<? extends T> i = coll.iterator();
T candidate = i.next();

while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0)
candidate = next;
}
return candidate;
}

其中使用 Object & 主要是因为泛型擦除,编译生成的 Java 字节码中是不包含泛型中的类型信息,泛型类型会被 Object 所代替(无限制通配符也用 Object),而有限制通配符则会被第一个边界的类型变量来替换,如上面的声明会被 Comparable 所代替,使用了 Object & 后将被 Object 所代替,参看 Why is T bounded by Object in the Collections.max() signature?

泛型单例类

实现一个泛型单例类,使得它能够对于传入任意类型的 Class 对象都创建一个单例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Singleton {

private final static Map<Class<?>, Object> INSTANCE_MAP = new HashMap<>();

public static <T> T getInstance(Class<T> tClass){

Object instance = INSTANCE_MAP.get(tClass);

if(instance == null){
synchronized (INSTANCE_MAP){
if(instance == null){
try {
instance = tClass.newInstance();
INSTANCE_MAP.put(tClass, instance);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
}

// Class的cast方法能够动态的将Object类型转换为Class对象所表示的类型,如果能转就返回,不能转就抛出类型转换失败异常
// 这样就不需要借助于未受检警告((T)instance)
return tClass.cast(instance);
}
}

创建 Persion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person {

/**
* 姓名
*/
String name;

/**
* 身份证
*/
String idcard;

/**
* 年龄
*/
Integer age;

/**
* 电子邮件
*/
String email;
}

将它传入泛型单例类,每次 getInstance 可以得到相同的对象

1
System.out.println(Singleton.getInstance(Person.class) == Singleton.getInstance(Person.class));  // True

最佳实践

1. 不应使用原生态类型

原生态类型即不带实际类型参数的泛型名称,如 List<E> 的原生态类型为 List。它逃避了泛型检查,当你不小心插入了类型错误的对象,在运行时转换对象会出现 ClassCastException 。因此应该摈弃这样的做法,取而代之使用泛型,优点有以下两点:

  1. 在编译期间进行类型检查
  2. 获取对象不需要手动转换类型

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原生态类型,不推荐
ArrayList list = new ArrayList();
list.add("str");
list.add(1);

for (Object o : list) {
String s = (String)o; // 抛出ClassCastException
}

// 泛型,推荐
ArrayList<String> list = new ArrayList();
list.add("str");
list.add(1); // 1. 编译期检查错误,不允许添加整形

for (String o : list) {
String s = o; // 2. 不需手动转换类型
}

2. 不要使用通配符类型作为返回类型

使用通配符类型作为返回类型将会强制在客户端代码中使用通配符类型,如:

1
2
3
4
5
6
7
8
// 使用通配符类型作为返回类型,不推荐
public void use(){
List<?> list = returnGenericsType();
}

public List<?> returnGenericsType(){
return new ArrayList<>();
}

3. 泛型无法使用instanceof

由于泛型擦除,编译期间会擦除类型参数,所以不能使用 instanceof

1
2
3
4
List<Apple> list = new ArrayList<>();
if(list instanceof List<Apple>){ // 错误
...
}

而对于无限制通配符是可以使用 instanceof

1
2
3
4
List<?> list = new ArrayList<>();
if(list instanceof List<?>){ // 错误
...
}

当然尖括号和 ? 有些多余,可以直接判断

1
list instanceof List

Commentaires

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×