Effective_java读书笔记2

 2019-11-260 Comments

Chapter 5.

Item 23.Don’t use raw types in new code

raw type就是对于用了泛型的类在声明时没有使用具体的类,例如List<E>使用时候直接使用List,在添加一个错误类型进去的时候不会出错,但是在获取的时候会出问题,如果加上了具体的类型就会正在编译时就会提醒,而且不需要进行强制类型转换。

If you use raw types, you lose all the safety and expressiveness benefits of generics.

List<String>是List的子类型,但是不是List<Object>的子类型(具体的看ITEM25)。

unbounded wildcard types(无限通配符):
看如下例子(获取两个Set中相同的元素的个数):

// Use of raw type for unknown element type - don't do this!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
    return result;
}

可以达到效果,但是很危险,在不知道参数是什么具体类型的情况下,可以用问好代替,例如Set<E>可以用Set<?>(read “set of some type”)表示

// Unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
    return result;
}

有两个例外点:

  1. You must use raw types in class literals.也就是说使用List.classString[].class,但是不能使用List<String>.class
  2. Because generic type information is erased at runtime, it is illegal to use the instanceof operator on parameterized types other than unbounded wildcard types.
    所以如果要使用可以如下:
    // Legitimate use of raw type - instanceof operator
    if (o instanceof Set) { // Raw type
    Set<?> m = (Set<?>) o; // Wildcard type
    ...
    }
    

Item 24.Eliminate unchecked warnings

如果无法消除一个unchecked warning,在保证类型安全(typesafe)的情况下,可以使用一个@SuppressWarnings("unchecked")来使这个warning消失。
作用域可以从单个局部声明变量到整个类都可以,但是最好尽可能的小。当发现SuppressWarnings作用在多于一行的方法或者constructor之上,最好把它移到一个局部变量上,例如:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        return (T[]) Arrays.copyOf(elements, size, a.getClass());
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

编译后,return (T[]) Arrays.copyOf(elements, size, a.getClass());这一行会报错,但是return上不可以加@SuppressWarnings("unchecked")那么就只能加在方法上。那么注解的作用就多余一行,那么可以新建一个局部变量。如下:

// Adding local variable to reduce scope of @SuppressWarnings
public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        // 这个cast是成立的,因为我们创建的类型和传入的类型是一致的都是T[]
        @SuppressWarnings("unchecked") T[] result =
            (T[]) Arrays.copyOf(elements, size, a.getClass());
        return result;
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

当使用@SuppressWarnings("unchecked")之后我们应该加上注释,为什么这里是安全的。

Item 25.Prefer lists to arrays

Arrays和Generic有两个很大的不同:

  1. Arrays are covariant. Generics are invariant.比如说Sub是Super的子类,那么Sub[]就是Super[]的子类,但是List<Sub>就不会是List<Super>的子类。例如书中的例子:
    // Fails at runtime!
    Object[] objectArray = new Long[1];
    objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
    
    // Won't compile!
    List<Object> ol = new ArrayList<Long>(); // Incompatible types
    ol.add("I don't fit in");
    
    前者会在运行时出现错误,而后者则直接无法编译,那么我们肯定更加偏向于后者,在运行之前就找到问题。
  2. Arrays are reified. Generics are implemented by erasure.也就是说Arrays会一直知道数组的类型,所以如上,在运行的时候如果添加了String到Long类型中会报错,而泛型会在编译的时候检查,而在运行的时候消除。Erasure是实现版本兼容的方式。

因为两者的不同,他们不能很好的结合,例如,不能创建泛型的数组(new E[]),带有泛型的List的数组(new List<E>[] 或new List<String>[])。如下:

// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1];     // (1)
List<Integer> intList = Arrays.asList(42);             // (2)
Object[] objects = stringLists;                     // (3)
objects[0] = intList;                                 // (4)
String s = stringLists[0].get(0);                     // (5)

假设1可以编译通过,那么2创建并初始化了一个List<Inter>,3由于Arrays are covariant可以编译通过,那么objects就存储了一个List<String>[],而4因为Generics are implemented by erasure,List<String>和List<Integer>在运行时都表示为List,所以objects[0]中就存入了一个intList,但是问题出现了,objects是List<String>类型的但是0位置上存的是List<Integer>所以5在读取的时候会出现ClassCastException

