第13条 使类和成员的可访问性最小化
信息隐藏 或 封装 information hiding Or encapsulation
优点:
- 解耦:各模块可以独立开发、测试、优化、使用、理解、修改
- 提高可重用性
- 有利于查找问题点,或可以提升性能的点
- 有利于构建大型系统
尽可能地使每个类或成员不被外部访问。
四种访问级别:
private 类私有
default 包级
protected 包级+子类
public(+interface)公有
由上到下是“尽量使用”到“最少使用”的顺序。
长度非0的数组总是可变的。公有的静态final数据域,或返回这种域的方法,都是错误的。
解决方法有二:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
要在这些方法之间进行选择,要考虑客户端可能如何处理返回的结果。 哪种返回类型会更方便,哪个会更好的表现。
总而言之,应该尽可能地减少程序元素的可访问性。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态final 字段之外,公共类不应该有公共字段。 确保 public static final 字段引用的对象是不可变的。
第14条 在公有类中,使用访问方法而非公有域
反面例子:
class Point {
public double x;
public double y;
}
如果一个类是公有的,在其包之外是可访问的,则提供访问方法来保留更改类内部表示的灵活性。
如果一个类是包级私有的,或者是一个私有的内部类,那么暴露它的数据属性就没有什么本质上的错误——假设它们提供足够描述该类提供的抽象。
Java 平台类库中的几个类违反了公共类不应直接暴露属性的建议。 著名的例子包括 java.awt 包中的 Point 和 Dimension 类。 这些类别应该被视为警示性的示例,而不是模仿的例子。 时至今日,暴露 Dimension 的内部结构的决定仍然导致着严重的性能问题。
公共类不应该暴露可变属性。 公共类暴露不可变属性的危害虽然仍然存在问题,但其危害较小。 有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。
第15条 使可变性最小化
我们的目标是,达到工业强度(即产品级的实现)级别的设计与实现。
不可变对象的优点:
- 不可变对象比较简单
- 不可变对象本质上是线程安全的,它们不要求同步。可以被自由地共享。
- 可以共享内部信息
- 为其他对象提供了大量的构件(building blocks)
唯一的缺点:
对于每个不同的值,需要一个单独的不同的对象
不可变的类永远不需要做任何拷贝,因为这些拷贝永远等于原始对象。 因此,不需要也不应该在一个不可变的类上提供一个 clone 方法或拷贝构造方法(copy constructor)
坚决不要为每一个getter方法编写一个相应的Setter方法。除非有很好的理由让类变成可变的,否则就应该是不可变的。
StringBuilder是String类的公有的可变配套类,为了更好的性能。
如果类不能做成不可变的,仍然要尽可能地限制可变性。降低对象的状态维度,可以更容易地分析对象的行为,同时降低出错的可能性。
尽量使每个域都成为final的,除非有另人信服的理由要使域变成是非final的。
构造器应该创建完全初始化好的对象,并建立起所有的约束关系。不要在构造器或静态工厂外,再提供一个公有的初始化方法,除非有另人信服的理由必须这么做。
同样也不应该提供公有的重新初始化的方法,它使得对象可以被重用,像是对象是由原对象更改了一些状态后构造出来的。这么做增加了复杂性,而且也没有带来太多的性能优势。
第16条 组合优先于继承
继承会打破封装性:
- 子类容易受到父类的影响。随着父类的实现细节的变化,子类的方法有可能会出现错误。
- 如果父类增加了新的方法,如一个校验方法,其他方法依赖于此方法。此时,可能由于子类未实现此方法,导致其他方法没有做校验动作。
- 如果子类不是override父类的方法,也可能存在问题。父类以后的发行版本增加的新方法,可能会和子类扩展的方法产生冲突。(同样的方法签名,不同的返回类型,或是子类完全覆盖了父类的方法),因为子类扩展在先,父类修改在后,子类扩展时没有可以参考的限制与约定。
继承使用场景:当子类真正是父类的子类型时,才适合用继承,它们之间是is-a的关系,是一种强归属关系。
除此之外,要考虑所谓的“父类”是否只是“子类”的一个实现手段,或实现细节?此时,应该用组合,使“子类”中包括一个“父类”对象的私有域,这样来实现。
在 Java 平台类库中有一些明显的违反这个原则的情况。 例如, stacks 实例并不是 vector 实例,所以 Stack 类不应该继承 Vector 类。 同样,一个属性列表不是一个哈希表,所以Properties 不应该继承 Hashtable 类。 在这两种情况下,组合方式更可取。
总之,继承是强大的,但它是有问题的,因为它违反了封装原则。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用组合和转发代替继承,特别是如果存在一个合适的接口来实现包装类。
包装类不仅比子类更健壮,而且更强大。
第17条 要么为继承设计,并提供良好的文档说明,要么就禁止继承
为了继承而进行的设计,需要做到:
- 良好的涉及protected方法的自用模式的文档说明
- 类必须通过某种方式提供适当的钩子,以便能够进入到它的内部工作流程中,一般是用protected方法。
- 设计可能被广泛继承的类时,必须在发布前进行高强度的测试,编写多个子类测试问题。
良好的文档说明:
- 每一个protected方法被覆盖时,带来的影响
- 每个public,protected方法或构造器,它们的文档要说明它们调用了哪些可覆盖的方法,以什么顺序调用的,如何影响后续处理过程
关于程序文档有句格言:好的API文档应该描述一个方法做了什么工作,而不是描述它是如何做到的。那么,上面这种做法是否违背了这句格言呢?是的,它确实违背了!这正是继承破坏了封装性所带来的不幸后果。所以,为了设计一个类的文档,以便它能够被安全地子类化,你必须描述清楚那些有可能未定义的实现细节
类的设计分为:为了继承而设计的类,和普通的类。
对于为了继承而实现的类,唯一的测试方法就是编写子类:
- 如果公开少了protected域或是方法,将使子类实现很痛苦。
- 如果多个子类都没有用到一个protected域或方法,则可以将这个域或方法改为private的。
继承的缺点:
- 为了继承而设计的有可能被广泛使用的类时,必须意识到:对于文档中所说明的自用模式(self-use pattern),以及对于其protected方法和域所隐含的实现策略,实际上你已经做出了永久的承诺。使得后续版本中,再提高这个类的性能或是增加新功能都变得异常困难,甚至不可能。因此,必须在发布前进行高强度的测试,编写多个子类测试问题。
- 如果继承了一个普通类(不是为了继承而设计的类),则以后每次对这样的父类进行修改,都有可能使子类遭到破坏。且在修改了父类的内部实现后,也很有可能会收到与子类相关的错误报告。
为了继承而进行的设计,不能做的:
- 构造器绝不能调用可覆盖方法,无论是直接还是间接调用。违反此规则将导致程序失败。
- 类似构造器的,还有clone方法 和 readObject方法,同第一条。
- 不要轻易地继承一个普通类:
对于刚才第3条的最佳解决方法是:对于那些并非为了继承而设计且编写良好文档的类,要禁止子类化:
1. 把类声明为final
2. 把所有构造器变为私有的,或是包级访问的,并增加公有的构造工厂方法
为了继承而进行的设计,需要注意:
如果为了继承而设计的类,一定要实现Serializable时,必须使ReadResolve writeReplace变成protected的,否则子类会忽略此方法。
同时,此做法也表明:为了允许继承,而把实现细节变成了一个类的API的一部分。
为了继承而设计类,会对这个类会增加很多实质性的限制。
如果想继承一个本不是为了继承而设计的类时:如果一个具体的类没有实现一个标准的接口,那么你禁止继承可能给一些程序员带来不便。 如果你觉得你必须允许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这个事实。 换句话说,完全消除类的自用(self-use)的可重写的方法。 这样做,你将创建一个合理安全的父类,用以派生子类。 重写一个方法不会影响任何其他方法的行为。
你可以机械地消除类的自我使用的重写方法,而不会改变其行为。 将每个可重写的方法的实现移动到一个私有的“帮助器方法”,并让每个可重写的方法调用此私有方法。 然后将原先调用可重写方法的地方改为 调用此私有方法。如此子类覆盖可重写方法,将不影响父类逻辑。父类的自实现与可覆写方法解耦。
总结,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有的自用模式,并且一旦建立了文档,在这个类的整个生命周期中都必须遵守。如果没有做到,子类就会依赖父类的实现细节,如果父类的实现发生了变化,它就有可能遭到破坏。为了允许其他人能编写出高效的子类,你还必须暴露一个或者多个受保护的方法。
除非意识到真的要设计一个可继承的类,否则最好通过将类声明为final ,或者确保没有可访问的构造器来禁止类被继承。
关键词:为继承而设计的类、良好的说明文档、自用模式、受保护的方法、子类测试、可重写方法、final、静态工厂构造
第18条 接口优于抽象类
Java 有两种机制来定义允许多个实现的类型:接口和抽象类。
由于在 Java 8 [JLS 9.4.3] 中引入了接口的默认方法(default methods ),因此这两种机制都允许为某些实例方法提供实现。
一个主要的区别是要实现由抽象类定义的类型,类必须是抽象类的子类。 因为 Java 只允许单一继承,所以对抽象类的这种限制严格限制了它们作为类型定义的使用。 任何定义所有必需方法并服从通用约定的类都可以实现一个接口,而不管类在类层次结构中的位置。
使用接口的好处:
- 使用接口,现有的类可以很容易被更新
- 接口是定义混合类型mixin的理想选择
- 接口允许我们构造扁平结构的类型框架,而不是继承那种层次结构
包装类中包含多个其他接口骨架类的子类的实例,这种方法称作“模拟多重继承”。
这项技术具有多重继承大多数优点,同时避免了相应的缺陷。
编写骨架类:
- 认真研究接口,并确定哪些是基本方法,哪些可以基于基本方法实现。
- 基本方法将成为抽象方法,然后为接口中其他方法提供基于基本方法具体的实现。
注意:
设计公有接口要非常谨慎。因为接口一旦被公开发行,并且已被广泛实现,再想改变这个接口是不可能的。所以必须在初次设计的时候就要保证接口是正确的。在接口发行前,要让尽可能多的程序员,用尽可能多的方式来实现接口,有助于发现接口的缺陷。
第19条 接口只用于定义类型
不要用“常量接口”,java.io.ObjectStreamConstants是一个反例。
公布常量有几种可选的方式:
- 放到与常量紧密相关的类或接口中
- 定义枚举
- 定义不可实例化的工具类(utility class)
接口应该只被用来定义类型,不应该用来导出常量。
第20条 类层次优于标签类(tagged class)
标签类的缺点:
- 多个实现放在一个类中,可读性差
- 增加了无必要的内存占用
- 域不能是final的,因为需要切换标签
- 构造器必须不借助编译器,来设置正确的标签及 初始化正确的域,容易出错
- 扩展性差:如果需要再添加一个标签时,要在每个条件判断语句处添加新标签的处理,很麻烦
- 数据类型字面值没有带任何标签信息,不知道具体是表示什么类
替换实现:使用抽象类,建立类层次结构。
总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑是否能被类层次结构替换。
当遇到一个带有标签字段的现有类时,考虑是否可以将其重构为一个类层次结构。
第21条 用函数对象表示策略
有些语言支持函数指针,其实是一个策略模式。
Java没有提供函数指针,但是可以用对象引用实现同样的功能。
Java实现:
- 声明一个接口表示此策略
- 为每一个策略定义一个实现了该接口的类
- 具体策略只使用一次,可以用匿名类实例化
- 策略会多次使用时,实现为静态成员域,并公布出去,类型为策略接口
第22条 优先考虑静态成员类
嵌套类:
静态成员类
内部类:
非静态成员类、匿名类、局部类
非静态成员类会持有一个外围类实例的引用,对于非静态成员类实例来说,构造时间变长,且浪费空间。
嵌套类不需要持有外部类的引用时,优先考虑使用“静态成员类”
481

被折叠的 条评论
为什么被折叠?



