第五章 泛型
23、 请不要在新代码中使用原生态类型
声明中具有一个或者多个类型参数的类或者接口,就是泛型类或者泛型接口。泛型类和接口统称为泛型。
每种泛型可以定义一种参数化的类型,格式为:先是类或者接口的名称,接着用尖括号(<>)把对应于泛型的类型参数的实际类型参数列表括起来。
每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称,也是没有泛型之前的类型。
泛型能将运行时期的错误提前到编译时期检测。
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。既然不应该使用原生态类型,为什么Java设计还要允许使用它们呢?这是为了提供兼容性,要兼容以前没有使用泛型的Java代码。
原生态类型List和参数化的类型List<Object>之间到底有什么区别呢?不严格地说,前者逃避了泛型检查,后者则明确告知编译器,它能够持有任意类型的对象。泛型有子类型化的规则:List<String>是原生态类型List的一个子类型,而不是参数化类型List<Object>的子类型(见25条)。因此,如果用不用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会。
在无限制通配类型Set<?>和原生态类型Set之间有什么区别呢?由于可以将任何元素放进使用原生态类型的集合中,因此很容易破坏该集合的类型约束条件;但不能将任何元素(除了null之外)放到Collection<?>中。
“不要在新代码中使用原生态类型”,这条规则有两个例外,这是因为“泛型信息在运行时就会被擦除”。在获取类信息中必须使用原生态类型(数组类型和基本类型也算原生态类型),规范不允许使用参数化类型。换句话说:List.class,String[].class和int.class都是合法,但是List<String>.class和List<?>.class都是不合法的。这条规则的第二个例外与instanceof操作符有关,由于泛型信息在运行时已被擦除,因此在参数化类型而不是无限制通配符类型(如List<?>)上使用instanceof操作符是非法的,用无限制通配符类型代替原生态类型,对instanceof操作的行为不产生任何影响。在这种情况下,尖括号<>和问号?就显得多余了。下面是利用泛型来使用instanceof操作符的首先方法:
if(o instanceof set){
Set<?> m = (Set<?>)o;
// ...
}
注意,一旦确定这个o是个Set,就必须将它转换成通配类型Set<?>,则不是转换成原生态类型Set,否则Set会引起编译时警告。
总之,使用原生态类型会在运行时导致异常,因此不要在新代码中使用。原生态类型只为了与引入泛型之前的遗留代码进行兼容和互用而提供的。另外Set<Object>是个参数化类型,表示可以包括任何对象类型的一个集合;Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;Set则是个原生态类型,它脱离了泛型系统。前两者是安全的,最后一种不安全。
术语介绍:
原生态类型:List
参数化的类型:List<String>
泛型:List<E>
有限制类型参数:List<E extends Number>
形式类型参数:E
无限制通配符类型:List<?>
有限制通配符类型:List<? extends Number>
递归类型限制:List <T extends Comparable<T>>
泛型方法: static<E> List<E> asList(E[] a)
24、 消除非受检警告
用泛型编程时,会遇到许多编译器警告:非受检强制转换警告、非受检方法调用警告、非受检普通数组创建警告,以及非受检转换警告。
要尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,只有在这种情况下才可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告。
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量到整个类都可以。应该始终在尽可能小的范围中使用SuppressWarnings注解。它通常是个变量声明,或者是非常简短的方法或者构造器。永远不要在整个类上使用SuppressWarnings,这么做可能会掩盖了重要的警告。
总而言之,非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时抛出ClassCastException异常。要尽最大的努力消除这些警告。如果无法消掉同时确实是类型安全的,就可以在尽可能小的范围中,用@SuppressWarnings("unchecked")注解来禁止这条警告。要用注释把禁止该警告的原因记录下来。
25、 列表优先于数组
数组与泛型相比,有两个重要的不同点:首先,数组是协变的,如Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。但泛型则是不可变的,对于任意两个不同的类型Type1和Type2,List<Type1>与List<Type2>没有任何父子关系。
下面的代码片段是合法的:
Object[] objectArray = new Long[1];
objectArray[0]= "";//运行时抛异常
但下面这段代码则在编译时就不合法:
List<Object> ol = new ArrayList<Long>();//编译时就不能通过
ol.add("");
利用数组,你会在运行时才可以发现错误,而利用列表,则可以在编译时发现错误。而我们最好是编译时发现错误,及早的处理它。
数组与泛型之间的第二大区别在于,数组是具体化的[JLS,4.7]。因此数组会在运行时才知道并检查它们的元素类型约束。相比,泛型则是通过擦除[JLS,4.6]来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
由于上述这些根本的区另,因此数组和泛型不能很好混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的,如:new List<E>[]、new List<String>[]、new E[]都是非法的。
为什么不允许创建泛型数组呢?看具体例子:
List<String>[] stringLists= new List<String>[1];//1
List<Integer> intList = Arrays.asList(42); //2
Object[] objects = stringLists; //3
objects[0] = intList; //4
String s = stringLists[0].get(0); //5
这里首先假设第一行可以,其他行本身编译是没有问题的,但运行到5行时肯定会抛出ClassCastException异常。为了防止出现这种情况,创建泛型数组第1行就不允许了。
从技术角度说,像List<Strign>、List<E>、E这样的类型应称作为不可具体化的类型[JLS,4.7]。直观地说,不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型是无限制的符类型,如List<?>和Map<?,?>(Map<?,?>[] maps = new Map<?,?>[1];),虽然不常用,但是创建无限制通配类型的数组是合法。
当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是E[]。这样可以会损失一些性能或者简洁性,但是挽回的是更高的类型安全性和互用性。
总之,数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的,泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表来代替数组。
26、 优先考虑泛型
考虑第6条中的堆栈实现,将它定义成泛型类。第一种是将elements定义成类型参数数组:
public class Stack<E> {
private E[] elements;//定义成类型参数数组
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 通过pust(E)我们只能将E类型的实例放入到elements中,这已充分确保类型
// 安全,所以这里可以强转。但是运行时数组的类型不是E[],它仍然是Objct[]类型的!
@SuppressWarnings("unchecked")
public Stack() {
//elements =new E[DEFAULT_INITIAL_CAPACITY];//不能创建类型参数数组
/*
* 编译器不可能证明你的程序是类型安全的,但是你可以证明。你自己必须确保未受
* 检的转换不会危及到程序的类型安全。因为elements保存在一个私有的域中,永远
* 不会返回到客户端。或者传给任何其他方法。这个数组中保存的唯一元素,是传给
* push方法的那些元素,它们的类型为E,因此未受检的转换不会有任何危害。一旦
* 你证明了未受检的转换是安全的,就要在尽可能小的范围中禁警告。然后你就可以
* 使用的它了,无需显示转换,也不需担心会出ClassCastException异常。
*/
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];//这里会有
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 解除过期引用
return result;
}
//...
}
第二种是将elements域的类型从E[]改为Object[]:
public class Stack<E> {
private Object[] elements;//直接定义成Object[]类型数组
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
//这里就不需要转型了
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
// Appropriate suppression of unchecked warning
public E pop() {
if (size == 0)
throw new EmptyStackException();
// 放入栈中的元素类型一定是E类型的,所以转换是没有问题的!
@SuppressWarnings("unchecked")//将@SuppressWarnings尽量应用到最小的范围上
E result = (E) elements[--size];//转型会移到这里,但会有警告
elements[size] = null; // 解除过期引用
return result;
}
// ...
}
总之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型时,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的,只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。
27、 优先考虑泛型方法
静态工具方法尤其适合泛型方法。Collections工具类中的所有算法方法都泛型化了。
public class Union {
// 泛型方法
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
}
union方法的局限性在于,三个集合的类型(两个输入参数和一个返回值)必须全部相同。利用有限制的通配符类型,可以使这个方法变得更加灵活。
泛型方法的一个显著特性是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算出类型参数的值。对于上面两个参数都是Set<String>类型,因此知道类型参数E必须为String,这个过程称作为类型推导。
可以利用泛型方法调用所提供的类型推导,使用创建参数化类型实例的过程变得更加轻松。下面的写法有点冗余:
Map<String, List<String>> anagrams = new HashMap<String, List<String>>();
为了消除这种冗余,可以编写一个泛型静态工厂方法,与想要的每个构造器相对应,如:
// 静态的泛型工厂方法
public static <K, V> HashMap<K, V> newHashMap() {
return new HashMap<K, V>();
}
通过这个泛型静态工厂方法,可以用下面这段简洁的代码来取代上面那个冗余的行:
// 使用泛型静态工厂方法来创建参数化的类的实例
Map<String, List<String>> anagrams = newHashMap();
在泛型上调用构造器时,如果语言本身支持类型推导与调用泛型方法时所做的相同,那就好了,将来也许可以,但现在还不行。
递归类型限制最普遍的用途与Comparable接口有关,例如在集合中找最大的元素泛型静态方法:
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while(i.hasNext()){
T t = i.next();
if(t.compareTo(result)>0){
result = t;
}
}
return result;
}
总之,泛型方法就像泛型一样,使用起来比要求客户端转换输出参数并返回值的方法来得更加安全,也更加容易,就像类型一样,你应该确保新的方法可以不用转换就能使用,这通常意味着要将它们泛型化。
28、 利用有限制通配符来提升API的灵活性
现在在前面前面第26条中的Stack<E>中加上以下方法,以便将某个集合一次性放入到栈中:
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
这样使用:
Stack<Number> numberStack = new Stack<Number>();//1
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9); //2
numberStack.pushAll(integers); //3
按照上面的写法,我们是无法将Integer类型的元素放入Number类型的栈中,这是不合理的,因为Integer为Number的子类。原因是第一行执行完后,pushAll方法中的类型参数E就会固定为Number类型,这就好比要将Iterable<Integer>赋值给Iterable<Number>一样,这显然是不可以的,因为Iterable<Integer>不是Iterable<Number>的子类型。这样就显得缺少灵活性了,幸好限制通配符类型可以帮我们解决:
// 限制通配符意味这这里的元素只少是E类型
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
经过上面的修改后src不只是可以接受E类型的Iterable,还可以接受E的子类型Iterable,而Integer恰好为Number的子类型,所以现在第3行可以正常编译与运行。
注:List<? extends E> src这种集合只能读不能写,即只能传进参数(如add方法),而不能返回E类型元素(如get方法),当然不带泛型类型参数与返回泛型类型的方法是可以随便调用的,比如size();
完成上面的pushAll方法的后,我们现在想编写一个popAll对应的方法,它从栈中弹出每个元素,并将这些元素到传进去参数集合中,下面如果这样设计:
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
应用代码如下:
Collection<Object> objects = new ArrayList<Object>();//1
numberStack.popAll(objects);//2
System.out.println(objects);
很不幸的是,第2行根本无法编译通过,按理来说我们完全可以将Number类型的元素放入到一个Object类型参数的集合中,可这里失败了,这不是不可以,是我们设计的错误。这里失败的原因与上面一样,是由于numberStack的类型为Number,所以popAll的类型参数固定为Number,因而不能将一个Collection<Object>赋值给Collection<Number>,因为Collection<Object>不是Collection<Number>的子类型。像上面一样,也有这样一样限制通配类型来解决这个问题:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
现在dst不只是可以接受E类型的Collection了,而且还可以接受E的父类型Collection,这里的Object为Number的父类,所以这里可以正常运行。
注:List<? super E> dst这种集合只能写不能读,即只能写入元素(如调用add方法)而不能读取元素(如调用get),当然不带泛型类型参数与返回泛型类型的方法是可以随便调用的,比如size();
XXX<? extends T> x:使用<? extends T>定义的引用x,x可以用来接受类型参数为T及T的所有子类类型的实例,但通过这个引用调用x上的泛型方法时有一定的限制:如果方法带泛型参数,则不能调用,因为真真实例方法的类型参数类型完全有可能比你传进这个泛型方法的参数的类型要窄,一个子类型的引用是不能接受一个父类类型引用的,所以不能通过这种限制通配类型定义的引用x来调用一切带泛型参数的方法;但你可以调用那此具有返回类型为泛型类型的方法,因为不管真真实例的类型如果,它们都是T的子类,所以此时的返回类型只少是T类型;最后如果方法不带泛型类型参数,则是可以随便调用的。总之,这种限制通配类型一般用于从集合中读取操作。
XXX<? super T> x:使用<? super T>定义的引用x,x可以用来接受类型参数为T及T的所有父类类型的实例,但通过这个引用调用x上的泛型方法时有一定的限制:如果方法的返回类型为泛型类型,则接收这个返回类型的变量类型不能是T,而只能是以Object类型变量来接收,因为方法的实际返回的类型完全有可能比方法定义的返回类型T要宽,但我们又不知道究竟比T宽多少,你总不能将Object类型对象赋值给T类型的引用吧,所以通过这种限制通配类型定义的引用x来调用返回类型为泛型参数的方法时会失去类型限制;但你可以调用方法参数类型为泛型类型的方法,因为现在方法的实例类型至少为T或比T要宽,所以可以接收T及T子类类型参数;最后如果方法的返回类型不为泛型类型参数时,则也是可以随便调用的。总之,这种限制通配类型一般用于将元素写入集合。
不要用符类型作为返回类型,除了为用户提供额外的灵活性外,它还会强制用户在客户端代码中使用通配符类型。
将第27条的union方法修改一下,让它能同时接受Integer与Double集合,由于这两个集合是用来读的,所以使用<? extends E>限制通配符:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) ;
如果有这以下两个集合:
Set<Integer> integers = new HashSet<Integer>();
Set<Double> doubles = new HashSet<Double>();
调用union(integers, doubles)时类型推断为<? extends Number>,所以以下编译不能通过:
Set<Number> nubers =Union.union(integers, doubles);
只能是这样显示的指定返回类型,而不使用类型推导:
Set<Number> nubers =Union.<Number>union(integers, doubles);
或者使用类型推导,则只能以通配类型来接受,因为Set<Number>并不是Set<Float>的父类:
Set<? extends Number> nubers = union(integers, doubles);
接下来,我们将第27条的max方法,让它更灵活,做如下修改:因为list只用来读取或生产元素(第1、2、4行都是从list中读,即只通过list直接或间接地调用过返回类型为泛型类型的方法,而没有去调用过参数为泛型类型的方法),所以从List<T>修改成List<? extends T>,让list可以接受T及其它的子类。而Comparable<T>应该修改成Comparable<? super T>,因为Comparable只是来消费T的实例(第5行属于消费,因为T的实例调用带有泛型类型参数的compareTo方法),传递进的参数要求是最宽的,这样可以确保compareTo中的参数能接受T及其T的子类类型:
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
//只能使用通配类型来接受,因为iterator()方法返回的为Iterator<E> 类型,又Iterator<Object>并不是Iterator<String>的父类,所以这里也需要修改一下
Iterator<? extends T> i = list.iterator();//1
//但这里不需要使用通配类型来接收,因为next()返回的类型直接就是类型参数E,而不像上面返回的为Iterator<E>泛型类型
T result = i.next();//2
while (i.hasNext()) {//3
T t = i.next();//4
if (t.compareTo(result) > 0) {//5
result = t;
}
}
return result;
}
假如现在有以下两个接口:
interface I1 extends Comparable<I1> {}
interface I2 extends I1 {}
如果上面不这样修改的话,下面第二行将不适用:
max(new ArrayList<I1>());
max(new ArrayList<I2>());
现在我们具体的分析一下上面代码:如果Comparable<T>不修改成Comparable<? super T>,第一行还是可正常运行,但是第二行则不可以,因为此时的T为I2,而I2又没有实现Comparable接口,而方法声明<T extends Comparable<T>>部分则要求I2直接实现Comparable接口,但父类I1实现了Comparable接口,I2又已经继承了I1,我们不需要再实现该接口,所以这里变通的作法是让Comparable<T>可以接收T及T的父类类型,所以修改成Comparable<? super T>即可,并且这样修改也符合前面的描述。
所以,使用时始终应该是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);
一般来说,如果类型参数只在方法声明中出现一次(即只在方法参数声明列表中出现过,而方法体没有出现),就可以用通配符取代它:如果是无限制的类型参数<E>,就用无限制的通配符取代它<?>;如果是有限制的类型参数<E extends Number>,就用有限制的通配符取代它<? extends Number>。
第二种实质上会有问题,下面是简单的实现都不能通过编译:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.get(j));
}
原因很简单了,不再累述,但可以修改它,编写一个私有的辅助方法来捕捉通配符类型:
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.get(j));
}
不过,还可以将List<? >修改成List<? super Object>也可以:
public static void swap(List<? super Object> list, int i, int j) {
list.set(i, list.get(j));
}
总之,在API中使用通配符类型使API变得灵活多。如果编写的是一个被广泛使用的类库,则一定要适当地利用通配类型。记住基本原则:producer-extends,consumer-super(PECS)。还要记住所有的Comparable和Comparator都是消费者,所以适合于<? super XXX>。
PECS:如果参数化类型表示一个T生产者,就使用<? extends T>;如果它表示一个T是消费者,就使用<? super T>。
29、 优先考虑类型安全的异构容器
通过对泛型的学习我们知道,泛型集合一旦实例化,类型参数就确定下来,只能存入特定类型的元素,比如:
Map<K, V> map = new HashMap<K, V>();
则只能将K、V及它们的子类放入Map中,就不能将其他类型元素存入。如果使用原生Map又会得到类型安全检查,也许你这样定义:
Map<Object,Object> map = new HashMap<Object,Object>();
map.put("Derive", new Derive());
map.put("Sub", new Sub());
这样是可以存入各种类型的对象,虽然逃过了警告,但取出时我们无法知道确切的类型,它们都是Object,那么有没有一种这样的Map,即可以存放各种类型的对象,但取出时还是可以知道其确切类型,这是可以的:
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, instance);
/*
* 防止客户端传进原生的Class对象,虽然这会警告,但这就不能
* 确保后面instance实例为type类型了,所以在这种情况下强制
* 检查
*/
favorites.put(type, type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
//返回的还是存入时真真类型
return type.cast(favorites.get(type));
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger,
favoriteClass.getName());
}
}
总之,集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。