• [Java读书笔记] Effective Java(Third Edition) 第 5 章 泛型


    第 26 条:请不要使用原生态类型

    声明中具有一个或多个类型参数的类或者接口,就是泛型(generic)。

    例如List接口只有单个类型参数E, 表示列表的元素类型。这个接口全称List<E>,泛型类和接口统称为泛型(generic type)。

    每一种泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。

    它的存在主要是为了兼容泛型之前的代码。

     

    如果使用原生态类型,就失去了泛型在安全性和描述性方面的优势。

    如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会。

    如何使用泛型,但不确定或者不关心实际的类型参数,可以用一个问号代替。

    例如,泛型Set<E>的无限制通配符类型为Set<?>。这是最普通的参数化Set类型,可以持有任何集合。

    通配符是类型安全的,原生态类型不安全。

    不要使用原生类型,有2个例外。

    1. 必须在类文字中使用原生态类型。

    例如:List.class, String[].class, int.class是合法,但是List<String.class>和List<?>.class是不合法。

    2. 由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。

    利用泛型来使用instanceof操作符的首选方法,例如:

    if (o instanceof Set) {
        Set<?> s = (Set<?>) o;
    }

    总之,使用原生态类型会在运行时导致异常。原生态类型只是为了与遗留代码兼容。

    第 27 条:消除非受检的警告

    用泛型编程会遇到很多编译器警告:

    非受检转换警告(unchecked cast warning)、非受检方法调用警告、非受检参数化可变参数类型警告(unchecked parameterized vararg type warning)

    以及非受检转换警告(unchecked conversion warning)。

    例如:

    Set<Lark> exaltation = new HashSet();

    编译器会提醒出错地方:warning:[unchecked] unchecked conversion

    Java7中开始引入菱形操作符(<>),编译器可以推断出正确的实际参数类型.

    Set<Lark> exaltation = new HashSet<>(); 

    尽可能地消除每一个非受检警告。

    如果无法消除警告,同时可以证明引起警告的代码是类型安全的,才可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。

    SuppressWarnings可以用在任何颗粒度,应该尽量小的范围使用SuppressWarnings注解。

     每当使用SuppressWarnings(“unchecked”)注解时,都要添加一条注解,说明为什么这么做是安全的。

    总之,每一条警告都表示可能在运行时抛出ClassCastException,要尽量消除这些警告。

    第 28 条:列表优于数组

    数组与泛型相比有两个重要不同点。

    第一,数组是协变的(covariant),泛型是可变的(invariant)

      表示如果Sub为Super的子类型,那么数组Sub[]就是Super[]的子类型。

      对于泛型,不同类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是超类型。

      例如,不能将String放入Long容器中,但是数组只会在运行时发现,用列表,则可以在编译时发现错误。

    第二,数组是具体化的,泛型是通过擦除来实现的。

      数组在运行时知道和强化他们的元素类型,如果将String放入Long数组中,会得到ArrayStoreException.

      泛型在编译时强化他们的类型信息,在运行时丢弃(擦除)他们的元素信息。擦除使泛型可以与没有泛型的代码互用。

      创建泛型、参数化类型或者类型参数的数组是非法的。

      例如:new List<E>[]、new List<String>[]和new E[]。

    创建泛型数组不是类型安全的。

    优先使用集合类型List<E>,而不是数组类型E[],可能会损失一些性能,但换回是更高的类型安全性和互动性。

    总之,数组和泛型有完全不同的类型规则。数组是协变且可以具体化的,泛型是不可变且可以被擦除的。

    因此,数组提供了运行时的类型安全,但没有编译时的类型安全,反之,泛型也一样。数组和泛型不能很好地混合使用,优先用列表代替数组。

    第 29 条: 优先考虑泛型

      第28条鼓励优先使用列表而非数组。实际上不可能总是在泛型中使用列表。

    Java并不是生来就支持列表,因此有些泛型如ArrayList必须在数组上实现。为了提升性能,其他泛型如HashMap也在数组上实现。

    总之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更容易。

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

    静态工具方法尤其适合于泛型化。Collections中所有“算法”方法(例如binarySearch和sort)都泛型化了。

    public static Set union(Set s1, Set s2) {
         Set result = new HashSet(s1);
         result.addAll(s2);
         return result;
    }

    这个方法可以编译,但有2条警告。

    为了修正警告,使方法变成类型安全的,将方法声明修改为一个类型参数(type parameter),表示这三个集合元素类型(两个参数和一个返回值),并在方法中使用类型参数。

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
         Set<E> result = new HashSet<>(s1);
         result.addAll(s2);
         return result;
    }

    声明类型参数的类型参数列表,处在方法的修饰符及其返回值之间。

    总之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并且返回值的方法更安全,也更容易。

    就像类型一样,应该确保方法不用转换就能使用,还应该将现有方法泛型化,使新用户使用起来更轻松,且不会破坏现有的客户端。

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

    Java提供了一种特殊的参数化类型,称为有限制的通知符类型(bounded wildcard type)。

    例如,E的某个子类型的Iterable接口,通配符类型Iterable<? extends E>。

    public void pushAll(Iterable<? extends E> src) {
      for (E e : src)
          push(e);
    }

    另一种,E的某种超类的集合,通用符Collection<? super E>。

    public void popAll(Collection<? super E> dst) {
      while(!isEmpty())
          dst.add(pop());
    }

    为了获取最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型。

    PESC表示producer – extends, consumer – super.

    如果参数化类型表示一个生产者T,就用<? extends T>; 如果它表示一个消费者T,就用<? super T>。

    例如第30条的union方法:

    public static <E> Set<E> union(Set<E> s1, Set<E> s2)

    用PESC原则:

    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

    注意:返回类型仍然是Set<E>,不要用通配符类型作为返回类型。

    修改声明后:

    Set<Integer> ints = Set.of(1, 3, 5);
    Set<Double> dob = Set.of(2.0, 4.0, 6.0);
    Set<Number> num = union(ints, dob);

    第30条的max方法,初始声明:

    public static <T extends Comparable<T>> T max(List<T> list)

    修改使用通配符后:

    public static <T extends Comparable<? super T>> T max(List<? extends T> list)

    这是将通配符运用到类型参数。参数化类型Comparable<T>被有限制通配符类型Comparable<? super T>取代。 comparable是消费者,因此使用时始终应该是Comparable<? super T>优先于Comparable<T>。对于comparator接口也一样,使用时始终应该是Comparator<? super T>优先于Comparator<T>

    还有一点,类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。如下:

    public static <E> void swap(List<E> list, int i, int j); // 使用无限制的类型参数

    public static void swap(List<?> list, int i, int j); // 使用无限制的通配符

    在公共API中,第二种更好些,因为它更简洁。

    如果类型参数只在一个方法声明中出现一次,就可以使用通配符取代它。

    总之,在API中使用通配符需要技巧,会是API变得很灵活。如果编写是将被广泛使用的类库,则一定要适当地利用通配符类型。基本原则:producer – extends, consumer – super.(PECS)。还有所有的comparable和comparator都是消费者。

    第 32 条:谨慎并用泛型和可变参数

    可变参数(vararg)方法和泛型都是Java5出现的,但他们不能良好地相互作用。

    可变参数作用在于让客户端能够将可变数量的参数传给方法,当调用一个可变参数方法时,会创建一个数组用来存放可变参数。这个数组是一个实现细节,可见的。因此,当可变参数有泛型或者参数化类型时,编译警告信息就会产生。

    当一个参数化类型的变量指向一个不是该类型对象时,会产生堆污染(heap pollution)。它导致编译器的自动生成转换失败,破坏了泛型系统的基本保证。

    例如,第28条代码改编:

    static void deangerous(List<String>… stringLists){
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        object[0] = intList;    // heap pollution
        String s = stringLists[0].get(0);  // ClassCastException
    }

    上述最后一行代码中有一个不可见转换,由编译器生成。这个转换失败证明类型不安全。

    因此将值保存在泛型可变参数数组参数中是不安全的。

    在Java7中,增加了SafeVarargs注解,它让带泛型vararg参数的方法的设计者能够自动禁止客户端的警告。通过方法的设计者承诺声明这是类型安全的。

    确定何时使用SafeVarargs注解的规则:对于每一个带有泛型可变参数或者参数化类型的方法,都要用@SafeVarargs进行注解。

    安全使用泛型可变参数的例子:

    @SafeVarargs
    static <T> List<T> flatten(List<? extends T>… lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

    如不想使用@SafeVarargs,也可用一个List参数代替可变参数:

    static <T> List<T> flatten(List<List<? extends T>> lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }

    总之,可变参数和泛型不能良好地合作。如果选择编写带泛型(或参数化)可变参数的方法,首先要确保该方法时类型安全的,然后用@SafeVarargs对它进行注解。

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

    泛型最常用于集合,如Set<E>和Map<K,V>,以及单个元素的容器,如ThreadLocal<T>和AtomicReference<T>。限制每个容器只能有固定数目的类型参数。

    有时候需要更多灵活性,将键(key)进行参数化而不是将容器(container)参数化,然后将参数化的键提交给容器来插入或者获取值。用泛型来确保值得类型与它的键相符。

    例如:

    public class Favorites {
        private Map<Class<?>, Object> favorites = new HashMap<>();
        public <T> void putFavorites(Class<T> type, T instance) {
            favorites.put(Objects.requireNonNull(type), instance);
        }
    
        public <T> T getFavorites(Class<T> type) {
            return type.cast(favorites.get(type));
        }
    }

    每个Favorites实例得到一个favorites的私有Map<Class<?>, Object>的支持。每个键都可以有一个不同的参数化类型:一个是Class<String>, 接下来是Class<Integer>,异构就是从这里来的。

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

    总之,集合API说明了泛型的一般用法,限制每个容器只有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于类型安全的异构容器,可以用Class对象作为键。

  • 相关阅读:
    自动化生成测试报告
    测试用例设计的常见几种方法
    python的七种数据类型
    python读写文件的几种方法
    测试工具之fiddler
    自动化前置用例和后置用例
    python的几种数据类型以及举例
    Selenium请求库
    第一篇帖子,上火了
    汉诺塔算法
  • 原文地址:https://www.cnblogs.com/fyql/p/11510332.html
Copyright © 2020-2023  润新知