EffectiveJava--泛型

[b]本章内容:[/b]
1. 请不要在新代码中使用原生态类型
2. 消除非受检警告
3. 列表优先于数组
4. 优先考虑泛型
5. 优先考虑泛型方法
6. 利用有限制通配符来提升API的灵活性
7. 优先考虑类型安全的异构容器

[b]1. 请不要在新代码中使用原生态类型[/b]
泛型是指声明中且有一个或者多个类型参数的类或者接口。每个泛型都定义了一个原生态类型,即不带任何实际类型参数的泛型名称。如List<E>相对应的原生态类型是List。原生态类型就像从类型声明中删除所有泛型信息一样。
如果不提供类型参数,使用集合类型和其他类型也仍然是合法的,但是不应该这么做。如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会,前者逃避了泛型检查,后者则明确告知编译器,它能够持有任意类型的对象。虽然你可以将List<String>传递给类型List的参数,但是不能将它传给类型List<Object>的参数,泛型有子类型化的规则。

在不确定或者不在乎集合中的元素类型的情况下,你也许会使用原生态类型,但是这是很危险的,Java提供了一种安全的替代方法,称作无限制的通配符类型。无限制的通配符类型用一个问号代替,如Set<E>的无限制通配符类型为Set<?>。由于可以将任何元素放进使用原生态类型的集合中,因此很容易破坏该集合的类型约束条件,但不能将任何元素放到Collection<?>中。故通配符类型是安全的,原生态类型则不安全。

不要在新代码中使用原生态类型,这条规则有两个小小的例外,两者都源于“泛型信息可以在支行时被擦除”这一事实。在类文字(class literal)中必须使用原生态类型。 如List.class、String[].class、int.class都合法,但是List<String>.class和List<?>.class则不合法。第二个例外与instanceof操作符有关,由于泛型信息可以在支行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。用<?>代替原生态类型对instanceof操作符的行为不会产生影响,在这种情况下<?>就显得多余了。

总之,使用原生态类型会在运行时导致异常,因此不要在新代码中使用。原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。

[b]2. 消除非受检警告[/b]
用泛型编程时,会遇到许多编译器警告:非受检强制转化警告(unchecked cast warnings)、非受检方法调用警告、非受检普通数组创建警告,以及非受检转换警告(unchecked conversion warnings)。
要尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的,这意味着不会在运行时出现ClassCastException异常。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有这种情况下)才可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告。
SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。应该始终在尽可能小的范围中使用SuppressWarnings注释。它通常是个变量声明,或是非常简短的方法或者构造器。永远不要在整个类上使用SuppressWarnings,这么做可能会掩盖了重要的警告。将SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明。也不要将注解放在整个方法上,应该声明一个局部变量来保存返回值(一段代码中出现警告),并注解其声明。
每当使用SuppressWarnings注释时,都要添加一条注释,说明为什么这么做是安全的。

[b]3. 列表优先于数组[/b]
数组和泛型相比,有两个重要的不同点。首先,数组是协变的,是说如果如果Sub为Super的子类型,那T么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的,对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。你可能认为,这意味着泛型是有缺陷的,但实际上可以说数组才是有缺陷的。
数组和泛型的第二个区别在于,数组是具体化的(reified),因此数组会在运行时才知道并检查他们的元素类型约束。泛型则是通过擦除(指泛型可以与没有使用泛型的代码随意进行互用)来实现的,因此泛型只在编译时强化他们的类型信息,并在运行时丢弃(或者擦除)他们元素的类型信息。
由于上述区别,数组和泛型不能很好的混合使用。创建泛型数组是非法的:List<String>[] stringLists = new List<String>[1],因为它不是类型安全的。
像E、List<E>、List<String>这样的类型应称作不可具体化类型(指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型)
总结,数据和泛型有着非常不同的类型规则,数组是协变且可以具体化的。泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数据和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。

[b]4. 优先考虑泛型[/b]
如下代码定义了一个非泛型集合类:
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 * size + 1);
}
}

