类和接口
1. 使类和成员的可访问性最小化
区分一个组件设计得好不好,唯一重要的因素在于,它对于外部的其他组件而言,是否隐藏了其内部数据和其他实现细节。
设计良好的组件之间只通过 API 进行通信,而不知道其他模块内部情况,这称为封装,有利于解耦。
类或接口的访问级别
package-private
包级私有public
公有的
成员 (域、方法、嵌套类、嵌套接口) 的访问级别
- 私有的
private
- 包级私有的
package-private
(缺省访问级别default
) - 受保护的
protected
- 公有的
public
总结:
- 尽可能使每个类或者成员不被外界访问
- 公有类的实例域决不能是公有的
2. 封装公有类的数据域
永远不要直接暴露公有类的数据域,而应该使用 getter
和 setter
方法来封装数据域。
// java.awt.Point 中的代码就是反例
class Point {
public int x;
public int y;
}
3. 使可变性最小化
不可变类就是指实例不能被修改的类,每个实例包含的信息在创建实例的时候就提供,并在对象整个生命周期中都不可改变。
构造器或静态工厂方法应该创建完全初始化的对象,并建立所有的约束关系。
不可变类
- 不可变类状态简单
- 天生线程安全,可以被自由的共享
- 唯一的缺点是,对于每个不同的值都需要一个单独的对象
可变类
如果类不能被做成不可变的,也应该尽可能地限制它地可变性。
java.util.concurrent
包下的 CountDownLatch
就是可变的,但它的状态空间被有意地设计得非常小。 比如创建一个实例,只使用一次它的任务就完成了;定时器的计数达到零,就不能被重用了。
4. 复合优先于继承
继承是实现代码重用的有力手段,但它并非适用于所有情况。
与方法调用不同的是,继承打破了封装性。 超类的代码的改变可能会导致子类的行为改变,即使子类的代码没有改变。
案例: jdk 中的 Properties
继承了 HashTable
- 实际上,
Properties
与HashTable
并不满足is a
的原则。 - 语义上的混淆:对于
Properties
实例,p.getProperty("key")
和p.get("key")
有可能产生不同的结果 - 更严重的是,客户端有可能直接修改超类,从而破坏子类的约束条件
Properties properties = new Properties();
//子类 properties 的约束条件是 key 是 String 类型,value 是 String 类型
String value = properties.getProperty("key");
//但客户端可以直接调用超类的方法,破坏了这种约束条件
Object object = properties.get(1);
5. 要么设计继承并提供文档说明,要么禁止继承
如果要设计一个可以继承的类,应该确保继承安全性
案例: springboot
框架中的 SpringBootCondition
public abstract class SpringBootCondition implements Condition {
//模板方法定义好了算法骨架,子类只需要实现 `getMatchOutcome` 方法细节
//核心方法使用了 final 修饰,确保子类无法改动超类核心行为
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
} catch (NoClassDefFoundError ex) {
throw new IllegalStateException(ex);
} catch (RuntimeException ex) {
throw new IllegalStateException(ex);
}
}
//子类只需专注该扩展细节
public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);
}
6. 接口优于抽象类
jdk8 引入了 (default
) 缺省方法,使得接口和抽象类都能为某些实例方法提供实现。相比之下,接口就比抽象类更加灵活,因为 Java 只允许单继承。
- 现有的类可以很容易被更新,以实现新的接口
- 接口是定义混合类型的理想选择 (一个类实现多个接口)
- 接口允许构造非层次结构的类型框架
接口的缺陷
- 虽然许多接口都定义了
Object
的行为,如equals
和hashCode
,但不允许为它们提供default
方法。 - 而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)
- 无法给不受你控制的接口添加
default
方法
最佳实践
可以把接口和抽象类的优点结合起来:对接口提供一个抽象的骨架实现类
接口负责定义类型,或许还能提供一些缺省方法,而骨架实现类则负责实现其余大部分工作。这就是模板方法模式
例如,集合框架中的 AbstractMap
、 AbstractList
、 AbstractSet
等都是骨架实现。
//这个例子就是骨架实现 装饰器模式 (将 int 数组装饰为 Integer 的List)
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<Integer>() {
@Override
public int size() {
return a.length;
}
@Override
public Integer get(int index) {
return a[index];
}
};
}
骨架实现不一定是抽象类,也可以是具体类。比如 AbstractMap.SimpleEntry
,它是为了继承而实现的,你可以直接使用它,也可以根据情况将它子类化。
public static class SimpleEntry<K,V>
implements Entry<K,V>, java.io.Serializable
7. 为后代设计接口
Java 8 为接口增加了缺省方法 (default
), Java 8 在集合接口中增加了许多新的缺省方法,主要是为了便于使用 lambda
。
但不要认为可以随意添加 default
方法,而不承担可能的代价。
案例: Collection
接口的 removeIf()
方法在 Apache Commons
库中的 SynchronizedCollection
会出错
//@since 1.8
// 用来移除所有元素,如果成功,断言返回 true
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
8. 接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型 (type)
9. 类层次优先于标签类
有的类包含多个风格的实例,比如一个类同时包含圆形和矩形,这种类称为标签类
标签类冗长,效率低下,应该定义一个图形抽象类,然后圆形类和矩形类继承这个抽象类
10. 静态成员类优于非静态成员类
成员类(内部类)存在的目的应该只是服务于它的外围类,如果内部类不需要访问外围类的实例,应该定义为静态内部类
11. 限制源文件为单个顶级类
虽然Java 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,只会带来巨大的风险。 因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义。哪一个定义会被用到,取决于源文件被传给编译器的顺序。