EFFECTIVE JAVA 3RD EDITION — 第五章 泛型

Item 26 : Don’t use raw types

不要使用原始类型(没有类型声明的泛型)

因为编译器无法知道你的一个集合装的是什么,比如下面的代码:

private List list = new ArrayList();
public void addE(Object o) {
    list.add(o); // got a unchecked warning
}

上面代码在编译期间是不会报错的,只有当你获取元素进行类型转换的时候会抛出类转换异常,所以最好的方式就是像下面一样写:

private List<String> strList = new ArrayList<>();
 public void addE(Object o) {
 strList.add(o); // 编译无法通过
}

编译器就会知道集合中应该装什么类型,在编译期间就会抛出异常,并且保证集合中的元素都是合法的元素。

ps : 不要忽略任何警告(检查警告等)

有时候我们不需要关心集合元素本身,比如比较两个Set集合中相同元素个数:

static int numElementsInCommon(Set s1, Set s2) {...}

这种情况最好使用通配类型,比如:

static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

Set<?>就代表了任意类型,由此来保证类型安全和灵活性.

Item 27 : Eliminate unchecked warnings

解决未检查警告

如果能够保证类型安全可以使用 SuppressWarnings("unchecked") 来忽略未检查警告,但尽可能使用小的范围.

无法在return上使用该注解,可以使用一个局部变量来使用使用这个注解
使用该注解之后记得加注释说明为什么

Item 28 : Prefer lists to arrays

使用集合而不是数组

  1. 数组是协变的:如果Sub是Super的子类,那么Sub[]也是Super[]的子类;而泛型则是不变的
    如果你往一个String的容器里放入Long,数组会在运行时报错,而泛型则会在编译时报错
  2. 数组是具体化的,也就是说会在运行时期强行类型检查;而泛型则会在编译时期进行类型检查,在运行时期忽略或者擦除(erasure)类型信息
    泛型的这种特性为Java5以前的代码过度到泛型提供了方便
    泛型数组的创建是不合法的:
  3. 类型不安全
  4. 当在可变参数方法中使用泛型时会收到警告,因为可变参数也是由一个数组保存的,可以使用 SafeVarargs 注解
  5. 如果收到一个泛型数组创建错误或者类型转换检查警告可以将数组转换成List,虽然可能会丢失一些性能但是保证了类型安全,比如:
public class Chooser<T> {
    private final T[] choiceArray;
    public Chooser(Collection<T> choices) {
        choiceArray = choices.toArray();
    }
}

可以使用List来替换数组:

public class Chooser<T> {
    private final List<T> choiceList;
    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
    }
}

总的来说数组提供了运行时类型安全而不是编译时类型安全,而泛型则刚好相反,搞不清或者为了安全最好使用List而不是数组

Item 29 : Favor generic types

偏向使用通用类型

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_SIZE = 16;
 
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[DEFAULT_INITIAL_SIZE];
    }
 
    public void push(E e) {
        // ensure capacity ...
        elements[size++] = e;
    }
 
    public E pop() {
        if (size == 0) throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null;
        return result;
    }
    // other operation in Stack ...
}

上面的代码中我们用E替换了Object,在构造器中由于不能使用泛型数组这里进行了强制类型转换,并且保证唯一一处数组初始化的时候类型安全,所以用 SuppressWarnings
泛型比需要在代码中强制类型转换的更为简单和安全,当你设计新类型并且需要类型转换的时候考虑泛型

Item 30 : Favor generic methods

偏向使用通用方法

public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (c.isEmpty())
        throw new IllegalArgumentException("Empty collection");
    E result = null;
    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);
 
    return result;
}

上面的代码说明了:

  1. 只有实现了Comparable接口才能进行比较
  2. 只能同类型进行比较
    上面代码会抛出异常,可以返回一个Optional,泛型方法和泛型有同样的优点

Item 31 : Use bounded wildcards to increase API flexibility

使用限定通配符来增加API灵活性

比如一个Stack的pushAll方法:

public void pushAll(Iterable<E> src) {
    for (E e : src)
       push(e);
}
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);

