0%

Effective Java

拓展自《高效Java第四版》

Effective Java

创建和销毁对象

【01】考虑以静态工厂方法代替构造函数

在 Java 中,获得一个类实例最简单的方法就是使用 new 关键字,通过构造函数来实现对象的创建。
就像这样:

1
2
3
Fragment fragment = new MyFragment();
// or
Date date = new Date();

不过在实际的开发中,我们经常还会见到另外一种获取类实例的方法:

1
2
3
4
5
Fragment fragment = MyFragment.newIntance();
// or
Calendar calendar = Calendar.getInstance();
// or
Integer number = Integer.valueOf("3");

↑ 像这样的:不通过 new,而是用一个静态方法来对外提供自身实例的方法,即为我们所说的静态工厂方法(Static factory method)

静态工厂方法与构造器不同的第一优势在于,它们有名字

由于语言的特性,Java 的构造函数都是跟类名一样的。这导致的一个问题是构造函数的名称不够灵活,经常不能准确地描述返回值,在有多个重载的构造函数时尤甚,如果参数类型、数目又比较相似的话,那更是很容易出错。

比如,如下的一段代码 :

1
2
3
4
5
6
Date date0 = new Date();
Date date1 = new Date(0L);
Date date2 = new Date("0");
Date date3 = new Date(1,2,1);
Date date4 = new Date(1,2,1,1,1);
Date date5 = new Date(1,2,1,1,1,1);

—— Date 类有很多重载函数,对于开发者来说,假如不是特别熟悉的话,恐怕是需要犹豫一下,才能找到合适的构造函数的。而对于其他的代码阅读者来说,估计更是需要查看文档,才能明白每个参数的含义了。

(当然,Date 类在目前的 Java 版本中,只保留了一个无参和一个有参的构造函数,其他的都已经标记为 @Deprecated 了)

而如果使用静态工厂方法,就可以给方法起更多有意义的名字,比如前面的 valueOfnewInstancegetInstance 等,对于代码的编写和阅读都能够更清晰。

第二个优势,不用每次被调用时都创建新对象

第三个优势,可以返回原返回类型的子类

这条不用多说,设计模式中的基本的原则之一——『里氏替换』原则,就是说子类应该能替换父类。
显然,构造方法只能返回确切的自身类型,而静态工厂方法则能够更加灵活,可以根据需要方便地返回任何它的子类型的实例。

1
2
3
4
5
6
7
8
9
10
Class Person {
public static Person getInstance(){
return new Person();
// 这里可以改为 return new Player() / Cooker()
}
}
Class Player extends Person{
}
Class Cooker extends Person{
}

比如上面这段代码,Person 类的静态工厂方法可以返回 Person 的实例,也可以根据需要返回它的子类 Player 或者 Cooker。(当然,这只是为了演示,在实际的项目中,一个类是不应该依赖于它的子类的。但如果这里的 getInstance () 方法位于其他的类中,就更具有的实际操作意义了)

第四个优势,在创建带泛型的实例时,能使代码变得简洁

可以减少对外暴露的属性

多了一层控制,方便统一修改

【02】当构造函数有多个参数时,考虑改用Builder

传统方法构造最短参数列表的构造函数

1
NutritionFacts cocaCola =new NutritionFacts(240, 8, 100, 0, 35, 27);

上面这种方式的缺点不再介绍,

有的时候可能会用JavaBean的setter和getter

1
2
3
4
5
6
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

但是考虑到我上一篇博文里面涉及的原子操作,需要程序员额外的努力来确保线程安全。

所以这种方法应运而生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 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 sodium = 0;
private int carbohydrate = 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 sodium(int val) {
sodium = val;
return this;
}

public Builder carbohydrate(int val) {
carbohydrate = 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;
}
}

NutritionFacts 类是不可变的,所有参数默认值都在一个位置。构建器的 setter 方法返回构建器本身,这样就可以链式调用,从而得到一个流畅的 API。下面是客户端代码的样子:

调用:

1
2
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();

建造者模式也有缺点。为了创建一个对象,你必须首先创建它的构建器。虽然在实际应用中创建这个构建器的成本可能并不显著,但在以性能为关键的场景下,这可能会是一个问题。而且,建造者模式比可伸缩构造函数模式更冗长,因此只有在有足够多的参数时才值得使用,比如有 4 个或更多参数时,才应该使用它。但是请记住,你可能希望在将来添加更多的参数。但是,如果你以构造函数或静态工厂开始,直至类扩展到参数数量无法控制的程度时,也会切换到构建器,但是过时的构造函数或静态工厂将很难处理。因此,最好一开始就从构建器开始。

【03】使用私有构造函数或枚举类型实施单例属性

本章先介绍了一种有缺陷的饿汉式的单例

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() { ... }
}

然后介绍了这种单例模式的例外

拥有特殊权限的客户端可以借助 AccessibleObject.setAccessible 方法利用反射调用私有构造函数. 如果需要防范这种攻击,请修改构造函数,使其在请求创建第二个实例时抛出异常。

1
2
3
4
5
6
7
8
9
Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);

Arrays.stream(constructors).forEach(name -> {
if (name.toString().contains("Elvis")) {
Elvis instance = (Elvis) name.newInstance();
instance.leaveTheBuilding();
}
});

随后介绍了静态工厂方法 — 最常见的饿汉式单例

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() { ... }
}

要使单例类使用这两种方法中的任何一种(Chapter 12),仅仅在其声明中添加实现 serializable 是不够的。要维护单例保证,应声明所有实例字段为 transient,并提供 readResolve 方法(Item-89)。否则,每次反序列化实例时,都会创建一个新实例,在我们的示例中,这会导致出现虚假的 Elvis。为了防止这种情况发生,将这个 readResolve 方法添加到 Elvis 类中:

1
2
3
4
5
6
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}

最后还介绍了通过Enum还构建单例,本文不在叙述,在本博客的《单例浅谈》里面会涉及到这个问题。

【04】用私有构造函数实施不可实例化

这个习惯用法也防止了类被子类化,这是一个副作用。所有子类构造函数都必须调用超类构造函数,无论是显式的还是隐式的,但这种情况下子类都没有可访问的超类构造函数可调用。

这章感觉没什么内容啊 ,介绍了私有化构造器的副作用,那就是

这个习惯用法也防止了类被子类化,这是一个副作用。所有子类构造函数都必须调用超类构造函数,无论是显式的还是隐式的,但这种情况下子类都没有可访问的超类构造函数可调用。

【05】依赖注入优于硬连接资源

Prefer dependency injection to hardwiring resources

许多类依赖于一个或多个底层资源。例如,拼写检查程序依赖于字典。常见做法是,将这种类实现为静态实用工具类

静态工具类通常通过单例实现,比如:拼写检查器

1
2
3
4
5
6
7
// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}

或者

1
2
3
4
5
6
7
8
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}

这两种方法都不令人满意,因为它们假设只使用一个字典。在实际应用中,每种语言都有自己的字典,特殊的字典用于特殊的词汇表。另外,最好使用一个特殊的字典进行测试。认为一本字典就足够了,是一厢情愿的想法。

