Effective Java - 对所有对象都通用的方法

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

  • 良好的设计模块在于对外部的其他模块隐藏其内部数据和其他实现细节
  • 实体可访问性由实体声明所在的位置以及该实体声明中出现的访问修饰符决定
  • 降低不必要的公有类的可访问性
  • 类实现了Serializable接口有可能“泄漏”私有或包私有域
  • 覆盖父类的方法,子类中的访问级别就不应该低于父类中的访问级别
  • 除了公有静态final域以外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。

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

  • 如果类可以在他所在的包的外部进行访问,就为它的域提供访问方法
  • 如果类是包级私有的,或者私有的嵌套类,直接暴露它的数据域并没有本质错误
  • 公有类暴露的数据域如果是不可变的这种做法危害比较小,可以通过构造器加强约束条件

使可变性最小化

强硬的不可变类遵守五条规则

  • 不提供任何修改对象状态的方法
  • 保证类不会被扩展,防止子类化
    • 使类成为final
    • 让类的所有构造器都变成私有的或者包级私有的(这样对包外的客户端来说该类相当于final的,因为没有公有的或受保护的构造器),并添加公有静态工厂代替公有的构造器
  • 使所有的域都是final
  • 使所有的域都是私有的
  • 如果类有可变域确保对于任何可变组件的互斥访问
    • 不要用客户端提供的对象初始化
    • 不要提供方法获取可变域对象的引用
    • 在构造器、访问方法和readObject方法中使用保护性拷贝
  • 不可变类:每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
    • java平台类库中包含的不可变类有:String,基本类型的包装类,BigIntegerBigDecimal
    • 优点:易于使用,更加安全
    • 缺点:对于每个不同的值都需要一个单独的对象,尤其是大型对象,如BigInteger上百万位改变其状态消耗很大的空间和时间。
      • 若能精确的预测客户端在不可变类上进行的复杂操作,可以通过包级私有的可变配套类。例如BigInteger包级私有配套类MutableBigInteger
      • 如果无法预测,提供一个公有的可变配套类。例如String的可变配套类StringBuilder
  • 不可变对象本质上是线程安全的,不要求同步,可以被自由的共享包括它们的内部信息,永远不需要保护性拷贝
  • 不可变对象为其他对象提供了大量的构件
  • 真的不可变类比较宽松,没有一个方法能对对象的状态产生外部可见的改变
    • 许多不可变类有一个或多个非final的域用来缓存一些计算开销昂贵的结果,如hashCode。对象不可变保证了计算相同的结果。
  • 如非必要,坚决不为每个get方法编写对应的set方法
  • 限制可变类的可变性,除非有令人信服的理由要使域变为非final的,否则都应当是final的

复合优先于继承

  • 包内部使用专门为了继承而设计并且具有很好的说明文档的类是非常安全的。
  • 普通的具体类进行跨越包便捷的继承会导致脆弱性
    • 继承打破了封装性,子类依赖父类中的实现细节。子类必须跟随父类的版本更新而演变。
    • 子类实现父类的自用方法或者其他方法很困难,也很耗时,且容易出错。尤其当父类方法中访问私有域时,子类则因为无法访问父类的私有域而无法实现
    • 子类会不必要的暴露父类的实现细节,会导致语义上的混淆。而且子类得到的API被限定在原始实现上,性能被永远限定。
  • 不扩展现有的类,而在新的类中添加私有域引用现有的类的一个实例,这种设计称之为复合(composition)
  • 新类中每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它们的结果,这称之为转发(forwarding),新类中的方法称之为转发方法
    • 新类可被称为包装类(wrapper class),这正是装饰者模式(Decorator pattern)
    • 包装类不适合在回调框架中使用。回调框架需要把对象自身的引用传递给其他的对象,用于后续的调用。被包装对象并不知道其外部的包装对象,所以它传递一个自身引用,避开了外边的包装对象。这被称为SELF问题。
    • 实践中转发方法的调用对性能影响不大,包装对象内存占用也不大
  • 只有当子类和父类之间确实存在子类型关系时使用继承才恰当。但如果它们在不同的包中,且父类不是为了继承专门设计的,依然会导致脆弱性。

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

对于为了继承专门设计并具有良好文档说明的类而言

  • 该类必须有文档来说明可覆盖的(overridable)方法的自用性(self-use)。在文档注释尾部介绍可覆盖方法内部工作情况和调用其他方法的描述信息

  • 为了实现更有效的子类,父类必须通过某种形式提供适当的钩子(hook),以便子类能顺利进入到它的内部工作流程中

    • 通过努力思考,尽可能少的暴露受保护的方法或域,域比较少见
    • 暴露的受保护的方法或域代表了关于实现细节的永久承诺,后续版本难以提高性能或增加新功能。因此必须在发布前编写子类测试。
  • 为继承专门设计的类的唯一的测试方法就是编写子类。经验表明,3个子类通常就足以测试一个可扩展的类。

  • 构造器决不能调用可被覆盖的方法,父类构造器执行次序早于子类构造器,如果该父类中的覆盖方法依赖子类的构造方法执行初始化,该方法不会如预期执行。

  • 实现CloneableSerializable接口readObjectclone方法都不可调用可覆盖的方法

    • 对于readObject,覆盖版本的方法在子类的状态反序列化之前执行
    • 对于clone方法,覆盖版本的方法在子类的clone方法之前运行,此时clone方法可能正在修正被克隆对象的状态
  • 禁止类继承的方法

    • 将类声明为final
    • 将该类构造器设为包私有或者私有,增加静态工厂代替构造器
  • 非要继承,合理的办法是保证该类永远不会调用自己的任何可覆盖方法,并在文档中说明。换句话说,完全消除这个类中可覆盖方法的自用特性。

    可以将每个可覆盖方法自用调用的方法的代码体移动到一个私有的“辅助方法”中,并让每个可覆盖的方法调用它的私有辅助方法,代替可覆盖方法的自用调用。

