先简单说一下泛型,这是从Java5开始的一个重要新特性:比方说,我现在想建立一个容器,容器里存储的元素类型可以使任意类型(可以存储dog,也可以存储cat),当然一个容器对象只能存储某一种,不能一个容器寄存dog又存cat。在泛型出现之前,我们想实现上述的功能需要这么做:
ArrayList dogs = new ArrayList(); // 元素都是Object类型
dogs.add(new Dog()); // 向上转型为Object对象
Dog dog = (Dog)dogs.get(0); // 手工向下转型为Dog类型
dogs.add(new Cat()); // 一个糊涂的程序员不小心加入了一个cat,不会报错,因为Cat也可以向上转型为Object类型
dog = (Dog)dogs.get(1); // 报类型转换异常,Cat转Dog转不了
上面这个例子展现了泛型出现前的困境:
- 用一个存储Object对象的容器来存储任意类型的时候,首先我们需要用容器名dogs来表明这个容器存的是什么类型的对象;
- 在添加对象的时候一旦添加了错误的对象,编译时期并不会报错。一般来说我们希望有错误能够及早发现,最好在编译器就发现,而不是运行的时候再发现;
- 获取对象的时候需要手动进行类型转换,比较繁琐,而且一旦出现了第二点中的问题,会报类型转换异常。
有了泛型后,只需要这样做:
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
Dog dog = dogs.get(0);
dogs.add(new Cat()); // 编译期报错
可以看到,上面的三个问题都得到了解决:
- 泛型类型表明容器存储的对象类型;
- 添加错误类型的对象,编译期报错;
- 获取对象的时候无需手动类型转换 ;
泛型类型可以是具体类型,也可以是?通配符,也可以是<? extends ClassName>或<? super ClassName>。
接下来介绍书里的建议:
第一条:不要使用原生态类型
上面的代码里,ArrayList<Dog>是泛型类型,ArrayList是原生态类型,之所以保留原生态类型是为了向前兼容(保留原生态类型,也就是说加不加泛型类型对于JVM而言都是一样的,这就是Java泛型的类型擦除,Java的泛型和C++不同,更多的是一种语法糖)。
即使对于容器中存储的元素类型没有限制,也应当使用Collections<?>的形式,也就是通配符,而不应该直接使用原生态类型。
第二条:消除非受检的警告
用泛型编程时会遇到许多编译器警告:非受检转换警告、非受检方法调用警告、非受检参数化可变参数类型警告。
要尽可能地消除每一个非受检警告,消除所有警告可以确保代码是类型安全的。
如果无法消除警告,且可以确保引起警告的代码是类型安全的,才可以用@SuppressWarnings("unchecked")
注解来禁止警告。
并且,@SuppressWarnings("unchecked")
的范围应该尽可能的小(后面直接跟一行代码),且给出注释,说明为什么一定是类型安全的。
第三条:列表优于数组
数组和泛型的两大不同点:
- 数组是协变的(若Son为Father的子类,那么Son[]便是Father[]的子类),泛型是可变的(List和List不存在继承关系)。
// 运行时才会出错
Object[] objects = new Long[1];
objects[0] = "hello"; // 运行时抛出ArrayStoreException
// 编译期就报错
List<Object> objects = new ArrayList<Long>(); // 报错Incompatible types
- 数组是具体化的,运行时知道元素类型;泛型原理是类型擦除,运行时并没有类型信息。不能创建泛型类型的数组(因为数组是具体化的,但是泛型类型在运行时不带类型信息),创建带通配类型的数组是合法的。
public class Chooser<T>{
private final T[] choiceArray;
public Chooser(Collections<T> choices){
choiceArray = choices.toArray();
}
}
在上面的代码中,我们试图调用Collections对象的toArray方法将其转换为T[]类型,其实Collections的泛型信息被擦除后相当于存储的是Object类型,Object[]转换成T[]会报错。更好的做法是吧choiceArray声明为List<T>。
第四条:优先考虑泛型
当我们自己编写一个容器类(或者其他不指定具体类型的类)时,优先考虑使用泛型,而不是维护一个Object数组。
在编写泛型类的时候会出现一些编译期的警告,我们在确保类型安全的情况下可以禁止警告,比如我们定义一个泛型类Stack:
@SuppressWarnings("unchecked")
public Stack(){
elements = new E[DEFUALT_INITIAL_SIZE];
}
elements字段类型是E[],上面代码中new一个E类型的数组是不合法的,会报未受检警告。可以这么写:
@SuppressWarnings("unchecked")
public Stack(){
elements = (E[])new Object[DEFUALT_INITIAL_SIZE];
}
当然,也可以直接把elements定义为Object数组,在取元素的时候转化成E(泛型类可以保证加进去的元素都是E类型的,所以虽然会有警告,但是同样可以通过@SuppressWarnings注解来禁止警告)。
但是上面代码里展示的方法可读性更强(通过elements的类型就知道装的是什么元素)也更简洁(取元素的时候直接取就行不用转换),所以上面的写法更好。这种写法的缺点在于会导致堆污染(但在这个场景下不会造成什么危害):数组的运行时类型与编译时类型不匹配。
第五条:优先考虑泛型方法
泛型方法就是带有泛型类型的方法,这个泛型类型可能是返回值或参数的类型。
什么时候需要用泛型方法呢?看下面的例子:
public static Set union(Set s1, Set s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
上面的代码里,两个参数用的都是Set类型,即原生态类型,编译会报出未受检警告,类型不安全。更好的做法是将参数改为Set<E>
类型,这样整个方法就变成了泛型方法,签名如下:
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
第六条:利用有限制通配符来提升API的灵活性
上面提到,List<Father>和List<Son>不存在继承关系,因此如果我们想在List<Number>里添加一个Integer对象是不可以的,虽然直觉上来说,Integer也是一个Number。
解决的方式呢,就是把List<Number>改成List<? extends Number>就ok了。这是在向容器中生产元素的情况。
当我们从容器中消费元素的时候,
Integer i = (Integer)stack.pop();
那么这个Stack的类型应当是Stack<? super Integer>。在选择通配符的时候,有一个PECS准则:producer-extends,consumer-super。
类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行生命。比如下面两个方法声明可以实现相同的效果:
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j)
一般来说,如果泛型类型仅在方法声明中出现,方法体中没有出现,用通配符比较好。
但是我们不能把除null以外的任何值放入List<?>类型的容器中,只能这么做:
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}
虽然辅助函数还是用到了具体类型泛型,但是这是一个private方法,对客户端是不可见的。
第七条:谨慎并用泛型和可变参数
可变参数和泛型并不能良好地相互作用。可变参数的作用是让客户端能够将不定数量的参数传给方法,其实就是传了一个参数数组。
允许另一个方法访问一个泛型可变参数数组是不安全的,但将数组传给另一个用@SafeVarargs正确注解的可变参数方法是安全的,将数组传给只计算数组内容部分函数的非可变参数方法也是安全的。
第八条:优先考虑类型安全的异构容器
一般来说泛型类用来存储一个或多个同类型的元素,但时候我们需要更多的灵活性。
比如对于一个数据库表而言,比方说所有行都是Record类型,那么我们想以行为单位存储和访问的话,只需要构建List<Record>就可以了,但如果我们想以列为单位存储呢?每一列代表不同的字段,类型是不同的。为了存储不同类型的元素,我们可以将键进行参数化we不是将容器参数化,然后将参数化的键提交给容器来插入或获取值。
以Favorites类为例,允许客户端保存并获取一个“最喜爱”的实例。
public class Favorites{
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
完整实例如下:
public class Favorites{
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance){
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
}
这个类有两大局限:
- 恶意客户端可以轻松破坏Favorites实例的类型安全(比如参数使用原生态的Class类型对象)
- 不能用在不可具体化的类型中,比如这个type不能为List<String>。