Skip to main content

Effective Java

第一章 引言

第二章 创建和销毁对象

本章涉及创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们被及时销毁,以及如何管理在销毁之前必须执行的清理操作。

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

  一个类允许客户端获取其实例的传统方式,是提供一个公共构造方法。 其实,还有另一种技术应该成为每个程序员工具箱的一部分。 一个类可以提供一个简单的、只返回该类实例的公共静态工厂方法。 下面是一...

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

  静态工厂和构造方法都有一个限制:它们在可选参数很多的情景下,无法很好得扩展。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里...

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

  单例是一个仅实例化一次的类[Gamma95]。单例对象通常表示无状态对象,如函数 (详见第 24 条) 或一个本质上唯一的系统组件。让一个类成为单例会使测试它的客户变得困难,因为除非实现一个...

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

  偶尔你会想写一个只包含静态方法和静态字段的类。 这些类的名声非常不好,因为有些人滥用这些类从而避免以面向对象方式思考从而编写过程化的程序,但是它们确实有着特殊的用途。 它们可以用来按照 ja...

5. 依赖注入优于硬连接资源(hardwiring resources)

  许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态工具类并不少见 (详见第 4 条): // Inappropriate use of static utilit...

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

  在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的(详见第 17 条),它总是可以被重用。   作为一个不应该这样做的极端例子,请考虑...

7. 消除过期的对象引用

  如果你从使用手动内存管理的语言(如 C 或 C++)切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次...

8. 避免使用 Finalizer 和 Cleaner 机制

  Finalizer 机制是不可预知的,往往是危险的,而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer 机制有一些特殊的用途,我们稍后会在这个条...

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

  Java 类库中包含许多必须通过调用 close 方法手动关闭的资源。 比如 InputStream,OutputStream 和 java.sql.Connection。 客户经常忽视关闭...

第三章 对象的通用方法

虽然 Object 是一个具体的类,但它主要是为扩展而设计的。它的所有非 final 方法(equals、hashCode、toString、clone 和 finalize)都有显式的通用约定...

第四章 类和接口

类和接口是 Java 编程语言的核心。它们是抽象的基本单位。该语言提供了许多强大的元素,你可以使用它们来设计类和接口。本章包含了帮助你充分利用这些元素的指导原则,以便让你的类和接口是可用的、健壮...

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

  将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通...

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

  有时候,你可能会试图写一些退化的类(degenerate classes),除了集中实例属性之外别无用处: // Degenerate classes like this should no...

17. 最小化可变性

  不可变类简单来说是其实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。 Java 平台类库包含许多不可变的类,包括 String 类、基本类...

18. 组合优于继承

  继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档...

19. 要么设计继承并提供文档说明,要么禁用继承

  条目 18 中提醒你注意继承没有设计和文档说明的「外来」类的子类化的危险。 那么对于专门为了继承而设计并且具有良好文档说明的类而言,这又意味着什么呢?   首先,这个类必须准确地描述重写每个...

20. 接口优于抽象类

  Java 有两种机制来定义允许多个实现的类型:接口和抽象类。 由于在 Java 8 [JLS 9.4.3] 中引入了接口的默认方法(default methods ),因此这两种机制都允许为...

