先简单说一下泛型,这是从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转不了
上面这个例子展现了泛型出现前的困境:
有了泛型后,只需要这样做:
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
Dog dog = dogs.get(0);
dogs.add(new Cat()); // 编译期报错
可以看到,上面的三个问题都得到了解决:
泛型类型可以是具体类型,也可以是?通配符,也可以是 extends ClassName>或 super ClassName>。
接下来介绍书里的建议:
上面的代码里,ArrayList
即使对于容器中存储的元素类型没有限制,也应当使用Collections>的形式,也就是通配符,而不应该直接使用原生态类型。
用泛型编程时会遇到许多编译器警告:非受检转换警告、非受检方法调用警告、非受检参数化可变参数类型警告。
要尽可能地消除每一个非受检警告,消除所有警告可以确保代码是类型安全的。
如果无法消除警告,且可以确保引起警告的代码是类型安全的,才可以用@SuppressWarnings("unchecked")
注解来禁止警告。
并且,@SuppressWarnings("unchecked")
的范围应该尽可能的小(后面直接跟一行代码),且给出注释,说明为什么一定是类型安全的。
数组和泛型的两大不同点:
// 运行时才会出错
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
当我们自己编写一个容器类(或者其他不指定具体类型的类)时,优先考虑使用泛型,而不是维护一个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
类型,这样整个方法就变成了泛型方法,签名如下:
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
上面提到,List
解决的方式呢,就是把List
当我们从容器中消费元素的时候,
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
以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));
}
}
这个类有两大局限: