Effective Java 笔记

1. 考虑使用静态工厂方法替代构造方法

优点

  1. 通过方法名体现不同构造方式的差异
  2. 不需要每次调用时都创建一个新对象 ( 单例模式, 享元模式 )
  3. 可以返回其返回类型的任何子类型的对象
  4. 在编写包含该方法的类时,返回的对象的类不需要存在

限制

  1. 没有公共或受保护构造方法的类不能被子类化 ( 因为大部分实现会将构造函数设为私有 )
  2. 没有构造函数那么明显

2. 当构造方法参数过多时使用 builder 模式

分析

  1. 可伸缩 (telescoping constructor) 构造方法模式, 多个不同参数数量的构造方法

    • 当有很多参数时,很难编写代码,而且很难读懂它
  2. JavaBeans 模式, 调用 setter 方法来设置参数

    • 由于构造方法在多次调用中被分割,在过程中可能处于不一致的状态
  3. Builder 模式, 构造方法为 private, 通过内嵌的 Builder 类调用 build() 返回本身

    1
    2
    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();
    • 可以在 build() 里加入参数检查
    • 编写简单, 易读
    • 能区分必须和可选字段
  4. 平行层次的 Builder, 抽象类有抽象的 builder, 具体的类有具体的 builder

    1
    2
    3
    4
    5
    NyPizza pizza = new NyPizza.Builder(SMALL)
    .addTopping(SAUSAGE).addTopping(ONION).build();

    Calzone calzone = new Calzone.Builder()
    .addTopping(HAM).sauceInside().build();

3. 使用私有构造方法或枚类实现 Singleton 属性

私有构造方法

  • 通过公共静态成员提供访问
1
2
3
4
5
6
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
  • 通过静态的工厂方法提供访问
    • 优点: 可以灵活地改变你的想法
    • 优点: 可以编写一个泛型单例工厂
    • 优点: 方法引用可以用 Supplier
1
2
3
4
5
6
7
 // 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() { ... }
}
  • 警告
    • 可以使用 AccessibleObject.setAccessible 方法,以反射方式调用私有构造方法
    • 为了维护单例的保证,声明所有的实例属性为 transient,并提供一个 readResolve 方法

枚举

  • 声明单一元素的枚举类
    • 简洁
    • 提供了免费的序列化机制
    • 提供了针对多个实例化的保证
1
2
3
4
5
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}

4. 使用私有构造方法执行非实例化

  • 如一些工具类, 只包含静态方法, 可以用来避免其实例化和被继承
1
2
3
4
5
6
7
8
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
... // Remainder omitted
}

5. 使用依赖注入取代硬连接资源(hardwiring

resources)

  • 不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类
    的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法 (或静态工厂或 builder 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。

6. 避免创建不必要的对象

  • 如果对象是不可变的,它总是可以被重用

  • 注意无意识的自动装箱 (autoboxing)

  • 在现代 JVM 实现上, 使用构造方法创建和回收小的对象其实是非常廉价的, 创建额外的对象以增强程序的清晰
    度,简单性或功能性通常是件好事

  • 除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意

7. 消除过期的对象引用

  • 当一个类自己管理内存时,程序员应该警惕内存泄漏问题
    • 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除
  • 另一个常见的内存泄漏来源是缓存
    • 只要在缓存之外存在对某个项 (entry) 的键 (key) 引用,那么这项就是明确有关联的,就可以用 WeakHashMap 来表示缓存
    • 可以通过一个后台线程或将新的项添加到缓存时顺便清理
  • 第三个常见的内存泄漏来源是监听器和其他回调
    • 客户端注册回调, 仅将它们保存在 WeakHashMap 的键 (key) 中

8. 避免使用 Finalizer 和 Cleaner 机制

缺点

  • 不能保证他们能够及时执行

  • 不要相信 System.gcSystem.runFinalization 方法, 可能会增加被执行的几率,但不能保证一定会执行

  • 在执行 Finalizer 机制过程中,未捕获的异常会被忽略

  • 导致严重的性能损失

  • 严重的安全问题: 它们会打开你的类来进行 Finalizer 机制攻击

正确做法

  • 实现 AutoCloseable 接口,并要求客户在不再需要时调用每个实例 close 方法,通常使用 try-with-resources 确保终止

合法用途

  • 作为一个安全网 (safetynet),以防资源的拥有者忽略了它的 close 方法
  • 与本地对等类(native peers)有关。本地对等类是一个由普通对象委托的本地 (非 Java) 对象。由于本地对等类不是普通的 Java 对象,所以垃圾收集器并不知道它,当它的 Java 对等对象被回收时,本地对等类也不会回收。

9. 使用 try-with-resources 语句替代 try-finally 语句

对比

  • try-finally
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
} }
  • try-with-resources