最好使用List<E> 而不是E[],如下例子:
假设有一个同步的List和一个方法(将两个传入的参数进行操作后返回第三个数,方法会随参数类型的改变而改变),我们需要设计一个reduce方法,根据function,把List中的数进行操作。reduce方法还带一个默认值。

// Reduction without generics, and with concurrency flaw!
static Object reduce(List list, Function f, Object initVal) {
    synchronized(list) {
        Object result = initVal;
        for (Object o : list)
            result = f.apply(result, o);
        return result;
    }
}
interface Function {
    Object apply(Object arg1, Object arg2);
}

toArray()方法内部加锁可以修改为如下:

// Reduction without generics or concurrency flaw
static Object reduce(List list, Function f, Object initVal) {
    Object[] snapshot = list.toArray(); // Locks list internally
    Object result = initVal;
    for (Object o : list)
        result = f.apply(result, o);
    return result;
}

如果我们将Function和reduce改写为泛型就会出现问题,如下所示:

interface Function<T> {
    T apply(T arg1, T arg2);
}
static <E> E reduce(List<E> list,Function<E> f,E initVal){
    E[] snapshot = list.toArray();
    E result=initVal;
    for(E e : snapshot){
        result=f.apply(result,e);
    }
    return result;
}

编译方法的时候会出现编译不通过,我们需要将lists.toArray()后的内容强制转换为E[]类型

E[] snapshot = (E[]) list.toArray();

但是这样会出现一个warning: [unchecked] unchecked cast因为泛型E的元素类型信息在运行的时候是被删除的,这样可以运行,但是不安全,E[]在运行是可以是String[],也可以是Integer[]或者其他任何的类型,所以可以能出现转换的错误。因此我们需要做的就是把Arrays转换为List

// List-based generic reduction
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    List<E> snapshot;
    synchronized(list) {
        snapshot = new ArrayList<E>(list);
    }
    E result = initVal;
    for (E e : snapshot)
        result = f.apply(result, e);
    return result;
}

Item 26.Favor generic types

改写下面的stack类为泛型

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; // Eliminate obsolete reference
        return result;
    }
    public boolean isEmpty() {
        return size == 0;
    }
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
  1. 为类添加一个或两个泛型的参数,Stack类中主要是一个elements数组,所以需要一个参数,Stack<E>。
  2. 将所有的Object改为E:
    class Stack<E> {
     private E[] elements;
     private int size = 0;
     private static final int DEFAULT_INITIAL_CAPACITY = 16;
     public Stack() {
         //ERROR - Type parameter 'E' cannot be instantiated directly,直接创建泛型数组。
         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; // Eliminate obsolete reference
         return result;
     }
     public boolean isEmpty() {
         return size == 0;
     }
     private void ensureCapacity() {
         if (elements.length == size)
             elements = Arrays.copyOf(elements, 2 * size + 1);
     }
    }
    
  3. 修改错误,有两种解决方式
    1. elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];生成Object数组然后强制转换为E[],但是会报错,由于elements是私有的,客户端不能直接使用,而唯一可以修改的地方是push中而这里传入的参数类型是固定的是E,所以这里可行,使用@SuppressWarnings("unchecked")消除警告。
    2. private Object[] elements;然后修改popE result =(E) elements[--size];可以消除Warning的原因类似。
      ps. 这里不使用List<E>的方式去修改elements主要是因为,List也是在Arrays的基础上搭建的,所以就像HashMap一样使用Arrays来提高性能。
      当代码中用在pop的点少是,使用第二种,只需要强制转换个别类型,但是第一种需要转换整个数组,建议第二中,但是当使用pop的点多的时候,建议第一种。

Item 27. Favor generic methods

如果不使用泛型而是使用raw type 经常会出现警告,如下:

// Uses raw types - unacceptable! (Item 23)
public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}
Union.java:5: warning: [unchecked] unchecked call to
HashSet(Collection<? extends E>) as a member of raw type HashSet
Set result = new HashSet(s1);
             ^
Union.java:6: warning: [unchecked] unchecked call to
addAll(Collection<? extends E>) as a member of raw type Set
result.addAll(s2);
              ^

