EffectiveJava笔记02

5. 泛型

有了泛型可以告诉编译器每个集合中接受那些对象类型,编译器自动为你的插入进行转换,在编译时告知是否插入了类型错误的对象,使程序更加安全。

26. 不要用原生态类型

概念

声明中具有一个或多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或接口。如List接口只有单个的类型参数E,表示列表的元素类型。该接口的全称为List<E>,人们简称为List。泛型类和接口统称为泛型(generic type)。

每一种泛型定义一组参数化的类型(parameterized type)。用尖括号<> 把对应泛型形式类型参数的实际类型参数(actual type parameter)列表括起来。如List<String>(读作字符串列表)是一个参数化的类型,String 是和形式的类型参数E对应的实际类型参数。

原生态类型

每一种泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。如List<E> 对应的原生态类型为List。他们的存在主要是和泛型出现之前的代码兼容。

如果用原生态类型,会失去泛型在安全和描述性方面的所有优势。原生态类型List和参数化类型List<Object> 有什么区别?前者逃避了泛型检查,后者明确告知编译器能够持有任意类型的对象。泛型有子类型化(subtyping)的规则,List<String> 是原生态类型List的一个子类型,而不是参数化类型List<Object> 的子类型。

public class GenericDemo01 {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings,Integer.valueOf(42));
        String s = strings.get(0);
    }

    private static void unsafeAdd(List list, Object o){
        list.add(o);
    }
}

上面这段代码,编译可通过,但运行时,在程序试图将strings.get(0) 的调用结果Integer转换为String时,收到ClassCastException的异常。如果在unsafeAdd声明中用参数化类型List<Object> 代替原生态类型List时,编译就不通过。

无限制通配符

在不确定或不在乎集合中的元素类型的情况下,你可能会用原生态类型。例如,假设想编写一个方法,有两个集合,并从中返回共有元素的数量,你可能会这样写

static int numElementsInCommon(Set s1,Set s2){
  int result=0;
  for(Object o1:s1)
    if(s2.contains(o1))
      result++;
  return result;    
}

这个方法可行,但因为使用原生态类型,很危险。安全的替代做法是使用无限制的通配符类型(unbounded wildcard type)。如果不确定或不关心实际的类型参数,可用? 来代替。如Set<E>无限制通配符类型为Set<?> (读作某个类型的集合)。上面代码可修改为

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

不能将任何元素(除了null)放到Collection<?> 中。可用泛型方法或有限制的通配符类型。

例外

例外:必须在类文字(class literal)中使用原生态类型。也就是说,List.class、String[].class和int.class都合法,但List<String>.classList<?>.class 不合法。

第二个例外,与instanceof操作符有关。泛型信息可在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。无限制通配符类型代替原生态类型,对instanceof无影响。下面是利用泛型来使用instanceof操作符的首选方法:

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

注意:一旦确定这个o是Set,必须转换为通配符类型Set<?> ,而不是转换为原生态类型Set,这是个checked转换,不会导致编译时警告。

27. 消除非受检的警告

泛型编程会遇到很多编译器警告:非受检转换警告(unchecked cast warning)、非受检方法调用警告、非受检参数化可变参数类型警告(unchecked parameterized vararg type warning),以及非受检转换警告(unchecked conversion warning)。

有些警告容易消除。如

Set<Lark> exaltation = new HashSet();

编译器会提醒你哪里出错,不必真正指定类型参数,只需要用菱形操作符(diamond operator) <> 将其括起来,编译器会推测正确的实际类型参数

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

有些警告非常难消除。当你遇到需要进行一番思考的警告时,坚持住!尽可能消除每一个非受检警告。才能保证代码的类型安全。

如果无法消除警告,同时可以证明引发警告的代码是类型安全的,才可以用@SuppressWarnings("unchecked") 注解禁止该警告。如果忽略(而非禁止)明知道是安全的非受检警告,当真正出现有问题的警告时,会淹没在所有的错误警告声中。

应尽可能小的范围内使用SuppressWarnings注解。可以用在任何粒度的级别,包括局部变量。

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

总之,非受检警告很重要,每一条警告都可能在运行时抛出ClassCastException异常。如果无法消除警告,且可证明引起警告的代码时类型安全的,尽可能小的范围内使用@SuppressWarnings("unchecked") 注解禁止该警告,用注释把禁止该警告的原因写下来。

28. 列表优于数组

数组和泛型的不同点

首先,数组是协变的(covariant),如果Sub是Super的子类型,数组类型Sub[ ]就是Super[ ]的子类型。相反,泛型则是可变的(invariant):对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。下面这两段代码:

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // throw ArrayStoreException
// won't compile!
List<Object> o1 = new ArrayList<Long>(); // Incompatible types
o1.add("I don't fit in");

利用数组,在运行期才会发现所犯的错误;而列表,在编译期就发现错误。

第二个区别是:数组是具体化的(reified),会在运行时知道和强化他们的元素类型。而泛型是通过擦除(erasure)实现。泛型只是在编译时强化类型信息,运行时擦除元素类型信息。

