Effective java学习笔记

  • 1.使用静态方法代替构造方法
  • 2.当构造方法参数过多时使用 builder 模式
  • 3.使用私有构造方法或枚类实现 Singleton 属性
    • 实现Singleton的三种方法
  • 4.使用私有构造方法执行非实例化
  • 5.依赖注入优于硬连接资源(hardwiring resources)
  • 6.避免创建不需要的对象
  • 7.消除过期的对象引用
  • 8.避免使用Finalizer 和 Cleaner 机制
    • 缺点:
    • 优点:
  • 9.使用 try-with-resources 语句替代 try-finally 语句
  • 10.重写 equals 方法时遵守通用约定
  • 11.重写 equals 方法时同时也要重写 hashcode 方法
  • 12.始终重写toString 方法
  • 13.Cloneable 接口
    • 总结:
  • 14.考虑实现 Comparable 接口
  • 15.使类和成员的可访问性最小化
  • 16.在公共类中使用访问方法而不是公共属性
  • 17.最小化可变性
  • 18.组合优于继承
  • 19.要么设计继承并提供文档说明,要么禁用继承
  • 20.接口优于抽象类
  • 21.为后代设计接口
  • 22.接口仅用来定义类型
  • 23. 类层次结构优于标签类
  • 24.支持使用静态成员类而不是非静态类
  • 25.将源文件限制为单个顶级类
  • 26.不要使用原始类型
  • 27.消除非检查警告
  • 28.列表优于数组
  • 29.优先考虑泛型
  • 30.优先使用泛型方法`*
  • 31.使用限定通配符来增加 API 的灵活性
  • 32.合理地结合泛型和可变参数
  • 33.优先考虑类型安全的异构容器
  • 34.使用枚举类型替代整型常量
  • 35.使用实例属性替代序数
  • 36.使用 EnumSet 替代位属性`*
  • 37.使用 EnumMap 替代序数索引`*
  • 38.使用接口模拟可扩展的枚举
  • 39.注解优于命名模式
  • 40.始终使用 Override 注解**
  • 41.使用标记接口定义类型
  • 42.lambda 表达式优于匿名类
  • 43.方法引用优于 lambda 表达式
  • 44.优先使用标准的函数式接口
  • 45.明智审慎地使用 Stream
  • 46.优先考虑流中无副作用的函数
  • 47.优先使用 Collection 而不是 Stream 来作为方法的返回类型
  • 48.谨慎使用流并行
  • 49.检查参数有效性
  • 50.必要时进行防御性拷⻉*
  • 51.仔细设计方法签名
  • 52.明智审慎地使用重载
  • 53.明智审慎地使用可变参数
  • 54.返回空的数组或集合,不要返回 null
  • 55.明智审慎地返回 Optional
  • 56.为所有已公开的 API 元素编写文档注释
  • 57.最小化局部变量的作用域
  • 58.for-each 循环优于传统 for 循环
  • 59.了解并使用库
  • 60.若需要精确答案就应避免使用 float 和 double 类型
  • 61.基本数据类型优于包装类
  • 62.当使用其他类型更合适时应避免使用字符串
  • 63.当心字符串连接引起的性能问题
  • 64.通过接口引用对象
  • 65.接口优于反射
  • 66.明智审慎地本地方法
  • 67.明智审慎地进行优化
  • 68.遵守被广泛认可的命名约定
  • 69.只针对异常的情况下才使用异常
  • 70.对可恢复的情况使用受检异常,对编程错误使用运行时异常
  • 71.避免不必要的使用受检异常
  • 72.优先使用标准的异常
  • 73.抛出与抽象对应的异常
  • 74.每个方法抛出的异常都需要创建文档
  • 75.在细节消息中包含失败一捕获信息
  • 76.保持失败原子性
  • 77.不要忽略异常
  • 78.同步访问共享的可变数据
  • 79.避免过度同步
  • 80.executor 、 task 和 stream 优先于线程
  • 81.并发工具优于 wait 和 notify
  • 82.文档应包含线程安全属性
  • 83.明智审慎的使用延迟初始化
  • 84.不要依赖线程调度器
  • 85.优先选择 Java 序列化的替代方案
  • 86.非常谨慎地实现 Serializable
  • 87.考虑使用自定义的序列化形式
  • 88.保护性的编写 readObject 方法
  • 89.对于实例控制,枚举类型优于 readResolve
  • 90.考虑用序列化代理代替序列化实例

1.使用静态方法代替构造方法

//静态工厂方法举例
public static Boolean valueOf(boolean b) {return b ? Boolean.TRUE : Boolean.FALSE;
}
优点:
1. 静态工厂方法有自己的名字,比起构造方法,使用时更易区分。
2. 不需要每次调用时都创建一个新对象,方法使用频繁的情况下,性能比使用构
造方法更好
3.静态工厂方法可以返回其返回类型的任何子类型的对象。
4.静态工厂方法返回对象的类可以根据输入参数的不同而不同(返回对象的类需
为该类的子类)
5.在编写包含该方法的类时,返回的对象的类不需要存在(没看懂)
缺点:
1.没有public和protected构造方法的类不能被子类化
2.难以找到静态工厂方法(API文档中没有明确的标注)

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

Example exampleOne = new Example().Construct(requireParam1,requireParam2).param3(optionalParam1).param4.(optionalParam2)........;
当设计类的构造方法或静态工厂的参数超过几个时, Builder 模式是一个不错的选
择,特别是如果许多参数是可选的或相同类型的。
builder 模式客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 模式比 JavaBeans 更安全。

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

实现Singleton的三种方法

一、私有构造方法

public class Elvis {public static final Elvis INSTANCE = new Elvis();private Elvis() { ... }public void leaveTheBuilding() { ... }
}

二、静态工厂方法

public class Elvis {private static final Elvis INSTANCE = new Elvis();private Elvis() { ... }public static Elvis getInstance() { return INSTANCE; }public void leaveTheBuilding() { ... }}
公共属性方法的主要优点是 API 明确表示该类是一个单例:公共静态属性是 final
的,所以它总是包含相同的对象引用。第二个好处是它更简单。
静态工厂方法的优势之一在于,它提供了灵活性:在不改变其 API 的前提下,我
们可以改变该类是否应该为单例的想法。工厂方法返回该类的唯一实例,但是,
它很容易被修改,比如,改为每个调用该方法的线程返回一个唯一的实例。
第二个优势,可以编写泛型单例工厂。
第三个优势,可以通过方法引用(method reference)作为提供者,例如
Elvis::instance 等同于 Supplier<Elvis>

三、枚举类

// Enum singleton - the preferred approach
public enum Elvis {INSTANCE;public void leaveTheBuilding() { ... }
}
         单一元素枚举类通常是实现单例的最佳方式。

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

// Noninstantiable utility class
public class UtilityClass {// Suppress default constructor for noninstantiabilityprivate UtilityClass() {throw new AssertionError();}// Remainder omitted
}
副作用:阻止了类的子类化。
因为所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访
问的父类构造方法来调用。

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

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

7.消除过期的对象引用

//栈的pop()方法
public Object pop() {if (size == 0)throw new EmptyStackException();return elements[--size];}
如果一个栈增⻓后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的
程序不再引用这些对象。
这是因为栈维护对这些对象的过期引用(obsolete references)。过期引用简单
来说就是永远不会解除的引用。在这种情况下,元素数组“活动部分(active
portion) ”之外的任何引用都是过期的。活动部分是由索引下标小于 size 的元素
组成。
这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。如下
//New pop()
public Object pop() {if (size == 0)throw new EmptyStackException();Object result = elements[--size];elements[size] = null; // Eliminate obsolete referencereturn result;}
                  *清空对象引用应该是例外而不是规范。*
消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围
内定义每个变量,这种自然就会出现这种情况。
那么什么时候应该清空一个引用呢? Stack 类的哪个方面使它容易受到内存泄漏
的影响?简单地说,它管理自己的内存。
存储池(storage pool)由 elements 数组的元素组成(对象引用单元,而不是对象
本身)。数组中活动部分的元素 (如前面定义的) 被分配,其余的元素都是空闲的。
垃圾收集器没有办法知道这些;对于垃圾收集器来说, elements 数组中的所有对象
引用都同样有效。
 *当一个类自己管理内存时,程序员应该警惕内存泄漏问题*
      另一个常⻅的内存泄漏来源是缓存。 第三个常⻅的内存泄漏来源是监听器和其他回调。

8.避免使用Finalizer 和 Cleaner 机制

缺点:

  • Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行
  • 在一个对象变得无法访问时,到 Finalizer 和 Cleaner 机制开始运行时,这期间的时间是任意⻓的。
  • Finalizer 机制的另一个问题是在执行 Finalizer 机制过程中,未捕获的异常会被忽略,并且该对象的Finalizer 机制也会终止。未捕获的异常会使其他对象陷入一种损坏的状态。如果另一个线程试图使用这样一个损坏的对象,可能会导致任意不确定的行为。通常情况下,未捕获的异常将终止线程并打印堆栈跟踪(stacktrace),但如果发生在 Finalizer 机制中,则不会发出警告。 Cleaner 机制没有这个问题,因为使用 Cleaner 机制的类库可以控制其线程。
  • finalizer 机制有一个严重的安全问题:它们会打开你的类来进行 finalizer 机制攻击。

优点:

  • 一个是作为一个安全网(safety net),以防资源的拥有者忽略了它的 close 方法。
  • 第二种合理使用 Cleaner 机制的方法与本地对等类(native peers)有关。本地对等类是一个由普通对象委托的本地 (非 Java) 对象。

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

  • 在处理必须关闭的资源时,使用 try-with-resources 语句替代 try-finally
    语句。生成的代码更简洁,更清晰,并且生成的异常更有用。 try-with-resources语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用 try-finally 语句实际上是不可能的。

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

重写equals如果满足以下条件,则说明是正确的做法:

  • 每个类的实例都是固有唯一的。
  • 类不需要提供一个「逻辑相等(logical equality)」的测试功能。
  • 父类已经重写了 equals 方法,则父类行为完全适合于该子类。
  • 类是私有的或包级私有的,可以确定它的 equals 方法永远不会被调用。

什么时候需要重写 equals 方法:

  • 使用 equals 方法比较值对象的引用,期望发现它们在逻辑上是否相等,而不是引用相同的对象。它还支持重写过的equals的实例作为 Map 的键(key),或者 Set 里的元素。

Object 的规范如下: equals 方法实现了一个等价关系(equivalence relation)。它有以下这些属性:

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

以下是编写高质量 equals 方法的配方:

  1. 使用 == 运算符检查参数是否为该对象的引用。如果是,返回 true。
  2. 使用 instanceof 运算符来检查参数是否具有正确的类型。如果不是,则返回 false。
  3. 参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功。
  4. 对于类中的每个「重要」的属性,请检查该参数属性是否与该对象对应的属性相匹配。如果所有这些测试成功,返回 true,否则返回 false。

以下是最后一些提醒:

  • 当重写 equals 方法时,同时也要重写 hashCode 方法(详⻅第 11 条)。
  • 不要让 equals 方法试图太聪明。
  • 在 equal 时方法声明中,不要将参数 Object 替换成其他类型。

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

根据 Object 规范,以下是具体约定。

  1. 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值
    可以是不一致的。
  2. 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
  3. 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。

hash 方法为集合中不相等的实例均匀地分配 int 范围内的哈希码,但非常困难,以下是一个合理的、近似的、简单的方式:

//Type为参数的类型  A为Integer则Type为Integer
//Type.hashCode(A)代表计算出参数A的哈希码
@Override
public int hashCode() {int result = Type.hashCode(A);result = 31 * result + Type.hashCode(B);result = 31 * result + Type.hashCode(C);return result;
}
  1. 声明一个 int 类型的变量 result,并将其初始化为对象中第一个重要属性 c 的哈希码,如下面步骤 2.a 中所计算的那样。(回顾条目 10,重要的属性是影响比较相等的领域。)

  2. 对于对象中剩余的重要属性 f,请执行以下操作:
    a. 比较属性 f 与属性 c 的 int 类型的哈希码:
    – i. 如果这个属性是基本类型的,使用 Type.hashCode(f) 方法计算,其中 Type 类是对应属性 f基本类型的包装类。
    – ii. 如果该属性是一个对象引用,并且该类的 equals 方法通过递归调用 equals 来比较该属性,并递归地调用 hashCode 方法。如果需要更复杂的比较,则计算此字段的“范式(“canonical representation) ”,并在范式上调用 hashCode。如果该字段的值为空,则使用 0(也可以使用其他常数,但通常来使用 0 表示)。
    – iii. 如果属性 f 是一个数组,把它看作每个重要的元素都是一个独立的属性。也就是说,通过递归地应用这些规则计算每个重要元素的哈希码,并且将每个步骤 2.b 的值合并。如果数组没有重要的元素,则使用一个常量,最好不要为 0。如果所有元素都很重要,则使用 Arrays.hashCode方法。
    b. 将步骤 2.a 中属性 c 计算出的哈希码合并为如下结果: result = 31 * result + Type.hashCode(f);

  3. 返回 result 值。

     可以从哈希码计算中排除派生属性(derived fields)。必须排除在 equals 比较中没有使用的任何属性。
    

另一种方法:

// One-line hashCode method - mediocre performance
//缺点是性能差
@Override
public int hashCode() {return Objects.hash(lineNum, prefix, areaCode);
}
  • 不要试图从哈希码计算中排除重要的属性来提高性能。
  • 不要为 hashCode 返回的值提供详细的规范,因此客户端不能合理地依赖它;你可以改变它的灵活性。

12.始终重写toString 方法

13.Cloneable 接口

Cloneable 接口决定了 Object 的受保护的 clone 方法实现的行为:如果一个类实现
了 Cloneable 接口,那么 Object 的 clone 方法将返回该对象的逐个属性(field-by-
field)拷⻉;否则会抛出 CloneNotSupportedException 异常。
在数组上调用 clone 会返回一个数组,其运行时和编译时类型与被克隆
的数组相同。这是复制数组的首选习语。事实上,数组是 clone 机制的
唯一有力的用途。
克隆复杂可变对象的最后一种方法是调用 super.clone,将结果对象中的所有属性设
置为其初始状态,然后调用更高级别的方法来重新生成原始对象的状态。以
HashTable 为例, bucket 属性将被初始化为一个新的bucket 数组,并且 put(key,
value) 方法(未示出)被调用用于被克隆的哈希表中的键值映射。
Object 类的 clone 方法被声明为抛出 CloneNotSupportedException 异
常,但重写方法时不需要。公共clone 方法应该省略 throws 子句,因为
不抛出检查时异常的方法更容易使用

总结:

实现 Cloneable 的所有类应该重写公共 clone 方法,而这个方法的返回类型是类本
身。这个方法应该首先调用 super.clone,然后修复任何需要修复的属性。通常,这
意味着复制任何包含内部「深层结构」的可变对象,并用指向新对象的引用来代替
原来指向这些对象的引用。虽然这些内部拷⻉通常可以通过递归调用 clone来实现,
但这并不总是最好的方法。如果类只包含基本类型或对不可变对象的引用,那么很
可能是没有属性需要修复的情况。这个规则也有例外。例如,表示序列号或其他唯
一 ID 的属性即使是基本类型的或不可变的,也需要被修正。
考虑到与 Cloneable 接口相关的所有问题,新的接口不应该继承它,新的可扩展类
不应该实现它。虽然实现Cloneable 接口对于 final 类没有什么危害,但应该将其视
为性能优化的⻆度,仅在极少数情况下才是合理的(详⻅第 67 条)。通常,复制功
能最好由构造方法或工厂提供。这个规则的一个明显的例外是数组,它最好用 clone
方法复制。

对象复制更好的方法是提供一个复制构造方法或复制工厂。复制构造方法接受参数,其类型为包含此构造方法的类,例如:

// Copy constructor
public Yum(Yum yum) { ... };

复制工厂类似于复制构造方法的静态工厂:

 //Copy factory
public static Yum newInstance(Yum yum) { ... };
复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点:它们不依赖⻛
险很大的语言外的对象创建机制;不要求遵守那些不太明确的惯例;不会与 final 属
性的正确使用相冲突; 不会抛出不必要的检查异常;而且不需要类型转换。此外,复制构造方法或复制工厂可以接受类型为该类实现的接口的参数

14.考虑实现 Comparable 接口

  • 实现类必须确保所有 x 和 y 都满足 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 不一致」。
PS:符号 sgn(expression) 表示数学中的 signum 函数,它根据表达式的
值为负数、零、正数,对应返回-1、 0 和 1。

无论何时实现具有合理排序的值类,你都应该让该类实现 Comparable 接口,以便在基于比较
的集合中轻松对其实例进行排序,搜索和使用。比较 compareTo 方法的实现中的字段值时,请避免使用「<」和
「>」运算符。相反,使用包装类中的静态 compare 方法或 Comparator 接口中的构建方法。

// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);public int compareTo(PhoneNumber pn) {return COMPARATOR.compare(this, pn);}
上述代码会以areaCode、prefix、linNum的顺序来进行比较
Comparator 类具有完整的构建方法。对于 long 和 double 基本类型,也有对应的
类似于 comparingInt和 thenComparingInt 的方法, int 版本的方法也可以应用于
取值范围小于 int 的类型上,如 short 类型,如PhoneNumber 实例中所示。对于
double 版本的方法也可以用在 float 类型上。这提供了所有 Java 的基本数字类型
的覆盖

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

经验法则很简单: 让每个类或成员尽可能地不可访问。
/*请注意,非零⻓度的数组总是可变的,所以类具有公共静态 final 数组字
段,或返回这样一个字段的访问器是错误的。*/
// Potential security hole!
public static final Thing[] VALUES = { ... };
以下是两种解决方法:
//可以使公共数组私有并添加一个公共的不可变列表
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(
Arrays.asList(PRIVATE_VALUES));
//可以将数组设置为 private,并添加一个返回私有数组拷⻉的公共方法
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {return PRIVATE_VALUES.clone();
}
总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。在仔细设
计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API
的一部分。除了作为常量的公共静态 final 字段之外,公共类不应该有公共字段。
确保 public static final 字段引用的对象是不可变的。

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

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

17.最小化可变性

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

  • 不要提供修改对象状态的方法(也称为 mutators)。
  • 确保这个类不能被继承。 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。防止子类化通常是通过 final 修饰类,但是我们稍后将讨论另一种方法。
  • 把所有属性设置为 final。 通过系统强制执行,清楚地表达了你的意图。另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型 [JLS, 17.5; Goetz06,16] 所述。
  • 把所有的属性设置为 private。 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。虽然技术上允许不可变类具有包含基本类型数值的公共 final 属性或对不可变对象的引用,但不
    建议这样做,因为它不允许在以后的版本中更改内部表示(详⻅第 15 和 16 条)。
  • 确保对任何可变组件的互斥访问。 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。在构造方法,访问方法和 readObject 方法(详⻅第 88 条)中进行防御性拷⻉(详⻅第 50 条)。
方法返回将操作数应用于函数的结果,而不修改它们,这种模式被称为函数式方法。

18.组合优于继承

继承的缺点:

与方法调用不同,继承打破了封装 [Snyder86]。 换句话说,一个子类依赖于其父
类的实现细节来保证其正确的功能。父类的实现可能会从发布版本不断变化,如
果是这样,子类可能会被破坏,即使它的代码没有任何改变。
不要继承一个现有的类,而应该给你的新类增加一个私有属性,该属性
是现有类的实例引用,这种设计被称为组合(composition),因为现
有的类成为新类的组成部分。新类中的每个实例方法调用现有类的包含
实例上的相应方法并返回结果。这被称为转发(forwarding),而新类
中的方法被称为转发方法。
只有在子类真的是父类的子类型的情况下,继承才是合适的。换句话说,只有在两
个类之间存在「is-a」关系的情况下, B 类才能继承 A 类。如果你试图让 B 类继承
A 类时,问自己这个问题:每个 B 都是 A 吗?如果你不能如实回答这个问题,那么
B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且
暴露一个不同的 API : A 不是 B 的重要部分,只是其实现细节。
总之,继承是强大的,但它是有问题的,因为它违反封装。只有在子类和父类之间
存在真正的子类型关系时才适用。即使如此,如果子类与父类不在同一个包中,并
且父类不是为继承而设计的,继承可能会导致脆弱性。为了避免这种脆弱性,使用
合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。包装类不仅
比子类更健壮,而且更强大。

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

构造方法绝不能直接或间接调用可重写的方法。 如果违反这个规则,将导致程序失
败。父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类
中的重写方法被调用。如果重写方法依赖于子类构造方法执行的任何初始化,则此
方法将不会按预期运行。
总之,设计一个继承类是一件很辛苦的事情。你必须文档说明所有的自用模式,一
旦你文档说明了它们,必须承诺为他们的整个生命周期。如果你不这样做,子类可
能会依赖于父类的实现细节,并且如果父类的实现发生改变,子类可能会损坏。为
了允许其他人编写高效的子类,可能还需要导出一个或多个受保护的方法。除非你
知道有一个真正的子类需要,否则你可能最好是通过声明你的类为 final 禁止继承,
或者确保没有可访问的构造方法。

20.接口优于抽象类

总而言之,一个接口通常是定义允许多个实现的类型的最佳方式。如果你导出一个
重要的接口,应该强烈考虑提供一个⻣架的实现类。在可能的情况下,应该通过接
口上的默认方法提供⻣架实现,以便接口的所有实现者都可以使用它。也就是说,
对接口的限制通常要求⻣架实现类采用抽象类的形式。
⻣架实现类(abstract skeletal implementation class)来与接口一起使用,将接口
和抽象类的优点结合起来。接口定义了类型,可能提供了一些默认的方法,而⻣架
实现类在原始接口方法的顶层实现了剩余的非原始接口方法。继承⻣架实现需要大
部分的工作来实现一个接口。

21.为后代设计接口

22.接口仅用来定义类型

一种失败的接口就是所谓的常量接口(constant interface)。这样的接口不包含任
何方法; 它只包含静态final 属性,每个输出一个常量。

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

标签类是冗⻓的,容易出错的,而且效率低下。

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

嵌套类(nested class)是在另一个类中定义的类。嵌套类应该只存在于其宿主类
(enclosing class)中。如果一个嵌套类在其他一些情况下是有用的,那么它应该是
一个顶级类。有四种嵌套类:静态成员类,非静态成员类,匿名类和局部类。除了
第一种以外,剩下的三种都被称为内部类(inner class)。
在语法上,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具
有 static 修饰符。尽管句法相似,但这两种嵌套类是非常不同的。如果嵌套类的实
例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有
宿主实例的情况下创建非静态成员类的实例。
  • 静态成员类的一个常⻅用途是作为公共帮助类,仅在与其外部类一起使用时才有用。
  • 私有静态成员类的常⻅用法是表示由它们的宿主类表示的对象的组件。
如果你声明了一个不需要访问宿主实例的成员类,那就把 static 修饰符放在它的声
明中,使它成为一个静态成员类,而不是非静态的成员类。  如果你忽略了这个修饰
符,每个实例都会有一个隐藏的外部引用给它的宿主实例。 如前所述,存储这个引
用需要占用时间和空间。更严重的是,并且会导致即使宿主类在满足垃圾回收的条
件时却仍然驻留在内存中(详⻅第 7 条)。由此产生的内存泄漏可能是灾难性的。
有四种不同的嵌套类,每个都有它的用途。如果一个嵌套的类需要在一个方法之外
可⻅,或者太⻓而不能很好地适应一个方法,使用一个成员类。如果一个成员类的
每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。假
设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预
置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。

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

永远不要将多个顶级类或接口放在一个源文件中。遵循这个规则保证在编译时不能有多个定义。这又保证了编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序无关。

26.不要使用原始类型

每个泛型定义了一个原始类型(raw type),它是没有任何类型参数的
泛型类型的名称.例如,对应于 List<E> 的原始类型是 List。原始类型的
行为就像所有的泛型类型信息都从类型声明中被清除一样。它们的存在
主要是为了与没有泛型之前的代码相兼容。如前所述,使用原始类型
(没有类型参数的泛型)是合法的,但是你不应该这样做。 如果你使用原始类型,
则会丧失泛型的所有安全性和表达上的优势。
原始类型 List 和参数化类型 List<Object> 之间有什么区别?松散地说,前者已经选
择了泛型类型系统,而后者明确地告诉编译器,它能够保存任何类型的对象。虽然
可以将 List<String> 传递给 List 类型的参数,但不能将其传递给 List<Object> 类型
的参数。
  • 对于不应该使用原始类型的规则,有一些小例外。 你必须在类字面值(class
    literals)中使用原始类型。规范中不允许使用参数化类型(尽管它允许数组类型和 基本类型) [JLS, 15.8.2]。换句话说,
    List.class,String[].class 和 int.class 都是 合法的,但List.class 和 List<?>.class 不是合法的。
  • 规则的第二个例外涉及 instanceof 操作符。因为泛型类型信息在运行时被删除,所 以在无限制通配符类型以外的参数化类型上使用instanceof 运算符是非法的。使用 无限制通配符类型代替原始类型不会以任何方式影响 instanceof 运算符的行为。在这种情况下,尖括号和问号就显得多余。 以下是使用泛型类型的instanceof 运算符 的首选方法
 // Legitimate use of raw type - instanceof operatorif (o instanceof Set) { // Raw typeSet<?> s = (Set<?>) o; // Wildcard type}

请注意,一旦确定 o 对象是一个 Set,则必须将其转换为通配符 Set<?>,而不是原始类型 Set。这是一个强制转换,所以不会导致编译器警告

总之,使用原始类型可能导致运行时异常,所以不要使用它们。它们仅用于与泛型
引入之前的传统代码的兼容性和互操作性。作为一个快速回顾, Set<Object> 是一
个参数化类型,表示一个可以包含任何类型对象的集合, Set<?> 是一个通配符类
型,表示一个只能包含某些未知类型对象的集合, Set 是一个原始类型,它不在泛
型类型系统之列。前两个类型是安全的,最后一个不是。

27.消除非检查警告

未 经 检 查 的 警 告 是 重 要 的。 不 要 忽 视 他 们。 每 个 未 经 检 查 的 警 告 代
表 在 运 行 时 出 现ClassCastException 异常的可能性。尽你所能消除这些警告。
如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则
可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked” )注解来禁止警
告。记录你决定在注释中抑制此警告的理由`