你可以尝试让 SpellChecker 支持多个字典:首先取消 dictionary 字段的 final 修饰,并在现有的拼写检查器中添加更改 dictionary 的方法。但是在并发环境中这种做法是笨拙的、容易出错的和不可行的。静态实用工具类和单例不适用于由底层资源参数化的类。

所需要的是支持类的多个实例的能力(在我们的示例中是 SpellChecker),每个实例都使用客户端需要的资源(在我们的示例中是 dictionary)。满足此要求的一个简单模式是在创建新实例时将资源传递给构造函数。 这是依赖注入的一种形式:字典是拼写检查器的依赖项,在创建它时被注入到拼写检查器中。

什么是依赖注入模式

依赖注入就是new好了依赖的对象注入进去,而不是在类中显式的new一个依赖的对象。

依赖注入的中心思想

高层模块不应依赖于低层模块,他们都应该依赖于抽象
抽象不依赖细节,细节依赖抽象

依赖注入的分类:

  1. 构造器注入
  2. 属性注入
  3. 方法注入

举个栗子

举例一个游戏,丈夫可以亲自己的妻子

undefined

1.1 经理说要改需求了:更改需求:男的也可以亲男的(上边是不用设计模式,下边是用设计模式)

undefined

undefined

1.2 经理又说游戏很火,但是需求不够丰富,还要改:更改需求:男的也可以亲自己的伴侣(包括猫和狗;上边是不用设计模式,下边是用设计模式)

undefined

undefined

undefined

这个例子可以很清楚的看出,如果需求不断的更改且一个类依赖多个类且依赖他们的抽象类,这样会导致测试很难而且代码很难维护。当使用了依赖注入设计模式后,会极大的降低耦合度,方便测试。但是,在实际应用中,我们通常需要实现一个容器去管理和实现依赖对象的注入,比如spring,xml等方式。

此即为依赖注入模式。

【06】避免创建不必要的对象

复用单个对象通常是合适的,不必每次需要时都创建一个新的功能等效对象。复用可以更快、更流行。如果对象是不可变的,那么它总是可以被复用的。

有些对象的创建的代价相比而言要昂贵得多。如果你需要重复地使用这样一个「昂贵的对象」,那么最好将其缓存以供复用。不幸的是,当你创建这样一个对象时,这一点并不总是很明显。假设你要编写一个方法来确定字符串是否为有效的罗马数字。下面是使用正则表达式最简单的方法:

1
2
3
4
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

这个实现的问题是它依赖于 String.matches 方法。虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能关键的情况下重复使用。 问题在于,它在内部为正则表达式创建了一个 Pattern 实例,并且只使用一次,之后就进行垃圾收集了。创建一个 Pattern 实例是很昂贵的,因为它需要将正则表达式编译成有限的状态机制。

为了提高性能,将正则表达式显式编译为 Pattern 实例(它是不可变的),作为类初始化的一部分,缓存它,并在每次调用 isRomanNumeral 方法时复用同一个实例:

1
2
3
4
5
6
7
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}

如果频繁调用 isRomanNumeral,改进版本将提供显著的性能提升。

如果加载包含改进版 isRomanNumeral 方法的类时,该方法从未被调用过,那么初始化字段 ROMAN 是不必要的。因此,可以用延迟初始化字段(Item-83)的方式在第一次调用 isRomanNumeral 方法时才初始化字段,而不是在类加载时初始化,但不建议这样做。通常情况下,延迟初始化会使实现复杂化,而没有明显的性能改善(Item-67)。

【07】排除过时的对象引用

众所周知,Java有垃圾回收机制,但是垃圾回收机制并不是没有缺点的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.Arrays;
import java.util.EmptyStackException;

// Can you spot the "memory leak"?
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();
return elements[--size];
}

/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

这个程序没有明显的错误。你可以对它进行详尽的测试,它会以优异的成绩通过所有的测试,但是有一个潜在的问题。简单地说,该程序有一个「内存泄漏」问题,由于垃圾收集器活动的增加或内存占用的增加,它可以悄无声息地表现为性能的降低。在极端情况下,这种内存泄漏可能导致磁盘分页,甚至出现 OutOfMemoryError 程序故障,但这种故障相对少见。

那么内存泄漏在哪里呢?如果堆栈增长然后收缩,那么从堆栈中弹出的对象将不会被垃圾收集,即使使用堆栈的程序不再引用它们。这是因为栈保留了这些对象的旧引用。一个过时的引用,是指永远不会被取消的引用。在本例中,元素数组的「活动部分」之外的任何引用都已过时。活动部分由索引小于大小的元素组成。

解决这类问题的方法很简单:一旦引用过时,就将置空。在我们的 Stack 类中,对某个项的引用一旦从堆栈中弹出就会过时。pop 方法的正确版本如下:

1
2
3
4
5
6
7
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

用 null 处理过时引用的另一个好处是,如果它们随后被错误地关联引用,程序将立即失败,出现 NullPointerException,而不是悄悄地做错误的事情。尽可能快地检测编程错误总是有益的。

那么,什么时候应该取消引用呢?Stack 类的哪些方面容易导致内存泄漏?简单地说,它管理自己的内存。存储池包含元素数组的元素(对象引用单元,而不是对象本身)数组的活动部分(如前面所定义的)中的元素被分配,而数组其余部分中的元素是空闲的。垃圾收集器没有办法知道这一点;对于垃圾收集器,元素数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。只要数组元素成为非活动部分的一部分,程序员就可以通过手动清空数组元素,有效地将这个事实传递给垃圾收集器。

另一个常见的内存泄漏源是缓存。

内存泄漏的第三个常见来源是侦听器和其他回调。

如果你实现了一个 API,其中客户端注册回调,但不显式取消它们,除非你采取一些行动,否则它们将累积。

取消过期引用应该是例外而不是规范

程序员不应该在程序结束后立即清空所有对象引用,这是不必要的,也是不可取的,消除过期引用的最好办法是包含引用的变量超出范围,简单来说就是最小化局部变量作用域的最强大的技术是在首次使用的地方声明它,我们应该在尽可能窄的范围内定义每个变量。

【08】避免使用终结器和清除器

Avoid finalizers and cleaners

终结器是不可预测的,通常是危险的,也是不必要的。 它们的使用可能导致不稳定的行为、糟糕的性能和可移植性问题。终结器有一些有效的用途,我们将在后面的文章中介绍,但是作为规则,你应该避免使用它们。在 Java 9 中,终结器已经被弃用,但是 Java 库仍然在使用它们。Java 9 替代终结器的是清除器。清除器的危险比终结器小,但仍然不可预测、缓慢,而且通常是不必要的。

终结器和清除器的一个缺点是不能保证它们会被立即执行 [JLS, 12.6]。当对象变得不可访问,终结器或清除器对它进行操作的时间是不确定的。这意味着永远不应该在终结器或清除器中执行任何对时间要求很严格的操作。例如,依赖终结器或清除器关闭文件就是一个严重错误,因为打开的文件描述符是有限的资源。如果由于系统在运行终结器或清除器的延迟导致许多文件处于打开状态,程序可能会运行失败,因为它不能再打开其他文件。

