转载

《Effective Java》Second Edition 第5章 泛型

第5章 泛型

在没有泛型之前,从集合中读取到的每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。有了泛型之后,可以告诉编译器每个集合中接受哪些对象类型。编译器自动地为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。

第23条:请不要在新代码中使用原生态类型

  1. 例如,从Java 1.5发行版本起,List接口就只有单个类型参数E,表示列表的元素类型。从技术角度来看,这个接口的名称应该是指现在的List<E>(读作“E的列表”),但是人们经常把它简称为List。泛型类和接口统称为泛型(generic type)。
  2. 虽然假设不小心将coin插入到stamp集合中可能显得有点牵强,但这类问题却是真实的。例如,很容易想象有人会不小心将一个java.util.Date实例放进一个原本只包含java.sql.Date实例的集合中。
  3. 虽然你可以将List<String>传递给类型List的参数,但是不能将它传给类型List<Object>的参数。泛型有子类型化(subtyping)的规则,List<String>是原生态类型List的一个子类型,而不是参数化类型List<Object>的子类型。因此,如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会。
  4. 从Java 1.5发行版本开始,Java就提供了一种安全的替代方法,称作无限制的通配符类型(unbounded wildcard type)。如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替。例如,泛型Set<E>的无限制通配符类型为Set<?>(读作“某个类型的集合”)。
  5. 你不仅无法将任何元素(除null之外)放进Collection<?>中,而且根本无法猜测你会得到哪种类型的对象。要是无法接受这些限制,就可以使用泛型方法(generic method)或者有限制的通配符类型(bounded wildcard type)。
  6. 在类文字(class literal)中必须使用原生态类型。规范不允许使用参数化类型(虽然允许数组类型和基本类型)。换句话说,List.class,String[].class和int.class都合法,但是List<String>.class和List<?>.class则不合法。
  7. 由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。用无限制通配符类型代替原生态类型,对instanceof操作符的行为不会产生任何影响。
  8. Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合。 
    有限制类型参数 <E extends Number> 
    递归类型限制 <T extends Comparable<T>> 
    有限制通配符类型 List<? extends Number> 
    泛型方法 static <E> List<E> asList(E[] a) 
    类型令牌 String.class

第24条:消除非受检警告

  1. 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才)可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。
  2. SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。应该始终在尽可能小的范围中使用SuppressWarnings注解。它通常是个变量声明,或是非常简短的方法或者构造器。永远不要在整个类上使用SuppressWarnings,这么做可能会掩盖了重要的警告。
  3. 每当使用SuppressWarnings(“unchecked”)注解时,都要添加一条注释,说明为什么这么做是安全的。

第25条:列表优先于数组

  1. 首先,数组是协变的(covariant)。这个词听起来有点吓人,其实只是表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的(invariant):对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。你可能认为,这意味着泛型是有缺陷的,但实际上可以说数组才是有缺陷的。
  2. 相比之下,泛型则是通过擦除(erasure)来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。
  3. 由于上述这些根本的区别,因此数组和泛型不能很好地混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的。这些数组创建表达式没有一个是合法的:new List<E>[]、new List<String>[]和new E[]。这些在编译时都会导致一个generic array creation(泛型数组创建)错误。
  4. 从技术的角度来说,像E、List<E>和List<String>这样的类型应称作不可具体化的(non-reifiable)类型。直观地说,不可具体化的(non-reifiable)类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。
  5. 这也意味着在结合使用可变参数(varargs)方法和泛型时会出现令人费解的警告。这是由于每当调用可变参数方法时,就会创建一个数组来存放varargs参数。如果这个数组的元素类型不是可具体化的(reifiable),就会得到一条警告。
  6. 编译器告诉你,它无法在运行时检查转换的安全性,因为它在运行时还不知道E是什么——记住,元素类型信息会在运行时从泛型中被擦除。

第26条:优先考虑泛型

  1. 如第25条中所述,你不能创建不可具体化的(non-reifiable)类型的数组,如E。每当编写用数组支持的泛型时,都会出现这个问题。解决这个问题有两种方法。第一种,直接绕过创建泛型数组的禁令:创建一个Object的数组,并将它转换成泛型数组类型。现在错误是消除了,但是编译器会产生一条警告。这种用法是合法的,但(整体上而言)不是类型安全的。
  2. 编译器不可能证明你的程序是类型安全的,但是你可以证明。你自己必须确保未受检的转换不会危及到程序的类型安全性。相关的数组(即elements变量)保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其他方法。这个数组中保存的唯一元素,是传给push方法的那些元素,它们的类型为E,因此未受检的转换不会有任何危害。
  3. 看来上述的示例与第25条相矛盾了,第25条鼓励优先使用列表而非数组。实际上并不可能总是或者总想在泛型中使用列表。Java并不是生来就支持列表,因此有些泛型如ArrayList,则必须在数组上实现。为了提升性能,其他泛型如HashMap也是数组上实现。