28.列表优于数组

数组和泛型不能很好地在一起混合使用。例如,创建泛型类型的数组,参数化类型
的数组,以及类型参数的数组都是非法的。因此,这些数组创建表达式都不合法:
new List<E>[],new List<String>[], new E[]。所有将在编译时导致泛型数组创建错
误。
类型 E, List<E> 和 List<String> 等在技术上被称为不可具体化的类型
(nonreifiable types) [JLS,4.7]。直观地说,不可具体化的类型是其运行时表示包
含的信息少于其编译时表示的类型。由于擦除,可唯一确定的参数化类型是无限定
通配符类型,如 List<?> 和 Map<?, ?>(详⻅第 26 条)。尽管很少有用,创建无限
定通配符类型的数组是合法的
总之,数组和泛型具有非常不同的类型规则。数组是协变和具体化的; 泛型是不变
的,类型擦除的。因此,数组提供运行时类型的安全性,但不提供编译时类型的安
全性,反之亦然。一般来说,数组和泛型不能很好地混合工作。如果你发现把它们
混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组

29.优先考虑泛型

泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。当你设计新
的类型时,确保它们可以在没有这种强制转换的情况下使用。这通常意味着使类型
泛型化。如果你有任何现有的类型,应该是泛型的但实际上却不是,那么把它们泛
型化。这使这些类型的新用户的使用更容易,而不会破坏现有的客户端