可以适当地强化这个类来利用泛型,如下:
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 * size + 1);
}
}
上面的泛型集合类Stack<E>在编译时会引发一个编译错误,即elements = new E[DEFAULT_INITIAL_CAPACITY]语句不能创建不可具体化的类型的数组。
修改方式一如下:elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY],只要我们保证所有push到该数组中的对象均为该类型的对象即可,剩下需要做的就是添加注解以消除该警告:
@SuppressWarning("unchecked")
public Stack() {
elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
}
消除Stack中泛型数组创建错误的第二种方法是,将elements域的类型从E[]改为Object[],然后在包含未受检转换的任务上禁止警告。
@SuppressWarning("unchecked")
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null;
return result;
}
因为代码多个地方需要从数组中读取元素,因此选择第二种方案需要多次转换成E,而不是转换成E[],故常采用第一种方案。

总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。

[b]5. 优先考虑泛型方法[/b]
和优先选用泛型类一样,我们也应该优先选用泛型方法。如下集合使用非泛型方法:
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
这个方法在编译时会有两条警告。为了修正这些警告,最好的方法就是使该方法变为类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的元素类型,并在方法中使用类型参数,见如下修改后的泛型方法代码:
public static <E> Set<E> union(Set<E> s1,Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
现在该方法编译时不会产生任何警告,并提供了类型安全性,也更容易使用。和调用泛型对象构造函数来创建泛型对象不同的是,在调用泛型函数时无须指定函数的参数类型,而是通过Java编译器的类型推导来填充该类型信息。
Set<String> s = union(new HashSet<String>(Arrays.asList("jj","dd")), new HashSet<String>(Arrays.asList("ff","gg")));
union方法的局限性在于三个集合的类型必须全部相同,利用有限制的通配符类型,可以使这个方法变得更加灵活(如下一小节)。
很明显,在等号的两边都显式的给出了类型参数,并且必须是一致的,显得有些冗余。为了消除这种冗余,可以编写一个泛型静态工厂方法,与想要使用的每个构造器相对应,如下:
public static <K,V> HashMap<K,V> newHashMap() {
return new HashMap<K,V>();
}
我们的调用方式也可以改为:Map<String,List<String>> anagrams = newHashMap();

泛型单例工厂模式。有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除实现的,可以给所有必要的类型参数使用单个对象。但是需要编写一个静态工厂方法,重复地给每个必要的类型参数分发对象。这种模式最常用于函数对象。如下示例:
public interface UnaryFunction<T> {
T apply(T arg);
}

private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
public Object apply(Object arg) {
return arg;
}
};
@SuppressWarning("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
return (UnaryFunction<T>)IDENTITY_FUNCTION;
}
调用方式如下:
public static void main(String[] args) {
String[] strings = {"jute","hemp","nylon"};
UnaryFunction<String> sameString = identityFunction();
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers = {1,2.0,3L};
UnaryFunction<Number> sameNumber = identityFunction();
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
对于该静态函数,如果我们为类型参数添加更多的限制条件,如参数类型必须是Comparable<T>的实现类,这样我们的函数对象便可以基于该接口做更多的操作,而不仅仅是像上例中只是简单的返回参数对象,见如下代码:
public static <T extends Comparable<T>> T max(List<T> l) {
Iterator<T> i = l.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compareTo(result) > 0)
result = T;
}
return result;
}
总而言之,泛型方法就想泛型对象一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化。

[b]6. 利用有限制通配符来提升API的灵活性[/b]
有时我们需要的灵活性比不可变类型所能提供的更多,如下:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
现在我们需要增加一个方法,将它按顺序放到堆栈中:
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
如果我们的E类型为Number,而我们却喜欢将Integer对象也插入到该容器中,现在的写法将会导致编译错误,因为即使Integer是Number的子类,由于类型参数是不可变的,因此这样的写法也是错误的。

幸运的是,Java提供了一种特殊的参数化类型,称为有限制的通配符类型(bounded wildcard type)来处理上面的情况,pushAll的输入参数类型不应该为“E的Iterable接口”而应该为“E的某个子类型的Iterable接口”,对上面的代码进行如下修改:
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
修改之后该方法便可以顺利通过编译了。因为参数中Iterable的类型参数被限制为E(Number)的子类型即可。

既然有了pushAll方法,我们可能也需要新增一个popAll的方法与之对应,如下:
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
popAll方法将当前容器中的元素全部弹出,并以此添加到参数集合中。如果Collections中的类型参数和Stack完全一致,这样的写法不会有任何问题,然而在实际的应用中,我们通常会将Collection中的元素视为更通用的对象类型,如Object,如下:
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objs = createNewObjectCollection();
numberStack.popAll(objs);
这样的应用方法将会导致编译错误,因为Object和Stack中Number参数类型是不匹配的,而我们对目标容器中对象是否为Number并不关心,Object就已经满足我们的需求了。为了到达这种更高的抽象,我们需要对popAll做如下的修改:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
修改之后,之前的使用方式就可以顺利通过编译了。因为参数集合的类型参数已经被修改为E(Number)的超类即可。