那么,清除器和终结器有什么用呢?

它们可能有两种合法用途。一种是充当一个安全网,以防资源的所有者忽略调用它的 close 方法。虽然不能保证清除器或终结器将立即运行(或根本不运行),但如果客户端没有这样做,最好是延迟释放资源。如果你正在考虑编写这样一个安全网络终结器,那就好好考虑一下这种保护是否值得。一些 Java 库类,如 FileInputStream、FileOutputStream、ThreadPoolExecutor 和 java.sql.Connection,都有终结器作为安全网。

清洁器的第二个合法使用涉及到与本机对等体的对象。本机对等点是普通对象通过本机方法委托给的本机(非 java)对象。因为本机对等点不是一个正常的对象,垃圾收集器不知道它,并且不能在回收 Java 对等点时回收它。如果性能是可接受的,并且本机对等体不持有任何关键资源,那么更清洁或终结器可能是完成这项任务的合适工具。如果性能不可接受,或者本机对等体持有必须立即回收的资源,则类应该具有前面描述的关闭方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sun.misc.Cleaner;

// An autocloseable class using a cleaner as a safety net
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();

// Resource that requires cleaning. Must not refer to Room!
private static class State implements Runnable {
int numJunkPiles; // Number of junk piles in this room

State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}

// Invoked by close method or cleaner
@Override
public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}

// The state of this room, shared with our cleanable
private final State state;
// Our cleanable. Cleans the room when it’s eligible for gc
private final Cleaner.Cleanable cleanable;

public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}

@Override
public void close() {
cleanable.clean();
}
}

带资源的try语句(try-with-resource)的最简形式为:

1
2
3
4
try(Resource res = xxx)//可指定多个资源
{
work with res
}

try块退出时,会自动调用res.close()方法,关闭资源。

This well-behaved client demonstrates that behavior:

1
2
3
4
5
6
7
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("Goodbye");
}
}
}

In summary, don’t use cleaners, or in releases prior to Java 9, finalizers,except as a safety net or to terminate noncritical native resources. Even then,beware the indeterminacy and performance consequences.

【09】使用 try-with-resources 优于 try-finally

从历史上看,try-finally 语句是确保正确关闭资源的最佳方法,即使在出现异常或返回时也是如此:

1
2
3
4
5
6
7
8
9
// try-finally - No longer the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}

This may not look bad, but it gets worse when you add a second resource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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();
}
}

当 Java 7 引入 try-with-resources 语句 [JLS, 14.20.3]时,所有这些问题都一次性解决了。要使用这个结构,资源必须实现 AutoCloseable 接口,它由一个单独的 void-return close 方法组成。Java 库和第三方库中的许多类和接口现在都实现或扩展了 AutoCloseable。如果你编写的类存在必须关闭的资源,那么也应该实现 AutoCloseable。

下面是使用 try-with-resources 的第一个示例:

1
2
3
4
5
6
// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}

下面是使用 try-with-resources 的第二个示例:

1
2
3
4
5
6
7
8
9
// 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)
out.write(buf, 0, n);
}
}

和使用 try-finally 的原版代码相比,try-with-resources 为开发者提供了更好的诊断方式。考虑 firstLineOfFile 方法。如果异常是由 readLine 调用和不可见的 close 抛出的,则后一个异常将被抑制,以支持前一个异常。实际上,还可能会抑制多个异常,以保留实际希望看到的异常。这些被抑制的异常不会仅仅被抛弃;它们会被打印在堆栈跟踪中,并标记它们被抑制。可以通过编程方式使用 getSuppressed 方法访问到它们,该方法是在 Java 7 中添加到 Throwable 中的。

The lesson is clear:在使用必须关闭的资源时,总是优先使用 try-with-resources,而不是 try-finally。前者的代码更短、更清晰,生成的异常更有用。使用 try-with-resources 语句可以很容易地为必须关闭的资源编写正确的代码,而使用 try-finally 几乎是不可能的。

对象的通用方法

【10】覆盖 equals 方法时应遵守的约定

Obey the general contract when overriding equals

Equals本身的一些特点

  • Each instance of the class is inherently unique.
  • There is no need for the class to provide a “logical equality” test.
  • A superclass has already overridden equals, and the superclass behavior is appropriate for this class.
  • The class is private or package-private, and you are certain that its equals method will never be invoked. If you are extremely risk-averse,you can override the equals method to ensure that it isn’t invoked accidentally:
1
2
3
4
@Override
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
  • So when is it appropriate to override equals? It is when a class has a notion of logical equality that differs from mere object identity and a superclass has not already overridden equals.

The equals method implements an equivalence relation. It has these properties:

  • Reflexive: For any non-null reference value x, x.equals(x) must return true.

  • Symmetric: For any non-null reference values x and y, x.equals(y) must return true if and only if y.equals(x) returns true.

  • Transitive: For any non-null reference values x, y, z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true.

  • Consistent: For any non-null reference values x and y, multiple invocations of x.equals(y) must consistently return true or consistently return false, provided no information used in equals comparisons is modified.

  • For any non-null reference value x, x.equals(null) must return false.

译注:里氏替换原则(Liskov Substitution Principle,LSP)面向对象设计的基本原则之一。里氏替换原则指出:任何父类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当衍生类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而衍生类也能够在父类的基础上增加新的行为。

Java 库中有一些类确实继承了一个可实例化的类并添加了一个值组件。例如,java.sql.Timestamp 继承 java.util.Date 并添加了纳秒字段。如果在同一个集合中使用时间戳和日期对象,或者以其他方式混合使用时间戳和日期对象,那么时间戳的 equals 实现确实违反了对称性,并且可能导致不稳定的行为。Timestamp 类有一个免责声明,警告程序员不要混合使用日期和时间戳。虽然只要将它们分开,就不会遇到麻烦,但是没有什么可以阻止你将它们混合在一起,因此产生的错误可能很难调试。时间戳类的这种行为是错误的,不应该效仿。

高质量构建 equals 方法的总结:

  1. Use the == operator to check if the argument is a reference to this object. If so, return true. This is just a performance optimization but one that is worth doing if the comparison is potentially expensive.

使用 == 运算符检查参数是否是对该对象的引用。

  1. Use the instanceof operator to check if the argument has the correct type. If not, return false. Typically, the correct type is the class in which the method occurs. Occasionally, it is some interface implemented by this class. Use an interface if the class implements an interface that refines the equals contract to permit comparisons across classes that implement the interface. Collection interfaces such as Set, List, Map, and Map.Entry have this property.

使用 instanceof 运算符检查参数是否具有正确的类型。

  1. Cast the argument to the correct type. Because this cast was preceded by an instanceof test, it is guaranteed to succeed.