因此,数组和泛型不能很好的混用。如,创建泛型、参数化类型或者类型参数的数组是非法的。如new List<E>[] 在编译时就会导致一个泛型数组创建(generic array creation)错误。因为其类型不安全。

从技术角度,像E、List<String> 这样的类型应该称作不可具体化的类型(nonreifiable)。不可具体化的(non-reifiable)类型是指其运行时表示法包含的信息比编译时表示法包含的信息更少的类型。唯一可具体化(reifiable)的参数化类型是无限制的通配符类型,如List<?>Map<?,?> 虽然不常用,但创建无限制通配类型的数组是合法的。

这也意味着在结合使用可变参数(varargs)方法和泛型时会出现令人费解的警告。这是因为调用可变参数其实会创建一个数组来存放varargs参数。如果数组的元素类型不是可具体化的,就会得到一条警告,利用SafeVarargs注解解决该问题。

当得到泛型数组创建错误,最好是优先使用集合类型List<E> ,而非数组类型E[] ,可能会损失一些性能或简洁性,换来的是更高的类型安全性和互用性。

public class Chooser { // 色子
    private final Object[] choiceArray;
    public Chooser(Collection choices){
        choiceArray = choices.toArray();
    }
    public Object choose(){
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

用构造器编写一个带有集合的Chooser类和一个方法,并用该方法返回在集合中随机选择的一个元素。根据传给构造器的集合类型,可用chooser充当游戏用的色子,上面是没用泛型的简单实现。要使用这个类,必须将choose方法的返回值,从Object转换为每次调用该方法时想要的类型,搞错类型的话,运行时失败。因此,要将Chooser修改为泛型。如下

public class Chooser<T> { // 色子
    private final T[] choiceArray;
    public Chooser(Collection<T> choices){
        choiceArray = choices.toArray();
    }
  //...

但这样,编译期就不通过,你可能会想到把Object数组转换为T数组

choiceArray = (T[])choices.toArray();

这样确实消除错误信息,但得到一条警告 unchecked cast

编译器告诉你,无法再运行时检查转换的安全性,因为程序在运行时不知道T是什么,元素类型信息在运行时从泛型中被擦除。

要想消除未受检的转换警告,必须选择用列表代替数组,下面是编译时没有出错或者警告的Chooser类版本

public class Chooser<T> { // 色子
    private final List<T> choiceList;
    public Chooser(Collection<T> choices){
        choiceList = new ArrayList<>(choices);
    }
    public T choose(){
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

这种代码稍冗长些,运行速度可能也会慢一些,但运行时不会得到ClassCastException异常。

总之,如果你发现将数组和泛型混用得到编译时错误或警告,第一反应就应该是用列表代替数组。

29. 优先考虑泛型

自己写泛型较为困难

将类泛型化

以第7条的简单堆栈实现为例:

public 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; // 清除过期引用
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

这个类型应该先被参数化,但没有,我们可以在后面将其泛型化(generify)。也就是,将它参数化,又不破坏原来非参数化版本额的客户端代码。也就是说,客户端必须转换从堆栈里弹出的对象,以及可能在运行时失败的那些转换。

将类泛型化的第一步是在它的声明中添加一个或多个类型参数。在这个例子中有一个类型参数,表示堆栈的元素类型,这个参数名称通常为E。

下一步是用相应的类型参数替换所有的Object类型,然后试着编译

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        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; // 清除过期引用
        return result;
    }
  // no changes in isEmpty or ensureCapacity

通常会得到至少一个错误提示或警告,这个类只产生一个错误,在

public Stack() {
  elements = new E[DEFAULT_INITIAL_CAPACITY];
}

提示是E不能直接被实例化(cannot be instantiated directly)

如第28条说的,不能创建不可具体化的(non-reifiable)类型的数组,如E。有两种方法解决。

克服泛型数组报错的两种方法

第一种,直接绕过创建泛型数组的禁令:创建一个Object数组,并将其转换为泛型数组类型。错误消除,会产生一个警告,合法,但不是类型安全的。编译器不能证明你的程序是类型安全的,但你可以。你必须确保未受检的转换不会危及程序的类型安全性。相关数组(即elements变量)保存在一个private域,永远不会返回给客户端,或传给其他方法。这个数组中保存的唯一元素,是传给push方法的那些元素,类型为E,因此,未受检的转换不会有任何危害。

一旦你证明未受检的转换是安全的,就要在尽可能小的范围中禁止警告。

@SuppressWarnings("unchecked")
public Stack() {
  elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

另一种方法是,将elements域的类型从E[]改为Object[] 再把从数组中得到的元素由Object转换为E,再禁止警告。

@SuppressWarnings("unchekced")
E result = (E)elements[--size];

这两种方法各有所长。第一种:可读性更高,数组被声明为E[ ] 清楚地表明它只包含E实例。也更加简洁:在一个典型的泛型类中,可在代码中的多个地方读取到该数组;第一种方法只需要转换一次(在创建数组的时候),第二种方法每次读取都需要转换。因此,第一种方法优先,实践中更常用。但是可能会导致堆污染(heap pollution),数组运行时类型和编译时类型不匹配,可能某些程序员会不舒服,因而选择第二种。

下面的程序展示了泛型Stack类的使用方法。程序以倒序的方式打印出命令行参数,并转换为大写字母。如果要从堆栈中弹出的元素上调用String的toUpperCase方法,并不需要显式的转换,并且确保自动生成的转换会成功。

完整版:

public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    @SuppressWarnings("unchecked")
    public Stack() {
        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();
        }
        E result = elements[--size];
        elements[size] = null; // 清除过期引用
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();
        for (String arg : args) {
            stack.push(arg);
        }
        while(!stack.isEmpty()){
            System.out.println(stack.pop().toUpperCase());
        }
    }
}

看上去它和第28条矛盾了。实际上不可能总是在泛型中使用列表。java并不是生来就支持列表。因此,有些泛型,如ArrayList必须在数组上实现。为了提高性能,其他泛型如HashMap也在数组上实现。

有限制的类型参数

有些泛型限制了可允许的类型参数值。如,以java.util.concurrent.DelayQueue为例,其声明内容如下:

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

类型参数列表要求实际的类型参数E必须是java.util.concurrent.Delayed的一个子类型。允许DelayQueue实现及其客户端在DelayQueue的元素上利用Delayed方法,无需显式的转换,也没有ClassCastException的风险。类型参数E被称为有限制的类型参数(bounded type parameter),子类型关系已经确定。

总之,使用泛型比使用需要在客户端代码中转换的类型更加安全,也更容易。在设计新类型的时候,确保不需要这种转换就能使用,通常意味着把类做成泛型。时间允许,就把现有的类型都泛型化。对新用户友好,又不会破坏现有的客户端。

30. 优先考虑泛型方法

正如类从泛型中受益,方法也一样。静态工具方法尤为适用泛型化。Collections的所有“算法”都泛型化了。

编写泛型方法和编写泛型类类似。如下面这个方法,返回两个集合的联合。

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

这个方法虽然可以编译,但有两个警告,unchecked call to HashSet(Collection<? extends E>) as a member of raw type HashSet

为了改正,使得方法类型安全,要将方法声明修改为声明一个类型参数(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;
}

简单测试

public static void main(String[] args) {
  Set<String> guys = Set.of("Tom","Dick","Harry");//jdk9之后的of方法
  Set<String> stooges = Set.of("Larry","Moe","Curly");
  Set<String> aflCio = union(guys,stooges);
  System.out.println(aflCio);
}

union方法的局限性:三个集合的类型(两个输入参数和一个返回值)必须完全相同。利用有限制的通配符类型(bounded wildcard type)可使得方法更加灵活。

有时可能需要创建一个不可变但又适合多种不同类型的对象。泛型是通过擦除实现,可给所有必要的类型参数使用单个对象,但需要编写一个静态工厂方法,让它重复给每个必要的类型参数分发对象。该模式称为泛型单例工厂(generic singleton factory),常用函数对象,如Collections.reverseOrder,也用于像Collections.emptySet这样的集合。

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

参数化类型是不变的(invariant)。List<String> 不是List<Object> 的子类型,与直觉相悖,但实际很有意义。你可以把任何对象放进List<Object> 中,但只能把字符串放到List<String> 中。由于List<String> 不像List<Object> 能做任何事情,它不是子类型。

有时候我们需要的灵活性比不变类型能提供的更多。如29条的堆栈,它的公共API

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

假设我们想增加一个方法,让它按顺序将一系列的元素全部放到堆栈中。第一次尝试:

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

这个方法编译通过,但如果有一个Stack<Number> 且调用push(intVal),这里的intVal就是Integer类型。这是可以的,因为Integer就是Number的子类型,逻辑上可行,但是编译不通过。

解决方案:java提供一种特殊的参数化类型,有限制的通配符类型(bounded wildcard type),处理类似的情况。pushAll的输入参数类型不应该是“E的Iterable接口”,而应该是“E的某个子类型的Iterable接口”,也就是Iterable<? extends E>

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

现在想编写个popAll方法,与pushAll方法呼应。popAll方法从堆栈中弹出每个元素,并将元素添加到指定的集合中。

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

同样,假如有一个Stack<Number> 和Object类型的变量,从堆栈中弹出一个元素,并保存在该变量中,编译和运行都不报错,但用popAll编译这段客户端代码就会报错。

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

也是用通配符解决。popAll输入的参数类型不应该是“E的集合”,而应该是“E的某个超类的集合”(这里的超类是确定的,因此E是他自身的一个超类型)。也就是Collection<? super E>

结论:为了获取最大程度的灵活性, 要在表示生产者和消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,通配符没啥用,因为你需要的是严格的类型匹配。

PECS表示producer-extends,consumer-super

也就是说生产者T,用<? extends T> ,消费者T,用<? super T>

32. 谨慎并用泛型和可变参数

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

33. 优先考虑类型安全的异构容器

如果需要更多的灵活性,例如,数据库的行有任意数量的列,如果能以类型安全的方式访问所有列就好了。将key进行参数化而不是将容器参数化,然后将参数化的key提交给容器来插入或获取值。用泛型系统确保值的类型和它的key相符。

下面以Favorites类为例,允许客户端从任意数量的其他类中,保存并获取一个“最喜爱”的实例。Class对象充当参数化key的部分。因为类Class被泛型化。类的类型从字面上看不再只是简单的Class,而是Class<T> 。例如String.Class属于Class<String> 类型,Integer.class属于Class<Integer> 类型。当一个类的字面被用在方法中,来传达编译时和运行时的类型信息时,被称作类型令牌(type token)。

Favorites类的API很简单。看起来像一个简单的映射,除了key被参数化。客户端在设置和获取最喜爱的实例时提交Class对象

public class Favorites {
    public <T> void putFavorite(Class<T> type,T instance);
    public <T> T getFavorite(Class<T> type);
}

下面示例程序,检验Favorites类,将保存、获取并打印一个最喜爱的String、Integer和Class实例:

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 favoriteString = f.getFavorite(String.class);
  Integer favoriteInteger = f.getFavorite(Integer.class);
  Class<?> favoriteClass = f.getFavorite(Class.class);
  System.out.printf("%s %x %s%n",favoriteString,favoriteInteger,favoriteClass.getName());
}

注意printf方法在C语言中使用\n的地方,在java中使用%n ,产生适用于特定平台的行分隔符

Favorites实例是类型安全(typesafe)的:当你向他请求String的时候,不会返回一个Integer给你。同时也是异构的(heterogeneous):不像普通的映射,所有的key都是不同类型的。因此我们成Favorites类为类型安全的异构容器(typesafe heterogeneous container)。

Favorites的实现非常小。完整实现如下

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

这里每个Favorites实例都得到一个称为favorites的private的Map<Class<?>, Object> 的支持。你可能认为由于无限制通配符类型的关系,将不能把任何东西放进Map中,但事实恰好相反。通配符类型是嵌套的:不是属于通配符类型的Map的类型,而是它的key的类型。每个key都可以有一个不同的参数化类型:一个可以是Class<String> ,接下来是Class<Integer> ,异构就是来自这里。

第二件事就是favorites Map的值类型只是Object。Map并不能保证key和value之间的类型关系。也就是说key和value的类型并不相同。

putFavorite方法的实现很简单,它只是把(从指定的Class对象到指定的favorite实例)的一个映射放到favorites中。这是放弃了key和value的“类型联系”。因此无法知道这个value是key的一个实例。但getFavorites方法能且的确重新建立了这种联系。

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

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

cast方法只返回它的参数,它能为我们做什么?cast方法的签名充分利用Class类被泛型化的这个事实。 返回类型是Class对象的类型参数

public class Class<T> {
  T cast(Object obj);
}

这正是getFavorite方法所需要的,也是让我们不必借助未受检的转换为T就能确保Favorites类型安全的东西。

Favorites类有两种局限性值需要注意。首先,恶意的客户端可以轻松的破坏Favorites实例的类型安全,只要以它的原生态形式(raw form)使用Class对象。但会造成客户端编译时产生未受检的警告。确保Favorites永远不违背它的类型约束条件的方式,让putFavorite方法检验instance是否真的是type表示的类型的实例。只需使用一个动态的转换。

public <T> void putFavorite(Class<T> type,T instance){
  favorites.put(type,type.cast(instance));
}

java.util.Collections中有一些集合包装类采用同样的技巧。称作checkedSet、checkedList、checkedMap等。除了一个集合(或者映射)外,他们的静态工厂还采用一个(或两个)Class对象。静态工厂属于泛型方法,确保Class对象和集合的编译时类型相匹配。包装类给他们所封装的集合增加具体化。如有人试图将Coin放到你的Collection<Stamp> ,包装类就会在运行时抛出ClassCastException异常。用这些包装类在混有泛型和原生态类型的应用程序中追溯“是谁把错误的类型元素添加到集合”很有用。

Favorites类的第二种局限是在于不能用在不可具体化(non-reifiable)类型中。也就是你可以保存最喜爱的String或者String[ ],但不能保存最喜爱的List<String> 。如果试图保存最喜爱的List<String> 程序无法编译通过。原因是无法为List<String> 获取一个Class对象:List<String>.Class 是个语法错误,这也是个好事。

Favorites使用的类型令牌(type token)是无限制的:getFavorite和putFavorite接受任何Class对象。如果需要限制哪些可以传给方法的类型,可以通过有限制的类型令牌(bounded type token)实现,它只是个类型令牌,利用有限制类型参数或者有限制通配符,来限制可以表示的类型。

注解API广泛利用有限制的类型令牌。如,下面这个是在运行时读取注解的方法,来自AnnotatedElement接口,通过表示类、方法、域以及其他程序元素的反射类型实现:

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

参数annotationType是一个表示注解类型的有限制的类型令牌。如果元素有这种类型的注解,该方法就返回它;如果没有,就返回null。被注解的元素本质是个类型安全的异构容器,容器的key属于注解类型。

如果你有个类型为Class<?> 的对象,并想把它传给一个需要有限制的类型令牌的方法,如getAnnotation。可以将对象转换为Class<? extends Annotation> ,但这种是非受检的,会产生编译时警告。类Class提供一个安全且动态的执行该转换的实例方法,asSubclass,将调用它的Class对象转换为用其参数表示的类的一个子类。如果转换成功,方法返回它的参数;如果失败,抛出异常ClassCastException。如下面代码,利用asSubclass方法在编译时读取类型未知的注解。

static Annotation getAnnotation(AnnotatedElement element,String annotationTypeName){
  Class<?> annotationType = null; // 无限制的类型令牌
  try{
    annotationType = Class.forName(annotationTypeName);
  }catch (Exception e){
    throw new IllegalArgumentException(e);
  }
  return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

6. 枚举和注解

java支持两种特殊用途的引用类型:一种是类,称为枚举类型(enum type);一种是接口。称为注解类型(annotation type)。

34. 用enum代替int常量

int枚举类型

枚举类型(enum type)是指由一组固定的常量组成合法值的类型,如一年中的季节、太阳系的行星或一副牌的花色。在java语言引入枚举类型前,通常用一组int常量表示枚举类型。其中每个int常量表示枚举类型的一个成员:

public static final int APPLE_FUJI = 0; // 各种苹果品种
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0; // 橘子品种
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

这种方法被称为int枚举模式(int enum pattern)。缺点:不具有类型安全性,也几乎没有描述性可言。例如将Apple传到想要orange的方法中,也不会发出警告,甚至可用== 比较apple和orange

采用int枚举类型很脆弱,因为int枚举时编译时常量(constant variable)。他们的int值会被编译到使用它们的客户端中。如果与int枚举类型常量关联的值发生改变,客户端必须重新编译。

很难将int枚举类型转换为可打印的字符串。就算将这种常量打印出来,得到的也是一个数字,没啥用处。当需要遍历一个int类型枚举模式中的所有常量,以及获取int枚举数组的大小时,在int枚举模式中,几乎没有靠谱的方式。

String枚举类型

该模式还有一种变体,使用String常量,而不是int常量。这样的变体被称为String枚举模式(String enum pattern),同样也不是我们期望的。虽然提供可打印的字符串,但会导致初级用户直接将字符串常量硬编码到客户端代码,而非使用对应的常量字段名(field)。如果硬编码字符串常量包含书写错误,编译时不会报错,运行期才会报错。而且导致性能问题,因为依赖字符串的比较操作。

枚举类型

java的另一种替代方案,避免int和String枚举模式的缺陷,且提供更多好处。就是枚举类型(enum type)。

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

和其他语言的枚举相比,如C、C++、C#,java的枚举类型功能齐全,非常强大,本质是int值。

枚举类型的基本原理很简单:这些类通过public的static final域为每个枚举常量导出一个实例。枚举类型没有可访问的构造器,所以是真正的final类。客户端不能创建枚举类型的实例,也不能对它扩展,因此不存在实例,只存在声明过的枚举常量。也就是说,枚举类型是实例受控的。是单例的泛型化,本质上是单元素的枚举。

枚举类型保证了编译时的类型安全。如声明参数的类型为Apple,就能保证传到该参数上的任何非空的对象引用一定属于三个有效的Apple值之一。其他任何试图传递类型错误的值都会导致编译时错误。

包含同名常量的多个枚举类型可在一个系统中和平共处,因为每个类型都有自己的命名空间。你可以增加或重新排列枚举类型的常量,无需重新编译客户端代码。因为导出的常量的域在枚举类型和客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码,而是在int枚举模式中。最终通过调用toString方法,将枚举转换为可打印的字符串。

除了完善int枚举模式的不足,枚举类型还允许添加任意的方法和域,并实现任意接口。提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对枚举类型的可任意改变性设计了序列化方式。

为什么要向枚举类型中添加方法或域呢?首先,可能是想将数据和它的常量关联起来。如,一个能返回水果颜色或者返回水果图片的方法,对于Apple和Orange类型就很有必要。可利用任何适当的方法增强枚举类型。枚举类型可先作为枚举常量的一个简单集合,随着时间的推移,演变为全功能的抽象

下面例子,太阳系8大行星,每颗行星有质量和半径,通过这两个属性可计算表面重力,从而给定物体的质量,进而计算一个物体在行星表面的重量。每个枚举常量后面括号中的数值就是传递给构造器的参数。也就是行星的质量和半径。

public enum Planet {
    MERCURY(3.302e+23,2.439e6),
    VENUS(4.869e+24,6.052e6),
    EARTH(5.975e+24,6.378e6),
    MARS(6.419e+23,3.393e6),
    JUPITER(1.899e+27,7.149e7),
    SATURN(5.685e+26,6.027e7),
    URANUS(8.683e+25,2.556e7),
    NEPTUNE(1.024e+26,2.477e7);

    private final double mass; // kg
    private final double radius; // m
    private final double surfaceGravity; // m/s^2

    private static final double G = 6.67300E-11; // 重力加速度
    
    Planet(double mass,double radius){
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
    public double mass(){return mass;}
    public double radius(){return radius;}
    public double getSurfaceGravity(){return surfaceGravity;}
    
    public double surfaceWeight(double mass){
        return mass * surfaceGravity; // F=ma
    }
}

编写这样的枚举类型并不难。为了将数据和枚举常量关联起来,需要声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,所有域都要final,可public,但最好private,并提供公有的访问方法。在Planet示例中,构造器还计算和保存表面重力,这正是一种优化。每当surfaceWeight方法用到重力时,都会根据质量和半径重新计算,并返回在该常量所表示的行星上的重量。

虽然Planet枚举很简单,但功能很强大。下面是简单的demo,根据某个物体在地球的重量,打印表格显示在所有8颗行星的重量

public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for(Planet p:Planet.values()){
            System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
        }
    }
}

就像所有的枚举一样,Planet有一个静态的values方法,按照声明顺序返回它的值数组。toString方法返回每个枚举值的声明名称,使得println和printf的打印更加容易。下面是带命令行参数为185来运行该程序的结果:

Weight on MERCURY is 69.912739
Weight on VENUS is 167.434436
Weight on EARTH is 185.000000
Weight on MARS is 70.226739
Weight on JUPITER is 467.990696
Weight on SATURN is 197.120111
Weight on URANUS is 167.398264
Weight on NEPTUNE is 210.208751

06年九大行星变为八大行星,引出一个问题,如果一个元素从一个枚举类型中移除,会发生什么情况?答案是:没有引用该元素的任何客户端程序都会继续正常工作。我们的WeightTable程序只会少打一行。对于引用了被删除的元素的客户端程序又如何呢?如果重新编译客户端代码,就会失败,并在引用被删除行星的那一条显示错误消息;如果没有重新编译,在运行时抛出异常。正符合我们的预期。

除非要将枚举方法导出到客户端,否则都声明为私有。

如果一个枚举具有普遍适用性,就应该成为一个顶层类(top-level class);如果只是被用在一个特定的顶层类中,应该成为顶层类的一个成员类。如java.math.RoundingMode枚举表示十进制小数的舍入模式(rounding mode)。这些舍入模式被用于BigDecimal类,但不属于BigDecimal类的一个抽象。通过使RoundingMode变成一个顶层类,库的设计者鼓励任何需要舍入模式的程序员重用该枚举,来增强API之间的一致性。

Planet示例中所示的方法对大多数枚举类型够用,但有时候需要更多方法。每个Planet常量关联不同的数据,但有时需要将不同的行为(behavior)与每个常量关联。如编写一个枚举类型,表示计算器的四大基本操作,提供一个方法执行每个常量所表示的算术运算。一种方法是通过启用枚举的值实现:

public enum Operation {
    PLUS, MUNUS, TIMES, DIVIDE;
    
    public double apply(double x,double y){
        switch (this){
            case PLUS: return x+y;
            case MUNUS: return x-y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("Unknown op: "+this);
    }
}

这种代码不太好看,且比较脆弱,如果添加新的枚举常量,却忘了给switch添加相应的条件,枚举依然可以编译,但用新的运算,就会失败。

特定于常量的方法实现

更好的办法可以将不同的行为和每个枚举常量联系起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体(constant-specific class body)中,用具体的方法覆盖每个常量的抽象apply方法,这种方法被称为特定于常量的方法实现(constant-specific method implementation)

public enum Operation {
    PLUS {public double apply(double x,double y){return x + y;}},
    MINUS {public double apply(double x,double y){return x-y;}},
    TIMES {public double apply(double x,double y){return x * y;}},
    DIVIDE {public double apply(double x,double y){return x / y;}};
    
    public abstract double apply(double x,double y);
}

这样的话,你就不会忘记提供apply方法了。

特定于常量的方法实现可以和特定于常量的数据结合。如下面Operation覆盖了toString方法以返回与该操作关联的符号:

public enum Operation {
    PLUS("+") {public double apply(double x,double y){return x + y;}},
    MINUS("-") {public double apply(double x,double y){return x-y;}},
    TIMES("*") {public double apply(double x,double y){return x * y;}},
    DIVIDE("/") {public double apply(double x,double y){return x / y;}};

    private final String symbol;
    Operation(String symbol){this.symbol = symbol; }

    @Override
    public String toString() {
        return symbol;
    }
    public abstract double apply(double x, double y);
}

下面测试

public static void main(String[] args) {
  double x = Double.parseDouble(args[0]);
  double y = Double.parseDouble(args[1]);
  for(Operation op:Operation.values())
    System.out.printf("%f %s %f = %f%n", x,op,y,op.apply(x,y));
}

用2和4作为命令行参数来运行这段程序,输出

2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000

枚举类型有个自动产生的valueOf(String)方法,将常量的名字转变为常量本身。如果在枚举类型中覆盖了toString,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举。下列代码就可以,只要每个常量都有一个独特的字符串表示法

// 枚举类的fromString方法实现
private static final Map<String,Operation> stringToEnum = Stream.of(values()).collect(
  toMap(Object::toString,e->e)
);
// 返回字符串操作符
public static Optional<Operation> fromString(String symbol){
  return Optional.ofNullable(stringToEnum.get(symbol));
}

除了编译时常量域,枚举构造器不可以访问枚举的静态域。 因为构造器运行的时候,这些静态域还没有被初始化。有个特例:枚举常量无法通过构造器访问另一个构造器。

还要注意返回Optional<Operation> 的fromString方法,利用该方法表明:传入的字符串并不代表一项有效的操作,并强制客户端面对这种可能

特定于常量的方法实现有个美中不足,枚举常量中共享代码变得更加困难。例如:使用枚举表示薪资包中的工作天数。有个方法,根据给定的某工人的基本工资(按小时)以及当天的工作时间,计算他当天的报酬。在五个工作日中,超过八小时的工作时间会产生加班工资;在节假日,所有工作产生加班工资。利用switch,很容易将多个case分别应用到两个代码片段,完成计算:


TODO

35. 用实例域代替序数

36. 用EnumSet代替位域

37. 用EnumMap代替序数索引

38. 用接口模拟可扩展的枚举

39. 注解优先于命名模式

40. 坚持使用Override注解

41. 用标记接口定义类型

标记接口(marker interface)是不包含方法声明的接口,只是指明(或者“标明”)一个类实现了具有某种属性的接口。例如,Serializable接口。

标记接口相较于标记注解的优点

标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。

标记接口可以被更加精确的进行锁定。

标记注解优点

是更大的注解机制的一部分

7. lambda和stream

8. 方法

主要讨论如何处理参数和返回值、如何设计方法签名、如何为方法编写文档

49. 检查参数的有效性

大多数方法和构造器对于传递给他们的参数值会有限制。应在文档中清楚地指明这些限制,并在方法体的开头处检查参数,以强制施加这些限制。没有验证参数的有效性,可能会导致违背失败原子性(failure atomicity)。

/**
     *  返回一个BigInteger,其值是对m取模
     * @param m 模数,必须为正值
     * @return this mod m
     * @throws ArithmeticException 如果模数小于等于0
     */
public BigInteger mod(BigInteger m){
  if(m.signum()<=0)
    throw new ArithmeticException("Modulus <=0: "+m);
  // 运算逻辑
}

总之,每当编写方法或构造器时,应考虑参数有哪些限制,把这些限制写到文档,并在方法体的开头处,显式的检查来实施这些限制。

50. 必要时保护性拷贝

51. 谨慎设计方法签名

谨慎选择方法名字,可以参考java类库的API

不要过于追求便利的方法

避免过长的参数列表 目标是4个或更少,相同类型的长参数序列格外有害

缩短过长的参数列表的方法

把一个方法分解为多个方法,每个方法只需要这些参数的一个子集。通过提升方法的正交性(orthogonality),可以减少方法的数目。如java.util.List接口,并没有提供在子列表sublist中查找元素的第一个索引和最后一个索引的方法,这两个方法都需要三个参数。相反,提供了subList方法,该方法带有两个参数,并返回子列表的一个视图view。该方法可以和indexOf或lastIndexOf方法结合,获得期望的功能,而且这两个方法都只有一个参数。而且,subList方法还可以和其他任何“针对List实例进行操作”的方法结合,在子列表上执行任意的计算。这样得到的API就有很高的功能-权重比(power-to-weight)

第二种技巧是创建辅助类(helper class),用来保存参数的分组。这些辅助类一般是静态成员类。 其实也就是bean,把参数当成bean的属性。

第三种是采用builder模式

对于参数类型,优先使用接口而不是类。

对于boolean参数,优先用两个元素的枚举类型。

52. 慎用重载

覆盖机制是标准规范,而重载机制是例外。不要导出两个相同参数数目的重载方法,可以给方法起不同的名字,而不是使用重载机制。如ObjectOutputStream类,对于每个基本类型,以及几种引用类型,它的write方法都有一种变形,并非重载write方法,而是具有如writeBoolean(boolean)、writeInt(int)、writeLong(long)这样的签名。和重载相比,这种命名模式的好处是,可以提供相应名称的读方法。

对于构造器,没得选择,一个类的多个构造器总是重载的。许多情况下,可以导出静态工厂,而不是构造器。

尽管有些重载显然违反了指导原则,但是只要当这两个重载方法在同样的参数上被调用,他们执行的是相同功能,就不会带来危害。只要两个方法返回的是相同结果就行。

53. 慎用可变参数

可变参数方法一般称为variable arity method(可匹配不同长度的变量的方法)。首先会创建一个数组,数组大小在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传给方法。

在定义参数数目不确定的方法时,可变参数是个很方便的方式,需要关注可变参数带来的性能影响。

54. 返回零长度的数组或集合,而不是null

经常见下面这种写法:

private final List<Cheese> cheessesInStock = ;
public List<Cheess> getChesses(){
  return cheessesInStock.isEmpty() ? null:new ArrayList<>(cheessesInStock);
}

把没有cheese可买的情况当成特例,不合理。这样会要求客户必须有额外的代码处理null返回值。如

List<Cheese> cheeses = shop.getCheeses();
if(cheesses !=null && cheeses.contains(cheese.STILTON)){
  System.out.println("Jolly good,just the thing.");
}

每次调用方法都需要这种处理方式,很容易出错,也不容易注意到。

改进方法

public List<Cheess> getChesses(){
  return new ArrayList<>(cheeseInStock);
}

如果有证据表明分配零长度的集合损害程序的性能,可以通过重复返回同一个不可变的零长度集合,避免分配的执行,因为不可变对象可被自由共享。例如

public List<Cheess> getChesses(){
  return cheessesInStock.isEmpty() ? Collections.emptyList():new ArrayList<>(cheessesInStock);
}

数组类似,注意,将一个零长度的数组传递给toArray方法,以表明期望的返回类型,即Cheese[ ]

public Cheess[] getChesses(){
  return cheessesInStock.toArray(new Cheese[0]);
}

优化的话也是

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheess[] getChesses(){
  return cheessesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

在性能优化的版本,将同一个零长度的数组传进每一次的toArray调用,每当cheeseInStock为空时,就会从getCheese返回该数组。千万不要指望通过预先分配传入toArray的数组提升性能。只会适得其反。

// 反面案例
return cheessesInStock.toArray(new Cheese[cheeseInStock.size()]);

55. 谨慎返回optional

在java8之前,要编写一个在特定环境下无法返回任何值的方法时,有两种方法:要么抛出异常,要么返回null(假设返回类型为一个对象引用类型)。但这两种方法都不够完美。异常应该根据异常条件保留起来,由于创建异常会捕捉整个堆栈轨迹,开销很高。返回null的不足是客户端必须包含特殊的代码处理返回null的可能性,否则随时可能发生NullPointerException异常。

java8中,第三种解决方案:Optional<T> 类代表一个不可变的容器。可以存放单个非null的T引用,或者什么内容都没有。不包含任何内容的optional为empty,非空的optional值称为present。optional本质是一个不可变的集合,最多存放一个元素。它没有实现Collection<T> 接口,但原则上可以。

在某些特定的条件下,可以改为声明返回Optional<T> ,允许方法返回空的结果,表名无法返回有效的结果。更加灵活,也更容易,不易出错。

如根据元素的自然顺序,求集合中的最大值

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

如果指定集合为空,方法抛出IllegalArgumentException。更好的替换方法是返回Optional<E>

public static <E extends Comparable<E>> Optional<E> max(Collection<E> c){
  if(c.isEmpty())
    return Optional.empty();
  E result = null;
  for(E e:c){
    if(result == null || e.compareTo(result)>0)
      result = Objects.requireNonNull(e);
  }
  return Optional.of(result);
}

永远不要通过返回Optional的方法返回null。 Optional本质上与受检异常相类似,因为他们强迫API用户面对没有返回值的限制。可能会带来灾难性的后果。方法返回optional时,客户端必须做出选择:如果该方法不能返回值时应该采取什么动作。可以指定默认值。

// optional提供默认选项
String lastWordInLexicon = max(words).orElse("no words...");

或者抛出任何适当的异常。注意此处传入的是一个异常工厂,而不是真正的异常。这避免了创建异常的开销,除非真正抛异常:

// optional抛出被选择的异常
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

如果能证明optional为非空,就不必指定optional为空要做什么动作了,直接从optional中获取值即可。如果你判断错了,代码会抛出NoSuchElementException:

// 当你知道有返回值的时候
Element lastNobleGas = max(Elements.NOBLE.GASES).get();

TODO

永远不应该返回基本包装类型的optional

几乎永远都不适合用optional作为key,value,或者数组中的元素。

56. 为所有导出的API元素编写文档注释

java编程环境提供了被称为Javadoc的实用工具,利用特殊格式的文档注释,根据源代码自动生成API文档。

为编写可维护的代码,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。

方法的文档注释应该简洁的描述它和客户端之间的约定。应该说明它做了什么,而不是如何完成工作。应该列举出这个方法的前提条件(precondition)和后置条件(postcondition)。所谓前提条件是指为了使客户能调用该方法,必须要满足的条件;后置条件是在调用成功满足后,哪些条件必须要满足。一般前提条件是由@throw 标签对未受检的异常所隐含描述的;每个未受检的异常都对应一个前提违例(precondition violation)。同样,也可在一些受影响的参数的@param 标记中指定前提条件。

除了前提条件和后置条件,每个方法应在文档中描述它的副作用(side effect)。副作用是指系统状态中可以观察到的变化,不是为了获取后置条件而明确要求的变化。如,方法启动了后台线程,文档中就应该说明这一点。

每个文档的第一句话是概述(summary description)。同一个类或接口中的两个成员或构造器,不应该具有同样的概述。

当为泛型或方法编写文档时,确保在文档中说明所有的类型参数。

为枚举类型编写文档时,确保在文档中说明常量。

API的两个特征在文档中容易被忽略,线程安全性和可序列化性。类或者静态方法是否线程安全,应该在文档中对它的线程安全级别说明。如82条。

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/94983166