《Effective Java》Second Edition中文版笔记(完整)

第2章 创建和销毁对象

第1条:考虑用静态工厂方法代替构造器

  1. 一个类只能有一个带有指定签名的构造器。编程人员通常知道如何避开这一限制:通过提供两个构造器,它们的参数列表只在参数类型的顺序上有所不同。实际上这并不是个好主意。面对这样的API,用户永远也记不住该用哪个构造器,结果常常会调用错误的构造器。并且,读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云。
  2. 由于静态工厂方法有名称,所以它们不受上述的限制。当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出它们之间的区别。
  3. 静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称作实例受控的类(instance-controlled)。编写实例受控的类有几个原因。实例受控使得类可以确保它是一个Singleton或者是不可实例化的。
  4. 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。

第2条:遇到多个构造器参数时要考虑用构建器

  1. 静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。
  2. 遇到许多构造器参数的时候,还有第二种代替方法,即JavaBeans模式,在这种模式下,调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。
  3. 遗憾的是,JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。
  4. 与此相关的另一点不足在于,JavaBeans模式阻止了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
  5. 注意NutritionFacts是不可变的,所有的默认参数值都单独放在一个地方。builder的setter方法返回builder本身,以便可以把调用链接起来。下面就是客户端代码:
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

builder模式模拟了具名的可选参数,就像Ada和Python中的一样。

// Builder Pattern
public class NutritionFacts {
    private final int ServingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required Parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories = 0;
        private int fat = 0;
        private int carbohydrate = 0;
        private int sodium = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) { calories = val; return this; }
        public Builder fat(int val) { fat = val; return this; }
        public Builder carbohydrate(int val) { carbohydrate = val; return this; }
        public Builder sodium(int val) { sodium = val; return this; }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

第3条:用私有构造器或者枚举类型强化Singleton属性

//Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }

私有构造器仅被调用一次,用来实例化公有的静态final域Elvis.INSTANCE。由于缺少公有的或者受保护的构造器,所以保证了Elvis的全局唯一性:一旦Elvis类被实例化,只会存在一个Elvis实例,不多也不少。

//Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }
}

公有域方法的主要好处在于,组成类的成员的声明很清楚地表明了这个类是一个Singleton:公有的静态域是final的,所以该域将总是包含相同的对象引用。公有域方法在性能上不再有任何优势:现代的JVM实现几乎都能够将静态工厂方法的调用内联化。

为了使利用这其中一种方法实现的Singleton类变成是可序列化的(Serializable),仅仅在声明中加上“implements Serializable”是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。

从Java 1.5发行版本起,实现Singleton还有第三种方法。只需编写一个包含单个元素的枚举类型:

//Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

第4条:通过私有构造器强化不可实例化的能力

  1. 有时候,你可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。尽管如此,它们也确实有它们特有的用处。我们可以利用这种类,以java.lang.Math或者java.util.Arrays的方式,把基本类型的值或者数组类型上的相关方法组织起来。我们也可以通过java.util.Collections的方式,把实现特定接口的对象上的静态方法(包括工厂方法)组织起来。最后,还可以利用这种类把final类上的方法组织起来,以取代扩展该类的做法。
  2. 这样的工具类(utility class)不希望被实例化,实例对它没有任何意义。然而,在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器(default constructor)。对于用户而言,这个构造器与其他的构造器没有任何区别。
  3. 企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。
// Noninstantiable utility class
public class UtilityClass {
    //Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }
    ... //Remainder omitted
}

第5条:避免创建不必要的对象

  1. 如果对象是不可变的(immutable),它就始终可以被重用。
  2. 传递给String构造器的参数(”stringette”)本身就是一个String实例,功能方面等同于构造器创建的所有对象。
  3. 而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
  4. 对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。
  5. 下面的版本用一个静态的初始化器(initializer),避免了这种效率低下的情况:
class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted

    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && 
            birthDate.compareTo(BOOM_END) < 0;
    }
}

改进后的Person类只在初始化的时候创建Calendar、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer的时候都创建这些实例。

把boomStart和boomEnd从局部变量改为final静态域,这些日期显然是被作为常量对待,从而使得代码更易于理解。

如果改进后的Person类被初始化了,它的isBabyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过延迟初始化(lazily initializing),即把对这些域的初始化延迟到isBabyBoomer方法第一次被调用的时候进行,则有可能消除这些不必要的初始化工作,但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。

考虑适配器(adpater)的情形,有时也叫做视图(view)。适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

例如,Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。

变量sum被声明成Long而不是long,意味着程序构造了大约 231 个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用(footprint),并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。

第6条:消除过期的对象引用

  1. 如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在elements数组的”活动部分(active portion)”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素。
  2. 在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持(unintentional object retention)”更为恰当。
  3. 清空对象引用应该是一种例外,而不是一种规范行为。
  4. 一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏问题。
  5. 如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
  6. 内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