将参数转换为正确的类型。

  1. For each “significant” field in the class, check if that field of the argument matches the corresponding field of this object. If all these tests succeed, return true; otherwise, return false. If the type in Step 2 is an interface, you must access the argument’s fields via interface methods; if the type is a class, you may be able to access the fields directly, depending on their accessibility.

对于类中的每个「重要」字段,检查参数的字段是否与该对象的相应字段匹配。

  1. 当你覆盖 equals 时,也覆盖 hashCode。
  2. 不要自作聪明。 如果你只是为了判断相等性而测试字段,那么遵循 equals 约定并不困难。如果你在寻求对等方面过于激进,很容易陷入麻烦。一般来说,考虑到任何形式的混叠都不是一个好主意。例如,File 类不应该尝试将引用同一文件的符号链接等同起来。值得庆幸的是,它不是。
  3. 不要用另一种类型替换 equals 声明中的对象。 对于程序员来说,编写一个类似于这样的 equals 方法,然后花上几个小时思考为什么它不能正常工作是很常见的:
1
2
3
4
// Broken - parameter type must be Object!
public boolean equals(MyClass o) {
...
}

这里的问题是,这个方法没有覆盖其参数类型为 Object 的 Object.equals,而是重载了它(Item-52)。即使是普通的方法,提供这样一个「强类型的」equals 方法是不可接受的,因为它会导致子类中的重写注释产生误报并提供错误的安全性。

总之,除非必须,否则不要覆盖 equals 方法:在许多情况下,从 Object 继承而来的实现正是你想要的。如果你确实覆盖了 equals,那么一定要比较类的所有重要字段,并以保留 equals 约定的所有 5 项规定的方式进行比较

【11】当覆盖 equals 时,始终覆盖 hashCode

Always override hashCode when you override equals

在覆盖 equals 的类中,必须覆盖 hashCode。 如果你没有这样做,你的类将违反 hashCode 的一般约定,这将阻止该类在 HashMap 和 HashSet 等集合中正常运行。以下是根据目标规范修改的约定:

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

如果根据 equals(Object) 方法判断出两个对象不相等,则不需要在每个对象上调用 hashCode 时必须产生不同的结果。

一个好的 hash 函数倾向于为不相等的实例生成不相等的 hash 代码。这正是 hashCode 约定的第三部分的含义。理想情况下, hash 函数应该在所有 int 值之间均匀分布所有不相等实例的合理集合。实现这个理想是很困难的。幸运的是,实现一个类似的并不太难。这里有一个简单的方式:

1、Declare an int variable named result, and initialize it to the hash code c for the first significant field in your object, as computed in step 2.a. (Recall from Item 10 that a significant field is a field that affects equals comparisons.)

声明一个名为 result 的 int 变量,并将其初始化为对象中第一个重要字段的 hash 代码 c,如步骤 2.a 中计算的那样。(回想一下 Item-10 中的重要字段是影响相等比较的字段。)

2、For every remaining significant field f in your object, do the following:

对象中剩余的重要字段 f,执行以下操作:

a. Compute an int hash code c for the field:

为字段计算一个整数 hash 码 c:

i. If the field is of a primitive type, compute Type.hashCode(f),where Type is the boxed primitive class corresponding to f’s type.

如果字段是基本数据类型,计算 Type.hashCode(f),其中 type 是与 f 类型对应的包装类。

ii. If the field is an object reference and this class’s equals method compares the field by recursively(adv.递归地) invoking equals, recursively invoke hashCode on the field. If a more complex comparison is required,compute a “canonical representation” for this field and invoke hashCode on the canonical representation. If the value of the field is null, use 0 (or some other constant, but 0 is traditional).

如果字段是对象引用,并且该类的 equals 方法通过递归调用 equals 来比较字段,则递归调用字段上的 hashCode。如果需要更复杂的比较,则为该字段计算一个「规范表示」,并在规范表示上调用 hashCode。如果字段的值为空,则使用 0(或其他常数,但 0 是惯用的)。

iii. If the field is an array, treat it as if each significant element were a separate field. That is, compute a hash code for each significant element by applying these rules recursively, and combine the values per step 2.b. If the array has no significant elements, use a constant, preferably not 0. If all elements are significant, use Arrays.hashCode.

如果字段是一个数组,则将其视为每个重要元素都是一个单独的字段。也就是说,通过递归地应用这些规则计算每个重要元素的 hash 代码,并将每个步骤 2.b 的值组合起来。如果数组中没有重要元素,则使用常量,最好不是 0。如果所有元素都很重要,那么使用 Arrays.hashCode

b. Combine the hash code c computed in step 2.a into result as follows:

将步骤 2.a 中计算的 hash 代码 c 合并到结果,如下所示:

1
result = 31 * result + c;

3、Return result.

返回 result。

当你完成了 hashCode 方法的编写之后,问问自己相同的实例是否具有相同的 hash 代码。编写单元测试来验证你的直觉(除非你使用 AutoValue 生成你的 equals 和 hashCode 方法,在这种情况下你可以安全地省略这些测试)。如果相同的实例有不相等的 hash 码,找出原因并修复问题。

可以从 hash 代码计算中排除派生字段。换句话说,你可以忽略任何可以从计算中包含的字段计算其值的字段。你必须排除不用于对等比较的任何字段,否则你可能会违反 hashCode 约定的第二个条款。

….

省略了一大段如何写Hash的方法,看着头疼,先跳过

总之,每次覆盖 equals 时都必须覆盖 hashCode,否则程序将无法正确运行。你的 hashCode 方法必须遵守 Object 中指定的通用约定,并且必须合理地将不相等的 hash 代码分配给不相等的实例。这很容易实现,如果有点乏味,可使用第 51 页的方法。如 Item-10 所述,AutoValue 框架提供了一种很好的替代手动编写 equals 和 hashCode 的方法,IDE 也提供了这种功能。

【12】始终覆盖 toString 方法

Always override toString

指定 toString 返回值的格式的缺点是,一旦指定了它,就会终生使用它,假设你的类被广泛使用。程序员将编写代码来解析表示、生成表示并将其嵌入持久数据中。如果你在将来的版本中更改了表示形式,你将破坏它们的代码和数据,它们将发出大量的消息。通过选择不指定格式,你可以保留在后续版本中添加信息或改进格式的灵活性。

在静态实用程序类中编写 toString 方法是没有意义的(Item-4),在大多数 enum 类型中也不应该编写 toString 方法(Item-34),因为 Java 为你提供了一个非常好的方法。但是,你应该在任何抽象类中编写 toString 方法,该类的子类共享公共的字符串表示形式。例如,大多数集合实现上的 toString 方法都继承自抽象集合类。

【13】明智地覆盖 clone 方法

Override clone judiciously

This item tells you how to implement a well-behaved clone method, discusses when it is appropriate to do so, and presents alternatives.

本章将告诉你如何实现行为良好的 clone 方法,讨论什么时候应该这样做,并提供替代方法。

如果 Cloneable 不包含任何方法,它会做什么呢?

It throws CloneNotSupportedException