30.优先使用泛型方法`*

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

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E>
s2)回类型仍然是 Set<E>。不要使用限定通配符类型作为返回类型。除了会为用户提供额外的灵活性,还强制他们在客户端代码中使用通配符类型。
如果类型参数在方法声明中只出现一次,请将其替换为通配符

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

当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)。它会导致编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。
为什么声明一个带有泛型可变参数的方法是合法的,当明确创建一个泛型数组是非
法的时候呢?答案是,具有泛型或参数化类型的可变参数参数的方法在实践中可能
非常有用,因此语言设计人员选择忍受这种不一致。@SafeVarargs 注解允许具有泛
型可变参数的方法的作者自动禁止客户端警告。实质上, @SafeVarargs 注解构成
了作者对类型安全的方法的承诺。调用方法时会创建一个泛型数组,以容纳可变参
数。如果方法没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引
用进行转义(这会使不受信任的代码访问数组),那么它是安全的。换句话
说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可
变参数的目的——那么该方法是安全的。给另一个方法访问一个泛型的可变参数数
组是不安全的,除了两个例外:将数组传递给另一个可变参数方法是安全的,这个
方法是用 @SafeVarargs 正确标注的,将数组传递给一个非可变参数的方法是安全
的,该方法仅计算数组内容的一些方法。请注意, SafeVarargs 注解只对不能被重
写的方法是合法的。在 Java 8 中,注解仅在静态方法和 final 实例方法上合法; 在
Java 9 中,它在私有实例方法中也变为合法
总而言之,可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建
的脆弱的抽象,并且数组具有与泛型不同的类型规则。虽然泛型可变参数不是类型
安全的,但它们是合法的。如果选择使用泛型(或参数化)可变参数编写方法,请
首先确保该方法是类型安全的,然后使用 @SafeVarargs 注解对其进行标注,以免
造成使用不愉快。

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

泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。
你可以通过将类型参数放在键上而不是容器上来解决此限制。可以使用 Class 对象
作为此类型安全异构容器的键。以这种方式使用的 Class 对象称为类型令牌。也可
以使用自定义键类型。例如,可以有一个表示数据库行(容器)的DatabaseRow 类
型和一个泛型类型 Column<T> 作为其键。

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

Java 枚举类型背后的基本思想很简单:它们是通过公共静态 final 属性为每个枚举常
量导出一个实例的类。由于没有可访问的构造方法,枚举类型实际上是 final 的。由
于客户既不能创建枚举类型的实例也不能继承它,除了声明的枚举常量外,不能有
任何实例。换句话说,枚举类型是实例控制的(第 6 ⻚)。它们是单例(详⻅第 3
条)的泛型化,基本上是单元素的枚举。
// Enum type that switches on its own value - questionablepublic enum Operation {PLUS, MINUS, TIMES, DIVIDE;// Do the arithmetic operation represented by this constantpublic double apply(double x, double y) {switch(this) {case PLUS: return x + y;case MINUS: return x - y;case TIMES: return x * y;case DIVIDE: return x / y;}throw new AssertionError("Unknown op: " + this);}}

此代码有效,但不是很漂亮。如果没有 throw 语句,就不能编译,因为该方法的结束在技术上是可达到的,尽管它永远不会被达到 [JLS, 14.21]。更糟的是,代码很脆弱。如果添加新的枚举常量,但忘记向 switch 语句添加相应的条件,枚举仍然会编译,但在尝试应用新操作时,它将在运行时失败。
幸运的是,有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的 apply 方法,并用常量特定的类主体中的每个常量的具体方法重写它。这种方法被称为特定于常量(constant-specific)的方法实现:

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

特定于常量的方法实现可以与特定于常量的数据结合使用。例如,以下是 Operation 的一个版本,它重写toString 方法以返回通常与该操作关联的符号:

// Enum type with constant-specific class bodies and data
public enum Operation {PLUS("+") {public double apply(double x, double y) { return x + y; }},MINUS("-") {public double apply(double x, double y) { return x - y; }},TIMES("*") {public double apply(double x, double y) { return x * y; }},DIVIDE("/") {public double apply(double x, double y) { return x / y; }};private final String symbol;Operation(String symbol) { this.symbol = symbol; }@Override public String toString() { return symbol; }public abstract double apply(double x, double y);
}

显示的 toString 实现可以很容易地打印算术表达式,正如这个小程序所展示的那样:

public static void main(String[] args) {double x = Double.parseDouble(args[0]);double y = Double.parseDouble(args[1]);for (Operation op : Operation.values())System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}
输出:
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000

枚举类型具有自动生成的 valueOf(String) 方法,该方法将常量名称转换为常量本身。如果在枚举类型中重写 toString 方法,请考虑编写 fromString 方法将自定义字符串表示法转换回相应的枚举类型。下面的代码(类型名称被适当地改变)将对任何枚举都有效,只要每个常量具有唯一的字符串表示形式:

// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));
// Returns Operation for string, if any
public static Optional<Operation> fromString(String symbol) {return Optional.ofNullable(stringToEnum.get(symbol));
}
枚举构造方法不允许访问枚举的静态属性。此限制是必需的,因为静态属性在枚举
构造方法运行时尚未初始化。这种限制的一个特例是枚举常量不能从构造方法中相
互访问。
应该什么时候使用枚举呢?任何时候使用枚举都需要一组常量,这些常量的成员在
编译时已知。当然,这包括“天然枚举类型”,如行星,星期几和棋子。但是它也包含
了其它你已经知道编译时所有可能值的集合,例如菜单上的选项,操作代码和命令
行标志。 一个枚举类型中的常量集不需要一直保持不变。枚举功能是专⻔设计用于
允许二进制兼容的枚举类型的演变。

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

36.使用 EnumSet 替代位属性`*