接口优于抽象类

  • java只允许单继承,抽象类作为类型定义受到限制。已继承父类的子类,如果想继承抽象类,则需要父类继承抽象类。这样类型层次(type hierarchy)受到伤害,父类的其他子类都会继承抽象类。而已实现接口的类实现新的接口很方便。
  • mixin(混合类型)除了实现它的基本类型外还可以提供某些可供选择的行为。例如:Comparable接口是个mixin,它的实例可以互相比较对象进行排序。Comparable是个接口,现有类都可以任选实现它混合到类的主要功能。抽象类不能更新mixin功能到现有类中,因为类不可能有一个以上的父类。
  • 接口可以构造非层次结构的类型框架。例如一个人既是Singer又是Songwriter接口可以实现它们,组合起来变得灵活,但会使类层次结构臃肿。抽象类则必须层次结构在顶端。
  • 通过包装类(wrapper class)模式,接口能安全地增强类的功能。
  • 虽然接口不可以包含具体的方法实现,但可以通过提供一个抽象的骨架实现(skeletal implementation)类,把抽象类(为实现提供默认方式)和接口(解耦合)的优点结合起来。接口作用仍然是定义类型,骨架实现类接管了所有与接口实现相关的工作。骨架实现被称为AbstractInterface。例如:AbastractCollection/AbastractSet/AbastractList/AbastractMap。骨架实现类是为了继承设计的因此应当遵守上一条的指导原则。
  • 但是抽象类也有一个明显优势:抽象类演化比接口容易得多,抽象类新增方法始终可以增加具体方法,包含合理的默认实现。接口增加新方法会影响所有的实现类,一旦公开发布想改基本不可能。

## 接口只用于定义类型

  • 常量接口模式是对接口的不良使用,它会暴露内部细节、对用户没有价值使用户更加困惑、接口意味着永久承诺,如果被修改常量被删除依然必须实现接口确保二进制兼容(不同版本的类文件能够兼容编译,2.1版本的类文件换到1.8版本中,编译依然能够通过。)。例如Restful风格的接口路径定义。
  • 如果常量与接口或者类紧密相关,就应该把这些常量添加到这个类或者接口中。例如:DoubleInteger中的MIN_VALUEMAX_VALUE常量。如果是枚举类型尽量用枚举。否则使用第4条原则使用不可实例化的工具类。1.5以后可以使用静态导入机制避免用类名修饰常量名。
  • java平台类库中的几个常量接口应该被认为是反面教材java.io.ObjectStreamConstants

类层次优于标签类

  • 标签类有很多缺点。包含标签域、枚举声明、条件判断语句,乱七八糟挤在一起,破坏可读性。实例存在不相关的域占用内存增加。总之,标签类过于冗长、容易出错,并且效率低下。
  • 类层次反映类型之间本质上的层次关系。相比标签类代码简单清楚、每个类型都有自己的类、所有域都是final的。编译器确保抽象方法在每个子类中实现,避免条件判断的失误。
  • 遇到包含标签域的类考虑重构到层次结构中。

用函数对象表示策略

  • 允许函数的调用者通过传入另一个已经存储的包含特定策略的函数作为参数来指定自己的行为。这种模式为策略模式。
  • 没有域的类是无状态的,所有的实例在功能上相互等价,非常适合作为单例类。典型的代表就是具体的策略类。
  • 传入一个具体的策略类客户端无法传递其他的策略,因此设计策略类时需要定义一个策略接口。定义方法参数为接口,使用时传入具体的实现策略。
  • 策略只用一次,通常使用匿名类声明和实例化具体策略。
  • 策略重复使用,通常被实现为私有的静态成员类。并通过使用类的公有的静态final域导出,这个静态final成员类实例就是策略接口。

优先考虑静态成员类

  • 嵌套类存在的目的应该只是为其外围类提供服务。
  • 静态成员类最简单的嵌套类,最好看成普通类,碰巧声明在另一个类内部而已。常见用法是作为公有的辅助类。例如:枚举类。
  • 内部类
    • 非静态成员类
      • 每个实例都隐含一个外围实例相关联。其实例方法内部可以调用外围实例上的方法,或者通过修饰过的this构造获得外围实例的引用。
    • 匿名类
      • 在使用的时候同时声明和实例化,非静态的环境中才有外围实例,静态环境中不可能拥有静态成员。必须保持简洁——10行以内——否则影响可读性。
        • 常见用法
          • 创建具体的策略对象
          • 创建过程对象,例如Runnable/Thread/TimerTask
          • 静态工厂方法内部
    • 局部类
      • 四种嵌套类中用得最少的类。
      • 在可以声明局部变量的地方都可以声明局部类。
      • 跟成员类一样有名字可以重复使用。跟匿名类一样,只有非静态的时候才有外围实例。不能包含静态成员。不能过长要保持可读性。
    • 如何选择嵌套类?
      • 如果方法太长、单个方法之外可见。用静态成员类或非静态类。需要一个指向外围实例的引用,则成员类用非静态的,否则成员类用静态的。
      • 属于一个方法内部,只需要创建一次实例,且有预置类型说明类的特征,用匿名类。否则用局部类。