Effective Java 读书笔记(三):类和接口

Effective Java 读书笔记(三):类和接口

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

设计良好的模块会隐藏所有的实现细节,把它的 API 和实现清晰地隔离开来。模块之间只通过 API 通信,一个模块不需要知道其他模块内部的工作情况。这个概念被称之为信息隐藏或封装,是软件设计的基本原则之一。

信息隐藏,或封装的好处是什么呢?
1. 模块间松耦合,方便各模块独立的进行开发、测试、优化、使用、理解、修改。
2. 提高了软件的可重用性,常见的就是各自基础组件。
3. 降低了构建大型系统的风险,因为即使整个系统不可用,但是这些独立的模块却可能是有用的。

在 Java 中,访问控制机制决定了类、接口、成员的可访问性。实体的可访问性由实体声明所在位置,以及该实体声明中所出现的访问修饰符(private、public、protected、package)共同决定。
1. 尽可能使每个类或成员不被外界访问。
2. 实体域绝对不能是公用的。
- 一旦公有,你就放弃了对域进行限制的权利,也放弃了域被修改时采取行动的能力。
- 一个域是公用的,也就放弃了“切换到新的内部数据表示法”的灵活性。
- 包含可变公有域的类无法做到线程安全。
3. 如果 final 域包含可变对象的引用,它便具有非 final 对象的所有缺点。因为虽然引用本身不能被修改,但是所指向的对象却可以修改,这容易引起灾难性的后果。

在公有类中使用访问方法而非公有域

使用公有域的化,不方便后续修改内部数据表示,无法强加约束条件。当域被访问的时候,无法添加辅助行为。

但是,如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质错误。

使可变性最小化

不可变对象有什么优点呢?
1. 对象简单,只有一种状态,行为可预见。
2. 线程安全,可以被自由地共享。
3. 为其他对象提供了大量构件,比如作为 Map 的 key 时,不用担心 key 的改变影响集合的不变性约束。

为了使类不可变,要遵循以下 5 条规则:
1. 不提供任何会修改对象状态的方法。
2. 保证类不会被扩展,要防止子类化,可以设置类为 final 的。或者构造函数设为 private 并提供静态工厂方法。
3. 使所有的域都是 final 的。
4. 使所有的域都是私有 private 的。
5. 如果类具有指向可变对象的域,则必须确保客户端无法获得指向可变对象的引用,否则可变对象就有被改变的风险。如果需要对外暴露可变对象的信息,可以使用保护性拷贝(defensive copy)技术。

不可变对象的唯一缺点是,对于每一个不同的值都需要一个单独的对象。生成对象过多,可能会有性能问题。为了应对性能问题,可以使用相对应的可变嵌套类,比如 StringBuilder 之于 String,以及包级私有的可变嵌套类 MutableBigInteger 之于 BigInteger。

如果类不能被做成是不可变的,仍然应该尽可能地限制其可变性。降低对象可以存在的状态数目,可以更容易地分析对象的行为,降低出错的可能性。

复合优先于继承

继承的功能很强大,但也存在一些问题。与方法调用不同的是,继承打破了封装性,子类依赖于超类中特定功能的实现细节。何时可以使用继承呢?
1. 在同一个 API 包的内部使用继承是相对安全的。继承一个不同包中的类,如果超类升级了,修改了实现或新增了方法(新增的方法与子类已有方法重名),就可能引起问题。
2. 对于两个类 A 和 B,只有当两者之间确实存在 is-a 关系的时候,类 B 才应该扩展类 A。在 Java 类库中也有许多违反这个原则的地方,比如 Stack 与 Vector、Properties 与 HashTable。

如何避免继承其他包里的类带来的问题呢?使用复合 composition:不用扩展现有类,而是在新的类中增加一个私有域,它引用现有类的一个实例。新类中的每一个方法都可以调用被包含的现有类中的对应方法,并返回其结果,这叫转发 forwarding。

要么为继承而设计,并提供文档说明,要么禁止继承

如果一个类是专门为继承设计的:
1. 必须具有详细的文档说明,说明它可覆盖方法的自用性。(也就是类中哪些地方调用了可覆盖方法)
2. 为了使程序员编写更加有效的子类,而无需承受不必要的痛苦,类必须通过某种形式提供适当的钩子 Hook,以便能够进入到它的内部工作流程中,这种形式可以是精心选择的 protected 方法。
3. 超类构造器不可调用可以被覆盖的方法,因为这个造成的问题,还真遇到过,一个 jar 包莫名抛出空指针异常,结果一看是在超类构造器里调用了可覆盖的方法,而这个方法调用了子类中尚未初始化的变量。
4. 对于为了继承而设计的类,唯一的测试方法就是编写子类。
5. 对于并非为了安全地进行子类化而设计的类,要禁止子类化。

接口优于抽象类

在 Java 中,抽象类只能是单继承的,然而从 Java8 开始,接口里也可以有方法了。所以接口和抽象类,区别还大吗?

因为抽象类是单继承的,所以想要定义混合类型,就使用接口。

接口也有缺点,接口一旦被公开发行,并且已被广泛实现,再想改变这个接口就几乎是不可能的。当演变的容易性比灵活性和功能更重要时,应该使用抽象类来定义类型。

接口只用于定义类型

接口应该只被用来定义类型,不能用来定义、导出常量。

类层次优于标签类

一个类不应该表示多种对象,比如不能即表示矩形,又表示圆。对于这种情况,可以使用继承、子类型化,来实现类层次。类层次可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时检查。

用函数对象表示策略

函数指针的主要用途就是实现策略模式,对应 Java 里的引用。例子见 Comparator 类。
1. 当一个具体的策略只被使用一次的时候,通常使用匿名类来声明和实例化这个具体策略类。
2. 当具体策略是被设计用来重复使用的时候,它的类通常就要设计为私有的静态成员类,并通过公有的静态 final 域导出。

优先考虑静态成员类

如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类。

非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。非静态成员类的内部,可以调用外围实例上的方法,典型例子如 Map 中的 KeySet、ValueSet。反过来说,成员类如果不需要访问外围实例,就声明为 static,这方面的例子如 Map 中的 Entry。

匿名类可以出现在任何地方,当匿名类处于非静态环境中时,它也可以有外围实例。但是处于静态环境中时,它也不能有静态成员。

局部类可以出现在任何能够声明局部变量的地方。同样的,当局部类处于非静态环境中时,它也可以有外围实例。但是处于静态环境中时,它也不能有静态成员。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值