如果枚举类型的元素主要用于集合中,一般来说使用 int 枚举模式。
仅仅因为枚举类型将被用于集合中,所以没有理由用位属性来表示它。 EnumSet 类
将位属性的简洁性和性能与条目 34 中所述的枚举类型的所有优点相结合。
EnumSet 的一个真正缺点是,它不像 Java 9 那样创建一个不可变的 EnumSet,但
是在即将发布的版本中可能会得到补救。同时,你可以用 Collections.unmodifiableSet 封装一个 EnumSet,但是简洁性和性能会受到影响。

37.使用 EnumMap 替代序数索引`*

总之,使用序数来索引数组很不合适:改用 EnumMap。 如果你所代表的关系是多
维的,请使用 EnumMap<..., EnumMap <... >>。应用程序员应该很少使用
Enum.ordinal(详⻅第 35 条),如果使用了,也是一般原则的特例。

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

虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本
的枚举类型,来对它进行模拟。 这允许客户端编写自己的枚举(或其它类型)来实
现接口。如果 API 是根据接口编写的,那么在任何使用基本枚举类型实例的地方,
都可以使用这些枚举类型实例。

39.注解优于命名模式

// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}
  1. @Retention(RetentionPolicy.RUNTIME)元注解指示 Test注解应该在运行时保留。没有它,测试工具就不会看到 Test 注解。
  2. @Target.get(ElementType.METHOD) 元注解表明 Test 注解只对方法声明合法:它不能应用于类声明,属性声明或其他程序元素。
@Test 被称为标记注解,因为它没有参数,只是“标记”注解元素。

从 Java 8 开始,还有另一种方法来执行多值注解。可以使用 @Repeatable 元注解来标示注解的声明,而不用使用数组参数声明注解类型,以指示注解可以重复应用于单个元素。该元注解采用单个参数,该参数是包含注解类型的类对象,其唯一参数是注解类型 [JLS, 9.6.3] 的数组。

40.始终使用 Override 注解**

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

标记接口与标记注解相比具有两个优点。首先,也是最重要的一点,
标记接口定义了一个由标记类实例实现的类型;标记注解则没有定义
这样的类型。 标记接口类型的存在允许在编译时捕获错误,如果使用
标记注解,则直到运行时才能捕获错误。 标记接口对于标记注解的另
一个优点是可以更精确地定位目标。 标记注解优于标记接口的主要优
点是它们是更大的注解工具的一部分。 因此,标记注解允许在基于注
解的框架中保持一致性
什么时候应该使用标记注解,什么时候应该使用标记接口?显然,如
果标记是应用于除类或接口以外的任何程序元素,则必须使用注解,
因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接
口,那么问自己问题:「可能我想编写一个或多个只接受具有此标记
的对象的方法呢?」如果是这样,则应该优先使用标记接口而不是注
解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译
时类型检查的好处。如果你能说服自己,永远不会想写一个只接受带
有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大
量使用注解的框架的一部分,则标记注解是明确的选择。

42.lambda 表达式优于匿名类

 lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入lambda 表达式中。 一行代码对于 lambda 说是理想的,三行代码是合理的最大值。
有些事情你可以用匿名类来做,而却不能用 lambdas 做。 Lambda 仅限于函数式接
口。如果你想创建一个抽象类的实例,你可以使用匿名类来实现,但不能使用
lambda。同样,你可以使用匿名类来创建具有多个抽象方法的接口实例。最后,
lambda 不能获得对自身的引用。在 lambda 中, this 关键字引用封闭实例,这通常
是你想要的。在匿名类中, this 关键字引用匿名类实例。如果你需要从其内部访问
函数对象,则必须使用匿名类。
从 Java 8 开始, lambda 是迄今为止表示小函数对象的最佳方式。 除非必须创建非
函数式接口类型的实例,否则不要使用匿名类作为函数对象。 另外,请记住,
lambda 表达式使代表小函数对象变得如此简单,以至于它为功能性编程技术打开了
一扇⻔,这些技术在 Java 中以前并不实用。

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

如果 lambda 变得太⻓或太复杂,它们也会给你一个结果:你可以从
lambda 中提取代码到一个新的方法中,并用对该方法的引用代替
lambda。你可以给这个方法一个好名字,并把它文档记录下来。
偶尔, lambda 会比方法引用更简洁。这种情况经常发生在方法与
lambda 相同的类中。
许多方法引用是指静态方法,但有 4 种方法没有引用静态方法。其中
两个 Lambda 等式是特定(bound)和任意(unbound)对象方法引
用。在特定对象引用中,接收对象在方法引用中指定。特定对象引用
在本质上与静态引用类似:函数对象与引用的方法具有相同的参数。
在任意对象引用中,接收对象在应用函数对象时通过方法的声明参数
之前的附加参数指定。任意对象引用通常用作流管道(pipelines)中的
映射和过滤方法(条目 45)。最后,对于类和数组,有两种构造方法
引用。构造方法引用用作工厂对象。下表总结了所有五种方法引用:
方法类型引用Method Ref Type 举例 Example Lambda 等式 Lambda Equivalent
Static Integer::parseInt str -> Integer.parseInt(str)
Bound Instant.now()::isAfter Instant then = Instant.now(); t-> then.isAfter(t)
Unbound String::toLowerCase str -> str.toLowerCase()
Class Constructor TreeMap<K, V>::new () -> new TreeMap<K, V>
Array Constructor int[]::new len -> new int[len]
总之,方法引用通常为 lambda 提供一个更简洁的选择。 如果方法引用看起来更简
短更清晰,请使用它们;否则,还是坚持 lambda。

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

在 java.util.Function 中有 43 个接口。不能指望全部记住它们,但是如
果记住了六个基本接口,就可以在需要它们时派生出其余的接口。基本
接口操作于对象引用类型。 Operator 接口表示方法的结果和参数类型
相同。 Predicate 接口表示其方法接受一个参数并返回一个布尔值。
Function 接口表示方法其参数和返回类型不同。 Supplier 接口表示一
个不接受参数和返回值 (或「供应」 ) 的方法。最后, Consumer 表示
该方法接受一个参数而不返回任何东西,本质上就是使用它的参数。六
种基本函数式接口概述如下:
接口 方法 示例
UnaryOperator <T`> T apply(T t) String::toLowerCase
BinaryOperator<T`> BinaryOperator BigInteger::add
Predicate<T`> Predicate Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier<T`> T get() Instant::now
Consumer<T`> void accept(T t) System.out::println
在处理基本类型 int, long 和 double 的操作上,六个基本接口中还有
三个变体。它们的名字是通过在基本接口前加一个基本类型而得到的。
因此,例如,一个接受 int 的 Predicate 是一个 IntPredicate,而一个接
受两个 long 值并返回一个 long 的二元运算符是一个
LongBinaryOperator。除 Function 接口变体通过返回类型进行了参数
化,其他变体类型都没有参数化。例如, LongFunction<int[]> 使用
long 类型作为参数并返回了 int[] 类型。Function 接口还有九个额外的
变体,当结果类型为基本类型时使用。源和结果类型总是不同,因为从
类型到它自身的函数是 UnaryOperator。如果源类型和结果类型都是基
本类型,则使用带有 SrcToResult 的前缀 Function,例如
LongToIntFunction(六个变体)。如果源是一个基本类型,返回结果是一
个对象引用,那么带有 ToObj 的前缀 Function,例如
DoubleToObjFunction (三种变体)。有三个包含两个参数版本的基本功
能接口,使它们有意义: BiPredicate <T, U>, BiFunction <T,U,
R> 和 BiConsumer <T, U>。也有返回三种相关基本类型的
BiFunction 变体: ToIntBiFunction<T, U>, ToLongBiFunction<T,
U> 和 ToDoubleBiFunction <T, U>。 Consumer 有两个变量,它们
带有一个对象引用和一个基本类型: ObjDoubleConsumer <T>,
ObjIntConsumer <T> 和ObjLongConsumer <T>。总共有九个两个参数
版本的基本接口。最后,还有一个 BooleanSupplier 接口,它是
Supplier 的一个变体,它返回布尔值。这是任何标准函数式接口名称中
唯一明确提及的布尔类型,但布尔返回值通过 Predicate 及其四种变体
形式支持。前面段落中介绍的 BooleanSupplier 接口和 42 个接口占所
有四十三个标准功能接口。
你应该什么时候写自己的接口?
当然,如果没有一个标准模块能够满足您的需求,例如,如果需要一个带有三个参
数的 Predicate,或者一个抛出检查异常的 Predicate,那么需要编写自己的代码。
但有时候你应该编写自己的函数式接口,即使与其中一个标准的函数式接口的结构
相同。