第27条:优先考虑泛型方法

  1. 静态工厂方法尤其适合于泛型化。Collections中的所有“算法”方法(例如binarySearch和sort)都泛型化了。
  2. 声明类型参数的类型参数列表,处在方法的修饰符及其返回类型之间。
  3. 类型限制<T extends Comparable<T>>,可以读作“针对可以与自身进行比较的每个类型T”,这与互比性的概念或多或少有些一致。

第28条:利用有限制通配符来提升API的灵活性

  1. 为了获得最大限度地灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。
  2. PECS表示producer-extends, consumer-super。
  3. 换句话说,如果参数化类型表示一个T生产者,就使用<? extends T>;如果它表示一个T消费者,就使用<? super T>。在我们的Stack示例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E>。
  4. 不要用通配符类型作为返回类型。
  5. 最初T被指定用来扩展Comparable<T>,但是T的comparable消费T实例(并产生表示顺序关系的整值)。因此,参数化类型Comparable<T>被有限制通配符类型Comparable<? super T>取代。comparable始终是消费者,因此使用时始终应该是Comparable<? super T>优先于Comparable<T>。对于comparator也一样,因此使用时始终应该是Comparator<? super T>优先于Comparator<T>。
// Won't compile - wildcards can require change in method body!
public static <T extends Comparable<? super T>> T max(List<? extends 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;
}

它意味着list不是一个List<T>,因此它的iterator方法没有返回Iterator<T>。它返回T的某个子类型的一个iterator,因此我们用它代替iterator声明,它使用了一个有限制的通配符类型:

Iterator<? extends T> i = list.iterator();

问题在于list的类型为List<?>,你不能把null之外的任何值放到List<?>中。幸运的是,有一种方式可以实现这个方法,无需求助于不安全的转换或者原生态类型(raw type)。这种想法就是编写一个私有的辅助方法来捕捉通配符类型。为了捕捉类型,辅助方法必须是泛型方法,像下面这样:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

还要记住所有的comparable和comparator都是消费者。

第29条:优先考虑类型安全的异构容器

  1. 泛型最常用于集合,如Set和Map,以及单元素的容器,如ThreadLocal和AtomicReference。在这些用法中,它都充当被参数化了的容器。
  2. 例如,数据库行可以有任意多的列,如果能以类型安全的方式访问所有列就好了。幸运的是,有一种方法可以很容易地做到这一点。这种想法就是将键(key)进行参数化而不是将容器(container)参数化。然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与它的键相符。
  3. Class对象充当参数化键的部分。之所以可以这样,是因为类Class在Java 1.5版本中被泛型化了。类的类型从字面上来看不再只是简单的Class,而是Class<T>。例如,String.class属于Class<String>类型,Integer.class属于Class<Integer>类型。
  4. Favorites实例是类型安全的(typesafe)的:当你向它请求String的时候,它从来不会返回一个Integer给你。同时它也是异构的(heterogeneous):不像普通的map,它的所有键都是不同类型的。因此,我们将Favorites称作类型安全的异构容器(typesafe heterogeneous container)。
// Typesafe heterogeneous container pattern - implementation
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);
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

这里发生了一些微妙的事情。每个Favorites实例都得到一个称作favorites的私有Map<Class<?>, Object>的支持。你可能认为由于无限制通配符类型的关系,将不能把任何东西放进这个Map中,但事实正好相反。要注意的是通配符类型是嵌套的:它不是属于通配符类型的Map的类型,而是它的键的类型。由此可见,每个键都可以有一个不同的参数化类型:一个可以是Class<String>,接下来是Class<Integer>等等。异构就是从这里来的。

它先从favorites映射中获得与指定Class对象相对应的值。这正是要返回的对象引用,但它的编译时类型是错误的。它的类型只是Object(favorites映射的值类型),我们需要返回一个T。因此,getFavorite方法的实现利用Class的cast方法,将对象引用动态地转换(dynamically cast)成了Class对象所表示的类型。

cast方法是Java的cast操作符的动态模拟。它只检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则就抛出ClassCastException异常。

换句话说,你可以保存最喜爱的String或者String[],但不能保存最喜爱的List<String>。如果试图保存最喜爱的List<String>,程序就不能进行编译。原因在于你无法为List<String>获得一个Class对象:List<String>.Class是个语法错误,这也是件好事。List<String>和List<Integer>共用一个Class对象,即List.class。

原文地址:http://www.jsdblog.com/article/63   

正文到此结束
Loading...