第7条:避免使用终结方法

  1. 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。
  2. C++的析构器也可以被用来回收其他的非内存资源。而在Java中,一般用try-finally块来完成类似的工作。
  3. 这意味着,注重时间(time-critical)的任务不应该由终结方法来完成。例如,用终结方法来关闭已经打开的文件,这是严重错误,因为打开文件的描述符是一种很有限的资源。
  4. 一位同事最近在调试一个长期运行的GUI应用程序的时候,该应用程序莫名其妙地出现OutOfMemoryError错误而死掉。分析表明,该应用程序死掉的时候,其终结方法队列中有数千个图形对象正在等待被终结和回收。
  5. 不要被System.gc和System.runFinalization这两个方法所诱惑,它们确实增加了终结方法被执行的机会,但是它们并不保证终结方法一定会被执行。唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,已经被废弃了[ThreadStop]。
  6. 正常情况下,未被捕获的异常将会使线程终止,并打印出轨迹栈(Stack Trace),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。
  7. 那么,如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法呢?只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。
  8. 显示终止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法。另一个例子是java.util.Timer上的cancel方法,它执行必要的状态改变,使得与Timer实例相关联的该线程温和地终止自己。java.awt中的例子还包括Graphics.dispose和Window.dispose。这些方法通常由于性能不好而不被人们关注。一个相关的方法是Image.flush,它会释放所有与Image实例相关联的资源,但是该实例仍然处于可用的状态,如果有必要的话,会重新分配资源。
  9. 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部调用显式的终止方法,可以保证即使在使用对象的时候有异常抛出,该终止方法也会执行。
  10. 但是如果终结方法发现资源还未被终止,则应该在日志中记录一条警告,因为这表示客户端代码中的一个Bug,应该得到修复。
  11. 显式终止方法模式的示例中所示的四个类(FileInputStream、FileOutputStream、Timer和Connection),都具有终结方法,当它们的终止方法未能被调用的情况下,这些终结方法充当了安全网。
  12. 本地对等体是一个本地对象(native object),普通对象通过本地方法(native method)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。在本地对等体并不拥有关键资源的前提下,终结方法正式执行这项任务最合适的工具。

第3章 对于所有对象都通用的方法

  1. 尽管Object是一个具体类,但是设计它主要是为了扩展。它所有的非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定(general contract),因为它们被设计成是要被覆盖(override)的。任何一个类,它在覆盖这些方法的时候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运作。
  2. 而Comparable.compareTo虽然不是Object方法,但是本章也对它进行讨论,因为它具有类似的特征。
  3. Object的equals方法:
public boolean equals(Object obj) {
    return (this == obj);
}

第8条:覆盖equals时请遵守通用约定

  1. 类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说确实如此,例如Thread。Object提供的equals实现对于这些类来说正是正确的行为。
  2. 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。例如,大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

java.util.AbstractSet的equals方法:

