mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2025-03-16 04:10:35 +08:00
parent
3a5a953b7d
commit
5116927665
@ -1,22 +1,19 @@
|
||||
# 17. 最小化可变性
|
||||
|
||||
不可变类简单来说是它的实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。 Java 平台类库包含许多不可变的类,包括 `String` 类,基本类型包装类以及 `BigInteger` 类和 `BigDecimal` 类。 有很多很好的理由:不可变类比可变类更容易设计,实现和使用。 他们不太容易出错,更安全。
|
||||
不可变类简单来说是其实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。 Java 平台类库包含许多不可变的类,包括 `String` 类、基本类型包装类以及 `BigInteger` 类和 `BigDecimal` 类。 有很多很好的理由:不可变类比可变类更易于设计,实现和使用。 他们不容易出错,并且更安全。
|
||||
|
||||
要使一个类不可变,请遵循以下五条规则:
|
||||
要使一个类成为不可变类,请遵循以下五条规则:
|
||||
|
||||
1. **不要提供修改对象状态的方法(也称为 mutators)。**
|
||||
2. **确保这个类不能被继承。** 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化通常是通过 `final` 修饰类,但是我们稍后将讨论另一种方法。
|
||||
3. **把所有属性设置为 final。** 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06,16] 所述。
|
||||
4. **把所有的属性设置为 private。** 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 `final` 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(详见第 15 和 16 条)。
|
||||
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。
|
||||
1. **不要提供修改对象状态的方法(也称为 mutators,设值方法)。**
|
||||
2. **确保这个类不能被继承。** 这可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏类的不可变行为。 防止子类化,通常是通过 `final` 修饰类,但是我们稍后将讨论另一种方法。
|
||||
3. **把所有字段设置为 final。** 通过系统强制执行的方式,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用在缺乏同步机制的情况下从一个线程传递到另一个线程,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06 16] 所述。
|
||||
4. **把所有的字段设置为 private。** 这可以防止客户端获得对字段引用的可变对象的访问权限,并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公有的 `final` 字段或对不可变对象的引用,但不建议这样做,因为这样使得在以后的版本中无法再改变内部的表示状态(详见第 15 和 16 条)。
|
||||
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。
|
||||
|
||||
|
||||
|
||||
以前条目中的许多示例类都是不可变的。 其中这样的类是条目 11 中的 `PhoneNumber` 类,它具有每个属性的访问方法(accessors),但没有相应的设值方法(mutators)。 这是一个稍微复杂一点的例子:
|
||||
以前条目中的许多示例类都是不可变的。 其中一个例子是条目 11 中的 `PhoneNumber` 类,它具有每个字段的访问方法(accessors),但没有相应的设值方法(mutators)。下面一个稍微复杂一点的例子:
|
||||
|
||||
```java
|
||||
// Immutable complex number class
|
||||
|
||||
public final class Complex {
|
||||
|
||||
private final double re;
|
||||
@ -69,7 +66,6 @@ public final class Complex {
|
||||
// See page 47 to find out why we use compare instead of ==
|
||||
return Double.compare(c.re, re) == 0
|
||||
&& Double.compare(c.im, im) == 0;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -84,11 +80,11 @@ public final class Complex {
|
||||
}
|
||||
```
|
||||
|
||||
这个类代表了一个复数(包含实部和虚部的数字)。 除了标准的 `Object` 方法之外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何创建并返回一个新的 `Complex` 实例,而不是修改这个实例。 这种模式被称为函数式方法,因为方法返回将操作数应用于函数的结果,而不修改它们。 与其对应的过程(procedural)或命令(imperative)的方法相对比,在这种方法中,将一个过程作用在操作数上,导致其状态改变。 请注意,方法名称是介词(如 plus)而不是动词(如 add)。 这强调了方法不会改变对象的值的事实。 `BigInteger` 和 `BigDecimal` 类没有遵守这个命名约定,并导致许多使用错误。
|
||||
这个类代表了一个复数(包含实部和虚部的数字)。 除了标准的 `Object` 方法之外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何创建并返回一个新的 `Complex` 实例,而不是修改这个实例。 这种模式被称为函数式方法,因为方法返回将操作数应用于函数的结果,而不修改它们。 与其对应的过程式的(procedural)或命令式的(imperative)的方法相对比,在这种方法中,将一个过程作用在操作数上,导致其状态改变。 请注意,方法名称是介词(如 plus)而不是动词(如 add)。 这强调了方法不会改变对象的值的事实。 `BigInteger` 和 `BigDecimal` 类没有遵守这个命名约定,并导致许多使用错误。
|
||||
|
||||
如果你不熟悉函数式方法,可能会显得不自然,但它具有不变性,具有许多优点。 **不可变对象很简单。** 一个不可变的对象可以完全处于一种状态,也就是被创建时的状态。 如果确保所有的构造方法都建立了类不变量,那么就保证这些不变量在任何时候都保持不变,使用此类的程序员无需再做额外的工作。 另一方面,可变对象可以具有任意复杂的状态空间。 如果文档没有提供由设置(mutator)方法执行的状态转换的精确描述,那么可靠地使用可变类可能是困难的或不可能的。
|
||||
如果你不熟悉函数式方法,可能会觉得它显得不自然,但它具有不变性,具有许多优点。 **不可变对象很简单。** 一个不可变的对象可以完全处于一种状态,也就是被创建时的状态。 如果确保所有的构造方法都建立了类不变量,那么就保证这些不变量在任何时候都保持不变,使用此类的程序员无需再做额外的工作。 另一方面,可变对象可以具有任意复杂的状态空间。 如果文档没有提供由设置(mutator)方法执行的状态转换的精确描述,那么可靠地使用可变类可能是困难的或不可能的。
|
||||
|
||||
**不可变对象本质上是线程安全的; 它们不需要同步。** 被多个线程同时访问它们时并不会被破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以**不可变对象可以被自由地共享。** 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。 例如,`Complex` 类可能提供这些常量:
|
||||
**不可变对象本质上是线程安全的;它们不需要同步。** 被多个线程同时访问它们时,不会遭到破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以**不可变对象可以被自由地共享。** 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。 例如,`Complex` 类可能提供这些常量:
|
||||
|
||||
```java
|
||||
public static final Complex ZERO = new Complex(0, 0);
|
||||
@ -103,9 +99,9 @@ public static final Complex I = new Complex(0, 1);
|
||||
|
||||
**不可变对象为其他对象提供了很好的构件(building blocks)**,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成 `Map` 对象的键和 `Set` 的元素,一旦不可变对象作为 `Map` 的键或 `Set` 里的元素,即使破坏了 `Map` 和 `Set` 的不可变性,但不用担心它们的值会发生变化。
|
||||
|
||||
**不可变对象提供了免费的原子失败机制(详见第 76 条)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
|
||||
**不可变对象无偿地提供了的原子失败机制(详见第 76 条)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
|
||||
|
||||
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是如果是大型的对象下。 例如,假设你有一个百万位的 `BigInteger` ,你想改变它的低位:
|
||||
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是是大型的对象下。 例如,假设你有一个百万位的 `BigInteger` ,你想改变它的低位:
|
||||
|
||||
```java
|
||||
BigInteger moby = ...;
|
||||
@ -145,7 +141,7 @@ public class Complex {
|
||||
```
|
||||
这种方法往往是最好的选择。 这是最灵活的,因为它允许使用多个包级私有实现类。 对于驻留在包之外的客户端,不可变类实际上是 `final` 的,因为不可能继承来自另一个包的类,并且缺少公共或受保护的构造方法。 除了允许多个实现类的灵活性以外,这种方法还可以通过改进静态工厂的对象缓存功能来调整后续版本中类的性能。
|
||||
|
||||
当 `BigInteger` 和 `BigDecimal` 被写入时,不可变类必须是有效的 `final`,因此它们的所有方法都可能被重写。不幸的是,在保持向后兼容性的同时,这一事实无法纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是“真实的”`BigInteger` 还是 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(详见第 50 条):
|
||||
当 `BigInteger` 和 `BigDecimal` 刚被编写出来的时候,“不可变类必须是 `final`”的说法还没有得到广泛地理解,因此它们的所有方法都可能被重写。不幸的是,为了保持向后兼容性,这一问题无法得以纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是否为“真实的”`BigInteger` 或者 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(详见第 50 条):
|
||||
|
||||
```java
|
||||
public static BigInteger safeInstance(BigInteger val) {
|
||||
@ -156,11 +152,11 @@ public static BigInteger safeInstance(BigInteger val) {
|
||||
|
||||
在本条目开头关于不可变类的规则说明,没有方法可以修改对象,并且它的所有属性必须是 `final` 的。事实上,这些规则比实际需要的要强硬一些,其实可以有所放松来提高性能。 事实上,任何方法都不能在对象的状态中产生外部可见的变化。 然而,一些不可变类具有一个或多个非 `final` 属性,在第一次需要时将开销昂贵的计算结果缓存在这些属性中。 如果再次请求相同的值,则返回缓存的值,从而节省了重新计算的成本。 这个技巧的作用恰恰是因为对象是不可变的,这保证了如果重复的话,计算会得到相同的结果。
|
||||
|
||||
例如,`PhoneNumber` 类的 `hashCode` 方法(第 53 页的条目 11)在第一次调用改方法时计算哈希码,并在再次调用时对其进行缓存。 这种延迟初始化(详见第 83 条)的一个例子,String 类也使用到了。
|
||||
例如,`PhoneNumber` 类的 `hashCode` 方法(详见第 11 条)在第一次调用改方法时计算哈希码,并在再次调用时 对其进行缓存。 这种延迟初始化(详见第 83 条)的一个例子,String 类也使用到了。
|
||||
|
||||
关于序列化应该加上一个警告。 如果你选择使您的不可变类实现 `Serializable` 接口,并且它包含一个或多个引用可变对象的属性,则必须提供显式的 `readObject` 或 `readResolve` 方法,或者使用 `ObjectOutputStream.writeUnshared` 和 `ObjectInputStream.readUnshared` 方法,即默认的序列化形式也是可以接受的。 否则攻击者可能会创建一个可变的类的实例。 这个主题会在条目 88 中会详细介绍。
|
||||
|
||||
总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 **除非有充分的理由使类成为可变类,否则类应该是不可变的。** 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。 你应该始终使用较小的值对象(如 `PhoneNumber` 和 `Complex`),使其不可变。 (Java 平台类库中有几个类,如 `java.util.Date` 和 `java.awt.Point`,本应该是不可变的,但实际上并不是)。你应该认真考虑创建更大的值对象,例如 `String` 和 `BigInteger` ,设成不可改变的。 只有当你确认有必要实现令人满意的性能(详见第 67 条)时,才应该为不可改变类提供一个公开的可变伙伴类。
|
||||
总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 **除非有充分的理由使类成为可变类,否则类应该是不可变的。** 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。 你应该始终使用较小的值对象(如 `PhoneNumber` 和 `Complex`),使其不可变。(Java 平台类库中有几个类,如 `java.util.Date` 和 `java.awt.Point`,本应该是不可变的,但实际上并不是)。你应该认真考虑创建更大的值对象,例如 `String` 和 `BigInteger` ,设成不可改变的。 只有当你确认有必要实现令人满意的性能(详见第 67 条)时,才应该为不可改变类提供一个公开的可变伙伴类。
|
||||
|
||||
对于一些类来说,不变性是不切实际的。**如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性** 。减少对象可以存在的状态数量,可以更容易地分析对象,以及降低出错的可能性。因此,除非有足够的理由把属性设置为非 `final` 的情况下,否则应该每个属性都设置为 `final` 的。把本条目的建议与条目 15 的建议结合起来,你自然的倾向就是:**除非有充分的理由不这样做,否则应该把每个属性声明为私有 final 的。**
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user