1
2
3
4
5
6
7
8
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
} }

分析

  • try-finally 语句关闭资源, 有多个资源时容易犯错

  • try-with-resources 块和 finally 块中的代码都可能抛出异常

    • finally 块中close()时抛出的异常会冲掉前面的异常

    • try-with-resources 块中close()(不可见) 时抛出的异常会被抑制 (suppressed), 这些抑制的异常没

      有被抛弃, 而是打印在堆栈跟踪中,并标注为被抑制了

10. 重写 equals 方法时遵守通用约定

不覆盖 equals 方法的场景

  • 每个类的实例都是固有唯一的
  • 类不需要提供一个“逻辑相等(logical equality)”的测试功能
  • 父类已经重写了 equals 方法,则父类行为完全适合于该子类
  • 类是私有的或包级私有的

覆盖 equals 方法的场景

  • 需要提供一个逻辑相等的判断, 且父类还没重写过equals

通用约定

  • 自反性 (Reflexivity): 对于任何非空引用 xx.equals(x) 必须返回 true
  • 对称性 (Symmetry): 对于任何非空引用 xy,如果且仅当 y.equals(x) == true, x.equals(y)必须为 true
  • 传递性 (Transitivity): 对于任何非空引用 x、y、z,如果 x.equals(y) == true, y.equals(z) == true, 则x.equals(z)必须返回 true
  • 一致性 (Consistent): 对于任何非空引用 xy,如果在 equals 比较中使用的信息没有修改,则x.equals(y)的多次调用必须始终返回 true 或始终返回 false
  • 非空性 (Non-nullity): 对于任何非空引用 x, x.equals(null) 必须返回 false

编写高质量 equals 方法

  • 使用 instanceof 运算符来检查参数是否具有正确的类型, 再将参数转换为正确的类型

  • 检查是否与类中的每个重要属性相匹配

  • 不同类型的比较方式

    • 对于类型为非 floatdouble 的基本类型,使用 == 运算符进行比较
    • 对于对象引用属性,递归地调用 equals 方法
    • 对于 float 基本类型的属性,使用静态方法 Float.compare(float, float)
    • 对于 double 基本类型的属性,使用静态方法 Double.compare(double, double)
    • 对于数组属性,将这些准则应用于每个元素
  • 某些对象引用的属性可能合法地包含 null。 为避免出现 NullPointerException 异常,请使用静态方法
    Objects.equals(Object, Object) 检查这些属性是否相等

  • 性能优化

    • == 运算符检查参数是否为该对象的引用, 如果是, 返回 true
    • 首先比较最可能不同的属性, 开销比较小的属性
    • 不要比较不属于对象逻辑状态的属性
    • 不需要比较可以从“重要属性”计算出来的派生属性
  • 注意

    • 当重写 equals 方法时,同时也要重写 hashCode 方法
    • 不要让 equals 方法试图太聪明, 例如File 类不应该试图将引用的符号链接等同于同一文件对象
    • equal 时方法声明中,不要将参数 Object 替换成其他类型

11. 重写 equals 方法时同时也要重写 hashcode 方法

Object 规范

  • 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。

  • 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。

  • 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

一个简单的配方

1
2
3
4
5
6
7
8
// Typical hashCode method
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
  • 基本类型,使用 Type.hashCode(f) 方法计算,其中 Type 类是对应属性 f 基本类型的包装类。

  • 如果该属性是一个对象引用,并且该类的 equals 方法通过递归调用 equals 来比较该属性,并递归地调用 hashCode 方法。 如果需要更复杂的比较,则计算此字段的“范式(“canonical representation)”,并在范式上调用 hashCode。 如果该字段的值为空,则使用 0(也可以使用其他常数,但通常来使用 0 表示)。

  • 如果属性 f 是一个数组,把它看作每个重要的元素都是一个独立的属性。 也就是说,通过递归地应用 这些规则计算每个重要元素的哈希码,并且将每个步骤的值合并。 如果数组没有重要的元素,则使用一个常量,最好不要为 0。如果所有元素都很重要,则使用 Arrays.hashCode 方法。

