diff --git a/docs/notes/15. 使类和成员的可访问性最小化.md b/docs/notes/15. 使类和成员的可访问性最小化.md index dd7f439..1e9e203 100644 --- a/docs/notes/15. 使类和成员的可访问性最小化.md +++ b/docs/notes/15. 使类和成员的可访问性最小化.md @@ -1,6 +1,6 @@ # 15. 使类和成员的可访问性最小化 -  将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,组件将其内部数据和其他组件的其他实现细节隐藏起来。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。 +  将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。   信息隐藏很重要有很多原因,其中大部分来源于它将组成系统的组件分离开来,允许它们被独立地开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以并行开发。它减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件。虽然信息隐藏本身并不会导致良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题(条目 67),则可以优化这些组件,而不会影响别人的正确的组件。信息隐藏增加了软件重用,因为松耦合的组件通常在除开发它们之外的其他环境中证明是有用的。最后,隐藏信息降低了构建大型系统的风险,因为即使系统不能运行,各个独立的组件也可能是可用的。 @@ -8,18 +8,18 @@   经验法则很简单:**让每个类或成员尽可能地不可访问。** 换句话说,使用尽可能低的访问级别,与你正在编写的软件的对应功能保持一致。 -  对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。如果一个顶层类或接口可以被做为包级私有,那么它应该是。通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的 API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。如果你把它公开,你就有义务永远地支持它,以保持兼容性。 +  对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。如果一个顶层类或接口可以被做为包级私有,那么它应该是。通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的 API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。如果你把它公开,你就有义务永远地支持它,以保持兼容性。 -  如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类 (条目 24)。这将它的可访问性从包级的所有类减少到使用它的一个类。但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的 API 的一部分,而包级私有的顶级类已经是这个包实现的一部分了。 +  如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类 (详见第 24 条)。这将它的可访问性从包级的所有类减少到使用它的一个类。但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的 API 的一部分,而包级私有的顶级类已经是这个包实现的一部分了。 -  对于成员 (属性、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出: +  对于成员(字段、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出: - private —— 该成员只能在声明它的顶级类内访问。 - - package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符 (接口成员除外,它默认是公共的),这是默认访问级别。 - - protected —— 成员可以从被声明的类的子类中访问(受一些限制,JLS,6.6.2),以及它声明的包中的任何类。 + - package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符(接口成员除外,它默认是公共的),这是默认访问级别。 + - protected —— 成员可以从被声明的类的子类中访问(会受一些限制 [JLS, 6.6.2]),以及它声明的包中的任何类。 - public —— 该成员可以从任何地方被访问。 -  在仔细设计你的类的公共 API 之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些属性可以「泄漏(leak)」到导出的 API 中。 +  在仔细设计你的类的公共 API 之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些字段可以「泄漏(leak)」到导出的 API 中。   对于公共类的成员,当访问级别从包私有到受保护级时,可访问性会大大增加。 受保护(protected)的成员是类导出的 API 的一部分,并且必须永远支持。 此外,导出类的受保护成员表示对实现细节的公开承诺(详见第 19 条)。 对受保护成员的需求应该相对较少。 @@ -27,25 +27,22 @@   为了便于测试你的代码,你可能会想要让一个类,接口或者成员更容易被访问。 这没问题。 为了测试将公共类的私有成员指定为包级私有是可以接受的,但是提高到更高的访问级别却是不可接受的。 换句话说,将类,接口或成员作为包级导出的 API 的一部分来促进测试是不可接受的。 幸运的是,这不是必须的,因为测试可以作为被测试包的一部分运行,从而获得对包私有元素的访问。 -  **公共类的实例属性很少公开(详见第 16 条)。** 如果一个实例属性是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在属性中的值的能力。这意味着你放弃了执行涉及该属性的不变量的能力。另外,当属性被修改时,就放弃了采取任何操作的能力,**因此公共可变属性的类通常不是线程安全的** 。即使属性是 final 的,并且引用了一个不可变的对象,通过使它公开,你就放弃切换到不存在属性的新的内部数据表示的灵活性。 +  **公共类的实例字段很少情况下采用 public 修饰(详见第 16 条)。** 如果一个实例字段是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在字段中的值的能力。这意味着你放弃了执行涉及该字段的不变量的能力。另外,当字段被修改时,就放弃了采取任何操作的能力,**因此带有公共可变字段的类通常不是线程安全的** 。即使一个字段是 final 的,并且引用了一个不可变的对象,通过将其公开,你放弃了切换到一个新的内部数据表示的灵活性,而该字段并不存在。 -  同样的建议适用于静态属性,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 属性暴露常量。 按照惯例,这些属性的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些属性包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的属性具有非 final 属性的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。 +  同样的建议适用于静态字段,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 字段暴露常量。 按照惯例,这些字段的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些字段包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的字段具有非 final 字段的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。 -  请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的。** 如果一个类有这样的属性或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源: +  请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组字段,或返回这样一个字段的访问器是错误的。** 如果一个类有这样的字段或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源: ```java // Potential security hole! public static final Thing[] VALUES = { ... }; ``` -  要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组属性的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表: +  要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组字段的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表: ```java private static final Thing[] PRIVATE_VALUES = { ... }; - -public static final List VALUES = - -Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); +public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); ```   或者,可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法: @@ -66,4 +63,4 @@ public static final Thing[] values() {   对于典型的 Java 程序员来说,不仅程序模块所提供的访问保护存在局限性,而且在本质上是很大程度上建议性的;为了利用它,你必须把你的包组合成模块,在模块声明中明确所有的依赖关系,重新安排你的源码树层级,并采取特殊的行动来适应你的模块内任何对非模块化包的访问[Reinhold, 3]。 现在说模块是否会在 JDK 之外得到广泛的使用还为时尚早。 与此同时,除非你有迫切的需要,否则似乎最好避免它们。 -  总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 `final` 属性之外,公共类不应该有公共属性。 确保 `public static final` 属性引用的对象是不可变的。 +  总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 `final` 字段之外,公共类不应该有公共字段。 确保 `public static final` 字段引用的对象是不可变的。 diff --git a/docs/notes/88. 保护性的编写 readObject 方法.md b/docs/notes/88. 保护性的编写 readObject 方法.md index ace8cc2..23c2fb3 100644 --- a/docs/notes/88. 保护性的编写 readObject 方法.md +++ b/docs/notes/88. 保护性的编写 readObject 方法.md @@ -32,11 +32,11 @@ public final class Period { } ``` -  假设决定要把这个类左晨可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式并没有什么不合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。 +  假设你决定要把这个类成为可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式是合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。   问题在于 `readObject` 方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样警惕所有的注意事项。构造器必须检查其参数的有效性(详见 49 条),并且在必要的时候对参数进行保护性拷贝(详见 50 条),同样的,`readObject` 方法也需要这样做。如果 `readObject` 方法无法做到这两者之一,对于攻击者来说要违反这个类的约束条件就相对容易很多。 -  不严格的说, `readObject` 方法是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。 +  不严格的说, `readObject` 方法是一个「用字节流作为唯一参数」的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。   假设我们仅仅在 Period 类的声明加上了 `implements Serializable` 字样。那么这个丑陋的程序代码将会产生一个 Period 实例,他的结束时间比起始时间还早。对于高位 byte 值进行强制类型转换是 Java 缺少 byte 并且做出 byte 类型签名的不幸决定的后果: @@ -76,7 +76,7 @@ public class BogusPeriod { } ``` -  被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出“ Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984”。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。 +  被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出「Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984」。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。   为了修整这个问题,可以为 `Period` 提供一个 `readObject` 方法,该方法首先调用 `defaultReadObject`,然后检查被反序列化之后的对象有效性。如果有效性检查失败,`readObject` 方法就会抛出一个 `InvalidObjectException` 异常,这使得反序列化过程不能成功的完成: @@ -91,7 +91,7 @@ private void readObject(ObjectInputStream s) } ``` -  尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法事:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的“恶意编制的对线引用”。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式: +  尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法事:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的「恶意编制的对线引用」。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式:   ```java public class MutablePeriod { @@ -160,7 +160,7 @@ Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978 Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969 ``` -  虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个“安全性依赖于 Period 的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。 +  虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个「安全性依赖于 Period 的不可变性」的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。   问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性: @@ -185,7 +185,7 @@ Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017 Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017 ``` -  有一个简单的“石蕊”测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。 +  有一个简单的「石蕊」测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。   对于非 final 的可序列化的类,在 `readObject` 方法和构造器之间还有其他类似的地方。与构造器一样,`readObject` 方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以(详见 19 条)。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。这个程序很可能会失败[Bloch05, Puzzle 91]。