@FunctionalInterface 注解。
这种注解在类型类似于 @Override。这是一个程序员意图的陈述,它有三个目的:它告诉读者该类和它的文档,
该接口是为了实现 lambda 表达式而设计的;它使你保持可靠,因为除非只有一个抽象方法,否则接口不会编译; 它
可以防止维护人员在接口发生变化时不小心地将抽象方法添加到接口中。始终使用 @FunctionalInterface注解标注你的函数式接口。

在 Java 已经有了 lambda 表达式,因此必须考虑 lambda 表达式来设计你的 API。
在输入上接受函数式接口类型并在输出中返回它们。一般来说,最好使用
java.util.function.Function 中提供的标准接口,但请注意,在相对罕⻅的情况下,最
好编写自己的函数式接口。

45.明智审慎地使用 Stream

过度使用流使程序难于阅读和维护。
字⺟顺序方法可以使用流重新实现,但基于流的字⺟顺序方法本来不太清楚,更难
以正确编写,并且可能更慢。这些缺陷是由于 Java 缺乏对原始字符流的支持(这并
不意味着 Java 应该支持 char 流;这样做是不可行的)。要演示使用流处理 char 值
的危害,请考虑以下代码:
"Hello world!".chars().forEach(System.out::print);
你可能希望它打印 Hello world!,但如果运行它,发现它打印
721011081081113211911111410810033。这是因为 “Hello world!” .chars() 返回的
流的元素不是 char 值,而是 int 值,因此调用了 print 的int 重载。理想情况下,应该避免使用流来处理 char 值。
有些任务最好使用流来完成,有些任务最好使用迭代来完成。将这两种
方法结合起来,可以最好地完成许多任务。对于选择使用哪种方法进行
任务,没有硬性规定,但是有一些有用的启发式方法。在许多情况下,
使用哪种方法将是清楚的;在某些情况下,则不会很清楚。 如果不确
定一个任务是通过流还是迭代更好地完成,那么尝试这两种方法,看看
哪一种效果更好。

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

流范式中最重要的部分是将计算结构化为一系列转换,其中每个阶段的结果尽可能
接近前一阶段结果的纯函数(pure function)。纯函数的结果仅取决于其输入:它不
依赖于任何可变状态,也不更新任何状态。为了实现这一点,你传递给流操作的任
何函数对象(中间操作和终结操作)都应该没有副作用。
Java 程序员知道如何使用 for-each 循环,而 forEach 终结操作是类似的。但
forEach 操作是终端操作中最不强大的操作之一,也是最不友好的流操作。它是明确
的迭代,因此不适合并行化。 forEach 操作应仅用于报告流计算的结果,而不是用
于执行计算。 有时,将 forEach 用于其他目的是有意义的,例如将流计算的结果添
加到预先存在的集合中
Collectors 的 API 令人生畏:它有 39 个方法,其中一些方法有多达 5 个类型参数。
好消息是,你可以从这个 API 中获得大部分好处,而不必深入研究它的全部复杂
性。对于初学者来说,可以忽略收集器接口,将收集器看作是封装缩减策略
(reductionstrategy)的不透明对象。在此上下文中, reduction 意味着将流的元素
组合为单个对象。收集器生成的对象通常是一个集合(它代表名称收集器)。将流
的元素收集到真正的集合中的收集器非常简单。有三个这样的收集器:
toList()、 toSet() 和toCollection(collectionFactory)。它们分别返回集合、列表和程
序员指定的集合类型。
总之,编程流管道的本质是无副作用的函数对象。这适用于传递给流和相关对象的
所有许多函数对象。终结操作 forEach 仅应用于报告流执行的计算结果,而不是用
于执行计算。为了正确使用流,必须了解收集器。最重要的收集器工厂是 toList,
toSet, toMap, groupingBy 和 join。

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

Collection 或适当的子类型通常是公共序列返回方法的最佳返回类型。数组还使用
Arrays.asList 和Stream.of 方法提供简单的迭代和流访问。如果返回的序列小到足以
容易地放入内存中,那么最好返回一个标准集合实现,例如 ArrayList 或 HashSet。
但是不要在内存中存储大的序列,只是为了将它作为集合返回。
总之,在编写返回元素序列的方法时,请记住,某些用户可能希望将它们作为流处
理,而其他用户可能希望迭代方式来处理它们。尽量适应两个群体。如果返回集合
是可行的,请执行此操作。如果已经拥有集合中的元素,或者序列中的元素数量足
够小,可以创建一个新的元素,那么返回一个标准集合,比如 ArrayList。否则,请
考虑实现自定义集合,就像我们为幂集程序里所做的那样。如果返回集合是不可行
的,则返回流或可迭代的,无论哪个看起来更自然。如果在将来的 Java 版本中,
Stream 接口声明被修改为继承 Iterable,那么应该随意返回流,因为它们将允许流
和迭代处理。

48.谨慎使用流并行

流类库不知道如何并行化此管道并且启发式失败(heuristics fail)。 即使在最
好的情况下,如果源来自 Stream.iterate 方法,或者使用中间操作 limit 方法,并化
管道也不太可能提高其性能。 不要无差别地并行化流管道(stream pipelines)。性
能后果可能是灾难性的
通常,并行性带来的性能收益在 ArrayList、 HashMap、 HashSet 和
ConcurrentHashMap 实例、数组、 int 类型范围和 long 类型的范围的流上最好。这
些数据结构的共同之处在于,它们都可以精确而廉价地分割成任意大小的子程序,
这使得在并行线程之间划分工作变得很容易。所有这些数据结构的共同点的另一个
重要因素是它们在顺序处理时提供了从良好到极好的引用位置(localityof
reference):顺序元素引用在存储器中存储在一块。并行化一个流不仅会导致糟糕
的性能,包括活性失败(liveness failures) ; 它会导致不正确的结果和不可预知的
行为 (安全故障)。

49.检查参数有效性

在 Java 7 中添加的Objects.requireNonNull 方法灵活方便,因此没有理由再手动执
行空值检查。如果愿意,可以指定自定义异常详细消息。在 Java 9 中,
java.util.Objects 类 中 添 加 了 范 围 检 查 工 具。 此 工 具 包 含 三 个 方 法:
checkFromIndexSize, checkFromToIndex和checkIndex。 此 工 具 不 如 空 检 查
方 法 灵 活。 它 不允许指定自己的异常详细消息,它仅用于列表和数组索引。它不
处理闭合范围(包含两个端点)。但如果它能满足你的需要,那就很方便了。
对于未导出的方法,作为包的作者,控制调用方法的环境,这样就可以并且应该保
只传入有效的参数值。因此,非公共方法可以使用断言检查其参数,如下所示:
// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {assert a != null;assert offset >= 0 && offset <= a.length;assert length >= 0 && length <= a.length - offset;
... // Do the computation
}
本质上,这些断言声称断言条件将成立,无论其客户端如何使用封闭包。与普通的
有效性检查不同,断言如果失败会抛出AssertionError。与普通的有效性检查不同的
是,除非使用-ea(或者-enableassertions)标记传递给 java 命令来启用它们,否则
它们不会产生任何效果,本质上也不会产生任何成本。