in practice, a class implementing Cloneable is expected to provide a properly(adv. 适当地;正确地;恰当地) functioning public clone method. In order to achieve(vt. 取得;获得;实现;) this, the class and all of its superclasses must obey a complex(adj. 复杂的;合成的), unenforceable, thinly documented protocol. The resulting mechanism is fragile, dangerous, and extralinguistic(adj. 语言以外的;语言学以外的): it creates objects without calling a constructor.

虽然规范没有说明,但是在实践中,一个实现 Cloneable 的类应该提供一个功能正常的公共 clone 方法。为了实现这一点,类及其所有超类必须遵守复杂的、不可强制执行的、文档很少的协议。产生的机制是脆弱的、危险的和非语言的:即它创建对象而不调用构造函数。

clone 方法的一般约定很薄弱。这里是从 Object 规范复制过来的:

创建并返回此对象的副本。「复制」的确切含义可能取决于对象的类别。一般的目的是,对于任何对象 x,表达式x.clone() != x值将为 true,并且这个表达式. x.clone().getClass() == x.getClass()值将为 true,但这些不是绝对的必要条件。通常情况下值将为 true,但这些不是绝对的必要条件。

在数组上调用 clone 将返回一个数组,该数组的运行时和编译时类型与被克隆的数组相同。这是复制数组的首选习惯用法。实际上,数组是 clone 工具唯一引人注目的用途。

更多的内容,我会在使用clone()的时候拓展。

【14】考虑实现 Comparable 接口

Consider implementing Comparable

compareTo 方法不是在 Object 中声明的。相反,它是 Comparable 接口中的唯一方法。它在性质上类似于 Object 的 equals 方法,除了简单的相等比较之外,它还允许顺序比较,而且它是通用的。一个类实现 Comparable,表明实例具有自然顺序。对实现 Comparable 的对象数组进行排序非常简单:

1
Arrays.sort(a);

通过让类实现 Comparable,就可与依赖于此接口的所有通用算法和集合实现进行互操作。你只需付出一点点努力就能获得强大的功能。实际上,Java 库中的所有值类以及所有枚举类型(Item-34)都实现了 Comparable。如果编写的值类具有明显的自然顺序,如字母顺序、数字顺序或时间顺序,则应实现 Comparable 接口:

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

compareTo 方法的一般约定类似于 equals 方法:

将一个对象与指定的对象进行顺序比较。当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数。如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException。

在下面的描述中,sgn(expression) 表示数学中的符号函数,它被定义为:根据传入表达式的值是负数、零或正数,对应返回 -1、0 或 1。

第三章看得很粗糙,主要目前用不到这些,先留个印象,如果有需要的话,再返回来翻一翻再补充。

第四章 类和接口

【15】尽量减少类和成员的可访问性

Minimize the accessibility of classes and members

For members (fields, methods, nested classes, and nested interfaces), there are four possible access levels, listed here in order of increasing accessibility:

对于成员(字段、方法、嵌套类和嵌套接口),有四个可能的访问级别,这里列出了增加可访问性的顺序:

  • private —The member is accessible only from the top-level class where it is declared.

私有,成员只能从声明它的顶级类中访问。

  • package-private —The member is accessible from any class in the package where it is declared. Technically known as default access, this is the access level you get if no access modifier is specified (except for interface members,which are public by default).

包级私有,成员可以从包中声明它的任何类访问。技术上称为默认访问,如果没有指定访问修饰符(接口成员除外,默认情况下,接口成员是公共的),就会得到这个访问级别。

  • protected —The member is accessible from subclasses of the class where it is declared (subject to a few restrictions [JLS, 6.6.2]) and from any class in the package where it is declared.

保护,成员可以从声明它的类的子类(受一些限制 [JLS, 6.6.2])和包中声明它的任何类访问。

  • public —The member is accessible from anywhere.

公共,该成员可以从任何地方访问。

Note that a nonzero-length array is always mutable, so it is wrong for a class to have a public static final array field, or an accessor that returns such a field. If a class has such a field or accessor, clients will be able to modify the contents of the array. This is a frequent source of security holes:

1
2
// Potential security hole!
public static final Thing[] VALUES = { ... };

要注意的是,一些 IDE 生成了返回私有数组字段引用的访问器,这恰恰导致了这个问题。有两种方法可以解决这个问题。你可以将公共数组设置为私有,并添加一个公共不可变列表:

1
2
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

Beware of the fact that some IDEs generate accessors that return references to private array fields, resulting in exactly this problem.

一些 IDE 生成了返回私有数组字段引用的访问器,这恰恰导致了这个问题。有两种方法可以解决这个问题。

You can make the public array private and add a public immutable list:

1
2
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

Alternatively, you can make the array private and add a public method that returns a copy of a private array:

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

【16】在公共类中,使用访问器方法,而不是公共字段

Java 库中的几个类违反了公共类不应该直接公开字段的建议。突出的例子包括 java.awt 包中的 Point 和 Dimension。这些类不应被效仿,而应被视为警示。正如 Item-67 所述,公开 Dimension 类的内部结构导致了严重的性能问题,这种问题至今仍存在。

While it’s never a good idea for a public class to expose fields directly, it is less harmful if the fields are immutable.

总之,公共类不应该公开可变字段。对于公共类来说,公开不可变字段的危害要小一些,但仍然存在潜在的问题。然而,有时候包私有或私有嵌套类需要公开字段,无论这个类是可变的还是不可变的。

【17】减少可变性

不可变类就是一个实例不能被修改的类。每个实例中包含的所有信息在对象的生命周期内都是固定的,因此永远不会观察到任何更改。Java 库包含许多不可变的类,包括 String、基本数据类型的包装类、BigInteger 和 BigDecimal。有很多很好的理由:不可变类比可变类更容易设计、实现和使用。它们不太容易出错,而且更安全。

【18】优先选择复合而不是继承

Favor composition over inheritance

composition 设计模式

继承的缺点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}

This class looks reasonable, but it doesn’t work.

1
2
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));

​ 我们希望 getAddCount 方法此时返回 3,但它返回 6。

原因:在内部,HashSet 的 addAll 方法是在其 add 方法之上实现的,尽管 HashSet 相当合理地没有记录这个实现细节。InstrumentedHashSet 中的 addAll 方法向 addCount 添加了三个元素,然后使用 super.addAll 调用 HashSet 的 addAll 实现。这反过来调用 add 方法(在 InstrumentedHashSet 中被重写过),每个元素一次。这三个调用中的每一个都向 addCount 添加了一个元素,总共增加了 6 个元素:使用 addAll 方法添加的每个元素都被重复计数。

幸运的是,有一种方法可以避免上述所有问题。与其扩展现有类,不如为新类提供一个引用现有类实例的私有字段。This design is called composition.因为现有的类成为新类的一个组件。新类中的每个实例方法调用现有类的包含实例上的对应方法,并返回结果。这称为转发,新类中的方法称为转发方法。生成的类将非常坚固,不依赖于现有类的实现细节。即使向现有类添加新方法,也不会对新类产生影响。为了使其具体化,这里有一个使用复合和转发方法的 InstrumentedHashSet 的替代方法。注意,实现被分成两部分,类本身和一个可重用的转发类,其中包含所有的转发方法,没有其他内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> 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<E> s;
public ForwardingSet(Set<E> 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<E> 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<? extends E> 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> 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(); }
}