看起来是没什么问题,但是会出现错误,因为参数类型是不变的,虽然Integer继承了Number但是编译器不知道,这个时候就需要限定通配类型来解决(bounded widlcard type):

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

又比如另外一个例子:

public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);

这里会出现类似的错误,因为编译器认为 Collection<Object> 不是Collection<Number> 的子类,这里就可以作类似的修改:

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

到这里就会有一个疑问了,到底什么时候用 extends 什么时候用 super?

通常在输入参数中用生产者和消费者来最大化灵活性:
如果是生产者用 extends 消费者用 super

比如上面的例子中pushAll方法是生产 E 实例,那么就用 extends,popAll方法消费实例就用 super

需要注意的是虽然输入参数使用了限定通配符但是返回类型不要使用,即不要有Set<? extends E>这样的返回值

当使用者需要去考虑统配类型的时候说明API的设计是有问题的

另外一个例子:

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

按照前面的规则,该方法产生集合最大值是生产者,所以参数可以修改为 List<? extends T>, 这里返回值依旧是 T

同时 Comparable也是消费者,所以修改为下面:

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

Comparable通常都是消费者,所以通配都可以写成 Comparable<? super T>, Comparator<? super T>

Item 32 : Combine generics and varargs judiciously

结合泛型和可变参数

Heap Pollution(堆污染):
指一个参数化的变量所引用的并不是他本身的类型

这就很可能会造成 ClassCastException,比如下面的例子:

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

所以在可变参数数组中存储是不安全的

我们使用SafeVarargs注解来保证一个方法是类型安全的,通常通用可变参数方法都会有警告提示可能会有堆污染

再来看下面的例子:

static <T> T[] toArray(T... args) {
    return args;
}

乍看是没有问题的,因为这个方法只是作了一个参数传递,那么看下面的调用:

static <T> T[] pickTwo(T a, T b, T c) {
    switch(ThreadLocalRandom.current().nextInt(3)) {
         case 0: return toArray(a, b);
         case 1: return toArray(a, c);
         case 2: return toArray(b, c);
    }
    throw new AssertionError(); // Can't get here
}

这个调用看起来也是没什么毛病,但是执行 toArray方法之后就会报 ClassCastException,因为 toArray的返回类型是由传入的参数决定的,
编译器是无法精确的预测具体类型,所以在 pickTwo方法中会统一返回 Object[] 数组,但是pickTwo方法传入的是String,所以返回也应该是String数组,
这里隐藏了一个类型转换并且转换失败了。

上面的例子说明了把可变参数传递到另一个方法是不安全的。

但是在Java 9 中 List.of()方法源码:

@SafeVarargs
@SuppressWarnings("varargs")
static <E> List<E> of(E... elements) {
	switch (elements.length) { // implicit null check of elements
        case 0:
            return ImmutableCollections.List0.instance();
        case 1:
            return new ImmutableCollections.List1<>(elements[0]);
        case 2:
            return new ImmutableCollections.List2<>(elements[0], elements[1]);
        default:
            return new ImmutableCollections.ListN<>(elements);
    }        
}

ListN方法:

@SafeVarargs
ListN(E... input) {
    // copy and check manually to avoid TOCTOU
    @SuppressWarnings("unchecked")
    E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
    for (int i = 0; i < input.length; i++) {
        tmp[i] = Objects.requireNonNull(input[i]);
    }
    this.elements = tmp;
}

可以看到用的是 Object[] 数组然后再作的类型转换,这里保证了类型安全

通用变长参数一个典型用法:

@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需要加在每一个通用变长方法上来保证类型安全,但是我们可以按照 Item28 来进行修改,不用变长参数:

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;
}

Item 33 : Consider typesafe heterogeneous containers

考虑使用封装好的类型安全的容器

比如 ThreadLocal<T>, AtomicReference<T>等等

在使用一些封装好了的类型安全的容器(集合框架)的时候其实限制了我们的参数个数,但是我们可以用 Class 对象来作为这些容器的key,这样的 Class 被叫做 type token

版权所有丨转载请注明出处:https://minei.me/archives/340.html