第23条: 不要在新代码中使用原生态类型
声明中具有一个或多个类型参数的类或接口,就是泛型类或接口。每种泛型都定义一组参数化的类型,每个泛型都定义一个原生态类型。例如List<E>相对应的原生态类型是List。
public class RawTypeTest { public static void main(String[] args) { Collection stamps = new ArrayList(); //raw type // Collection<Stamp> stamps = new ArrayList(); stamps.add(new Stamp()); stamps.add(new Stamp()); stamps.add(new Coin()); for(Iterator i = stamps.iterator(); i.hasNext(); ){ Stamp s = (Stamp)i.next(); System.out.println("xxx"); } } } class Stamp{} class Coin{}
该程序会出现ClassCastException。但如果使用泛型,则会在编译期就能准确告诉我们哪里出错了。而且也不用对元素进行手工转换,编译器会插入隐式转换。可以看出,如果不提供类型参数,使用集合类型和其他泛型仍然是合法的,但如果使用原生态类型,就会失掉类型安全性。看下面的例子:
public class RawTypeTest2 { public static void main(String[] args) { // TODO Auto-generated method stub List<String> strings = new ArrayList<String>(); unsafeAdd(strings, new Integer(42)); String s = strings.get(0); } private static void unsafeAdd(List list, Object o){ list.add(o); } }
该程序也会出现ClassCastException,但在编译期没有任何错误。泛型有子类化规则,List<String>是原生态类型List的一个子类型,但不是List<Object>的子类型。所以List<String>
引用可以传递给类型List的参数,但不能传递给类型list<Object>的参数。原生态类型和参数化的类型List<Object>之间的区别是,前者逃避了泛型检查,后者则明确告诉编译期,它能够持有任意类型的对象。如果将unsafeAdd(List list, Object o)改为unsafeAdd(List<Object> list, Object o),则在编译期就会报错。所以使用原生态类型很危险,如果要使用泛型,但不确定或不关心实际的类型参数,就可以使用一个问号代替。
private static int numElementsInCommon(Set<?> s1, Set<?> s2){ int result = 0; for(Object o1 : s1){ if(s2.contains(o1)){ result++; } } return result; }
无限制通配类型Set<?>和原生态类型Set的区别是前者是类型安全的,原生态类型可以将任何元素放进使用原生态类型的集合中,很容易破坏该集合的类型约束条件。但我们不能把任何元素(除null之外)放到Collection<?>中,也就是说只能往Collection<?>放null,这样就不会破坏类型约束条件。
不要在新代码中使用原生态类型这个规则有两个小小的例外:
1.在类文字中必须使用原生态类型。如List.class, int.class合法,而List<String>.class, List<?>.class则不合法。
2.instanceof操作符中使用原生态类型。
if(o instanceof Set){ //raw type Set<?> m = (Set<?>)o; //wildcard type //remainder omitted }
这是泛型使用instanceof操作符的首选方法。一旦确定o是个set,就必须把它转换成通配符类型。
第24条: 消除非受检警告
用泛型编程时,会遇到很多编译期警告。有些非受检警告难以消除,但也要尽可能消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的。如果无法消除警告,同时可以证明引起警告的代码是类型安全的,就可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告。但也应注意要在尽可能小的范围内使用SuppressWarning注解。
public <T> T[] toArray(T[] a) { if (a.length < size){ // Make a new array of a's runtime type, but my contents: @SuppressWarnings("unchecked") T[] result= (T[]) Arrays.copyOf(elementData, size, a.getClass()); return result; } System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
每当使用SuppressWarning注解时,都要添加一条注解,说明为什么这么做是安全的。总之,非受检警告很重要,不要忽略他们。
第25条: 列表优先于数组
数组与泛型相比,有两个重要的不同点。首先,数组是协变的。表示如果sub为super的子类型,那么数组类型sub[]就是 super[]的子类型。泛型则是不可变的,对于任意两个不同的type1和type2,List<Type1>既不是 List<Type2>的子类型,也不是 List<Type2>的超类型。
public static void main(String[] args) { // fails at runtime Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in"; System.out.println(objectArray[0]); //won't compile! // List<Object> ol = new ArrayList<Long>(); // ol.add("I don't fit in"); }
第二大区别在于,数组是具体化的,因此数组在运行时才知道并检查他们元素的约束类型。相比之下,泛型则是通过擦除来实现的。泛型只在编译期强化它们的类型信息,并在运行时丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。由于这些区别,因此数组和泛型不能很好地混用,new List<E>[], new List<String>[], new E[] 这些都会导致一个“泛型数组创建”的错误。所以发现自己将它们混合起来使用,第一反应就应该是用列表代替数组。
第26条: 优先考虑泛型
使用泛型和泛型方法比较容易,但自己编写泛型类则比较困难。
我们将前面写过的stack类改为泛型类,在这过程中至少会碰到一个问题:
public Stack(){ elements = new E[DEFAULT_INITIAL_CAPACITY]; //generic array creation exception }
在构造器中这样写,会出现generic array creation 异常。我们不能创建不可具体化的类型数组。有两种方法可以选:
public class Stack<E> { // private E[] elements; private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; // @SuppressWarnings("unchecked") public Stack(){ elements = new Object[DEFAULT_INITIAL_CAPACITY]; // 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(); } @SuppressWarnings("unchecked") E result = (E) elements[--size]; elements[size] = null; return result; } private void ensureCapacity(){ if(elements.length == size){ elements = Arrays.copyOf(elements, 2 * size + 1); } } }
一种方法是把私有域E[]改为Object[],并修改相应的pop方法。第二种是在构造器中创建Object数组,并把它转为E数组。但要证实未受检的转换是安全的,因此可以禁止该警告。具体选哪种方法看个人偏好。如在代码中有许多地方需要从数组中读取元素,因此采用第一种方法需要多次转换成E,所以第二种方法可能更常用些。
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。所以只要时间允许,就把现有的类型都泛型化。这对于这些类的新用户来说会变得更加轻松,又不会破坏现有的客户端。
第27条: 优先考虑泛型方法
就如类可以从泛型中受益一样,方法也一样。静态工具方法尤其适合泛型化。
考虑两个集合联合的方法:
public static Set union(Set s1, Set s2){ Set result = new HashSet(s1); result.addAll(s2); return result; }
上述代码会有两条警告,指出不应使用原生态类。为了修正这些警告,使方法成为类型安全的,需要将方法声明修改为一个类型参数。
public class GenericMethods { public static <E> Set<E> union(Set<E> s1, Set<E> s2){ Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; } public static void main(String[] args) { // TODO Auto-generated method stub Set<String> guys = new HashSet<String>(Arrays.asList("Tom", "Dick", "Harry")); Set<String> stooges = new HashSet<String>(Arrays.asList("Larry", "Moe", "Curly")); Set<String > xxx = union(guys, stooges); System.out.println(xxx); } }
对于简单的泛型方法而言,就是这么回事。union方法的局限在于,三个集合类型必须全部相同。利用有限制的通配符类型,可以使这个方法变得更加灵活。泛型方法的一个显著特点是,无需明确指出类型参数的值,编译器通过检查方法参数的类型来计算类型参数的值。对于上述程序而言,union两个参数都是Set<String>,从而知道E必须是String,这个过程称作为类型推导。总之,应确保新方法不用转换就能使用,这意味着要将它们泛型化。
第28条: 利用有限制通配符来提升API的灵活性
之前我们提到了<?>形式的无限制通配符,这里则是有限制通配符。上一条目中已经出现了有限制通配符,它一共有这么2种:
<? extends E>:表示可接受E类型的子类型;
<? super E>:表示可接受E类型的父类型。
增加pushAll和popAll后的stack程序:
public class Stack<E> { // private E[] elements; private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; // @SuppressWarnings("unchecked") public Stack(){ // elements = new E[DEFAULT_INITIAL_CAPACITY]; //generic array creation exception elements = new Object[DEFAULT_INITIAL_CAPACITY]; // 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(); } @SuppressWarnings("unchecked") E result = (E) elements[--size]; elements[size] = null; return result; } private void ensureCapacity(){ if(elements.length == size){ elements = Arrays.copyOf(elements, 2 * size + 1); } } public void pushAll(Iterable<? extends E> src){ for(E e : src){ push(e); } } private boolean isEmpty(){ return size == 0; } public void popAll(Collection<? super E> dst){ while(!isEmpty()){ dst.add(pop()); } } public static void main(String[] args){ Stack<Number> numberStack = new Stack<>(); final Collection<Integer> intList = new ArrayList<>(); intList.add(1); intList.add(2); intList.add(3); Iterable<Integer> integers = new Iterable<Integer>() { public Iterator<Integer> iterator() { // TODO Auto-generated method stub return intList.iterator(); } }; numberStack.pushAll(integers); Collection<Object> objects = new ArrayList<>(); numberStack.popAll(objects); System.out.println(objects.toString()); } }
第29条: 优先考虑类型安全的异构容器
一个Favorites类:
public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<>(); public <T> void putFavorite(Class<T> type, T instance){ if(type == null){ throw new NullPointerException("type is null"); } favorites.put(type, 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 favoritesString = f.getFavorite(String.class); Integer favoritesInteger = f.getFavorite(Integer.class); Class<?> favoritesClass = f.getFavorite(Class.class); System.out.println(favoritesString + " | " + favoritesInteger + " | " + favoritesClass.getName()); } }
Favorites实例是类型安全的,当向它请求String的时候,它从来不会返回一个Integer给你。同时也是异构的,不像普通的map,它的所有键都是不同类型的,因此我们将Favorites称为类型安全的异构容器。从代码我们可以看出该类跟据键取出来的值是Object,这是很危险的一件事情。我们写的代码能在编译时检查就在编译时检查而不要等到真正运行起来才做检查,这也就是上面Favorites所带来的好处,它是类型安全的,同时它也是异构的,这个例子值得细细品味。