21. 为后代设计接口

  在 Java 8 之前,不可能在不破坏现有实现的情况下为接口添加方法。 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误。 在 Java 8 中,添加了默认方法(d...

22. 接口仅用来定义类型

  当类实现接口时,该接口作为一种类型(type),可以用来引用类的实例。因此,一个类实现了一个接口,因此表明客户端可以如何处理类的实例。为其他目的定义接口是不合适的。   一种失败的接口就是所...

23. 类层次结构优于标签类

  有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形: // Tagged class...

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

  嵌套类(nested class)是在另一个类中定义的类。 嵌套类应该只存在于其宿主类(enclosing class)中。 如果一个嵌套类在其他一些情况下是有用的,那么它应该是一个顶级类。...

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

  虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。 风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递...

第五章 泛型

自 Java 5 以来,泛型一直是 Java 语言的一部分。在泛型出现之前,从集合中读取的每个对象都必须进行强制转换。如果有人不小心插入了错误类型的对象,强制类型转换可能在运行时失败。对于泛型,...

26. 不要使用原始类型

  首先,有几个术语。一个类或接口,它的声明有一个或多个类型参数(type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。 例如,List 接口具有单个类...

27. 消除非检查警告

  使用泛型编程时,会看到许多编译器警告:未经检查的强制转换警告,未经检查的方法调用警告,未经检查的参数化可变长度类型警告以及未经检查的转换警告。 你使用泛型获得的经验越多,获得的警告越少,但不...

28. 列表优于数组

  数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型 Sub[] 是数组类型 Super[] 的...

29. 优先考虑泛型

  参数化声明并使用 JDK 提供的泛型类型和方法通常不会太困难。 但编写自己的泛型类型有点困难,但值得努力学习。   考虑条目 7 中的简单堆栈实现: // Object-based coll...

30. 优先使用泛型方法

  正如类可以是泛型的,方法也可以是泛型的。 对参数化类型进行操作的静态工具方法通常都是泛型的。 集合中的所有“算法”方法(如 binarySearch 和 sort)都是泛型的。   编写泛型...

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

  如条目 28 所述,参数化类型是不变的。换句话说,对于任何两个不同类型的 Type1 和 Type2,List<Type1> 既不是 List<Type2> 的子类型也不是其父类型。尽管 L...

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

  在 Java 5 中,可变参数方法(详见第 53 条)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一...

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

  泛型的常见用法包括集合,如 Set<E> 和 Map<K,V> 和单个元素容器,如 ThreadLocal<T> 和 AtomicReference<T>。 在所有这些用途中,它都是参数化的...

第六章 枚举和注解

JAVA 支持两种特殊用途的引用类型:一种称为枚举类型的类,以及一种称为注解类型的接口。本章将讨论这些类型在实际使用时的最佳方式。

34. 使用枚举类型替代整型常量

  枚举是其合法值由一组固定的常量组成的一种类型,例如一年中的季节,太阳系中的行星或一副扑克牌中的花色。 在将枚举类型添加到该语言之前,表示枚举类型的常见模式是声明一组名为 int 的常量,每个...

35. 使用实例属性替代序数

  许多枚举通常与单个 int 值关联。所有枚举都有一个 ordinal 方法,它返回每个枚举常量类型的数值位置。你可能想从序数中派生一个关联的 int 值: // Abuse of ordin...

36. 使用 EnumSet 替代位属性

  如果枚举类型的元素主要用于集合中,则传统上使用 int 枚举模式(详见第 34 条),将 2 的不同次幂赋值给每个常量: // Bit field enumeration constants...

37. 使用 EnumMap 替代序数索引

  有时可能会看到使用 ordinal 方法(详见第 35 条)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物: class Plant { enum LifeCy...

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

  在几乎所有方面,枚举类型都优于本书第一版中描述的类型安全模式[Bloch01]。 从表面上看,一个例外涉及可扩展性,这在原始模式下是可能的,但不受语言结构支持。 换句话说,使用该模式,有可能...

39. 注解优于命名模式

  过去,通常使用命名模式(naming patterns)来指示某些程序元素需要通过工具或框架进行特殊处理。 例如,在第 4 版之前,JUnit 测试框架要求其用户通过以 test[Beck0...

40. 始终使用 Override 注解

  Java 类库包含几个注解类型。对于典型的程序员来说,最重要的是 @Override。此注解只能在方法声明上使用,它表明带此注解的方法声明重写了父类的声明。如果始终使用这个注解,它将避免产生...

41. 使用标记接口定义类型

  标记接口(marker interface),是不包含方法声明的接口,只是指定(或「标记」)一个类实现了具有某些属性的接口。 例如,考虑 Serializable 接口(第 12 章)。通过...

第七章 Lambda表达式和流

在 Java 8 中,为了更容易地创建函数对象,添加了函数式接口、lambda 表达式和方法引用;流 API 也与这些语言特性一并添加进来,为处理数据元素序列提供库支持。在这一章中,我们将讨论如...

42. lambda 表达式优于匿名类

  在 Java 8 中,添加了函数式接口,lambda 表达式和方法引用,以便更容易地创建函数对象。 Stream API 随着其他语言的修改一同被添加进来,为处理数据元素序列提供类库支持。 ...

43. 方法引用优于 lambda 表达式

  lambda 优于匿名类的主要优点是它更简洁。Java 提供了一种生成函数对象的方法,比 lambda 还要简洁,那就是:方法引用(method references)。下面是一段程序代码片...

44. 优先使用标准的函数式接口

  现在 Java 已经有 lambda 表达式,编写 API 的最佳实践已经发生了很大的变化。 例如,模板方法模式[Gamma95],其中一个子类重写原始方法以专门化其父类的行为,变得没有那么...

45. 明智审慎地使用 Stream

  在 Java 8 中添加了 Stream API,以简化串行或并行执行批量操作的任务。 该 API 提供了两个关键的抽象:流 (Stream),表示有限或无限的数据元素序列,以及流管道 (s...

46. 优先考虑流中无副作用的函数

  如果你是一个刚开始使用流的新手,那么很难掌握它们。仅仅将计算表示为流管道是很困难的。当你成功时,你的程序将运行,但对你来说可能没有意识到任何好处。流不仅仅是一个 API,它是基于函数式编程的...

47. 优先使用 Collection 而不是 Stream 来作为方法的返回类型

  许多方法返回元素序列(sequence)。在 Java 8 之前,通常方法的返回类型是 Collection,Set 和 List 这些接口;还包括 Iterable 和数组类型。通常,很容...

48. 谨慎使用流并行

  在主流语言中,Java 一直处于提供简化并发编程任务的工具的最前沿。 当 Java 于 1996 年发布时,它内置了对线程的支持,包括同步和 wait / notify 机制。 Java 5...

第八章 方法

本章讨论了方法设计的几个方面:如何处理参数和返回值,如何设计方法签名,以及如何编写方法文档。本章的大部分内容不仅适用于方法,也适用于构造函数。与第四章一样,本章重点讨论可用性、健壮性和灵活性。

49. 检查参数有效性

  本章(第 8 章)讨论了方法设计的几个方面:如何处理参数和返回值,如何设计方法签名以及如何记载方法文档。 本章中的大部分内容适用于构造方法和其他普通方法。 与第 4 章一样,本章重点关注可用...

50. 必要时进行防御性拷贝

  愉快使用 Java 的原因,它是一种安全的语言(safe language)。 这意味着在缺少本地方法(native methods)的情况下,它不受缓冲区溢出,数组溢出,野指针以及其他困扰...

51. 仔细设计方法签名

  这一条目是 API 设计提示的大杂烩,但它们本身并足以设立一个单独的条目。综合起来,这些设计提示将帮助你更容易地学习和使用 API,并且更不容易出错。   仔细选择方法名名称。名称应始终遵守...

52. 明智审慎地使用重载

  下面的程序是一个善意的尝试,根据 Set、List 或其他类型的集合对它进行分类: // Broken! - What does this program print? public cla...

53. 明智审慎地使用可变参数

  可变参数方法正式名称称为可变的参数数量方法「variable arity methods」 [JLS, 8.4.1],接受零个或多个指定类型的参数。 可变参数机制首先创建一个数组,其大小是在...

54. 返回空的数组或集合,不要返回 null

  像如下的方法并不罕见: // Returns null to indicate an empty collection. Don't do this! private final List<...

55. 明智审慎地返回 Optional

  在 Java 8 之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回 null(假设返回类型是对象是引用类型)。但这两种方法都不完美。应该为异常条件保留异...

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

  如果 API 要可用,就必须对其进行文档化。传统上,API 文档是手工生成的,保持文档与代码的同步是一件苦差事。Java 编程环境使用 Javadoc 实用程序简化了这一任务。Javadoc...

第九章 通用程序设计

本章主要讨论了 Java 语言的具体细节,包括局部变量、控制结构、类库、数据结构和两种不是由语言本身提供的机制:反射和本地方法。最后,讨论了优化和命名惯例。

57. 最小化局部变量的作用域

  这条目在性质上类似于条目 15,即“最小化类和成员的可访问性”。通过最小化局部变量的作用域,可以提高代码的可读性和可维护性,并降低出错的可能性。   较早的编程语言(如 C)要求必须在代码块...

58. for-each 循环优于传统 for 循环

  正如在条目 45 中所讨论的,一些任务最好使用 Stream 来完成,一些任务最好使用迭代。下面是一个传统的 for 循环来遍历一个集合: // Not the best way to it...

59. 了解并使用库

  假设你想要生成 0 到某个上界之间的随机整数。面对这个常见任务,许多程序员会编写一个类似这样的小方法: // Common but deeply flawed! static Random ...

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

  float 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很大范围内快速提供精确的近似值。但是,它们不能提供准确的结果,也不应该在需要精...

61. 基本数据类型优于包装类

  Java 有一个由两部分组成的类型系统,包括基本类型(如 int、double 和 boolean)和引用类型(如 String 和 List)。每个基本类型都有一个对应的引用类型,称为包装...

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

  字符串被设计用来表示文本,它们在这方面做得很好。因为字符串是如此常见,并且受到 Java 的良好支持,所以很自然地会将字符串用于其他目的,而不是它们适用的场景。本条目讨论了一些不应该使用字符...

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

  字符串连接操作符 (+) 是将几个字符串组合成一个字符串的简便方法。对于生成单行输出或构造一个小的、固定大小的对象的字符串表示形式,它是可以的,但是它不能伸缩。使用 字符串串联运算符重复串联...

64. 通过接口引用对象

  条目 51 指出,应该使用接口而不是类作为参数类型。更一般地说,你应该优先使用接口而不是类来引用对象。如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。 惟一真正需...

65. 接口优于反射

  核心反射机制 java.lang.reflect 提供对任意类的编程访问。给定一个 Class 对象,你可以获得 Constructor、Method 和 Field 实例,分别代表了该 C...

66. 明智审慎地本地方法

  Java 本地接口(JNI)允许 Java 程序调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。从历史上看,本地方法主要有三种用途。它们提供对特定于平台的设施(如注册中心)...

67. 明智审慎地进行优化

  有三条关于优化的格言是每个人都应该知道的: 比起任何其他单一原因(包括盲目愚蠢),计算上的过失更多的是以效率为名(不一定能实现)而犯下的。 ​ ...

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

  Java 平台有一组完善的命名约定,其中许多约定包含在《The Java Language Specification》[JLS, 6.1]。不严格地讲,命名约定分为两类:排版和语法。   ...

第十章 异常

当充分利用好异常时,可以提高程序的可读性、可靠性和可维护性。如果使用不当,则会产生负面效果。本章提供了有效使用异常的指南。

69. 只针对异常的情况下才使用异常

  假如你某一天不走运的话,可能遇到如下代码: /* Horrible abuse of exceptions. Don't ever do this! */ try { int i =...

70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常

  Java 程序设计语言提供了三种 throwable:受检异常(checked exceptions)、运行时异常(runtime exceptions)和错误(errors)。程序员中存在...

71. 避免不必要的使用受检异常

  Java 程序员不喜欢受检异常,但是如果使用得当,它们可以改善 API 和程序。不返回码和未受检异常的是,它们强迫程序员处理异常的条件,大大增强了可靠性。也就是说,过分使用受检异常会使 AP...

72. 优先使用标准的异常

  专家级程序员与缺乏经验的程序员一个最主要的区别在于,专家追求并且通常也能够实现高度的代码重用。代码重用是值得提倡的,这是一条通用的规则,异常也不例外。Java 平台类库提供了一组基本的未受检...

73. 抛出与抽象对应的异常

  如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况。除了使人感到困惑之外,这也“污染”了具有实现细节的更高层的 ...

74. 每个方法抛出的异常都需要创建文档

  描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分。因此,花点时间仔细地为每个方法抛出的异常建立文档是特别重要的。   始终要单独地声明受检异常, 并且利用 Javadoc...

75. 在细节消息中包含失败一捕获信息

  当程序由于未被捕获的异常而失败的时’候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法 (string representation),即它的 toString 方...

76. 保持失败原子性

  当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中, 即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者期望能从这种异常中进行恢复。一...

77. 不要忽略异常

  尽管这条建议看上去是显而易见的,但是它却常常被违反,因而值得再次提出来。当 API 的设计者声明一个方法将抛出某个异常的时候,他们等于正在试图说明某些事情。所以,请不要忽略它!要忽略一个异常...

第十一章 并发

线程允许多个活动并发进行。并发编程比单线程编程更困难,容易出错的地方更多,而且失败很难重现。你无法避开并发。它是平台中固有的,并且多核处理器现在也是无处不在,而你会有从多核处理器获得良好的性能的...

78. 同步访问共享的可变数据

  关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。许多程序员把同步的概念仅仅理解为一种互斥( mutual exclusion )的方式...

79. 避免过度同步

  第 78 条告诫过我们缺少同步的危险性。本条目则关注相反的问题。依据情况的不同,过度同步则可能导致性能降低、死锁,甚至不确定的行为。   为了避免活性失败和安全性失败,在一个被同步的方法或者...

80. executor 、task 和 stream 优先于线程

  本书第 1 版中阐述了简单的工作队列(work queue)[Bloch01 ,详见第 49 条]代码。它利用一个后台线程,允许客户端可以插入异步处理任务到队列中。当不再需要这个工作队列时,...

81. 并发工具优于 wait 和 notify

  本书第 1 版中专门用了一个条目来说明如何正确地使用 wait 和 notify ( Bloch01,详见第 50 条) 。它提出的建议仍然有效,并且在本条目的最后也对此做了概述,但是这条建...

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

  类在其方法并发使用时的行为是其与客户端约定的重要组成部分。如果你没有记录类在这一方面的行为,那么它的用户将被迫做出假设。如果这些假设是错误的,生成的程序可能缺少足够的同步(详见 78 条)或...

83. 明智审慎的使用延迟初始化

  延迟初始化是延迟字段的初始化,直到需要它的值。如果不需要该值,则不会初始化字段。这种技术既适用于静态字段,也适用于实例字段。虽然延迟初始化主要是一种优化,但是它也可以用于破坏类中的有害循环和...

84. 不要依赖线程调度器

  当许多线程可以运行时,线程调度器决定哪些线程可以运行以及运行多长时间。任何合理的操作系统都会尝试公平地做出这个决定,但是策略可能会有所不同。因此,编写良好的程序不应该依赖于此策略的细节。任何...

第十二章 序列化

本章关注对象序列化,它是 Java 的框架,用于将对象编码为字节流(序列化),并从对象的编码中重构对象(反序列化)。对象序列化后,可以将其编码从一个 VM 发送到另一个 VM,或者存储在磁盘上,...

85. 优先选择 Java 序列化的替代方案

  当序列化在 1997 年添加到 Java 中时,它被认为有一定的风险。这种方法曾在研究语言(Modula-3)中尝试过,但从未在生产语言中使用过。虽然程序员不费什么力气就能实现分布式对象,这...

86. 非常谨慎地实现 Serializable

  使类的实例可序列化非常简单,只需实现 Serializable 接口即可。因为这很容易做到,所以有一个普遍的误解,认为序列化只需要程序员付出很少的努力。而事实上要复杂得多。虽然使类可序列化的...

87. 考虑使用自定义的序列化形式

  当你在时间紧迫的情况下编写类时,通常应该将精力集中在设计最佳的 API 上。有时,这意味着发布一个「一次性」实现,你也知道在将来的版本中会替换它。通常这不是一个问题,但是如果类实现 Seri...

88. 保护性的编写 readObject 方法

  第 50 条介绍了一个不可变的日期范围类,它包含可变的私有变量 Date。该类通过在其构造器和访问方法(accessor)中保护性的拷贝 Date 对象,极力维护其约束条件和不可变性。该类代...

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

  第 3 条讲述了 Singletion(单例)模式,并且给出了以下这个 Singletion 示例。这个类限制了对其构造器的访问,以确保永远只创建一个实例。 public class Elv...

90. 考虑用序列化代理代替序列化实例

  正如 85 条和第 86 条提到的,以及本章一直在讨论的,决定实现 Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是使用普通的...