Java1.5版本中增加了泛型。在没有泛型之前,从集合中读取到的每一个对象都必须进行转换。如果不小心插入了错误类型的对象,在运行时的转换处理就会出错。有了泛型之后,可以告诉编译器每个集合中可以接受哪些对象类型,编译器自动为插入操作进行转换,并在编译时告知是否插入了类型错误的对象,使得程序更加安全,代码意图也更加清楚。
由编译器进行类型匹配与检查,若对象类型错误,编译时即可发现错误,而非要等到运行时。
本章内容导图:
1.不要在新代码中使用原生态类型
泛型类/接口:声明中具有一个或者多个类型参数的类/接口
每种泛型定义一组参数化的类型,如List<String>,读作字符串列表,表示元素类型为String的列表。
每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。如与List<E>相对应的原生态类型就是List。
原生态类型没有泛型在安全性和表述性方面的优势,它的存在仅是为了兼容引入泛型之前的遗留代码,不应在新代码中继续使用。
//使用原生态类型
private final List stamps = new ArrayList();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //可以正常添加
Stamp stamp = (Stamp)stamps.get(1); //运行时错误,抛出ClassCastException。
//使用泛型
private final List<Stamp> stamps = new ArrayList<Stamp>();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //提示错误,无法通过编译
Stamp stamp = stamps.get(0); //使用时无需进行手工转换
由上述代码可以看出,使用泛型的两个好处为:
1.由编译器确保插入正确的元素类型
2.从集合获取元素时不再需要手工转换了
如果要使用泛型,但不确定或不关心实际的类型参数,可以使用一个?代替,称作无限制的通配符类型,如泛型Set<E>的无限制通配符类型为Set<?>,读作某个类型的集合。通配符类型是安全的,原生态类型不安全。
不在新代码中使用原生态类型这条规则有两种例外情况:
1.在类文字中必须使用原生态类型
//正确的用法
List.class
String[].class
int.class
//错误的用法
List<String.class>
List<?>.class
2.在instanceof操作符中必须使用原生态类型
if (o instanceof Set) {
Set<?> m = (Set<?>)o;
}
上述两种例外都是源于泛型信息可以在运行时被擦除。
使用原生态类型会在运行时导致异常,因此不要在新代码中使用。
原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。
Set<Object>是个参数化类型,表示可以保护任意对象类型的一个集合;
Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;
Set则是个原生态类型。
2.消除非受检警告
用泛型编程时,会遇到很多编译器警告:
非受检强制转换警告
非受检方法调用警告
非受检普通数组创建警告
非受检转换警告
要尽可能地消除每一个非受检警告,这可以确保代码是类型安全的,意味着代码在运行时不会出现ClassCastException异常。
现代IDE工具都会提示这种警告信息,使用泛型时,如有这种非受检警告,按照IDE工具的提示逐个消除就可以了。
//含警告信息的泛型使用
Set<Lark> exaltation = new HashSet();
//消除警告信息的、类型安全的泛型使用
Set<Lark> exaltation = new HashSet<Lark>();
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类的定义都可以。应该始终在尽可能小的范围中使用SuppressWarnings注解,永远不要在整个类上使用SuppressWarnings,因为这么做可能会掩盖重要的警告信息。
每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的。这样做可以帮助他人理解代码,更重要的是,可以尽量减少其他人修改代码后导致计算不安全的概率。
非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时抛出ClassCastException异常,要尽最大的努力消除这些警告。如果无法消除非受检警告,同时又足以证明引起警告的代码是类型安全的,就可以在尽可能小的范围中,用@SuppressWarnings("unchecked")注解禁止该警告,并把禁止该警告的原因注释记录下来。
3.列表优先于数组
数组与泛型相比,有两个重要的不同点:
1.数组是协变的
协变指的是如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型;
泛型是不可变的,对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。
2.数组是具体化的
数组在运行时才知道并检查它们的元素类型约束;
泛型则通过类型擦除来实现,它在编译时强化它们的类型信息,在运行时丢弃(或擦除)它们的元素类型信息。
由于数组的协变性和具体化,它是有缺陷的:
//数组具有协变性,Object是Long的父类,声明合法
Object[] objectArray = new Long[1];
//Long[] 退化为Object[],此处赋值也是合法的
objectArray[0] = "I don't fit in";
上述代码可以通过编译,但运行时却抛出ArrayStoreException。
改为列表后,则无法通过编译时的类型检查:
//无法通过编译,List<Object>和List<Long>是不同的类型
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");
因为数组和泛型之间有着根本性的区别,数组和泛型不能很好地混合使用。如下列类型的表达式都是非法的:new List<E>[]、new List<String>[]、new E[]。
创建泛型数组是非法的,是因为泛型数组不是类型安全的。如下代码所示:
List<String>[] strLists = new List<String>[1]; //假设此处合法
List<Integer> intList = Arrays.asList(42);
Object[] objects = strLists; //数组是协变的,此处合法
objects[0] = intList;
String s = strLists[0].get(0); //运行时ClassCastException异常
当得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或简洁性,但换回的却是更高的类型安全性和互用性。
数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但没有编译时的类型安全,对于泛型也一样。
一般来说,数组和泛型不能很好地混合使用,如果将它们混合使用,且得到了编译器的错误或警告,第一反应就应该是用列表代替数组。
4.优先考虑泛型
编写自己的泛型相对比较困难,但很值得花时间去学习如何编写。
下面以一个Stack类为例来说明:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * DEFAULT_INITIAL_CAPACITY + 1);
}
}
}
上述Stack类的实现,主要问题有如下两点:
1.push操作无法保证类型安全
//可以向stack中放入任意类型
Stack stack = new Stack();
stack.push("stack");
stack.put(new Integer(100));
2.pop操作获得元素需要外部手工进行类型转换,且可能会产生ClassCastException异常。
String str = (String)stack.pop();
将上述Stack类进行泛型化,主要步骤为:
1.给它的声明添加一个或者多个类型参数
2.用相应的类型参数替换所有的Object类型,尝试编译
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
//此处提示错误,无法通过编译,因为无法创建泛型数组
elements = new E[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;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * DEFAULT_INITIAL_CAPACITY + 1);
}
}
}
消除泛型数组的方法有两种:
1.直接绕过创建泛型数组,创建一个Object数组
//用法合法,但整体上而言不是类型安全的
elements = (E[])Object[DEFAULT_INITIAL_CAPACITY];
2.将域的类型从E[]改为Object[](推荐使用此种方法)
public class Stack<E> {
private Object[] elements;
...
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = (E) elements[--size];
elements[size] = null;
return result;
}
}
使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型时,要确保它们不需要这种转换就可以使用,这通常意味着要把类做成是泛型的。
5.优先考虑泛型方法
静态工具方法通常比较适合泛型化。
编写泛型方法与编写泛型类相似,如下述代码:
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
上述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方法不仅适用性更强,也是类型安全的,它确保了待合并集合的类型一致性,外部使用也无需进行手工强制转换。
泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来的更加安全,也更加容易。
6.利用有限制通配符来提升API的灵活性
参数类型是不可变的,对于任意两个截然不同的类型Type1和Type2而言,List<Type1>既不是List<Type2>的子类型,也不是它的超类型。如List<String>并不是List<Object>的子类型,这似乎与直觉相悖,但实际上是有意义的,你可以将任何对象放进一个List<Object>中,却只能将字符串放进List<String>中。
个人理解:泛型的参数类型是给编译器使用的,供编译器在获取实参时进行类型检查,在返回结果时进行类型转换,在编译完成后的字节码中,泛型参数类型信息是被擦除了的。即是说,Java的泛型机制是在编译阶段实现的,编译生成的字节码在运行期间并不包含泛型的类型信息。正是由于这种类型擦除机制,导致泛型并不具备协变性,才能保证泛型的类型安全。
泛型不具备协变性,但有时,我们又需要使用协变带来的灵活性,于是Java提供了有限制的通配符类型这种特殊的参数化类型:
GenericType<? extends E>:子类型通配符,通配符?表示E的某个子类型
GenericType<? super E>:超类型通配符,通配符?表示E的某个超类型
考虑Stack的公共API:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
假如我们想增加一个方法,让它按顺序把一系列元素添加到Stack中,尝试如下:
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
如果src中的元素类型与Stack的泛型参数类型完全匹配,是完全没有问题的。但考虑这样一种情形:有一个Stack<Number>,且调用了push(int val),从逻辑上讲,下面的实现应该是可以的:
Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);
实际情况是上述办法并不可行,会导致编译错误。
显然,我们的目的是想将E的某个子类型也放入Stack中,可以利用子类型通配符来做有限制的规定:
public void pushAll(Iterable<? extends E> src) {
for (E e : src) {
push(e);
}
}
假设现在需要编写一个popAll方法,使之与pushAll方法相呼应,popAll方法从Stack中弹出每个元素,并将这些元素添加到指定的集合中,尝试如下:
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
如果dst的元素类型与Stack完全匹配,上述实现是没有问题的。但考虑这样一种情形:有一个Stack<Number>和Collection<Object>,从逻辑上讲,下面的实现应该是可以的:
Stack<Number> numStack = new Stack<Number>();
Collection<Object> coll = ...;
numStack.popAll(coll);
实际情况是上述办法并不可行,会导致编译错误。Collection<Object>并不是Collection<Number>的超类型。
我们的目的是为了将类型为E的元素加入到目标泛型集合中,且目标集合的泛型参数类型只要是类型E的父类型即可,Java提供了父类型通配符来实现这种需求:
//此处的限定是:通配符类型是泛型参数类型的父类即可
public void popAll(Collection<? super E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
为了便于记住要使用哪种通配符,引入下面的助记符:
PECS表示producer-extends,consumer-super。
如果参数化类型表示一个T生产者,就使用<? extends T>;如果它表示一个T消费者,就使用<? super T>。在Stack示例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E>。
PECS助记符突出了使用通配符类型的基本原则。
在API中使用通配符类型虽然比较需要技巧,但它可以使API变得灵活得多。
如果编写的是将被广泛使用的类库,则一定要适当地利用通配符类型。
需要记住的基本原则是:producer-extends,comsumer-super(PECS)。
所有的comparable和comparator都是消费者。
7.优先考虑类型安全的异构容器
泛型最常用于集合,如Set和Map,以及单元素的容器,如ThreadLocal和AtomicReference。在这些用法中,它都充当了被参数化了的容器。这样就限制了每个容器只能有固定数目的类型参数。一般来说,这种情况正是所想要的,一个Set只有一个类型参数,表示它的元素类型;一个Map有两个类型参数,表示它的键和值类型。
但是,有时候你会需要更多的灵活性。例如,数据库行可以有任意多列,如何才能以类型安全的方式访问所有列哪?Java泛型提供了一种方法来解决这个问题:将键(key)进行参数化而不是将容器参数化,然后将参数化的键提交给容器,来插入或者获取值,用泛型系统来确保值的类型与它的键相符。
类Class在Java1.5中被泛化了,类的类型从字面上看不再只是简单的Class,而是Class<T>,意味着String.class是属于Class<String>类型,Integer.class属于Class<Integer>类型。
当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,被称作type token。
假如需要设计一个Favorites类,它允许其客户端从任意数量的其他类中,保存并获得一个“最喜爱”的实例,代码如下:
public class Favorites {
private Map<Class<?>, Object> favorities = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException("Type is null");
}
favorities.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorities.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 fString = f.getFavorite(String.class);
int fInteger = f.getFavorite(Integer.class);
Class<?> fClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", fString, fInteger, fClass.getSimpleName());
}
}
//代码打印结果为:Java cafebabe Favorites
Favorites实例是类型安全的:当你向它请求String的时候,它不会返回一个Integer。同时它也是异构的:不像普通的map,它的所有键都是不同类型的。
像Favorites这种类被称为类型安全的异构容器。
Favorites使用的类型令牌是无限制的,还可以利用有限制类型参数或有限制通配符来限制可以表示的类型:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception e) {
throw new IllegalArgumentException();
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
集合API说明了泛型的常见用法,它限制每个容器只能有固定数目的类型参数。
可以将类型参数放在键上而不是容器上来避开这一限制。
对于这种类型安全的异构容器,可以用Class对象作为键。
以这种方式使用的Class对象被称作类型令牌。
也可以使用定制的键类型,例如,用一个DatabaseRow类型表示一个数据库行(容器),用泛型Column<T>作为它的键。