50.必要时进行防御性拷⻉*

必须防御性地编写程序,假定类的客户端尽力摧毁类其不变量。在可能的情况下,
应该使用不可变对象作为对象的组件,这样就不必担心防御性拷⻉(详⻅第 17
条)。
记住,非零⻓度数组总是可变的。因此,在将内部数组返回给客户端之前,应该始
终对其进行防御性拷⻉。或者,可以返回数组的不可变视图。
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性
地拷⻉这些组件。如果拷⻉的成本太高,并且类信任它的客户端不会不适当地修改
组件,则可以用文档替换防御性拷⻉,该文档概述了客户端不得修改受影响组件的
责任。

51.仔细设计方法签名

  • 仔细选择方法名名称。名称应始终遵守标准命名约定
  • 不要过分地提供方便的方法。每种方法都应该“尽其所能”。
  • 避免过⻓的参数列表。目标是四个或更少的参数。
  • 对于参数类型,优先选择接口而不是类
  • 与布尔型参数相比,优先使用两个元素枚举类型,除非布尔型参数的含义在方法名中是明确的。

52.明智审慎地使用重载

重载(overloaded)方法之间的选择是静态的,而重写(overridden)
方法之间的选择是动态的。应该避免混淆使用重载。
根据调用方法的对象的运行时类型,在运行时选择正确版本的重写方法。作为提
醒,当子类包含与父类中具有相同签名的方法声明时,会重写此方法。如果在子类
中重写实例方法并且在子类的实例上调用,则无论子类实例的编译时类型如何,都
会执行子类的重写方法。
一个安全和保守的策略是永远不要导出两个具有相同参数数量的重载。不要在相同参数位置重载采用不同函数式接口的方法。
总而言之,仅仅可以重载方法并不意味着应该这样做。通常,最好避免重载具有相
同数量参数的多个签名的方法。在某些情况下,特别是涉及构造方法的情况下,可
能无法遵循此建议。在这些情况下,至少应该避免通过添加强制转换将相同的参数
集传递给不同的重载。如果这是无法避免的,例如,因为要对现有类进行改造以实
现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果
做不到这一点,程序员将很难有效地使用重载方法或构造方法,也无法理解为什么
它不能工作。

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

性能关键的情况下使用可变参数时要小心。每次调用可变参数方法都会导致数组分
配和初始化。如果你从经验上确定负担不起这个成本,但是还需要可变参数的灵活
性,那么有一种模式可以让你⻥与熊掌兼得。假设你已确定 95%的调用是三个或更
少的参数的方法,那么声明该方法的五个重载。每个重载方法包含 0 到 3 个普通参
数,当参数数量超过 3 个时,使用一个可变参数方法。在所有参数数量超过 3 个的
方法调用中,只有 5% 的调用需要支付创建数组的成本。与大多数性能优化一样,
这种技术通常不太合适,但一旦真正需要的时候,它是一个救星
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }
当需要使用可变数量的参数定义方法时,可变参数非常有用。在使用可变参数前加
上任何必需的参数,并注意使用可变参数的性能后果。

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

数组的情况与集合的情况相同。永远不要返回 null,而是返回⻓度为零的数组。
永远不要返回 null 来代替空数组或集合。它使你的 API 更难以使用,更容易出错,
并且没有性能优势

55.明智审慎地返回 Optional

在 Java 8 中,除了抛出异常和返回null还有第三种方法来编写可能无法
返回任何值的方法。Optional<T>类表示一个不可变的容器,它可以包含
一个非 null 的T引用,也可以什么都不包含。不包含任何内容的
Optional 被称为空(empty)。非空的包含值称的 Optional 被称为存在
(present)。 Optional 的本质上是一个不可变的集合,最多可以容纳
一个元素。 Optional<T>没有实现Collection<T>接口,但原则上是可
以。
// Returns maximum value in collection - throws exception if empty
public static <E extends Comparable<E>> E max(Collection<E> c) {if (c.isEmpty())throw new IllegalArgumentException("Empty collection");E result = null;for (E e : c)if (result == null || e.compareTo(result) > 0)result = Objects.requireNonNull(e);return result;
}
// Returns maximum value in collection as an Optional<E>
public static <E extends Comparable<E>>Optional<E> max(Collection<E> c) {if (c.isEmpty())return Optional.empty();E result = null;for (E e : c)if (result == null || e.compareTo(result) > 0)result = Objects.requireNonNull(e);return Optional.of(result);
}
如上所⻅,返回 Optional 很简单。你所要做的就是使用适当的静态工厂创建
Optional。在这个程序中,我们使用两个: Optional.empty() 返回一个空的
Optional, Optional.of(value) 返回一个包含给定非null 值的 Optional。将 null 传递
给 Optional.of(value) 是一个编程错误。永远不要通过返回 Optional 的方法返回一个
空值:它破坏 Optional 设计的初衷。
那么,如何选择返回 Optional 而不是返回 null 或抛出异常呢? Optional在本质上类
似于检查异常(checked exceptions)(详⻅第 71 条),因为它们迫使 API 的用户
面对可能没有返回任何值的事实。抛出未检查的异常或返回 null 允许用户忽略这种
可能性,从而带来潜在的可怕后果。但是,抛出一个检查异常需要在客户端中添加
额外的样板代码。
Optional 提供 isPresent() 方法,可以将其视为安全阀。如果Optional 包含值,则返
回 true;如果为空,则返回 false。你可以使用此方法对可选结果执行任何喜欢的处
理,但请确保明智地使用它。 isPresent 的许多用途都可以被上面提到的一种方法(orElse("...")等)所替代。生成的代码通常更短、更清晰、更符合习惯。
并不是所有的返回类型都能从 Optional 的处理中获益。 容器类型,包
括集合、映射、 Stream、数组和Optional,不应该封装在 Optional
中。与其返回一个空的Optional<List<T>>,不还如返回一个空的
List<T>(详⻅第 54 条)。
那么什么时候应该声明一个方法来返回 Optional<T> 而不是 T 呢?通
常, 如果可能无法返回结果,并且在没有返回结果,客户端还必须执
行特殊处理的情况下,则应声明返回 Optional <T> 的方法。也就是说,
返回Optional<T> 并非没有成本。 Optional 是必须分配和初始化的对
象,从Optional 中读取值需要额外的迂回。这使得 Optional 不适合在
某些性能关键的情况下使用。特定方法是否属于此类别只能通过仔细测
量来确定。
类库设计人员认为为基本类型 int、 long 和 double 提供类似 Option<T> 是合
适的。这些 Option 是 OptionalInt、 OptionalLong 和 OptionalDouble。它们包含
Optional<T>上的大多数方法,但不是所有方法。因此,除了「次要基本类型
(minor primitive types)」 Boolean, Byte,Character, Short 和 Float 之外,
永远不应该返回装箱的基本类型的 Optional。
总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调
用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该
意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回
null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用
Optional

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

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

用于最小化局部变量作用域的最强大的技术是再首次使用的地方声明它。几乎每个
局部变量声明都应该包含一个初始化器。优先选择 for 循环而不是 while 循环。

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

有三种常⻅的情况是你不能分别使用 for-each 循环的:

  • 有损过滤(Destructive filtering) ——如果需要遍历集合,并删除指定选元素,则需要使用显式迭代器,以便可以调用其 remove 方法。通常可以使用在 Java 8 中添加的 Collection 类中的 removeIf 方法,来避免显式遍历
  • 转换——如果需要遍历一个列表或数组并替换其元素的部分或全部值,那么需要列表迭代器或数组索引来替换元素的值
  • 并行迭代——如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步进行 (如上面错误的 card 和 dice 示例中无意中演示的那样)。

如果发现自己处于这些情况中的任何一种,请使用传统的 for 循环,并警惕本条目中提到的陷阱。

59.了解并使用库

通过使用标准库,你可以利用编写它的专家的知识和以前使用它的人的经验.
每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包。
Collections 框架和 Streams 库(详⻅第 45 到 48 条)应该是每个程序员的基本
工具包的一部分, java.util.concurrent 中的并发实用程序也应该是其中的一部分。
总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常⻅的事
情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你
不知道,检查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时
间的推移可能会得到改进。这并不反映你作为一个程序员的能力。规模经济决定了
库代码得到的关注要远远超过大多数开发人员所能承担的相同功能。

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

 使用 BigDecimal、 int 或 long 进行计算。对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。

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

在操作中混合使用基本类型和包装类型时,包装类型就会自动拆箱,这种情况无一
例外。如果一个空对象引用自动拆箱,那么你将得到一个NullPointerException。
只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更
快。如果必须使用包装类型,请小心! 自动装箱减少了使用包装类型的冗⻓,但没
有减少危险。 当你的程序使用 == 操作符比较两个包装类型时,它会执行标识比
较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类
型计算时,它将进行拆箱, 当你的程序执行拆箱时,将抛出
NullPointerException。 最后,当你的程序将基本类型装箱时,可能会导致代价高
昂且不必要的对象创建

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

  • 字符串是其他值类型的糟糕替代品。
  • 字符串是枚举类型的糟糕替代品。
  • 字符串是聚合类型的糟糕替代品。
  • 字符串不能很好地替代 capabilities
总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果
使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符
串经常被误用的类型包括基本类型、枚举和聚合类型。

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

字符串连接操作符 (+) 是将几个字符串组合成一个字符串的简便方法。使用 字符串
串联运算符重复串联 n 个字符串需要n 的平方级时间。 要获得能接受的性能,请使
用 StringBuilder 代替 String。
道理很简单: 不要使用字符串连接操作符合并多个字符串,除非性能无关紧要。否
则使用 StringBuilder 的append 方法。或者,使用字符数组,再或者一次只处理一
个字符串,而不是组合它们。

64.通过接口引用对象

Set<Son> sonSet = new LinkedHashSet<>();

65.接口优于反射