12. 始终重写 toString 方法

  • 便于调试

13. 谨慎地重写 clone 方法

缺陷

  • Cloneable 接口缺少 clone 方法, 而 Object 的 clone 方法是受保护的, 所以不能保证调用成功

clone 方法的通用规范 ( 非绝对 )

  • x.clone() != x
  • x.clone().getClass() == x.getClass()
  • x.clone().equals(x) == true
  • 如果一个 final 类有一个不调用 super.clone 的 clone 方法, 那么这个类没有理由实现 Cloneable 接口,因为它不依赖于 Object 的 clone 实现的行为

复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点

  • 不依赖风险很大的语言外的对象创建机制
  • 不要求遵守那些不太明确的惯例
  • 不会与 final 属性的正确使用相冲突
  • 不会抛出不必要的检查异常
  • 不需要类型转换

14. 考虑实现 Comparable 接口

  • 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现 Comparable 接口:

  • 1
    2
    3
    public interface Comparable<T> {
    int compareTo(T t);
    }

compareTo 方法的通用约定

  • 将此对象与指定的对象按照排序进行比较, 返回值可能为负整数, 零或正整数, 因为此对象对应小于,等于或大于指定的对象。 如果指定对象的类型与此对象不能进行比较,则引发 ClassCastException 异常

  • 实现类必须确保所有 xy 都满足 sgn(x.compareTo(y)) == -sgn(y. compareTo(x))。 (这意味着 当且仅当 y.compareTo(x) 抛出异常时, x.compareTo(y) 必须抛出异常。)

  • 实现类还必须确保该关系是可传递的:(x. compareTo(y) > 0 && y.compareTo(z) > 0) 意味着x.compareTo(z) > 0

  • 对于所有的 z,实现类必须确保 x.compareTo(y) == 0 意味着 sgn(x.compareTo(z)) == sgn(y.compareTo(z))

  • 推荐 (x.compareTo(y) == 0) == (x.equals(y)) ,但不是必需的。 一般来说,任何实现了 Comparable 接口的类违反了这个条件都应该清楚地说明这个事实。 推荐的语言是 “注意:这个类有一个自然顺序,与 equals 不一致”。

使用包装类中的静态 compare 方法或 Comparator 接口中的构建方法

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Comparator based on static compare method
    static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
    return Integer.compare(o1.hashCode(), o2.hashCode());
    }
    };

    // Comparator based on Comparator construction method
    static Comparator<Object> hashCodeOrder =
    Comparator.comparingInt(o -> o.hashCode());
  • 请避免使用 “<” 和 “>” 运算符

15. 使类和成员的可访问性最小化

  • 使用尽可能低的访问级别
  • 类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的
1
2
// Potential security hole!
public static final Thing[] VALUES = { ... };
  • 有两种方法可以解决这个问题

    • 你可以使公共数组私有并添加一个公共的不可变列表:

    • 1
      2
      3
      private static final Thing[] PRIVATE_VALUES = { ... };
      public static final List<Thing> VALUES =
      Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
    • 可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法:

    • 1
      2
      3
      4
      private static final Thing[] PRIVATE_VALUES = { ... };
      public static final Thing[] values() {
      return PRIVATE_VALUES.clone();
      }

16. 在公共类中使用访问方法而不是公共属性

  • 如果一个类在其包之外是可访问的,则提供访问方法来保留更 改类内部表示的灵活性
  • 如果一个类是包级私有的,或者是一个私有的内部类,那么暴露它的数据属性就没有什么本质上的错误

17. 最小化可变性

要使一个类不可变,请遵循以下五条规则

  1. 不要提供修改对象状态的方法(也称为 mutators)

  2. 确保这个类不能被继承

  3. 把所有属性设置为 final

  4. 把所有的属性设置为 private

  5. 确保对任何可变组件的互斥访问 ( 确保该类的客户端无法获得对这些对象的引用 )

  • 不可变对象本质上是线程安全的

  • 不需要也不应该在一个不可变的类上提供一个 clone 方法或拷贝构造方法(copy constructor)

  • 一个不可变的类可以提供静态的工厂来缓存经常被请求的实例

  • 一个类不得允许子类化, 可以设置类为 final , 更灵活的方式是使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法

  • 如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性

