Skip to content

类和接口

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

区分一个组件设计得好不好,唯一重要的因素在于,它对于外部的其他组件而言,是否隐藏了其内部数据和其他实现细节

设计良好的组件之间只通过 API 进行通信,而不知道其他模块内部情况,这称为封装,有利于解耦

类或接口的访问级别

  • package-private 包级私有
  • public 公有的

成员 (域、方法、嵌套类、嵌套接口) 的访问级别

  • 私有的 private
  • 包级私有的 package-private (缺省访问级别 default)
  • 受保护的 protected
  • 公有的 public

总结:

  • 尽可能使每个类或者成员不被外界访问
  • 公有类的实例域决不能是公有的

2. 封装公有类的数据域

永远不要直接暴露公有类的数据域,而应该使用 gettersetter 方法来封装数据域。

java
// java.awt.Point 中的代码就是反例
class Point {
    public int x;
    public int y;
}

3. 使可变性最小化

不可变类就是指实例不能被修改的类,每个实例包含的信息在创建实例的时候就提供,并在对象整个生命周期中都不可改变。

构造器或静态工厂方法应该创建完全初始化的对象,并建立所有的约束关系。

不可变类

  • 不可变类状态简单
  • 天生线程安全,可以被自由的共享
  • 唯一的缺点是,对于每个不同的值都需要一个单独的对象

可变类

如果类不能被做成不可变的,也应该尽可能地限制它地可变性。

java.util.concurrent 包下的 CountDownLatch 就是可变的,但它的状态空间被有意地设计得非常小。 比如创建一个实例,只使用一次它的任务就完成了;定时器的计数达到零,就不能被重用了。

4. 复合优先于继承

继承是实现代码重用的有力手段,但它并非适用于所有情况。

与方法调用不同的是,继承打破了封装性。 超类的代码的改变可能会导致子类的行为改变,即使子类的代码没有改变。

案例: jdk 中的 Properties 继承了 HashTable

  • 实际上, PropertiesHashTable 并不满足 is a 的原则。
  • 语义上的混淆:对于 Properties 实例,p.getProperty("key")p.get("key") 有可能产生不同的结果
  • 更严重的是,客户端有可能直接修改超类,从而破坏子类的约束条件
java
Properties properties = new Properties();
//子类 properties 的约束条件是 key 是 String 类型,value 是 String 类型
String value = properties.getProperty("key");
//但客户端可以直接调用超类的方法,破坏了这种约束条件
Object object = properties.get(1);

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

如果要设计一个可以继承的类,应该确保继承安全性

案例: springboot 框架中的 SpringBootCondition

java
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 的行为,如 equalshashCode ,但不允许为它们提供 default 方法。
  • 而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)
  • 无法给不受你控制的接口添加 default 方法

最佳实践

可以把接口和抽象类的优点结合起来:对接口提供一个抽象的骨架实现类

接口负责定义类型,或许还能提供一些缺省方法,而骨架实现类则负责实现其余大部分工作。这就是模板方法模式

例如,集合框架中的 AbstractMapAbstractListAbstractSet 等都是骨架实现。

java
//这个例子就是骨架实现 装饰器模式 (将 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,它是为了继承而实现的,你可以直接使用它,也可以根据情况将它子类化。

java
 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 会出错

java
//@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 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,只会带来巨大的风险。 因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义。哪一个定义会被用到,取决于源文件被传给编译器的顺序。