核心反射机制 java.lang.reflect 提供对任意类的编程访问。给定一个 Class 对象,你
可以获得 Constructor、Method 和 Field 实例,分别代表了该 Class 实例所表示的
类的构造器、方法和字段。这些对象提供对类的成员名、字段类型、方法签名等的
编程访问。

反射的缺点:

  • 你失去了编译时类型检查的所有好处, 包括异常检查如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施。
  • 执行反射访问所需的代码既笨拙又冗⻓。 写起来很乏味,读起来也很困难。
  • 性能降低。 反射方法调用比普通方法调用慢得多。到底慢了多少还很难说,因为有很多因素在起作用。在我的机器上,调用一个没有输入参数和返回 int类型的方法时,用反射执行要慢 11 倍。
通过非常有限的形式使用反射,你可以获得反射的许多好处,同时花费的代价很少。

66.明智审慎地本地方法

  • 为了提高性能,很少建议使用本地方法。

67.明智审慎地进行优化

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

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

  • 异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程
  • 设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异

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

决定使用受检异常还是非受检异常时,主要的原则是:

  • 如果期望调用者能够合理的恢复程序运行,对于这种情况就应该使用受检异常。
  • 用运行时异常来表明编程错误。
  • 错误(Error)往往被 JVM 保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件。由于这已经是个几乎被普遍接受的管理,因此最好不需要在实现任何新的 Error 的子类。因此, 你实现的所有非受检的 throwable 都应该是 RuntimeExceptiond子类(直接或者间接的)。不仅不应该定义 Error 的子类,也不应该抛出 AssertionError 异常。

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

抛出受检异常的方法不能直接在 Stream 中使用
在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使
API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果
可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个 optional 值。
当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。

72.优先使用标准的异常

重用标准的常有多个好处。其中最主要的好处是,它使 API 更易于学习和使用,因
为它与程序员已经熟悉的习惯用法一致。第二个好处是,对于用到这些 API 程序而
言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常。最后(也
是最不重要的)一点是,异常类越少,意味着内存占用(footprint)就越小,装载这
些类的时间开销也越少

不要直接重用 Exception、 RuntimeException、 Throwable 或者 Error。

异常 使用场合
IllegalArgumentException 非 null 的参数值不正确
IllegalStateException 不适合方法调用的对象状态
NullPointerException 在禁止使用 null 的情况下参数值为 null
IndexOutOfBoundsExecption 下标参数值越界
ConcurrentModificationException 在禁止并发修改的情况下,检测到对象的并发修改
UnsupportedOperationException 对象不支持用户请求的方法

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

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。
这种做法称为异常转译(exception translation),如下代码所示:
/* Exception Translation */
try {... /* Use lower-level abstraction to do our bidding */
} catch (LowerLevelException e) {throw new HigherLevelException(...);
}
一种特殊的异常转译形式称为异常链(exception chaining),如果低
层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合
适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法
(Throwable的 getCause 方法)来获得低层的异常:
// Exception Chaining
try {... // Use lower-level abstraction to do our bidding
} catch (LowerLevelException cause) {throw new HigherLevelException(cause);
}
高层异常的构造器将原因传到支持链(chaining-aware)的超级构造器,因此它最终
将被传给 Throwable的其中一个运行异常链的构造器,例如 Throwable(Throwable) :
/* Exception with chaining-aware constructor */
class HigherLevelException extends Exception {HigherLevelException( Throwable cause ) {super(cause);
}
}
大多数标准的异常都有支持链的构造器。对于没有支持链的异常,可以利用
Throwable 的 initCause 方法设置原因。异常链不仅让你可以通过程序(用
getCause)访问原因,还可以将原因的堆战轨迹集成到更高层的异常中。尽管异常
转译与不加选择地从低层传递异常的做法相比有所改进,但是也不能滥用它。
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转
译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适
的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最
佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析

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

始终要单独地声明受检异常, 并且利用 Javadoc 的 @throws 标签,准
确地记录下抛出每个异常的条件。如果一个类中的许多方法出于同样的原因而抛出
同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的, 而不是
为每个方法单独建立文档。

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

为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。

76.保持失败原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。 具有这种属性的方法被称为具有失败原子性
  • 获得失败原子性最常⻅的办法是,在执行操作之前检查参数的有效性
  • 一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
  • 第三种获得失败原子性的办法是,在对象的一份临时拷⻉上执行操作,当操作完成之后再用临时拷⻉中的结果代替对象的内容。
  • 最后一种获得失败原子性的办法远远没有那么常用,做法是编写一段恢复代码(recovery code),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上。

77.不要忽略异常

空的 catch 块会使异常达不到应有的目的。
如果选择忽略异常, catch 块中应该包含一条注释,说明为什么可以这么做,并且
变量应该命名为 ignored:
Future<Integer> f = exec.submit(planarMap::chromaticNumber);int numColors = 4; // Default: guaranteed sufficient for any map
try {numColors = f.get( 1L, TimeUnit.SECONDS );
} catch ( TimeoutException | ExecutionException ignored ) {// Use default: minimal coloring is desirable, not required
}

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

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。
千万不要使用 Thread.stop 方法。 要阻止一个线程妨碍另一个线程,建议的做法是
让第一个线程轮询(poll )一个 boolean 字段,这个字段一开始为 false ,但是可以
通过第二个线程设置为 true ,以表示第一个线程将终止自己。
多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。 如果没
有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可
变数据会造成程序的活跃性失败(livenessfailure )和安全性失败(safety failure
)。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为
在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互
斥, volatile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些
技巧。

79.避免过度同步

为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放
弃对客户端的控制。
通常来说,应该在同步区域内做尽可能少的工作。 获得锁,检查共享数据,根据需
要转换数据,然后释放锁。如果你必须要执行某个很耗时的动作,则应该设法把这
个动作移到同步区域的外面
为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。更通俗地
讲,要尽量将同步区字段内部的工作量限制到最少。当你在设计一个可变类的时
候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永
远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,
才应该这么做,同时还应该将这个决定清楚地写到文档中

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

 java.util.concurrent 。这个字包中包含了一个Executor Framework 它是一个很灵活的基于接口的任务执行工具。为特殊的应用程序选择 executor service 是很有技巧的。如果编写的是小程序,或者是轻量负载的服务器,使用 Executors.newCachedThreadPool 通常是个不错的选择,因为它不需要配置,并且一般情况下能够「正确地完成工作」。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了!在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的 CPU 都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。因此,在大负载的产品服务器中,最好使用 Executors.newFixedThreadPool ,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用 ThreadPoolExecutor 类。不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。当直接使用线程时, Thread 是既充当工作单元,又是执行机制。在 Executor Framework 中,工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务(task)。任务有两种: Runnable 及其近亲 Callable (它与 Runnable 类似,但它会返回值,并且能够抛出任意的异常)。执行任务的通用机制是 executor service 。如果你从任务的⻆度来看问题,并让一个 executor service 替你执行任务,在选择适当的执行策略方面就获得了极大的灵活性。本质上, Executor框架执行的功能与 Collections 框架聚合(aggregation)的功能相同。在 Java 7 中, Executor 框架被扩展为支持 fork-join 任务,这些任务是通过一种称作 fork-join 池的特殊 executor 服务运行的。 fork-join 任务用 ForkJoinTask 实例表示,可以被分成更小的子任务,包含ForkJoinPool 的线程不仅要处理这些任务,还要从另一个线程中“偷”任务,以确保所有的线程保持忙碌,从而提高 CPU 使用率、提高吞吐量,并降低延迟。 fork-join 任务的编写和调优是很有技巧的。并行流 Parallelstreams (详⻅第 48 条)是在 fork join 池上编写的,我们不费什么力气就能享受到它们的性能优势,前提是假设它们正好适用于我们手边的任务。

81.并发工具优于 wait 和 notify

既然正确地使用 wait 和 notify 比较困难,就应该用更高级的并发工具来代替。
java.util.concurrent 中更高级的工具分成三类: Executor Framework 、并发集合(
Concurrent Collection)以及同步器(Synchronizer)
并发集合为标准的集合接口(如 List 、 Queue 和 Map )提供了高性能的并发实
现。为了提供高并发性,这些实现在内部自己管理同步(详⻅第 79 条)。因此,
并发集合中不可能排除并发活动;将它锁定没有什么作用,只会并发集合为标准的
集合接口(如 List 、 Queue 和 Map )提供了高性能的并发实现。为了提供高并发
性,这些实现在内部自己管理同步(详⻅第 79 条)。因此, 并发集合中不可能排
除并发活动;将它锁定没有什么作用,只会使程序的速度变慢。
同步器(Synchronizer)是使线程能够等待另一个线程的对象,允许它们协调动
作。最常用的同步器是CountDownLatch 和 Semaphore 。较不常用的是
CyclicBarrier 和 Exchanger 。功能最强大的同步器是 Phaser 。
简而言之,直接使用 wait 方法和 notify 方法就像用“并发汇编语言”进行编程一样,
而 java.util.concurrent 则提供了更高级的语言。 没有理由在新代码中使用 wait 方法
和 notify 方法,即使有,也是极少的。 如果你在维护使用 wait 方法和 notify 方法的
代码,务必确保始终是利用标准的模式从 while 循环内部调用 wait 方法。一般情况
下,应该优先使用 notifyAll 方法,而不是使用 notify 方法。如果使用notify 方法,请
一定要小心,以确保程序的活性

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

 要启用安全的并发使用,类必须清楚地记录它支持的线程安全级别。 下面的列表总结了线程安全级别。它并非详尽无遗,但涵盖以下常⻅情况:
  • 不可变的 — 这个类的实例看起来是常量。不需要外部同步。示例包括 String、 Long 和 BigInteger(详⻅第 17 条)。
  • 无条件线程安全 — 该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。
  • 有条件的线程安全 — 与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。
  • 非线程安全 — 该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap
  • 线程对立 — 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。
