不要在新代码中使用原生态类型

首先看一个反例,如何往 String 类型的集中中添加 int 类型的数据:

1
2
3
4
5
6
7
8
List<String> str = new ArrayList<>();
str.add("1");
str.add("2");
add(str, 3);

private void add(List list, Object o) {
list.add(o);
}

上面的代码在编译器并不会抛出异常,而是会有个一个 Unchecked 的警告。但是当获取到 int 类型的时就会抛出 ClassCastException 异常

  • 如果使用像 List这样的原生态类型,就会失掉类型安全性,但是如果使用List这样的参数化类型, 则不会
  • 不确定或者不在乎集合中元素类型的情况下,可以使用无限制的通配符类型:Set<?>
  • 通配符类型是安全的,也可以限制了添加到集合中元素类型
  • List 参数化类型,可以包含任何对象类型的一个集合
  • List<?> 通配符类型,只能包含某种未知对象类型的集合
  • List 原生态类型,不安全
术语 示例
参数化的类型 List
实际类型参数 String
泛型 List
形式类型参数 E
无限制通配符类型 List<?>
原生态类型 List
有限制类型参数
递归类型限制 <T extends Comparable>
有限制通配符类型 List<? extends Number>
泛型方法 static List asList(E[] a)
类型令牌 String.class

消除非受检警告

  • Unchecked Cast Warning
  • Unchecked Conversion Warning
  • 非受检方法调用警告
  • 非受检普通数组创建警告
  • 非受检转换警告
    尽可能消除每一个非受检警告,如果无法消除警告,同时可以证明引起警告的代码是类型安全的,可以用一个注解来禁止这条警告。由于这个注解可以用在任何粒度的级别中,比如说类,方法以及一个变量。所以应该注意尽可能的缩小注解的范围。
    永远不要在类上用这个注解。
    尽量用注释把禁止这个警告的原因记录下来

列表优先于数组

数组与泛型相比,有两个不同点:

  • 数组是协变的,如果A是B的子类型,那么数组类型A[]就是B[]的子类型。泛型是不可变的,List不是List的子类,也不是它的父类
  • 数组是具体化的,数组在运行时检查它的元素类型约束;相对来说,泛型是不可具体化的,因为泛型是通过在编译期擦除类型来实现的
1
2
3
4
5
6
7
// 由于数组是协变的,所以这种写法在编译期并不能检测出来问题
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";

// 由于泛型是不可变的,所以在编译期就会报错
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

相对于运行期报错,最好是在编译期就能检测出来

数组和泛型不能很好的混合使用,如果在混合使用的过程中。发现了编译期错误或者警告。应该在第一时间用列表代替数组

优先使用泛型

使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端

优先考虑泛型方法

下面是一个带有返回值的泛型方法:

1
2
3
4
5
6
7
8
9
10
11
12
public <E extends Comparable<E>> E max(List<E> list) {
Iterator<E> iterator = list.iterator();
E max = iterator.next();
while (iterator.hasNext()) {
E next = iterator.next();
if (next.compareTo(max) > 0) {
max = next;
}
}

return max;
}

泛型方法使用起来比让客户端自行装换输入参数和返回值要安全

利用有限通配符来提升 API 的灵活性

关键思想就是怎么使用有限通配符,使用有限通配符的原则:Producer-Extends,Consumer-Super

如果只从 List<String> producer 中读取数据,那么这个 List 就叫做 Producer
如果只从 List<String> consumer 中添加数据,那么这个 List 就交错 Consumer

生产者使用 extends,消费者使用 super 的意思是,当使用 ? 定义一个通用类型的通配符时,如果该类是 Producer,那么应该使用 extends 关键字为此类型指定一个最高父类,如果该类是 Consumer,那么应该使用 super 关键字为此类型指定一个最低子类

extends 用法

首先看一下 extends 的用法:

1
2
3
List<? extends Number> producer1 = new ArrayList<Integer>();
List<? extends Number> producer2 = new ArrayList<Float>();
List<? extends Number> producer3 = new ArrayList<Double>();

<? extends Number> 指定 Number 为最高父类,所以所有的 Number 子类包括它自身都可以实例化这样的 List,但是不能用跟 Number 没有继承关系或者父类进行初始化:

1
2
List<? extends Number> producer4 = new ArrayList<String>();
List<? extends Number> producer5 = new ArrayList<Object>();

List<? extends Number> 的写操作是不允许的,如果允许向 List<? extends Number> 中写入数据,那么所有 Number 的子类都可以写入到该 List 中去,此时从该 List 中取数据时就不能保障拿出数据的具体类型

super 用法

super 的用法刚好和 extends 的用法相反:

1
2
List<? super Number> consumer1 = new ArrayList<Object>();
List<? super Number> consumer2 = new ArrayList<Number>();

<? super Number> 指定 Number 为最低子类,所以所有的 Number 的父类包括自身都可以实例化这样的 List,但是不能用 Number 的子类进行实例化:

1
List<? super Number> consumer3 = new ArrayList<Integer>();

List<? super Number> 允许写操作,但是写入的对象必须是 Number 的子类对象,但是对于读操作,由于没有限制最大父类,所以从 List<? super Number> 中读出来的对象只能是 Object,确定不了数据的类型

关于 PECS 的作用,下面是 Collections#copy 的源码:

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));
} 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());
}
}
}

Collections#copy 的用法:

1
2
3
List<Number> numberList = new ArrayList<Number>();
List<Integer> integerList = new ArrayList<Integer>();
Collections.copy(numberList, integerList);

利用 PECS,该函数对集合的 copy 操作通用性非常强

优先考虑类型安全的异构容器

SetMap 的原生类型就是一个类型不安全的异构容器,想要做到类型安全的异构容器,需要将键(key)进行参数化而不是将容器(container)参数化,然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与它的键相符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Favorites {
private Map<Class<?>, Object> favorites =
new HashMap<Class<?>, Object>();

public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type, type.cast(instance));
}

public <T> getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}

其中 Class#cast 保证键和值之间的类型关系,每个值的类型都与键的类型相同,Favorites 类有一个局限性,它不能用在不可具体化的(non-reifiable)类型中:
如果试图保存最喜爱的 List<String>,程序就不能进行编译.原因在于你无法为 List<String> 获得一个 Class 对象:List<String>.class 是个语法错误,这也是件好事。 List<String>List<Integer> 共用一个 Class 对象,即 List.class。如果从“字面(type literal)”上来看,List<String>.classList<Integer>.class 是合法的,并返回了相同的对象引用,就会破坏 Favorites 对象的内部结构

至今还没有完全令人满意的解决办法。有一种方法称作 super type token,它在解决这一局限性方面做了很多努力,但是这种方法仍然有它自身的局限性。集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上类避开这一限制