18. 组合优于继承

  • 与方法调用不同,继承打破了封装 , 父类的实现可能会不断变化, 子类可能会被破坏,即使它的代码没有任何改变

包装类 ( 装饰器模式 )

  • 有时组合和转发的结合被不精确地地称为委托 (delegation). 从技术上讲,除非包装对象把自身传递给被包装对象,否则不是委托

  • 包装类的缺点

    • 包装类不适合在回调框架(callback frameworks)中使用,其中对象将自我引用传递给其他对象以用于后续调用(“回调”)
    • 编写转发方法有些繁琐
  • 只有在子类真的是父类的子类型的情况下,继承才是合适的

19. 如使用继承则设计,应当文档说明,否则不该使用

  • 这个类必须准确地描述重写这个方法带来的影响
  • 测试为继承而设计的类的唯一方法是编写子类, 经验表明,三个子类通常足以测试一个可继承的类。 这些子类应该由父类作者以外的人编写
  • 构造方法绝不能直接或间接调用可重写的方法
  • 如果你决定在为继承而设计的类中实现 CloneableSerializable 接口, 那么 clonereadObject 都不会直接或间接调用可重写的方法

20. 接口优于抽象类

  • Java 只允许单一继承

  • 接口是定义混合类型(mixin)的理想选择, 允许构建非层级类型的框架

  • 可以通过提供一个抽象的骨架实现类(abstract skeletal implementation class)来与接口一起使用,将接口和抽象类的优点结合起来。 接口定义了类型,可能提供了一些默认的方法,而骨架实现类在原始接口方法的顶层实现了剩余的非原始接口方法。 继承骨架实现需要大部分的工作来实现一个接口。 这就是模板方法设计模式

21. 为后代设计接口

  • 编写一个默认方法并不总是可能的,它保留了每个可能的实现的所有不变量

  • 在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败

22. 接口仅用来定义类型

  • 常量接口模式是对接口的糟糕使用

23. 优先使用类层次而不是标签类

24. 优先考虑静态成员类

  • 非静态成员类的每个实例都隐含地与其包含的类的宿主实例相关联, 可以调用宿主实例上的方法, 不可能在没有宿主实例的情况下创建非静态成员类的实例

    • 非静态成员类的一个常见用法

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // Typical use of a nonstatic member class
      public class MySet<E> extends AbstractSet<E> {
      ... // Bulk of the class omitted
      @Override
      public Iterator<E> iterator() {
      return new MyIterator();
      }
      private class MyIterator implements Iterator<E> {
      ...
      }
      }
  • 如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类

  • 有四种不同的嵌套类,每个都有它的用途。 如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。 如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。 假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类; 否则,把它变成局部类。

25. 将源文件限制为单个顶级类

26. 不要使用原始类型

  • 如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势

  • 以下是使用泛型类型的 instanceof 运算符的首选方法

    • 1
      2
      3
      4
      5
      // Legitimate use of raw type - instanceof operator
      if (o instanceof Set) { // Raw type
      Set<?> s = (Set<?>) o; // Wildcard type
      ...
      }

27. 消除非检查警告

  • 尽可能 地消除每一个未经检查的警告

  • 如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用@SuppressWarnings(“unchecked”) 注解来抑制警告

  • 每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的

28. 列表优于数组

29. 优先考虑泛型

30. 优先使用泛型方法

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

1
2
3
4
5
// pushAll method without wildcard type - deficient!
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
  • 假设有一个 Stack<Number>,并调用 push(intVal),其中 intVal 的类型是 Integer, 会得到错误消息
1
2
3
4
5
// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
  • 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型

  • producer-extends, consumer-super(PECS)

    • 如果一个参数化类型代表一个 T 生产者,使用 <? extends T>
    • 如果它代表 T 消费者,则使用 <? super T>
  • 改造 max 方法

  • 1
    2
    3
    public static <T extends Comparable<T>> T max(List<T> list)

    public static <T extends Comparable<? super T>> T max(List<? extends T> list)
    • max 方法返回 T , 所以 List<? extends T> list
    • ComparableT 消费 T 实例(并生成指示顺序关系的整数), 所以 <T extends Comparable<? super T>>

32. 合理地结合泛型和可变参数

  • 在 Java 7 中, @SafeVarargs 注解已添加到平台,以允许具有泛型可变参数的方法的作者自动禁止客户端警 告
  • 如果可变参数数组仅用于从调用者向方法传递可变数量的参数, 那么该方法是安全的

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