Lock 字段应该始终声明为 final。 无论使用普通的监视器锁(如上所示)还是
java.util.concurrent 包中的锁,都是这样。
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属
性。 synchronized 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪
些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编
写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保
护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本
中采用复杂的并发控制方式。

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

与大多数优化一样,延迟初始化的最佳建议是「除非需要,否则不要这样做」(详
⻅第 67 条)。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代
价是增加了访问延迟初始化字段的成本。根据这些字段中最终需要初始化的部分、
初始化它们的开销以及初始化后访问每个字段的频率,延迟初始化实际上会损害性
能(就像许多「优化」一样)。延迟初始化也有它的用途。如果一个字段只在类的
一小部分实例上访问,并且初始化该字段的代价很高,那么延迟初始化可能是值得
的。唯一确定的方法是以使用和不使用延迟初始化的效果对比来度量类的性能。
  • 在大多数情况下,常规初始化优于延迟初始化。
  • 如果使用延迟初始化来取代初始化的循环(circularity),请使用同步访问器
  • 如果需要在静态字段上使用延迟初始化来提高性能,使用 lazy initialization holder class 模式。这个用法可保证一个类在使用之前不会被初始化 [JLS, 12.4.1]。它是这样的:
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
总之,您应该正常初始化大多数字段,而不是延迟初始化。如果必须延迟初始化字
段以实现性能目标或为了破坏友有害的初始化循环,则使用适当的延迟初始化技
术。对于字段,使用双重检查模式;对于静态字段,则应该使用 the lazy
initialization holder class idiom。例如,可以容忍重复初始化的实例字段,您还可以
考虑单检查模式。

84.不要依赖线程调度器

任何依赖线程调度器来保证正确性或性能的程序都可能是不可移植的

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

避免序列化利用的最好方法是永远不要反序列化任何东西。没有理由在你编写的任何新系统中使用 Java 序列化
序列化是危险的,应该避免。如果你从头开始设计一个系统,可以使用跨平台的结
构化数据,如 JSON或 protobuf。不要反序列化不可信的数据。如果必须这样做,
请使用对象反序列化过滤,但要注意,它不能保证阻止所有攻击。避免编写可序列
化的类。如果你必须这样做,一定要非常小心。

86.非常谨慎地实现 Serializable

  • 实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低更改该类实现的灵活性。
  • 实现 Serializable 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性
  • 实现 Serializable 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。

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

  • 在没有考虑默认序列化形式是否合适之前,不要接受它。
  • 如果对象的物理表示与其逻辑内容相同,则默认的序列化形式可能是合适的。
  • 即使你认为默认的序列化形式是合适的,你通常也必须提供 readObject 方法来确保不变性和安全性。

当对象的物理表示与其逻辑数据内容有很大差异时,使用默认的序列化形式有四个缺点:

  • 它将导出的 API 永久地绑定到当前的内部实现。
  • 它会占用过多的空间。
  • 它会消耗过多的时间。
  • 它可能导致堆栈溢出。

transient 修饰符表示要从类的默认序列化表单中省略该
实例字段。在决定使字段非 transient 之前,请确信它的值是对象逻辑状态的一部分。 如果使用默认的序列化形式,并且标记了一个或多个字段为 transient,请记住,当反序列化实例时,这些字段将初始化为默认值:对象引用字段为 null,数字基本类型字段为 0,布尔字段为 false。无论选择哪种序列化形式,都要在编写的每个可序列化类中声明显式的序列版本 UID。

总而言之,如果你已经决定一个类应该是可序列化的(详⻅第 86 条),那么请仔细
考虑一下序列化的形式应该是什么。只有在合理描述对象的逻辑状态时,才使用默
认的序列化形式;否则,设计一个适合描述对象的自定义序列化形式。设计类的序
列化形式应该和设计导出方法花的时间应该一样多,都应该严谨对待(详⻅第 51
条)。正如不能从未来版本中删除导出的方法一样,也不能从序列化形式中删除字
段;必须永远保存它们,以确保序列化兼容性。选择错误的序列化形式可能会对类
的复杂性和性能产生永久性的负面影响。

88.保护性的编写 readObject 方法

当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段
包含了这样的对象引用,就必须做保护性拷⻉,这是非常重要的。
总而言之,在编写 readObject 方法的时候,都要这样想:你正在编写一个公有的构
造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这
个字节流一定代表着一个真正被序列化的实例。虽然在本条目的例子中,类使用了
默认的序列化形式,但是所有讨论到的有可能发生的问题也同样适用于自定义序列
化形式的类。下面以摘要的形式给出一些指导方针,有助于编写出更健壮的
readObject 方法。- 类中的对象引用字段必须保持为私有属性,要保护性的拷⻉这些字段
中的每个对象。不可变类中的可变组 件就属于这一类别- 对于任何约束条件,如果检查失败就抛出一个InvalidObjectException
异常。这些检查动作应该跟在所有的保护性拷⻉之后。- 如果整个对象图在被反序列化之后必须进行验证,就应该使用
ObjectInputValidation 接口(本书 没有讨论)- 无论是直接方法还是间接方法,都不要调用类中任何可被覆盖的方法

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

如果依赖readResolve 进行实例控制,带有对象引用类型的所有实例字段都必须声
明为 transient。
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,
同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 readResolve 方
法,并确保该类的所有实例化字段都被基本类型,或者是 transient 的。

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

当你发现必须在一个不能被客户端拓展的类上面编写 readObject 或者 writeObject
方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象
序列化时,这种模式是最容易的方法
序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精
确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),
它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只是从它的
参数中复制数据:它不需要进行任何一致性检验或者保护性拷⻉。从设计的⻆度
看,列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理
都必须声明实现 Serializable接口。
当你发现必须在一个不能被客户端拓展的类上面编写 readObject 或者 writeObject
方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象
序列化时,这种模式是最容易的方法。

Effective java学习笔记相关推荐

  1. Java:Effective java学习笔记之 考虑实现Comparable 接口

    Java 考虑实现Comparable 接口 考虑实现Comparable 接口 1.Comparable接口 2.为什么要考虑实现Comparable接口 3.compareTo 方法的通用约定 4 ...

  2. Effective Java学习笔记之第5条 避免创建不必要的对象

    第5条 避免创建不必要的对象 一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的对象. 反面例子: String s = new String("stringette&quo ...

  3. Effective Java 学习笔记(第53条:接口优先于反射机制)

    核心反射机制(core reflection facility)java.lang.reflect,提供了"通过程序来访问关于已装载的类的信息"的能力. 丧失了编译时类型检查的好处 ...

  4. Effective Java 学习笔记 1

    Item 1: Consider static factory methods instead of constructors  (多考虑使用静态工厂方法而不是构造方法) 使用静态工厂方法有以下几点好 ...

  5. Effective Java学习笔记之第6条 消除过期的引用对象

    第6条 消除过期的引用对象 所谓过期引用是指永远也不会再被解除的引用. 一旦对象引用已经过期,只需清空这些引用即可. public Object pop() { if (size == 0) { th ...

  6. Java学习笔记---多线程并发

    Java学习笔记---多线程并发 (一)认识线程和进程 (二)java中实现多线程的三种手段 [1]在java中实现多线程操作有三种手段: [2]为什么更推荐使用Runnable接口? [3][补充知 ...

  7. java学习笔记11--Annotation

    java学习笔记11--Annotation Annotation:在JDK1.5之后增加的一个新特性,这种特性被称为元数据特性,在JDK1.5之后称为注释,即:使用注释的方式加入一些程序的信息. j ...

  8. java学习笔记13--反射机制与动态代理

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note13.html,转载请注明源地址. Java的反射机制 在Java运行时环境中,对于任意 ...

  9. 准备写java学习笔记

    准备写java学习笔记 java int 转载于:https://blog.51cto.com/cryingcloud/1975267

  10. Java学习笔记--StringTokenizer的使用

    2019独角兽企业重金招聘Python工程师标准>>> Java Tips: 使用Pattern.split替代String.split String.split方法很常用,用于切割 ...

最新文章

  1. pytorch学习 中 torch.squeeze() 和torch.unsqueeze()的用法
  2. 系统芯片(SOC)架构- Aviral Mittal
  3. python 字典_Python数据结构:字典那些事儿
  4. linux里的dd权限不够怎么办,Linux dd 遇到 容量不足 的 resize 解法
  5. 飞鸽传书 宣传单和电话说辞
  6. 小目标检测的福音:Stitcher,简单又有效
  7. python八大排序算法 间书_Python 八大排序算法速度比较
  8. OkHttp之BridgeInterceptor简单分析
  9. JavaScript、canvas小球加速和减速运动
  10. 前端 express使用教程
  11. app开发流程:手机软件开发app的6个步骤
  12. jsonp 跨域 java_Java web支持jsonp跨域
  13. AR学习笔记(四):相关文献查阅
  14. (转载)小米9开发者选项在哪里怎么打开
  15. 金笛JDMAIL邮件服务器证券行业邮件归档解决方案
  16. 数据库增量备份 - DB INCR DB FULL
  17. 英语学习经验分享(四六级、竞赛、口语)
  18. 算法分析与设计期末总结
  19. 成都旅游攻略 —— 拜水都江堰 问道青城山
  20. Adobe Flash CS6 配置错误,错误代码:1

热门文章

  1. 乔巴机器人 番外篇_超神学院之暮光之眼
  2. 35岁学太极系列(1)-缘起功夫梦
  3. android 11.0 12.0去掉前置摄像头闪光灯功能
  4. 1158: 零基础学C/C++158——删除中间的*
  5. Python数据分析与挖掘实战第三章笔记之贡献度分析代码
  6. 手机网页通过js打开app
  7. java计算机毕业设计交通事故档案管理系统源程序+mysql+系统+lw文档+远程调试
  8. 如何缓解眼疲劳(眼疲劳敷眼睛是热敷还是冷敷)
  9. cogs2398 切糕 最小割
  10. Centos7安装elasticsearch7.14.0遇到问题(无法远程访问;内存小;bootstrap checks failed)