InstrumentedSet 类的设计是通过 Set 接口来实现的,这个接口可以捕获 HashSet 类的功能。除了健壮外,这个设计非常灵活。InstrumentedSet 类实现了 Set 接口,有一个构造函数,它的参数也是 Set 类型的。实际上,这个类可以将一个 Set 转换成另一个 Set,添加了 instrumentation 的功能。基于继承的方法只适用于单个具体类,并且需要为超类中每个受支持的构造函数提供单独的构造函数,与此不同的是,包装器类可用于仪器任何集合实现,并将与任何现有构造函数一起工作:

1
2
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

​ InstrumentedSet 类甚至还可以用来临时配置一个不用插装就可以使用的 set 实例:

1
2
3
4
static void walk(Set<Dog> dogs) {
InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
... // Within this method use iDogs instead of dogs
}

InstrumentedSet 类被称为包装类,因为每个 entedset 实例都包含(「包装」)另一个集合实例。这也称为 Decorator 模式[Gamma95],因为 InstrumentedSet 类通过添加插装来「修饰」一个集合。有时组合和转发的组合被松散地称为委托。严格来说,除非包装器对象将自身传递给包装对象,否则它不是委托[Lieberman86; Gamma95]。

包装类的缺点很少。一个警告是包装类不适合在回调框架中使用,在回调框架中,对象为后续调用(「回调」)将自定义传递给其他对象。因为包装对象不知道它的包装器,所以它传递一个对它自己的引用(this),回调避开包装器。这就是所谓的自我问题。有些人担心转发方法调用的性能影响或包装器对象的内存占用影响。这两种方法在实践中都没有多大影响。编写转发方法很麻烦,但是你必须只为每个接口编写一次可重用的转发类,而且可能会为你提供转发类。例如,Guava 为所有的集合接口提供了转发类[Guava]。

只有在子类确实是超类的子类型的情况下,继承才合适。换句话说,只有当两个类之间存在「is-a」关系时,类 B 才应该扩展类 a。如果你想让 B 类扩展 a 类,那就问问自己:每个 B 都是 a 吗?如果你不能如实回答是的这个问题,B 不应该延长 a,如果答案是否定的,通常情况下,B 应该包含一个私人的实例,让不同的 API:不是 B 的一个重要组成部分,只是一个细节的实现。

在 Java 库中有许多明显违反这一原则的地方。例如,堆栈不是向量,因此堆栈不应该扩展向量。类似地,属性列表不是 hash 表,因此属性不应该扩展 hash 表。在这两种情况下,复合都是可取的。

总而言之,继承是强大的,但是它是有问题的,因为它违反了封装。只有当子类和超类之间存在真正的子类型关系时才合适。即使这样,如果子类与超类不在一个不同的包中,并且超类不是为继承而设计的,继承也可能导致脆弱性。为了避免这种脆弱性,使用组合和转发而不是继承,特别是如果存在实现包装器类的适当接口的话。包装类不仅比子类更健壮,而且更强大。

【19】继承要设计良好并且具有文档,否则禁止使用

Design and document for inheritance or else prohibit it

Item-18 提醒你注意子类化不是为继承设计和文档化的「外部」类的危险。那么,为继承而设计和文档化的类意味着什么呢?

【20】接口优于抽象类

Prefer interfaces to abstract classes

Java 有两种机制来定义允许多种实现的类型:接口和抽象类。由于 Java 8 [JLS 9.4.3]中引入了接口的默认方法,这两种机制都允许你为一些实例方法提供实现。一个主要区别是,一个类要实现抽象类定义的类型,该类必须是抽象类的子类。因为 Java 只允许单一继承,所以这种对抽象类的限制严重制约了它们作为类型定义的使用。任何定义了所有必需的方法并遵守通用约定的类都允许实现接口,而不管该类驻留在类层次结构中何处。

译注:第一段可拆分出有关抽象类和接口的描述

1、抽象类的局限:一个类要实现抽象类定义的类型,该类必须是抽象类的子类。因为 Java 只允许单一继承,所以这种对抽象类的限制严重制约了它们作为类型定义的使用。

2、接口的优点:任何定义了所有必需的方法并遵守通用约定的类都允许实现接口,而不管该类驻留在类层次结构中何处。

可以很容易地对现有类进行改造,以实现新的接口。 你所要做的就是添加所需的方法(如果它们还不存在的话),并向类声明中添加一个 implements 子句。例如,许多现有的类在添加到平台时进行了修改,以实现 Comparable、Iterable 和 Autocloseable 接口。一般来说,现有的类不能被修改以扩展新的抽象类。如果你想让两个类扩展同一个抽象类,你必须把它放在类型层次结构的高层,作为两个类的祖先。不幸的是,这可能会对类型层次结构造成巨大的附带损害,迫使新抽象类的所有后代对其进行子类化,无论它是否合适。

总之,接口通常是定义允许多种实现的类型的最佳方法。如果导出了一个重要的接口,则应该强烈考虑提供一个骨架实现。尽可能地,你应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。也就是说,对接口的限制通常要求框架实现采用抽象类的形式。

【21】为后代设计接口

Design interfaces for posterity

在 Java 8 之前,在不破坏现有实现的情况下向接口添加方法是不可能的。如果在接口中添加新方法,现有的实现通常会缺少该方法,从而导致编译时错误。在 Java 8 中,添加了默认的方法构造 [JLS 9.4],目的是允许向现有接口添加方法。但是向现有接口添加新方法充满了风险。

除非必要,否则应该避免使用默认方法向现有接口添加新方法,在这种情况下,你应该仔细考虑现有接口实现是否可能被默认方法破坏。然而,在创建接口时,默认方法对于提供标准方法实现非常有用,以减轻实现接口的任务(Item-20)。

【22】接口只用于定义类型

Use interfaces only to define types

当一个类实现了一个接口时,这个接口作为一种类型,可以用来引用类的实例。因此,实现接口的类应该说明客户端可以对类的实例做什么。为任何其他目的定义接口都是不合适的。

Java 库中有几个常量接口,例如 java.io.ObjectStreamConstants。这些接口应该被视为反例,不应该被效仿。

如果你想导出常量,有几个合理的选择。如果这些常量与现有的类或接口紧密绑定,则应该将它们添加到类或接口。例如,所有装箱的数值包装类,比如 Integer 和 Double,都导出 MIN_VALUE 和 MAX_VALUE 常量。如果最好将这些常量看作枚举类型的成员,那么应该使用 enum 类型导出它们(Item-34)。否则,你应该使用不可实例化的工具类(Item-4)导出常量。下面是一个之前的 PhysicalConstants 例子的工具类另一个版本:

