0%

Effective Java 第三版 (3) 所有对象都通用的方法

类和接口

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

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

Java 中对于成员(字段、方法、嵌套类、嵌套接口),有四种可能的访问级别:

  • 私有的。private
  • 包级私有的。default 或未指定访问修饰符
  • 受保护的。protected,声明该成员的子类可以访问这个成员(会受到一些限制)
  • 公有的。public,任何地方都可以访问该成员。

常量是类抽象的一个组成部分,可以通过 public static final 暴露常量,这些字段应当是包含 基本类型的值,或 不可变对象的引用。包含可变对象虽然引用不能修改,但引用的对象可以被修改,这可能会带来灾难性的结果。

注意,非零长度的数组总是可变的,所以类具有公共静态 final 数组是错误的。

1
2
// 错误的
public static final Thing[] VALUES = {...}

可以通过以下两个方法解决:

1
2
3
4
5
6
7
8
9
10

// 1. 私有化公共数组,并添加一个公共的不可变列表
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

// 2. 私有化公共数组,返回私有数组拷贝的公共方法
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}

§ 在公有类中使用方法而非公有属性

对于可变的类,应该用包含 私有字段和公有访问方法 以及 公有设值方法 的类代替。即 getter 与 setter

§ 可变性最小化

不可变类只是其实例不能被修改的类,每个实例包含的所有信息必须在创建该实例的时候提供,并在对象整个生命周期内固定不变。

Java 中包含许多不可变的类,其中有 String、基本类型的包装类、BigInteger。好处是易于设计、实现和使用,不容易出错,且更安全。

遵循规则:

  • 不要提供任何会修改对象状态的方法
  • 保证类不会被继承。为防止子类化,可通过 final 修饰类
  • 所有的字段都是 final 的。
  • 所有的字段都是私有的。
  • 确保对于任何可变组件的互斥访问。

不可变对象本质上是线程安全的,它们不要求同步。

唯一的缺点是:对于每一个不同的值都需要一个单独的对象。

§ 组合优先于继承

继承打破了封装性。

子类依赖于父类中特定功能的实现细节。父类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,因此子类必须要跟着父类的更新而演变。

举例说明:

有个程序使用了 HashSet,为了调优程序性能,需要统计它被创建以来曾经添加了多少个元素。因为 HashSet 类包含两个可以添加元素的方法:add()addAll(),所以这两个方法都要被重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 继承 HashSet
public class InstrumentedHashSet<E> extends HashSet<E> {
// 统计变量
private int addCount = 0;

public InstrumentedHashSet() {
}

public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}

然而,直接会导致统计结果变成了 2 倍,因为在 HashSet 内部,addAll() 方法是基于 add() 方法来实现的。并且这种实现细节是可能会变化的。这种设计会非常脆弱。

解决办法是,不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计叫做 “组合”

因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,如 clear() 方法,并返回它的结果,这被称为“转发”,新类中的方法被称为“转发方法”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 转发类
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;

public ForwardingSet(Set<E> s) {
this.s = s;
}

// 转发方法
public void clear() {
s.clear();
}
...
}

// 包装类
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;

public InstrumentedSet(Set<E> s) {
super(s);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
s}
}

因为每一个新类的实例都把另一个实例包装起来了,所以新类被称为“包装类”,这也正是 Decorator (装饰者)模式。

因此。只有当子类真正是父类子类型时,才适合用继承,两者之间确实存在 “is-a” 关系。

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

该类必须有文档准确描述重写每个方法带来的影响。

换句话说必须说明可重写的方法的自用性。可参考 java.util.AbstractCollection 的规范。

§ 接口优于抽象类

Java 8 中引入了接口的默认方法,因此这两种机制都允许为某些实例方法提供实现。

因为 Java 只允许单一继承,所以对抽象类的这种限制严格限制了它们作为类型定义的使用。

现有的类可以很容易被更新,以实现新的接口,但一般来讲现有的类不能改进以继承一个新的抽象类。

接口是定义mixin(混合类型)的理想选择。

接口允许我们构造非层次结构的类型框架

§ 为后代设计接口

Java 8 添加了默认方法,目的是允许将方法添加到现有的几口,但是增加新的方法到现有接口有很大的风险。

§ 接口仅用于定义类型

当类实现接口时,接口就充当可以引用这个类的实例的类型,因此,类实现了接口就表明客户端可以对这个类的实例实施某些动作。

有一种失败的接口被称为常量接口,没有包含任何方法,只包含静态的 final 字段。每个字段都导出一个常量。如:

1
2
3
4
5
public interface PhysicalConstants {
static final double PI = 3.1415926;

...
}

常量接口是对接口的不良使用。Java 类库也有几个常量接口,如 java.io.ObjectStreamConstants。这些都是反面典型。

如果要导出常量,有几种合理的选择方案:

  • 如果这些常量与某个现有的类或接口紧密相关,应该把这些常量添加到这个类或接口中。如 Integer.MAX_VALUE
  • 使用不可实例化的工具类导出这些常量

示例:

1
2
3
4
5
6
public class PhysicalConstants {
public static final double PI = 3.1415926;

// 不可实例化
private PhysicalConstants() {}
}

如果大量使用实用工具类导出的常量,可以通过静态导入来限定

§ 类层次优于标签类

类的实例有两个和更多风格,并且包含了一个标签字段,表示实例的风格。

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

§ 优先考虑静态成员类而不是非静态类

嵌套类是指被定义在另一个类的内部类。其存在的目的应该只是为它的 外围类提供服务。嵌套类有四种:

  • 静态成员类
  • 非静态成员类
  • 匿名类
  • 局部类

除了第一种之外,其他三种都被称为内部类。

静态成员类,可以访问外围类的所有成员,包括声明为私有的成员,静态成员类是外围类的一个静态成员,与其他静态成员一样,遵守可访问性规则。如果它被声明为私有的,它就只能在外围类内部才可以被访问。

静态成员类常见用法是作为公有的辅助类,如建造者模式中的 Builder。

非静态成员类的每个实例都隐含与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法。

非静态成员类的常见用法是定义一个 Adapter,允许外部类实例被看作是另一个不相关的类的实例,如 Map 接口中的集合视图,包括由 Map 的 keySetentrySet

如果声明成员类 不要求访问外围实例,就要始终把 static 修饰符放在声明中,使它成为静态成员类。

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

虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。

在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递给编译器的顺序的影响。