public boolean equals(Object o) {
    if (o==this)
        return true;
    if(!(o instanceof Set))
        return false;
    Collection c = (Collection) o;
    if (c.size() != size())
        return false;
    try {
        return containsAll(c);
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
}

java.util.AbstractMap的equals方法:

public boolean equals(Object o) {
    if (o==this)
        return true;
    if (!(o instanceof Map))
        return false;
    Map m = (Map) o;
    if (m.size()!=size())
        return false;
    try {
        Iterator> i = entrySet().iterator();
        while(i.hasNext()) {
            Entry e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value==null) {
                if(!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if(!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
    return true;
}

类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防它被意外调用:

@override public boolean equals(Object o) {
    throw new AssertionError(); //Method is never called
}

这通常属于“值类(value class)”的情形。值类仅仅是一个表示值的类,例如Integer或者Date。程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。

有一种“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事,因此Object的equals方法等同于逻辑意义上的equals方法。

对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。对float和double域进行特殊的处理是有必要的,因为存在着Float.NaN、-0.0f以及类似的double常量;详细信息请参考Float.equals的文档。对于数组域,则要把以上这些指导原则应用到每个元素上。如果数组域中的每个元素都很重要,就可以使用发行版本1.5中新增的其中一个Arrays.equals方法。

有些对象引用域包含null可能是合法的,所以,为了避免可能导致NullPointerException异常,则使用下面的习惯用法来比较这样的域:

(field==null ? o.field==null : field.equals(o.field))

域的比较顺序可能会影响到equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。你不应该去比较那些不属于对象逻辑状态的域,例如用于同步操作的Lock域。

覆盖equals时总要覆盖hashCode。

把任何一种别名形式考虑到等价的范围内,往往不会是个好主意。例如,File类不应该试图把指向同一个文件的符号链接(symbolic link)当作相等的对象来看。所幸File类没有这样做。

public boolean equals(MyClass o) {
    ...
}

问题在于,这个方法并没有覆盖Object.equals,因为它的参数应该是Object类型,相反,它重载(overload)了Object.equals。在原有equals方法的基础上,再提供一个“强类型(strongly typed)”的equals方法,只要这两个方法返回同样的结果,那么这就是可以接受的。在某些特定的情况下,它也许能够稍微改善性能,但是与增加的复杂性相比,这种做法是不值得的。@override注解的用法一致,就如本条目中所示,可以防止犯这种错误。这个equals方法不能编译,错误消息会告诉你到底哪里出了问题:

@Override public boolean equals(MyClass o) {
    ...
}

第9条:覆盖equals时总要覆盖hashCode

  1. 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。
  2. 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
  3. 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。
  4. 由于PhoneNumber类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。因此,put方法把电话号码对象存放在一个散列桶(hash bucket)中,get方法却在另一个散列桶中查找这个电话号码。即使这两个实例正好被放到同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。

    1. 把某个非零的常量值,比如说17,保存在一个名为result的int类型的变量中。
    2. 对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
      a. 为该域计算int类型的散列码c:
      i. 如果该域是boolean类型,则计算(f ? 1:0)。
      ii. 如果该域是byte、char、short或者int类型,则计算(int)f。
      iii. 如果该域是long类型,则计算(int)(f ^ (f>>>32))。
      iv. 如果该域是float类型,则计算Float.floatToIntBits(f)。
      v. 如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值。
      vi. 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
      vii. 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤b.2中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。
      b. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
      result=31×result+c;
      返回result。
  5. 必须排除equals比较计算中没有用到的任何域,否则很有可能违反hashCode约定的第二条。

  6. 步骤2.b中的乘法部分使得散列值依赖于域的顺序,如果一个类包含多个相似的域,这样的乘法运算就会产生一个更好的散列函数。例如,如果String散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码。之所以选择31,是因为它是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。31有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31 * i == (i << 5) - i。现代的VM可以自动完成这种优化。
  7. 如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果你觉得这种类型的大多数对象会被用作散列键(hash keys),就应该在创建实例的时候计算散列码。否则,可以选择“延迟初始化”散列码,一直到hashCode被第一次调用的时候才初始化。
  8. 不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。虽然这样得到的散列函数运行起来可能更快,但是它的效果不见得会好,可能会导致散列表慢到根本无法使用。特别是在实践中,散列函数可能面临大量的实例,在你选择忽略的区域之中,这些实例仍然区别非常大。如果是这样,散列函数就会把所有这些实例映射到极少数的散列码上,基于散列的集合将会显示出平方级的性能指标。这不仅仅是个理论问题。在Java 1.2发行版本之前实现的String散列函数至多只检查16个字符,从第一个字符开始,在整个字符串中均匀选取。对于像URL这种层次状名字的大型集合,该散列函数正好表现出了这里所提到的病态行为。
  9. java.lang.String的hashCode方法:
public int hashCode() {
    int h = hash; //hash: cache the hash code for the string
    if (h==0) { //h==0说明是第一次计算哈希值,hash域还没有缓存
        int off = offset; // the offset is the first index of the storage that is used.
        char val[] = value; // the value is used for character storage.
        int len = count;
        for (int i=0;i31*h + val[off++];
        }
        hash = h;
    }
    return h;

第10条:始终要覆盖toString

  1. 在实现toString的时候,必须要做出一个很重要的决定:是否在文档中指定返回值的格式。
  2. 这种表示法可以用于输入和输出,以及用在永久的适合于人类阅读的数据对象中,例如XML文档。
  3. 无论你是否决定指定格式,都应该在文档中明确地表明你的意图。如果你要指定格式,则应该严格地这样去做。

第11条:谨慎地覆盖clone

  1. 其主要的缺陷在于,它缺少一个clone方法,Object的clone方法是受保护的。如果不借助于反射(reflection),就不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法。即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的clone方法。
  2. 既然Cloneable并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的一种极端非典型的用法,也不值得效仿。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneable接口,它改变了超类中受保护的方法的行为。
  3. 如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例,在其size域中具有正确的值,但是它的elements域将引用与原始Stack实例相同的数组。
  4. 为了使Stack类中的clone方法正常地工作,它必须要拷贝栈的内部信息。最容易的做法是,在elements数组中递归地调用clone:
@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

虽然被克隆对象有它自己的散列桶数组,但是,这个数组引用的链表与原始对象是一样的,从而很容易引起克隆对象和原始对象中不确定的行为。为了修正这个问题,必须单独地拷贝并组成每个桶的链表。下面是一种常见的做法:

public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry next;
        Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
        // Iteratively copy the linked list headed by this Entry
        Entry deepCopy() {
            Entry result = new Entry(key, value, next);
            for (Entry p = result; p.next != null; p = p.next)
                p.next = new Entry(p.next.key, p.next.value, p.next.next);
            return result;
        }
    }
    @Override public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for(int i=0;iif (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    ... // Remainder omitted
}

如果专门为了继承而设计的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected、抛出CloneNotSupportedException异常,并且该类不应该实现Cloneable接口。这样做可以使子类具有实现或不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样。

简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。

拷贝构造器的做法,以及静态工厂方法的变形,都比Cloneable/clone方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好文档的规范;它们不会与final域的正常使用发生冲突;它们不会抛出不必要的受检查异常(checked exception);它们不需要进行类型转换。虽然你不可能把拷贝构造器或者静态工厂放到接口中,但是由于Cloneable接口缺少一个公有的clone方法,所以它也没有提供一个接口该有的功能。因此,使用拷贝构造器或者拷贝工厂来代替clone方法时,并没有放弃接口的功能特性。

第12条:考虑实现Comparable接口

  1. 一旦类实现了Comparable接口,它就可以跟许多泛型算法(generic algorithm)以及依赖于该接口的集合实现(collection implementation)进行协作。你付出很小的努力就可以获得非常强大的功能。事实上,Java平台类库中的所有值类(value classes)都实现了Comparable接口。
  2. 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但这并非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应该明确予以说明。推荐使用这样的说法:“注意:该类具有内在的排序功能,但是与equals不一致。”
  3. 与equals不同的是,在跨越不同类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同类的对象,compareTo可以抛出ClassCastException异常。通常,这正是compareTo在这种情况下应该做的事情,如果类设置了正确的参数,这也正是它所要做的事情。
  4. 就好像违法了hashCode约定的类会破坏其他依赖于散列做法的类一样,违反compareTo约定的类也会破坏其他依赖于比较关系的类。依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部包含有搜索和排序算法。
  5. 因此,下面的告诫也同样适用:无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面向对象的抽象优势。针对equals的权宜之计也同样适用于compareTo方法。如果你想为一个实现了Comparable接口的类增加值组件,请不要扩展这个类;而是要编写一个不相关的类,其中包含第一个类的一个实例。然后提供一个“视图(view)”方法返回这个实例。这样既可以让你自由地在第二个类上实现compareTo方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例。
  6. 例如,考虑BigDecimal类,它的compareTo方法与equals不一致。如果你创建了一个HashSet实例,并且添加new BigDecimal (“1.0”)和new BigDecimal (“1.00”),这个集合就将包含两个元素,因为新增到集合中的两个BigDecimal实例,通过equals方法来比较时是不相等的。然而,如果你使用TreeSet而不是HashSet来执行同样的过程,集合中将只包含一个元素,因为这两个BigDecimal实例在通过compareTo方法进行比较时是相等的。
  7. 因为Comparable接口是参数化的,而且comparable方法是静态的类型(意思是compareTo方法的参数类型是一定的,就是实现Comparable接口的时候指定的参数类型。而equals方法的参数类型是Object,所以要进行额外的类型检查,instanceof。),因此不必进行类型检查,也不必对它的参数进行类型转换。如果参数的类型不合适,这个调用甚至无法编译。
  8. 比较整数类型基本类型的域,可以使用关系操作符<和>。例如,浮点域用Double.compare或者Float.compare,而不用关系操作符,当应用到浮点值时,它们没有遵守compareTo的通用约定。对于数组域,则要把这些指导原则应用到每个元素上。
  9. 如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。你必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果(零代表相等),则整个比较操作结束,并返回该结果。
  10. 这项技巧有时不能正常工作的原因在于,一个有符号的32位的整数还没有大到足以表达任意两个32位整数的差。如果i是一个很大的正整数(int类型),而j是一个很大的负整数(int类型),那么(i-j)将会溢出,并返回一个负值。

第4章 类和接口

第13条:使类和成员的可访问性最小化

  1. 信息隐藏之所以非常重要有许多原因,其中大多数理由都源于这样一个事实:它可以有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试、优化、使用、理解和修改。
  2. 对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的(package-private)和公有的(public)。如果类或者接口能够被做成包级私有的,它就应该被做成包级私有。通过把类或者接口做成包级私有,它实际上成了这个包的实现的一部分,而不是该包导出的API的一部分,在以后的发行版本中,可以对它进行修改、替换,或者删除,而无需担心会影响到现有的客户端程序。
  3. 如果一个包级私有的顶层类(或者接口)只是在某一个类的内部被用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。然而,降低不必要公有类的可访问性,比降低包级私有的顶层类的更重要得多:因为公有类是包的API的一部分,而包级私有的顶层类则已经是这个包的实现的一部分。
  4. 包级私有的(package-private)——声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省(default)访问级别”,如果没有为成员指定访问修饰符,就采用这个访问级别。
  5. 其实,只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除private修饰符,使该成员变成包级私有的。如果你发现自己经常要做这样的事情,就应该重新检查你的系统设计,看看是否另一种分解方案所得到的类,与其他类之间的耦合度会更小。然而,如果这个类实现了Serializable接口,这些域就有可能被“泄漏(leak)”到导出的API中。
  6. 受保护的成员是类的导出的API的一部分,必须永远得到支持。导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺。受保护的成员应该尽量少用。
  7. 如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例。
  8. 为了测试而将一个公有类的私有成员变成包级私有的,这还可以接受,但是要将访问级别提高到超过它,这就无法接受了。换句话说,不能为了测试,而将类、接口或者成员变成包的导出的API的一部分。幸运的是,也没有必要这么做,因为可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有的元素。
  9. 实例域决不能是公有的。如果域是非final的,或者是一个指向可变对象的final引用,那么一旦使这个域成为公有的,就放弃了对存储在这个域中的值进行限制的能力;这意味着,你也放弃了强制这个域不可变的能力。因此,包含公有可变域的类并不是线程安全的。即使域是final的,并且引用不可变的对象,当把这个域变成公有的时候,也就放弃了“切换到一种新的内部数据表示法”的灵活性。
  10. 假设常量构成了类提供的整个抽象中的一部分,可以通过公有的静态final域来暴露这些常量。按惯例,这种域的名称由大写字母组成,单词之间用下划线隔开。很重要的一点是,这些域要么包含基本类型的值,要么包含指向不可变对象的引用。
  11. 注意,长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。
  12. 要注意,许多IDE会产生返回指向私有数组域的引用的访问方法,这样就会产生这个问题。修正这个问题有两种方法。可以使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List VALUES = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

第14条:在公有类中使用访问方法而非公有域

  1. Java平台类库中有几个类违反了“公有类不应该直接暴露数据域”的告诫。显著的例子包括java.awt包中的Point和Dimension类。它们是不值得效仿的例子,相反,这些类应该被当作反面警告示例。正如第55条中所讲述的,决定暴露Dimension类的内部数据造成了严重的性能问题,而且这个问题至今依然存在。
  2. 总之,公有类永远都不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变的域其危害比较小。但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变的还是不可变的。

第15条:使可变性最小化

  1. Java平台类库中包含许多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。
  2. 保证类不会被扩展。这样可以防止粗心或者恶意的子类假装对象的状态已经改变,而破坏该类的不可变行为。为了防止子类化,一般做法是使这个类成为final的,但是后面我们还会讨论到其他的做法。
  3. 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法(accessor)中返回该对象的引用。在构造器、访问方法和readObject方法中请使用保护性拷贝(defensive copy)技术。
  4. 注意这些算术运算是如何创建并返回新的Complex实例,而不是修改这个实例。大多数重要的不可变类都使用了这种模式。它被称为函数的(functional)做法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它。与之相对应的更常见的是过程的(procedural)或者命令式的(imperative)做法,使用这些方式时,将一个过程作用在它们的操作数上,会导致它的状态发生改变。
  5. “不可变对象可以被自由地共享”导致的结果是,永远也不需要进行保护性拷贝。实际上,你根本无需做任何拷贝,因为这些拷贝始终等于原始的对象。因此,你不需要,也不应该为不可变的类提供clone方法或者拷贝构造器(copy constructor)。这一点在Java平台的早期并不好理解,所以String类仍然具有拷贝构造器,但是应该尽量少用它。
  6. 如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作得很好。如果无法预测,最好的办法是提供一个公有的可变配套类。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类是StringBuilder(和基本上已经废弃的StringBuffer)。可以这样认为,在特定的环境下,相对于BigInteger而言,BitSet同样扮演了可变配套类的角色。
  7. 例如,假设你希望提供一种“基于极坐标创建复数”的方式。如果使用构造器来实现这样的功能,可能会使得这个类很零乱,因为这样的构造器与已用的构造器Complex(double, double)具有相同的签名。通过静态工厂,这很容易做到。只需添加第二个静态工厂,并且工厂的名字清楚地表明了它的功能即可:
public static Complex valueOfPolar(double r, double theta) {
    return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}

当BigInteger和BigDecimal刚被编写出来的时候,对于“不可变的类必须为final的”还没有得到广泛地理解,所以它们的所有方法都有可能会被覆盖。遗憾的是,为了保持向后兼容,这个问题一直无法得以修正。如果你编写一个类,它的安全性依赖于(来自不可信客户端的)BigInteger或者BigDecimal参数的不可变性,就必须进行检查,以确定这个参数是否为“真正的”的BigInteger或者BigDecimal,而不是不可信任子类的实例。如果是后者的话,就必须在假设它可能是可变的前提下对它进行保护性拷贝:

public static BigInteger safeInstance(BigInteger val) {
    if (val.getClass() != BigInteger.class)
        return new BigInteger(val.toByteArray());
    return val;
}

总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类称为可变的类,否则就应该是不可变的。你应该总是使一些小的值对象,比如PhoneNumber和Complex,成为不可变的(在Java平台类库中,有几个类如java.util.Date和java.awt.Point,它们本应该是不可变的,但实际上却不是)。你也应该认真考虑把一些较大的值对象做成不可变的,例如String和BigInteger。只有当你确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类。

可以通过TimerTask类来说明这些原则。它是可变的,但是它的状态空间被有意地设计得非常小。你可以创建一个实例,对它进行调度使它执行起来,也可以随意地取消它。一旦一个定时器任务(timer task)已经完成,或者已经被取消,就不可能再对它重新调度。

第16条:复合优先于继承

  1. 在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对普通的具体类(concrete class)进行跨越包边界的继承,则是非常危险的。
  2. 我们只要去掉被覆盖的addAll方法,就可以“修正”这个子类。虽然这样得到的类可以正常工作,但是,它的功能正确性则需要依赖于这样的事实:HashSet的addAll方法是在它的add方法上实现的。这种“自用性(self-use)”是实现细节,不是承诺,不能保证在Java平台的所有实现中都保持不变,不能保证随着发行版本的不同而不发生变化。因此,这样得到的InstrumentedHashSet类将是非常脆弱的。
  3. 上面这两个问题都来源于覆盖(overriding)动作。如果在扩展一个类的时候,仅仅是增加新的方法,而不是覆盖现有的方法,你可能会认为这是安全的。虽然这种扩展方式比较安全一些,但是也并非完全没有风险。如果超类在后续的发行版本中获得了一个新的方法,并且不幸的是,你给子类提供了一个签名相同但返回类型不同的方法,那么这样的子类将无法通过编译。如果给子类提供的方法带有与新的超类方法完全相同的签名和返回类型,实际上就覆盖了超类中的方法,因此又回到上述的两个问题上去了。
  4. 不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作“复合(composition)”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为转发(forwarding),新类中的方法被称为转发方法(forwarding method)。这样得到的类将会非常稳固,它不依赖于现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。(因为新类中的实例方法没有转发这个新的方法的方法。
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;
    public InstrumentedSet(Set s) {
        super(s);
    }
    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    @Override public boolean addAll(Collection c) {
        addCount+=c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }
}

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
    private final Set s;
    public ForwardingSet(Set s) { this.s = s; }

    public void clear() { s.clear(); }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty() { return s.isEmpty(); }
    public int size() { return s.size(); }
    public Iterator iterator() { return s.iterator(); }
    public boolean add(E e) { return s.add(e); }
    public boolean remove(Object o) { return s.remove(o); }
    public boolean containsAll(Collection c) { return s.containsAll(c); }
    public boolean addAll(Collection c) { return s.addAll(c); }
    public boolean removeAll(Collection c) { return s.removeAll(c); }
    public boolean retainAll(Collection c) { return s.retainAll(c); }
    public Object[] toArray() { return s.toArray(); }
    public  T[] toArray(T[] a) { return s.toArray(a); }
    @Override public boolean equals(Object o) { return s.equals(o); }
    @Override public int hashCode() { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

Set接口的存在使得InstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。从本质上讲,这个类把一个Set转变成了另一个Set,同时增加了计数的功能。前面提到的基于继承的方法只适用于单个具体的类,并且对于超类中所支持的每个构造器都要求有一个单独的构造器,与此不同的是,这里的包装类(wrapper class)可以被用来包装任何Set实现,并且可以结合任何先前存在的构造器一起工作。例如:

Set s = new InstrumentedSet(new TreeSet(cmp));
Set s2 = new InstrumentedSet(new HashSet(capacity));

因为每一个InstrumentedSet实例都把另一个Set实例包装起来了,所以InstrumentedSet类被称作包装类(wrapper class)。这也正是Decorator模式,因为InstrumentedSet类对一个Set惊醒了修饰,为它增加了计数特性。有时候,复合和转发的结合也被错误地称为“委托(delegation)”。从技术的角度而言,这不是委托,除非包装对象把自身传递给被包装的对象。

换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、较简单的API:A本质上不是B的一部分,只是它的实现细节而已。

在Java平台类库中,有许多明显违反这条原则的地方。例如,栈(stack)并不是向量(vector),所以Stack不应该扩展Vector。同样地,属性列表也不是散列表,所以Properties不应该扩展Hashtable。在这两种情况下,复合模式才是恰当的。

对于你正试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把那些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。

为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

  1. 对于为了继承而设计的类,唯一的测试方法就是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦变得更加明显。相反,如果编写了多个子类,并且无一使用受保护的成员,或许就应该把它做成私有的。经验表明,3个子类通常就足以测试一个可扩展的类。
  2. 构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条规则,很有可能导致程序失败。超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。
  3. 如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接方式。对于readObject方法,覆盖版本的方法将在子类的状态被反序列化(deserialized)之前先被运行;而对于clone方法,覆盖版本的方法则是在子类的clone方法有机会修正被克隆对象的状态之前先被运行。无论哪种情形,都不可避免地将导致程序失败。
  4. 这条建议可能会引来争议,因为许多程序员已经习惯于对普通的具体类进行子类化,以便增加新的功能设施,比如仪表功能(instrumentation,如计数显示等)、通知机制或者同步功能,或者为了限制原有类中的功能。如果类实现了某个能够反映其本质的接口,比如Set、List或者Map,就不应该为了禁止子类化而感到后悔。第16条中介绍的包装类(wrapper class)模式提供了另一种更好的办法,让继承机制实现更多的功能。
  5. 如果你认为必须允许从这样的类继承,一种合理的办法是确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明这一点。

第18条:接口优于抽象类

  1. 这两种机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。一个更为重要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次(class hierarchy)的哪个位置。
  2. 虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。
  3. 按照惯例,骨架实现被称为AbstractInterface,这里的Interface是指所实现的接口的名字。例如,Collections Framework为每个重要的集合接口都提供一个骨架实现,包括AbstractCollection、AbstractSet、AbstractList和AbstractMap。将它们称作SkeletalCollection、SkeletalSet、SkeletalList和SkeletalMap也是有道理的,但是现在Abstract的用法已经根深蒂固。
  4. 编写骨架实现类相对比较简单,只是有点单调乏味。首先,必须认真研究接口,并确定哪些方法是最为基本的(primitive),其他的方法则可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法。然后,必须为接口中所有其他的方法提供具体的实现。例如,Map.Entry接口的骨架实现类:
// Skeletal Implementation
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    // Primitive operations
    public abstract K getKey();
    public abstract V getValue();

    // Entries in modifiable maps must override this method
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    // Implements the general contract of Map.Entry.equals
    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (! (o instanceof Map.Entry))
            return false;
        Map.Entry arg = (Map.Entry) o;
        return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
    }
    private static boolean equals(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }

    // Implements the general contract of Map.Entry.hashCode
    @Override public int hashCode() {
        return hashCode(getKey()) ^ hashCode(getValue());
    }
    private static int hashCode(Object obj) {
        return obj == null ? 0 : obj.hashCode();
    }
}

骨架实现上有个小小的不同,就是简单实现(simple implementation),AbstractMap.SimpleEntry就是个例子。简单实现就像个骨架实现,这是因为它实现了接口,并且是为了继承而设计的,但是区别在于它不是抽象的:它是最简单的可能的有效实现。你可以原封不动地使用,也可以看情况将它子类化。

使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变比接口的演变要容易得多。如果在后续的发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现。然后,该抽象类的所有现有实现都将提供这个新的方法。对于接口,这样做是行不通的。

一般来说,要想在公有接口中增加方法,而不破坏实现这个接口的所有现有的类,这是不可能的。之前实现该接口的类将会漏掉新增加的方法,并且无法再通过编译。在为接口增加新方法的同时,也为骨架实现类增加同样的新方法,这样可以在一定程度上减小由此带来的破坏,但是,这样做并没有真正解决问题。所有不从骨架实现类继承的接口实现仍然会遭到破坏。

简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。

第19条:接口只用于定义类型

  1. 当类实现接口时,接口就充当可以引用这个类的实例的类型(type)。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。
  2. 常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄漏到该类的导出API中。类实现常量接口,这对于这个类的用户来讲并没有什么价值。实际上,这样做反而会让他们更加糊涂。更糟糕的是,它代表了一种承诺:如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保二进制兼容性。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。
  3. 在Java平台类库中有几个常量接口,例如java.io.ObjectStreamConstants。这些接口应该被认为是反面的典型,不值得效仿。
  4. 如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中。例如,在Java平台类库中所有的数值包装类,如Integer和Double,都导出了MIN_VALUE和MAX_VALUE常量。如果这些常量最好被看作枚举类型的成员,就应该用枚举类型(enum type)来导出这些常量。否则,应该使用不可实例化的工具类(utility class)来导出这些常量。下面的例子是前面的PhysicalConstants例子的工具类翻版:
// Constant utility class
public class PhysicalConstants {
    private PhysicalConstants() { } // Prevents instantiation
    public static final double AVOGADROS_NUMBER = 6.02214199e23;
    public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    public static final double ELECTRON_MASS = 9.10938188e-31;
}

第20条:类层次优于标签类

  1. 这种标签类(tagged class)有着许多缺点。它们中充斥着样板代码,包括枚举声明、标签域以及条件语句。由于多个实现乱七八糟地挤在了单个类中,破坏了可读性。内存占用也增加了,因此实例承担着属于其他风格的不相关的域。域不能做成是final的,除非构造器初始化了不相关的域,产生更多的样板代码。
  2. 当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。

第21条:用函数对象表示策略

  1. 然而,我们也可能定义这样一种对象,它的方法执行其他对象(other objects)(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(function object)。
  2. 作为典型的具体策略类,StringLengthComparator类是无状态的(stateless):它没有域,所以,这个类的所有实例在功能上都是相互等价的。因此,它作为一个Singleton是非常合适的,可以节省不必要的对象创建开销:
class StringLengthComparator {
    private StringLengthComparator() { }
    public static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

为了把StringLengthComparator实例传递给方法,需要适当的参数类型。使用StringLengthComparator并不好,因为客户端将无法传递任何其他的比较策略。相反,我们需要定义一个Comparator接口,并修改StringLengthComparator来实现这个接口。换句话说,我们在设计具体的策略类时,还需要定义一个策略接口(strategy interface)。

因为策略接口被用做具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成公有的。相反,“宿主类(host class)”还可以导出公有的静态域(或者静态工厂方法),其类型为策略接口,具体的策略类可以是宿主类的私有嵌套类。下面的例子使用静态成员类,而不是匿名类,以便允许具体的策略类实现第二个接口Seriablizable:

// Exporting a concrete strategy
class Host {
    private static class StrLenCmp implements Comparator<String>, Serializable {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }
    // Returned comparator is serializable
    public static final Comparator STRING_LENGTH_COMPARATOR = new StrLenCmp();

    ... // Bulk of class omitted

String类利用这种模式,通过它的CASE_INTENSITIVE_ORDER域,导出一个不区分大小写的字符串比较器。

简而言之,函数指针的主要途径就是实现策略(Strategy)模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。

第22条:优先考虑静态成员类

  1. 嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。
  2. 静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。例如,考虑一个枚举,它描述了计算器支持的各种操作。Operation枚举应该是Calculator类的公有静态成员类,然后,Calculator类的客户端就可以用诸如Calculator.Operation.PLUS和Calculator.Operation.MINUS这样的名称来引用这些操作。
  3. 非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。
  4. 如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
  5. 非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。例如,Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view),这些集合视图是由Map的keySet、entrySet和Values方法返回的。同样地,诸如Set和List这种集合接口的实现往往也使用非静态成员类来实现它们的迭代器(iterator):
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted
    public Iterator iterator() {
        return new MyIterator();
    }
    private class MyIterator implements Iterator<E> {
        ...
    }
}

如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。

私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件。例如,考虑一个Map实例,它把键(key)和值(value)关联起来。许多Map实现的内部都有一个Entry对象,对应于Map中的每个键-值对。虽然每个entry都与一个Map关联,但是entry上的方法(getKey、getValue和setValue)并不需要访问该Map。因此,使用非静态成员来表示entry是很浪费的:私有的静态成员类是最佳的选择。如果不小心漏掉了entry声明中的static修饰符,该Map仍然可以工作,但是每个entry中将会包含一个指向该Map的引用,这样就浪费了空间和时间。

正如你所想象的,匿名类没有名字。它不是外围类的一个成员。

匿名类的另一种常见用法是创建过程对象(process object),比如Runnable、Thread或者TimerTask实例。

第5章 泛型

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

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

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

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

    第25条:列表优先于数组

    1. 首先,数组是协变的(covariant)。这个词听起来有点吓人,其实只是表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的(invariant):对于任意两个不同的类型Type1和Type2,List既不是List的子类型,也不是List的超类型。你可能认为,这意味着泛型是有缺陷的,但实际上可以说数组才是有缺陷的。
    2. 相比之下,泛型则是通过擦除(erasure)来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。
    3. 由于上述这些根本的区别,因此数组和泛型不能很好地混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的。这些数组创建表达式没有一个是合法的:new List[]、new List[]和new E[]。这些在编译时都会导致一个generic array creation(泛型数组创建)错误。
    4. 从技术的角度来说,像E、List和List这样的类型应称作不可具体化的(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”,这与互比性的概念或多或少有些一致。

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

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

    Iterator 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  void swapHelper(List 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。例如,String.class属于Class类型,Integer.class属于Class类型。
    4. Favorites实例是类型安全的(typesafe)的:当你向它请求String的时候,它从来不会返回一个Integer给你。同时它也是异构的(heterogeneous):不像普通的map,它的所有键都是不同类型的。因此,我们将Favorites称作类型安全的异构容器(typesafe heterogeneous container)。
    // Typesafe heterogeneous container pattern - implementation
    public class Favorites {
        private Map, Object> favorites = new HashMap, Object>();
        public  void putFavorite(Class type, T instance) {
            if (type==null)
                throw new NullPointerException("Type is null");
            favorites.put(type, instance);
        }
        public  T getFavorite(Class type) {
            return type.cast(favorites.get(type));
        }
    }

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

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

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

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

    第6章 枚举和注解

    Java 1.5发行版本中增加了两个新的引用类型家族:一种新的类称作枚举类型(enum type),一种新的接口称作注解类型(annotation type)。

    第30条:用enum代替int常量

    1. 这种方法称作int枚举模式(int enum pattern),存在着诸多不足。它在类型安全性和使用方便性方面没有任何帮助。如果你将apple传到想要orange的方法中,编译器也不会出现警告,还会用==操作符将apple与orange进行对比,甚至更糟糕。
    2. 采用int枚举模式的程序是十分脆弱的。因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以运行,但是它们的行为就是不确定的。
    3. 将int枚举常量翻译成可打印的字符串,并没有很便利的方法。
    4. Java的枚举类型是功能十分齐全的类,功能比其他语言中的对等物要更强大得多,Java的枚举本质上是int值。
    5. Java枚举类型背后的基本想法非常简单:它们就是通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。
    6. 它们是单例(Singleton)的泛型化,本质上是单元素的枚举。
    7. 你可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式之中。最终,可以通过调用toString方法,将枚举转换成可打印的字符串。
    // Enum type with data and behavior
    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; // In kilograms
        private final double radius; // In meters
        private final double surfaceGravity; // In m / s^2
    
        // Universal gravitational constant in m^3 / kg s^2
        private static final double G = 6.67300E-11;
    
        // Constructor
        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 surfaceGravity() { return surfaceGravity; }
        public double surfaceWeight(double mass) {
            return mass * surfaceGravity; // F = ma
        }
    }

    为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都应该为final的。

    注意Plant就像所有的枚举一样,它有一个静态的values方法,按照声明顺序返回它的值数组。还要注意toString方法返回每个枚举值的声明名称,使得println和printf的打印变得更加容易。如果你不满意这种字符串表示法,可以通过覆盖toString方法对它进行修改。

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

    // Enum type with constant-specific method implementations
    public enum Operation {
        PLUS { double apply(double x, double y){return x+y;}},
        MINUS { double apply(double x, double y){return x-y;}},
        TIMES { double apply(double x, double y){return x*y;}},
        DIVIDE { double apply(double x, double y){return x/y;}};
    
        abstract double apply(double x, double y);
    }

    即使你真的忘记了,编译器也会提醒你,因为枚举类型中的抽象方法必须被它所有常量中的具体方法所覆盖。

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

    // Enum type with constant-specific class bodies and data
    public enum Operation {
        PLUS("+") {
            double apply(double x, double y) { return x+y; }
        },
        MINUS("-") {
            double apply(double x, double y) { return x-y; }
        },
        TIMES("*") {
            double apply(double x, double y) { return x*y; }
        },
        DIVIDE("/") {
            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; }
        abstract double apply(double x, double y);
    }

    枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成常量本身。

    枚举构造器不可以访问枚举的静态域,除了编译时常量域之外。这一限制是有必要的,因为构造器运行的时候,这些静态域还没有被初始化。

    你真正想要的就是每当添加一个枚举常量时,就强制选择一种加班报酬策略。幸运的是,有一种很好的方法可以实现这一点。这种想法就是将加班工资计算移到一个私有的嵌套枚举中,将这个策略枚举(strategy enum)的实例传到PayrollDay枚举的构造器中。之后PayrollDay枚举将加班工资计算委托给策略枚举,PayrollDay中就不需要switch语句或者特定于常量的方法实现了。虽然这种模式没有switch语句那么简洁,但更加安全,也更加灵活:

    // The strategy enum pattern
    enum PayrollDay {
        MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
        WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
        FRIDAY(PayType.WEEKDAY), 
        SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
    
        private final PayType payType;
        PayrollDay(PayType payType) { this.payType = payType; }
    
        double pay(double hoursWorked, double payRate) {
            return payType.pay(hoursWorked, payRate);
        }
        // The strategy enum type
        private enum PayType {
            WEEKDAY {
                double overtimePay(double hours, double payRate) {
                        return hours<=HOURS_PER_SHIFT ? 0 :
                            (hours - HOURS_PER_SHIFT) * payRate / 2;
                }
            },
            WEEKEND {
                double overtimePay(double hours, double payRate) {
                    return hours * payRate / 2;
                }
            };
            private static final int HOURS_PER_SHIFT = 8;
            abstract double overtimePay(double hrs, double payRate);
            double pay(double hoursWorked, double payRate) {
                double basePay = hoursWorked * payRate;
                return basePay + overtimePay(hoursWorked, payRate);
            }
        }
    }

    枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。

    那么什么时候应该使用枚举呢?每当需要一组固定常量的时候。当然,这包括“天然的枚举类型”,例如行星、一周的天数以及棋子的数目等等。但它也包括你在编译时就知道其所有可能值得其他集合,例如菜单的选项、操作代码以及命令行标记等。枚举类型中的常量集并不一定要始终保持不变。专门设计枚举特性是考虑到枚举类型的二进制兼容演变。

    第31条:用实例域代替序数

    1. 永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中:
    public enum Ensemble {
        SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
        SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
        NONET(9), DECTET(10), TRIPLE_QUARTET(12);
        private final int numberOfMusicians;
        Ensemble(int size) { this.numberOfMusicians = size; }
        public int numberOfMusicians() { return numberOfMusicians; }
    }

    Enum规范中谈到ordinal时这么写道:“大多数程序员都不需要这个方法。它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的。”除非你在编写的是这种数据结构,否则最好完全避免使用ordinal方法。

    第32条:用EnumSet代替位域

    1. java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set接口,提供了丰富的功能、类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体的实现上,每个EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素——大多如此——整个EnumSet就是单个long来表示,因此它的性能比得上位域的性能。批处理,如removeAll和retainAll,都是利用位算法来实现的,就像手工替位域实现得那样。但是可以避免手工位操作时容易出现的错误以及不太雅观的代码,因为EnumSet替你完成了这项艰巨的工作。
    // EnumSet - a modern replacement for bit fields
    public class Text {
        public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
        // Any Set could be passed in, but EnumSet is clearly best
        public void applyStyles(Set