1
2
3
4
5
6
7
8
9
// Constant utility class
package com.effectivejava.science;

public class PhysicalConstants {
private PhysicalConstants() { } // Prevents instantiation(将构造私有,阻止实例化)
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
public static final double BOLTZMANN_CONST = 1.380_648_52e-23;
public static final double ELECTRON_MASS = 9.109_383_56e-31;
}

顺便说一下,请注意在数字字面值中使用了下划线( _ )。下划线自 Java 7 以来一直是合法的,它对数字字面值没有影响,如果谨慎使用,可以使它们更容易阅读。考虑添加下划线到数字字面值,无论是固定的浮点数,如果它们包含五个或多个连续数字。对于以 10 为基数的字面值,无论是整数还是浮点数,都应该使用下划线将字面值分隔为三位数,表示 1000 的正幂和负幂。

总之,接口应该只用于定义类型。它们不应该用于导出常量。

【23】类层次结构优于带标签的类

Occasionally you may run across a class whose instances come in two or more flavors and contain a tag field indicating the flavor of the instance. For example, consider this class, which is capable of representing a circle or a rectangle:

有时候,你可能会遇到这样一个类,它的实例有两种或两种以上的样式,并且包含一个标签字段,指示实例的样式。例如,考虑这个类,它能够表示一个圆或一个矩形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape {RECTANGLE, CIRCLE};

// Tag field - the shape of this figure
final Shape shape;

// These fields are used only if shape is RECTANGLE
double length;

double width;

// This field is used only if shape is CIRCLE
double radius;

// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}

// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}

double area() {
switch (shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}

简而言之,标签类冗长、容易出错和低效。

面向对象的语言(如 Java)提供了一个更好的选择来定义能够表示多种类型对象的单一数据类型:子类型。标签的类只是类层次结构的(简单)的模仿。

接下来,为原始标签类的每个类型定义根类的具体子类。在我们的例子中,有两个:圆形和矩形。在每个子类中包含特定于其风格的数据字段。在我们的例子中,半径是特定于圆的,长度和宽度是特定于矩形的。还应在每个子类中包含根类中每个抽象方法的适当实现。下面是原 Figure 类对应的类层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Class hierarchy replacement for a tagged class
abstract class Figure {
abstract double area();
}

class Circle extends Figure {
final double radius;

Circle(double radius) {
this.radius = radius;
}

@Override
double area() {
return Math.PI * (radius * radius);
}
}

class Rectangle extends Figure {
final double length;
final double width;

Rectangle(double length, double width) {
this.length = length;
this.width = width;
}

@Override
double area() {
return length * width;
}
}

类层次结构的另一个优点是,可以使它们反映类型之间的自然层次关系,从而提高灵活性和更好的编译时类型检查。假设原始示例中的标签类也允许使用正方形。类层次结构可以反映这样一个事实,即正方形是一种特殊的矩形(假设两者都是不可变的):

1
2
3
4
5
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}

【24】支持使用静态成员类而不是非静态类

Favor static member classes over nonstatic

嵌套类是在另一个类中定义的类。嵌套类应该只为外部类服务。如果嵌套类在其他环境中有用,那么它应该是顶级类。有四种嵌套类:静态成员类、非静态成员类、匿名类和局部类。除了第一种,所有的类都被称为内部类。

静态成员类是最简单的嵌套类。最好把它看做是一个普通的类,只是碰巧在另一个类中声明而已,并且可以访问外部类的所有成员,甚至那些声明为 private 的成员。静态成员类是其外部类的静态成员,并且遵守与其他静态成员相同的可访问性规则。如果声明为私有,则只能在外部类中访问,等等。

静态成员类的一个常见用法是作为公有的辅助类,只有与它的外部类一起使用时才有意义。

从语法上讲,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有修饰符 static。尽管语法相似,但这两种嵌套类有很大不同。非静态成员类的每个实例都隐式地与外部类的外部实例相关联。在非静态成员类的实例方法中,你可以调用外部实例上的方法,或者使用受限制的 this 构造获得对外部实例的引用 [JLS, 15.8.4]。如果嵌套类的实例可以独立于外部类的实例存在,那么嵌套类必须是静态成员类:如果没有外部实例,就不可能创建非静态成员类的实例。非静态成员类的一个常见用法是定义一个适配器 [Gamma95],它允许外部类的实例被视为某个不相关类的实例。例如,Map 接口的实现通常使用非静态成员类来实现它们的集合视图,这些视图由 Map 的 keySet、entrySet 和 values 方法返回。类似地,集合接口的实现,例如 Set 和 List,通常使用非静态成员类来实现它们的迭代器.

匿名类的另一个常见用法是实现静态工厂方法

局部类是四种嵌套类中最不常用的。局部类几乎可以在任何能够声明局部变量的地方使用,并且遵守相同的作用域规则。局部类具有与其他嵌套类相同的属性。与成员类一样,它们有名称,可以重复使用。与匿名类一样,它们只有在非静态环境中定义的情况下才具有外部类实例,而且它们不能包含静态成员。和匿名类一样,它们应该保持简短,以免损害可读性。

简单回顾一下,有四种不同类型的嵌套类,每一种都有自己的用途。

如果嵌套的类需要在单个方法之外可见或者太长不适合放入方法中则使用成员类如果成员类的每个实例都需要引用其外部类实例则使其非静态;否则,让它保持静态。假设嵌套类属于方法内部,如果你只需要从一个位置创建实例,并且存在一个能够描述类的现有类型,那么将其设置为匿名类;否则,将其设置为局部类。

【25】源文件仅限有单个顶层类

Limit source files to a single top-level class

教训很清楚:永远不要将多个顶层类或接口放在一个源文件中。遵循此规则可以确保在编译时单个类不能拥有多个定义。这反过来保证了编译所生成的类文件,以及程序的行为,是独立于源代码文件传递给编译器的顺序的。

第五章 泛型

【26】不要使用原始类型

Don’t use raw types

比如说List<E>

使用原始类型(没有类型参数的泛型)是合法的,但是你永远不应该这样做。如果使用原始类型,就会失去泛型的安全性和表现力。 既然你不应该使用它们,那么为什么语言设计者一开始就允许原始类型呢?答案是:为了兼容性。Java 即将进入第二个十年,泛型被添加进来时,还存在大量不使用泛型的代码。

虽然你不应该使用原始类型(如 List),但是可以使用参数化的类型来允许插入任意对象,如 List<Object>。

【27】消除 unchecked 警告

Eliminate unchecked warnings

当你使用泛型编程时,你将看到许多编译器警告:unchecked 强制转换警告、unchecked 方法调用警告、unchecked 可变参数类型警告和 unchecked 自动转换警告。使用泛型获得的经验越多,得到的警告就越少,但是不要期望新编写的代码能够完全正确地编译。

每个 unchecked 警告都代表了在运行时发生 ClassCastException 的可能性。

【28】list 优于数组

Prefer lists to arrays

使用数组的时候需要注意的一些问题

首先,数组是协变的(covariant)。如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。

