第23条:请不要在新代码中使用原生态类型
声明中具有一个或者多个类型参数( type parameter)的类或者接口,就是泛型(generic)类或者接口。
每种泛型定义一组参数化的类型(parameterized type),构成格式为: 先是类或者接口的名称,接着用尖括号(<>)把对应于泛型形式类型参数的实际类型参数列表括起来。例如,List
最后点,每个泛型都定义一个原生态类型[raw type],即不带任何实际类型参数的泛型名称。例如,List
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。
从Java 1.5发行版本开始,Java就提供了一种安全的替代方法,称作无限制的通配符类型(unbounded wildcard type). 如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替。例如,泛型Set
通配符类型是安全的,原生态类型则不安全。
不能将任何元素(除了null之外)放到Colllection<>中。
不要在新代码中使用原生态类型,这条规则有两个小小的例外,两者都源于“泛型信息可以在运行时被擦除”(见第25条)这一事实。在类文字(class literal)中必须使用原生态类型。规范不允许使用参数化类型(虽然允许数组类型和基本类型)。换句话说,List.class, String[].class和int.class都合法,但是List
这条规则的第二个例外与instanceof操作符有关。由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceo躁作符是非法的。用无限制通配符类型代替原生态类型,对instanceof操作符的行为不会产生任何影响。在这种情况下,尖括号(<>)和问号(?)就显得多余了。下面是利用泛型来使用instanceof操作符的首选方法:
第24条: 消除非受检警告
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。应该始终在尽可能小的范围中使用SuppressWarnings注解。它通常是个变量声明、或是作常简短的方法或者构造器。永远不要在整个类上使用SuppressWarnings,这么做可能会掩盖了重要的警告。
如果你发现自己在长度不止行的方法或者构造器中使用了SuppressWarnings注解,可以将它移到一个局部变最的声明中。
将SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明。
每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的。
第25条:列表优先于数组
数组协变性的缺陷
数组与泛型相比。有两个重要的不同点。首先,数组是协变的(covariant)。即表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的(invariant): 对于任意两个不同的类型Type1和Type2 , List
数组是具体的,泛型是擦除的
数组与泛型之间的第二大区别在于,数组是具体化的(reified )。因此数组会在运行时才知道并检查它们的元素类型约束。相比之下,泛型则是通过擦除(erasure)来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。
由于上述这些根本的区别,因此数组和泛型不能很好地混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的。
非法定义的示例:
new List
Note:
为什么创建泛型数组是非法的?因为它不是类型安全的。要是它合法,编译器在其他正确的程序中发生的转换就会在运行时失败,并出现一个CIassCastException异常。这就违背了泛型系统提供的基本保证。
一个反例:
假设以下第一行是可以编译通过的,那么会在第五行得到CIassCastException。
创建无限制通配类型的数组是合法
从技术的角度来说,像E, List
列表:用性能换取类型安全性
当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List
总而言之,数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的,泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也样。一般来说,数组和泛型不能很好地棍合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。
第27条:优先考虑泛型方法
类型导出
泛型方法的一个显著特性是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算类型参数的值。这个过程称为类型导出(type inference)。
相关的模式是泛型单例工厂(generic singleton factory )。有时,会需要创建不可变但又适合于许多不同类型的对象。
第28条:利用有限制通配符来提升API的灵活性
例子:
假设我们想要在Stack中增加一个方法,让它按顺序将一系列的元素全部放到堆栈中。
考虑以下反例:
但是,如果尝试这么做,就会在以下代码得到错误消息。因为如前所述,参数化类型是不可变的。
幸运的是,有一种解决办法。Java提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type),来处理类似的情况。pushAll的输入参数类型不应该为“E的Iterable接口”,而应该为“E的某个子类型的Iterable接口”,有一个通配符类型正符合此意:
Iterable
修改后的代码如下:
"E的某种超类的集合”(这里的超类是确定的,因此E是它自身的一个超类), 仍然有一个通配符类型正是符合此意:Collection<? super E)。
PECS
结论很明显口为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者。又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。
下面的助记符便于让你记住要使用哪种通配符类型:
PEGS表示producer-extends, consumer-super.
换句话说,如果参数化类型表示一个T生产者,就使用
Note:
不要用通配符类型作为返回类型。
总而言之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活得多。如果编写的是将被广泛使用的类库,则一定要适当地利用通配符类型。记住基本的原则:producer-extends, consumer-super (PECS)。还要记住所有的comparable和comparator都是消费者。
第29条:优先考虑类型安全的异构容器
异构容器是指能够容纳不同类型对象的容器。像我们通常用的List、Map等容器,它们的原生态类型本身就是异构容器,一旦给它们设置了泛型参数,例如List
使用Map实现类型安全的异构容器
我们将要实现一个Favorites类,用来对每个类型保存一个最喜欢的实例。它的API如下:
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
Favorite,的实现小得出奇。它的完整实现如下
但是,恶意的客户端可以破坏Favorites实例的类型安全。如果客户端传入原生态的Class对象和不一致的值对象,则会在getFavorite的cast时抛出ClassCastException异常。不过好在我们可以对这一情况加以约束。只需要在put时使用一个动态的转换就可以了:
将put方法更新为以下方法: