第 26 条:请不要使用原生态类型
声明中具有一个或多个类型参数的类或者接口,就是泛型(generic)。
例如List接口只有单个类型参数E, 表示列表的元素类型。这个接口全称List<E>,泛型类和接口统称为泛型(generic type)。
每一种泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。
它的存在主要是为了兼容泛型之前的代码。
如果使用原生态类型,就失去了泛型在安全性和描述性方面的优势。
如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会。
如何使用泛型,但不确定或者不关心实际的类型参数,可以用一个问号代替。
例如,泛型Set<E>的无限制通配符类型为Set<?>。这是最普通的参数化Set类型,可以持有任何集合。
通配符是类型安全的,原生态类型不安全。
不要使用原生类型,有2个例外。
1. 必须在类文字中使用原生态类型。
例如:List.class, String[].class, int.class是合法,但是List<String.class>和List<?>.class是不合法。
2. 由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。
利用泛型来使用instanceof操作符的首选方法,例如:
if (o instanceof Set) { Set<?> s = (Set<?>) o; }
总之,使用原生态类型会在运行时导致异常。原生态类型只是为了与遗留代码兼容。
第 27 条:消除非受检的警告
用泛型编程会遇到很多编译器警告:
非受检转换警告(unchecked cast warning)、非受检方法调用警告、非受检参数化可变参数类型警告(unchecked parameterized vararg type warning)
以及非受检转换警告(unchecked conversion warning)。
例如:
Set<Lark> exaltation = new HashSet();
编译器会提醒出错地方:warning:[unchecked] unchecked conversion
Java7中开始引入菱形操作符(<>),编译器可以推断出正确的实际参数类型.
Set<Lark> exaltation = new HashSet<>();
尽可能地消除每一个非受检警告。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,才可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。
SuppressWarnings可以用在任何颗粒度,应该尽量小的范围使用SuppressWarnings注解。
每当使用SuppressWarnings(“unchecked”)注解时,都要添加一条注解,说明为什么这么做是安全的。
总之,每一条警告都表示可能在运行时抛出ClassCastException,要尽量消除这些警告。
第 28 条:列表优于数组
数组与泛型相比有两个重要不同点。
第一,数组是协变的(covariant),泛型是可变的(invariant)。
表示如果Sub为Super的子类型,那么数组Sub[]就是Super[]的子类型。
对于泛型,不同类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是超类型。
例如,不能将String放入Long容器中,但是数组只会在运行时发现,用列表,则可以在编译时发现错误。
第二,数组是具体化的,泛型是通过擦除来实现的。
数组在运行时知道和强化他们的元素类型,如果将String放入Long数组中,会得到ArrayStoreException.
泛型在编译时强化他们的类型信息,在运行时丢弃(擦除)他们的元素信息。擦除使泛型可以与没有泛型的代码互用。
创建泛型、参数化类型或者类型参数的数组是非法的。
例如:new List<E>[]、new List<String>[]和new E[]。
创建泛型数组不是类型安全的。
优先使用集合类型List<E>,而不是数组类型E[],可能会损失一些性能,但换回是更高的类型安全性和互动性。
总之,数组和泛型有完全不同的类型规则。数组是协变且可以具体化的,泛型是不可变且可以被擦除的。
因此,数组提供了运行时的类型安全,但没有编译时的类型安全,反之,泛型也一样。数组和泛型不能很好地混合使用,优先用列表代替数组。
第 29 条: 优先考虑泛型
第28条鼓励优先使用列表而非数组。实际上不可能总是在泛型中使用列表。
Java并不是生来就支持列表,因此有些泛型如ArrayList必须在数组上实现。为了提升性能,其他泛型如HashMap也在数组上实现。
总之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更容易。
第 30 条: 优先考虑泛型方法
静态工具方法尤其适合于泛型化。Collections中所有“算法”方法(例如binarySearch和sort)都泛型化了。
public static Set union(Set s1, Set s2) { Set result = new HashSet(s1); result.addAll(s2); return result; }
这个方法可以编译,但有2条警告。
为了修正警告,使方法变成类型安全的,将方法声明修改为一个类型参数(type parameter),表示这三个集合元素类型(两个参数和一个返回值),并在方法中使用类型参数。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
声明类型参数的类型参数列表,处在方法的修饰符及其返回值之间。
总之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并且返回值的方法更安全,也更容易。
就像类型一样,应该确保方法不用转换就能使用,还应该将现有方法泛型化,使新用户使用起来更轻松,且不会破坏现有的客户端。
第31 条:利用有限制通配符来提升API的灵活性
Java提供了一种特殊的参数化类型,称为有限制的通知符类型(bounded wildcard type)。
例如,E的某个子类型的Iterable接口,通配符类型Iterable<? extends E>。
public void pushAll(Iterable<? extends E> src) { for (E e : src) push(e); }
另一种,E的某种超类的集合,通用符Collection<? super E>。
public void popAll(Collection<? super E> dst) { while(!isEmpty()) dst.add(pop()); }
为了获取最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型。
PESC表示producer – extends, consumer – super.
如果参数化类型表示一个生产者T,就用<? extends T>; 如果它表示一个消费者T,就用<? super T>。
例如第30条的union方法:
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
用PESC原则:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
注意:返回类型仍然是Set<E>,不要用通配符类型作为返回类型。
修改声明后:
Set<Integer> ints = Set.of(1, 3, 5); Set<Double> dob = Set.of(2.0, 4.0, 6.0); Set<Number> num = union(ints, dob);
第30条的max方法,初始声明:
public static <T extends Comparable<T>> T max(List<T> list)
修改使用通配符后:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
这是将通配符运用到类型参数。参数化类型Comparable<T>被有限制通配符类型Comparable<? super T>取代。 comparable是消费者,因此使用时始终应该是Comparable<? super T>优先于Comparable<T>。对于comparator接口也一样,使用时始终应该是Comparator<? super T>优先于Comparator<T>。
还有一点,类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。如下:
public static <E> void swap(List<E> list, int i, int j); // 使用无限制的类型参数
public static void swap(List<?> list, int i, int j); // 使用无限制的通配符
在公共API中,第二种更好些,因为它更简洁。
如果类型参数只在一个方法声明中出现一次,就可以使用通配符取代它。
总之,在API中使用通配符需要技巧,会是API变得很灵活。如果编写是将被广泛使用的类库,则一定要适当地利用通配符类型。基本原则:producer – extends, consumer – super.(PECS)。还有所有的comparable和comparator都是消费者。
第 32 条:谨慎并用泛型和可变参数
可变参数(vararg)方法和泛型都是Java5出现的,但他们不能良好地相互作用。
可变参数作用在于让客户端能够将可变数量的参数传给方法,当调用一个可变参数方法时,会创建一个数组用来存放可变参数。这个数组是一个实现细节,可见的。因此,当可变参数有泛型或者参数化类型时,编译警告信息就会产生。
当一个参数化类型的变量指向一个不是该类型对象时,会产生堆污染(heap pollution)。它导致编译器的自动生成转换失败,破坏了泛型系统的基本保证。
例如,第28条代码改编:
static void deangerous(List<String>… stringLists){ List<Integer> intList = List.of(42); Object[] objects = stringLists; object[0] = intList; // heap pollution String s = stringLists[0].get(0); // ClassCastException }
上述最后一行代码中有一个不可见转换,由编译器生成。这个转换失败证明类型不安全。
因此将值保存在泛型可变参数数组参数中是不安全的。
在Java7中,增加了SafeVarargs注解,它让带泛型vararg参数的方法的设计者能够自动禁止客户端的警告。通过方法的设计者承诺声明这是类型安全的。
确定何时使用SafeVarargs注解的规则:对于每一个带有泛型可变参数或者参数化类型的方法,都要用@SafeVarargs进行注解。
安全使用泛型可变参数的例子:
@SafeVarargs static <T> List<T> flatten(List<? extends T>… lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; }
如不想使用@SafeVarargs,也可用一个List参数代替可变参数:
static <T> List<T> flatten(List<List<? extends T>> lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; }
总之,可变参数和泛型不能良好地合作。如果选择编写带泛型(或参数化)可变参数的方法,首先要确保该方法时类型安全的,然后用@SafeVarargs对它进行注解。
第 33 条:优先考虑类型安全的异构容器
泛型最常用于集合,如Set<E>和Map<K,V>,以及单个元素的容器,如ThreadLocal<T>和AtomicReference<T>。限制每个容器只能有固定数目的类型参数。
有时候需要更多灵活性,将键(key)进行参数化而不是将容器(container)参数化,然后将参数化的键提交给容器来插入或者获取值。用泛型来确保值得类型与它的键相符。
例如:
public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public <T> void putFavorites(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public <T> T getFavorites(Class<T> type) { return type.cast(favorites.get(type)); } }
每个Favorites实例得到一个favorites的私有Map<Class<?>, Object>的支持。每个键都可以有一个不同的参数化类型:一个是Class<String>, 接下来是Class<Integer>,异构就是从这里来的。
getFavorites的cast方法时Java转换操作的动态模拟。它只检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数,否则抛出ClassCastException异常。
总之,集合API说明了泛型的一般用法,限制每个容器只有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于类型安全的异构容器,可以用Class对象作为键。