对于任何两个不同类型 Type1 和 Type2,List<Type1> 既不是 List<Type2> 的子类型

你可能认为这意味着泛型是有缺陷的,但可以说数组才是有缺陷的。

1
2
3
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

使用Array会报运行时错误。

1
2
3
// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

使用List会报错编译错误,直接在编译的时候就提醒出来。

Java历史遗留问题:数组和泛型之间的第二个主要区别:数组是具体化的 [JLS, 4.7]。这意味着数组在运行时知道并强制执行他们的元素类型。如前所述,如果试图将 String 元素放入一个 Long 类型的数组中,就会得到 ArrayStoreException。相比之下,泛型是通过擦除来实现的 [JLS, 4.6]。这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。擦除允许泛型与不使用泛型的遗留代码自由交互操作(Item-26),确保在 Java 5 中平稳地过渡。

【29】优先使用泛型

Favor generic types

自己编写泛型的时候,会比使用Java提供的麻烦一点。

【30】优先使用泛型方法

Favor generic methods

类可以是泛型的,方法也可以是泛型的。操作参数化类型的静态实用程序方法通常是泛型的。Collections 类中的所有「算法」方法(如 binarySearch 和 sort)都是泛型的。

【31】使用有界通配符增加 API 的灵活性

Use bounded wildcards to increase API flexibility

如果参数化类型表示 T 生成器,则使用 <? extends T>;如果它表示一个 T 消费者,则使用 <? super T>。在我们的 Stack 示例中,pushAll 的 src 参数生成 E 的实例供 Stack 使用,因此 src 的适当类型是 Iterable<? extends E>;popAll 的 dst 参数使用 Stack 中的 E 实例,因此适合 dst 的类型是 Collection<? super E>。PECS 助记符捕获了指导通配符类型使用的基本原则。Naftalin 和 Wadler 称之为 Get and Put 原则[Naftalin07, 2.4]。

【32】明智地合用泛型和可变参数

Combine generics and varargs judiciously

可变参数方法(Item-53)和泛型都是在 Java 5 中添加的,因此你可能认为它们能够优雅地交互;可悲的是,他们并不能。可变参数的目的是允许客户端向方法传递可变数量的参数,但这是一个漏洞百出的抽象概念:当你调用可变参数方法时,将创建一个数组来保存参数;该数组的实现细节应该是可见的。因此,当可变参数具有泛型或参数化类型时,会出现令人困惑的编译器警告。

【33】考虑类型安全的异构容器

Consider typesafe heterogeneous containers

1
2
3
4
5
6
7
8
9
10
11
12
// Typesafe heterogeneous container pattern - implementation
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));
}
}

第 6 章 枚举和注解

【34】用枚举类型代替 int 常量

Use enums instead of int constants

【35】使用实例字段替代序数

Use instance fields instead of ordinals

【36】用 EnumSet 替代位字段

Use EnumSet instead of bit fields

【37】使用 EnumMap 替换序数索引

Use EnumMap instead of ordinal indexing

【38】使用接口模拟可扩展枚举

Emulate extensible enums with interfaces

【39】注解优于命名模式

Prefer annotations to naming patterns

【40】坚持使用 @Override 注解

Consistently use the Override annotation

【41】使用标记接口定义类型

Use marker interfaces to define types

第 7 章 λ 表达式和流

【42】λ 表达式优于匿名类

Prefer lambdas to anonymous classes

【43】方法引用优于 λ 表达式

Prefer method references to lambdas

【44】优先使用标准函数式接口

Favor the use of standard functional interfaces

【45】明智地使用流

Use streams judiciously

【46】在流中使用无副作用的函数

Prefer side-effect-free functions in streams

【47】优先选择 Collection 而不是流作为返回类型

Prefer Collection to Stream as a return type

【48】谨慎使用并行流

Use caution when making streams parallel

第 8 章 方法

【49】检查参数的有效性

Check parameters for validity

【50】在需要时制作防御性副本

Make defensive copies when needed

【51】仔细设计方法签名

Design method signatures carefully

【52】明智地使用重载

Use overloading judiciously

【53】明智地使用可变参数

Use varargs judiciously

【54】返回空集合或数组,而不是 null

Return empty collections or arrays, not nulls

【55】明智地的返回 Optional

Return optionals judiciously

【56】为所有公开的 API 元素编写文档注释

Write doc comments for all exposed API elements

第 9 章 通用程序设计

【57】将局部变量的作用域最小化

Minimize the scope of local variables

【58】for-each 循环优于传统的 for 循环

Prefer for-each loops to traditional for loops

【59】了解并使用库

Know and use the libraries

【60】若需要精确答案就应避免使用 float 和 double 类型

Avoid float and double if exact answers are required

【61】基本数据类型优于包装类

Prefer primitive types to boxed primitives

【62】其他类型更合适时应避免使用字符串

Avoid strings where other types are more appropriate

【63】当心字符串连接引起的性能问题

Beware the performance of string concatenation

【64】通过接口引用对象

Refer to objects by their interfaces

【65】接口优于反射

Prefer interfaces to reflection

【66】明智地使用本地方法

Use native methods judiciously

【67】明智地进行优化

Optimize judiciously

【68】遵守被广泛认可的命名约定

Adhere to generally accepted naming conventions

第 10 章 异常

【69】仅在确有异常条件下使用异常

Use exceptions only for exceptional conditions

【70】对可恢复情况使用 checked 异常,对编程错误使用运行时异常

Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

【71】避免不必要地使用 checked 异常

Avoid unnecessary use of checked exceptions

【72】鼓励复用标准异常

Favor the use of standard exceptions

【73】抛出能用抽象解释的异常

Throw exceptions appropriate to the abstraction

【74】为每个方法记录会抛出的所有异常

Document all exceptions thrown by each method

【75】异常详细消息中应包含捕获失败的信息

Include failure capture information in detail messages

【76】尽力保证故障原子性

Strive for failure atomicity

【77】不要忽略异常

Don’t ignore exceptions

第 11 章 并发

【78】对共享可变数据的同步访问

Synchronize access to shared mutable data

【79】避免过度同步

Avoid excessive synchronization

【80】Executor、task、流优于直接使用线程

Prefer executors, tasks, and streams to threads

【81】并发实用工具优于 wait 和 notify

Prefer concurrency utilities to wait and notify

【82】文档应包含线程安全属性

Document thread safety

【83】明智地使用延迟初始化

Use lazy initialization judiciously

【84】不要依赖线程调度器

Don’t depend on the thread scheduler

第 12 章 序列化

【85】Java 序列化的替代方案

Prefer alternatives to Java serialization

【86】非常谨慎地实现 Serializable

Implement Serializable with great caution

【87】考虑使用自定义序列化形式

Consider using a custom serialized form

【88】防御性地编写 readObject 方法

Write readObject methods defensively

【89】对于实例控制,枚举类型优于 readResolve

For instance control, prefer enum types to readResolve

【90】考虑以序列化代理代替序列化实例

Consider serialization proxies instead of serialized instances