改写为泛型后不会出现警告和错误:

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

但是有一个限制,参数和返回类型必须是一样的,可以用有界通配符类型(bounded wildcard types)来解决。

泛型类型的创建通常要两边都声明类型,Map<String, List<String>> anagrams = new HashMap<String, List<String>>(); (现在右边的似乎不需要。)
为了减少这个冗余,我们可以用generic static factory method。例如给HashMap增加一个:

public static <K,V> HashMap<K,V> newHashMap(){
    return new HashMap<K,V>();
}

调用时就不许要右侧的类型声明,Map<String, List<String>> anagrams = newHashMap();

generic singleton factory较多的用在function object。如下例子:

public interface UnaryFunction<T> {
    T apply(T arg);
}

在每次使用的时候创建会很浪费内存,所以可以有一个静态的,但是需要满足任何类型都适用,因此:

private static UnaryFunction<Object> IDENTITY_FUNCTION =new UnaryFunction<Object>() {
    @Override
    public Object apply(Object arg) {
        return arg;
    }
};
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction(){
    return (UnaryFunction<T>)IDENTITY_FUNCTION;
}

使用的时候可以像上面的newHashMap一样UnaryFunction<String> sameString = identityFunction();

其他类似<E extends Comparable<E>>可以被理解为可以与自身进行比较的任何类型 E,也就是说方法中如果使用了这个作为类型,那么只有实现类可比性的类型才可以,这种就是递归类型限定(Recursive type bound),实例如下:

// Returns the maximum value in a list - uses recursive type bound
public static <T extends Comparable<T>> T max(List<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;
}

Item 28.Use bounded wildcards to increase API flexibility

由于参数化类型(parameterized types)是不可变的,因此两个不同的类型Type1和Type2即使是继承关系,他们在List<Type1>和List<Type2>中也是毫不相关的,比如说List<String>和List<Object>不是子类与父类的关系,所以当我们将类定义为List<String>就不可以在其中加入Object类型的数据,为了使这种参数化类型更加灵活,我们可以使用有界通配符类型(bounded wildcard type)例如:

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

向其中加入方法pushAll

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

当想要加入一个Iterable<Integer>到Stack<Number>中的时候:

Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);

虽然Integer是Number的子类,但是由于参数化类型的不可变,会无法通过编译。这个时候需要加入一个有界通配符类型:

// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}

类似的,添加一个popAll方法,并将结果集合加入到传入的参数中:

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

当想要定义一个Object的集合用来当Number类型的数时候:

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);

依然会出现问题。加入有界通配符类型之后:

// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

popAll使用了super,pushAll使用了extends。因为Collection消费了Stack中的值,Iterable生产了Stack中的值。(PECS:producer-extends, consumer-super.)
三个例子:

  1. static <E> E reduce(List<E> list, Function<E> f, E initVal)list生产出了用于reduce的数,所以是extends,static <E> E reduce(List<? extends E> list, Function<E> f,E initVal)
  2. public static <E> Set<E> union(Set<E> s1, Set<E> s2)Set生产了用于union的值,所以也是extends,但是要主要不要再返回类型中使用通配符号类型,public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2)
  3. public static <T extends Comparable<T>> T max(List<T> list) list是生产者,Comparable消费一个T,然后生产一个int的数,对于Comparable 或Comparator都是用<? super T> public static <T extends Comparable<? super T>> T max(List<? extends T> list)

两种swap方法的定义形式:

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

if a type parameter<E> appears only once in a method declaration, replace it with a wildcard<?>.
有一个问题,直接实现,会出现 set(int,capture#282 of ?) in List<capture#282 of ?>

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

因为list.get(i)读取的元素是?类型,解决办法是写一个私有的helper方法来捕获通配符的类型

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}
// Private helper method for wildcard capture
//list.get(i)是E类型的,所以可以直接添加到list中
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

这样不仅客户端可以方便的获取,也可以消除上面的问题。1.8的包中采用rawtype并且用SuppressWarnings,但是1.8推荐使用上面这个:

@SuppressWarnings({"rawtypes", "unchecked"})
public static void swap(List<?> list, int i, int j) {
    // instead of using a raw type here, it's possible to capture
    // the wildcard but it will require a call to a supplementary
    // private method
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}