结论很明显,主了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配。
这里给出了一个助记方式便于我们记住需要使用哪种通配符类型:PECS(producer-extends, consumer-super)。
解释一下,如果参数化类型表示一个T生产者,就使用<? extends T>,如果它表示一个T消费者,就使用<? super T>。在我们上面的例子中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E>。PECS这个助记符突出了使用通配符类型的基本原则。

在上一个条目中给出了下面的泛型示例函数:
public static <E> Set<E> union(Set<E> s1, Set<E> s2);
这里的s1和s2都是生产者,根据PECS原则,它们的声明可以改为:
public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);
由于泛型函数在调用时,其参数类型是可以通过函数参数的类型推演出来的,如果上面的函数被如下方式调用时,将会导致Java的编译器无法推演出泛型参数的实际类型,因此引发了编译错误。
Set<Integer> integers = new Set<Integer>();
Set<Double> doubles = new Set<Double>();
Set<Number> numbers = union(integers,doubles);
如果想顺利通过编译并得到正确的执行结果,我们只能通过显示的方式指定该函数类型参数的实际类型,从而避免了编译器的类型参数自动推演,见修改后的代码:
Set<Number> numbers = Union.<Number>union(integers,doubles);

现在我们再来看一下前面也给出过的max方法,其初始声明为:
public static <T extends Comparable<T>> T max<List<T> srcList);
下面是修改过的使用通配符类的声明:
public static <T extends Comparable<? super T>> T max(List<? extends T> srcList);
下面将逐一给出新声明的解释:
1. 函数参数srcList产生了T实例,因此将类型从List<T>改为List<? extends T>;
2. 最初T被指定为扩展Comparable<T>,然而Comparable又是T的消费者,用于比较两个T之间的顺序关系。因此参数化类型Comparable<T>被替换为Comparable<? super T>。
注:Comparator和Comparable一样,他们始终都是消费者,因此Comparable<? super T>优先于Comparable<T>。

总而言之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活得多。如果编写的是将被广泛使用的类库,则一定要适当地利用通配符类型。

[b]7. 优先考虑类型安全的异构容器[/b]
泛型最常用于集合,如Set和Map等。这样的用法也就限制了每个容器只能有固定数目的类型参数,如一个Set只有一个类型参数,一个Map有两个类型参数,一般来说,这也确实是我们想要的。然而有的时候我们需要更多的灵活性,如数据库可以用任意多的Column,如果能以类型安全的方式访问所有Columns就好了,幸运的是有一种方法可以很容易的做到这一点,就是将键(key)进行参数化,而不是将容器参数化,如下:
public class Favorites {
public <T> void putFavorite(Class<T> type,T instance);
public <T> T getFavorite(Class<T> type);
}
下面是该类的使用示例,它保存、获取并打印一个String、Integer和Class实例:
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());
}
正如所料,打印结果为:Java cafebabe Favorites。
这里Favorites实例是类型安全的:当你请求String的时候,它是不会给你Integer的。同时它也是异构的容器,不像普通的Map,他的所有键都是不同类型的。下面就是Favorites的完整实现:
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,type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
可以看出每个Favorites实例都得到一个Map<Class<?>,Object>容器的支持。通配符类型是嵌套的,它不是属于通配符类型的Map的类型,而是它的键的类型,由此可见第个键都可以有一个不同的参数化类型。异构就是从这里来的。
getFavorite方法的实现利用Class的cast方法,将对象引用动态地转换成了Class对象所表示的类型。它检验它的参数是否为Class对象所表示的类型的实例。如果是就返回参数,否则就抛出ClassCastException异常,getFavorite中的cast调用永远不会抛出ClassCastException异常,favorites映射中的值会始终与键的类型相匹配。
Favorites类有两种局限性,首先,恶意的客户端可以很轻松地破坏Favorites实例的类型安全,只要以它的原生态使用Class对象。第二种局限性在于它不可具体化的类型中,可以保存String或String[],但不能保存List<String>,否则程序不能编译。原因在于你无法为List<String>获得一个Class对象。

总而言之,集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称作类型令牌。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值