Effective Java 读书笔记 - 第五章
Java 1.5 版本新增了泛型,但是又增加了 Java 程序的复杂度,这章介绍如何最大化的利用泛型,同时简化开发。
不要使用原生类型
比如在使用List
接口时,不要直接new ArrayList()
,而应该new ArrayList<Object>()
,因为后者可以在编译期检查添加到List
中的元素是否时同一种类型,前者极易导致运行时的ClassCastException
。
由于 Java 中所有的对象都是Object
的对象,那么List
和List<Object>
是否等价呢?显然不等价,因为,编译器不会对List
进行类型检查,而List<Object>
则告诉编译器可以接受任何类型的对象,但是如果定义了一个方法,这个方法的一个参数是List<Object>
,如果在调用该方法时,传递了List<String>
类型的参数,编译不会通过。
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0);// 1
unsafeAddGeneric(strings, new Integer(43)); // 2
}
public static void unsafeAdd(List list, Object object) {
list.add(object);
}
public static void unsafeAddGeneric(List<Object> list, Object object) {
list.add(object);
}
这段代码中 1 处编译通过,但是在运行时会报ClassCastException
。2 处编译不通过,报错信息为
The method unsafeAddGeneric(List<Object>, Object) in the type Generic is not applicable for the arguments (List<String>, Integer)
Java 1.5 为泛型提供了通配符类型(?
),如果要使用泛型,但不确定或者不关心具体类型,就可以使用通配符类型,如List<?>
。那么带通配符的泛型和原生类型有什么区别呢?带通配符的泛型时类型安全的,如无法将任何一种类型(除null
外)添加到该类型的集合中。
public static void wildcardAdd(Collection<?> collection, Object object) {
collection.add(object);
}
这段代码中,collection.add(object)
编译不通过。报错信息如下。
The method add(capture#1-of ?) in the type Collection<capture#1-of ?> is not applicable for the arguments (Object)
目前有两种情况必须使用原生类型。
- 在使用支持泛型的类的
class
类时,例如不能使用List<String>.class
和List<?>.class
,而必须使用List.class
。因为 Java 代码在编译之后,泛型的类型参数被擦出了,所以无法知道class
对象的参数类型。 - 在使用
instanceOf
操作符时,如o instanceOf Set
,因为类型参数被擦除了。
消除非受检查警告
每一个非受检查警告都代表一个潜在的运行时类型转换异常,所以尽量利用泛型的消除这种警告,如果无法利用泛型,并且能否确保代码是类型安全的,可以使用SupressWarning
注解来消除警告,但是需要记住,总是在最小范围内使用SupressWarning
注解,比如下报警告的代码行增加该注解。
列表优于数组
Java 中虽然列表时基于数组实现的,但是在使用上列表和数组有两点不同。
- 数组是协变的,而列表是不可变的,即对于数组来说,如果
Sub
是Super
的子类,那么Sub[]
是Super[]
的子类。 比如这段代码可以编译通过,但是运行会报java.lang.ArrayStoreException
。Object[] objArray = new Long[1]; objArray[0] = "I don't fit it";
- 数组元素的类型在运行时时时具体类型,而泛型在运行时,类型已经在编译时被擦除了。
优于数组的类型问题在运行时才被发现,而泛型在编译后类型参数会被擦除,所以无法将数组与与泛型混用。比如无法创建new List<E>[]
,new List<String>[]
, new E[]
这样的对象。考虑下面一段代码(假设第以行可以编译通过)。
public void genericListArray() {
// 这段代码会报 Cannot create a generic array of List<String> 假设这段代码可以编译通过
List<String>[] stringLists = new List<String>[1];
Object[] objects = stringLists;
List<Integer> intList = Arrays.asList(42);
objects[0] = intList;
String s = stringLists[0].get(0);
}
stringLists
是一个List
数组,并且List
的泛型类型为String
,然后使用一个Object[]
指向这个列表数组,下面将一个整形的列表intList
赋值给stringLists
的第一个元素,因为List[]
是Object[]
的子类型,最后取出列表数组的第一个列表中的第一个元素赋值给一个String
类型的变量。因为取出的元素是整型,赋值给字符串类型的变量肯定会报ClassCastException
。
无法创建泛型数组导致一般情况下无法返回泛型元素类型数组,并且在使用泛型可变参数方法时,会有警告。所以如果遇到泛型与数组混用时,尽量选择List
来代替数组。
来看下面一段代码。
public static <E> E reduce(List<E> list, Function<E> f, E initVal) {
E[] snapshot = (E[]) list.toArray();
E result = initVal;
for(E e : snapshot) {
f.apply(result, e);
}
return result;
}
假设参数list
是一个同步列表(由Collections.synchronizedList
方法返回),所以在reduce
方法中尽量不直接操作这个同步队列,那么就创建一个副本,即snapshot
这个数组。但是创建数组后需要强制转型为E[]
类型,编译器会给出警告Type safety: Unchecked cast from Object[] to E[]
,那么这不是类型安全的,在运行过程中可能会抛ClassCastException
,将这段代码使用List
替换数组修改为下面这个版本。
public static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snapshot;
synchronized (list) {
snapshot = new ArrayList<E>(list);
}
E result = initVal;
for(E e : snapshot) {
f.apply(result, e);
}
return result;
}
这也就消除了潜在的类型转换异常。
优先使用泛型类
在定义的类中需要支持多种类型时,首先应该考虑这个类是否能够支持泛型参数。
优先使用泛型方法
泛型方法与泛型类型类似,只不过泛型方法是在方法上使用泛型参数,比如定义一个求两个集合的并集的方法。
public static <T> Set<T> union(Set<T> s1, Set<T> s2) {
Set<T> result = new HashSet<T>();
result.addAll(s1);
result.addAll(s2);
return result;
}
union
方法中的参数T
就是这个方法泛型参数,所以这个方法也称为泛型方法。泛型方法多用于静态工具方法中,比如Collections
类中实现的排序和查找方法。
使用泛型方法时,不需要特别指定参数类型,因为编译器会根据参数类型推断泛型参数的类型。
泛型方法的另一个使用场景是针对需要应用于多种类型的不可变的对象,与之相关的模式是泛型单例工厂。比如Collections.reverseOrder()
,Collections.emptySet
,Collections.emptyList
等方法。
假如有个需求是提供一个恒等函数,首先定义接口。
interface UnaryFunction<T> {
T apply(T arg);
}
由于目标是提供一个恒等函数,那么可以这样实现这个接口
private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
@Override
public Object apply(Object arg) {
return arg;
}
};
定义一个方法用于返回恒等函数。
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
return (UnaryFunction<T>)IDENTITY_FUNCTION;
}
由于可以确定IDENTITY_FUNCTION
返回的是一个不变类型,所以,可以使用@SuppressWarnings("unchecked")
消除类型转换的警告。这样就创建了一个用于返回恒等函数的方法。对于identityFunction
方法返回的对象,可以将其应用与任何类型。
利用有限制的通配符提升 API 的灵活性
由于泛型的参数类型是不可变的,所以如果某个方法接收的泛型参数类型固定,那么它是无法接受其他泛型类型参数的,甚至泛型参数为子类都不可以。Java 提供了有限制的通配符类型,它的使用方式是将方法中的泛型类型参数替换成<? extends E>
或者<? super E>
,再泛型参数中使用通配符类型可以增强 API 的灵活度。那么<? extends E>
和<? super E>
该如何使用呢?总结起来就是 PECS,它的意思是如果参数化的类型对方法来说是生产者,那么就是用extends
,如果参数化的类型是消费者,那么就使用super
。
假设有一个Stack
类,定义了两个方法pushAll
和popAll
,pushAll
方法表示向Stack
中增加元素,那么它的参数中的集合类型就是生产者,popAll
方法表示从Stack
中移除元素,存放于参数的集合中,那么参数的集合类就是消费者,所以这两个方法的定义如下。
public void pushAll(Iterable<? extends E> src) {
for(E e : src) {
push(e);
}
}
public void popAll(Collection<? super E> dst) {
while(!this.isEmpty()) {
dst.add(pop());
}
}
再比如列表优于数组一节中的reduce
方法,它的方法声明是
public static <E> E reduce(List<E> list, Function<E> f, E initVal)
对于reduce
方法来说,list
参数是一个生产者,而f
相对于类型参数E
既是生产者,又是消费者,所以Function
的泛型参数类型不便,那么reduce
的方法声明可以是
public static <E> E reduce(List<? extends E> list, Function<E> f, E initVal)
对于方法的返回类型,无需对其声明为通配类型。
另一个比较典型的方法是优先使用泛型方法一节中使用到的max
方法,将 PECS 规则应用与该方法之后,方法定义变为
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
Iterator<? extends T> i = list.iterator();
T result = i.next();
while(i.hasNext()) {
T t = i.next();
if(t.compareTo(result) > 0) {
result = t;
}
}
return result;
}
从方法体中可以看到Comparable
其实是一个消费者,因此应该对其泛型参数使用super
关键字来定义通配类型。所以从上面的几个例子中可以看到,对于类型或者方法的使用者来说,如果还需要考虑如何设置通配类型,那么可能是 API 的设计出问题了。
相对于有限定的通配符,再 Java 中还可以使用无限定的通配符,比如定义一个swap
方法用于交换一格列表中的两个位置的元素,两种方法声明如下。
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
对于定义 API 来说,第二种更灵活一些,因为它无需考虑类型,所以如果一种类型再方法声明中如果仅仅出现了一次,就可以用通配符来替换,即,如果是无限制的类型参数,就用无限制的通配符来替换,如果是有限制的类型参数,就用有限制的通配符替换。
针对无限制的通配符参数的swap
,其实现方法体如下。
public static void swap(List<?> list, int i, int j) {
return list.set(i, list.set(j, list.get(i));
}
但是这样的方法实现会出现编译错误,错误信息是The method set(int, capture#4-of ?) in the type List<capture#4-of ?> is not applicable for the arguments (int, capture#5-of ?)
,从错误信息可以看出,无法向List<?>
列表中中设置非null
的元素值。可以通过另一种方法来绕过,代码如下。
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <T> void swapHelper(List<T> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
通过新定义一格swapHelper
方法,再该方法中指定泛型参数类型,那么就能绕过无法设置List<?>
无法设置值的问题,因为在编译后泛型都是擦除了的。
考虑类型安全的异构容器
在使用泛型的场景如List
,Set
,Map
中,泛型参数只能是一种类型,有时候希望拥有更灵活的使用方式,比如在一个容器中拥有多种不同的类型,并且能类型安全的存取该容器。一种简单的实现方式是参数化Key
,而不是整个容器,如下面的Favorites
类。
public class Favorites {
Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if(instance == null) {
throw new NullPointerException("type is null");
}
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
Favorites
类是类型安全的,它以Class<T>
类型作为key
,以具体的类型作为value
,当调用putFavorite
方法时,它将T
类的Class
与其对象放入Map
中,可以确保value
就是key
类型的对象,当调用getFavorite
从Map
中取出对象,再强制转换成具体的类型,所以它时类型安全的。
但是Favorites
的实现由两个限制,第一个限制是针对putFavorite
,恶意的客户端很容易绕过该方法的泛型类型参数,比如下面这段调用代码。
Favorites f = new Favorites();
Class clazz = Integer.valueOf(1).getClass();
f.putFavorite(clazz, "java");
f.getFavorite(Integer.class);
调用putFavorite
方法时传递的键值对分别是Class<Integer>
和字符串类型的”java”,实际上再代码层面,并没有传递Class
的泛型参数,所以骗过了编译器,但是再运行过程中就报了类型转换异常。
另外一个限制是,putFavorite
方法的 Key 的类型无法是List.class
这样的类型,因为无论是List<String>.class
还是List<Integer>.class
编译后都是List.class
,所以编译完成后,List<String>.class
与List<Integer>.class
其实是等价的,这样它们就不是异构类型。