Effective Java(3rd)-Item20 使用接口而不是抽象类

  Java有两种机制来定义允许多种实现的类型:接口和抽象类。自从Java8[JLS 9.4.3]引入了接口的default方法以来,这两种机制都允许你为某些实例方法提供实现。一个主要的区别是,为了实现抽象类定义的类型,类必须是抽象类的子类。因为Java只允许单继承,所以对抽象类的这种限制严重限制了它们作为类型定义的使用。无论类在类层次结构的哪里,都允许任何定义所有必需方法并遵守常规契约的类实现接口。
  现有类可以很容易实现新接口来改造这个类。你所要做的就是添加所需的方法(如果它们还不存在),并在类声明中添加implements子句。例如,许多现有类被改进以实现Comparable,Iterable和Autocloseable接口,当这些接口出现在平台的时候。但是通常现有类不能通过扩展新的抽象类被改进。如果你想要两个类都扩展相同的抽象类,你不得不将这个抽象类放到类型层级高处,使得抽象类是这两个类的祖先。不幸的是,这会对类型层次接口造成极大地附带损害,强迫抽象类的所有后代对其进行子类化,无论是否合适。
  接口是定义mixins的理想选择。松散地说,mixins是类可以实现的类型,除了它的”主类型“,以声明它提供了一些可选行为。例如,Comparable是一个mixin接口,允许类声明其实例相对于其他可相互比较的对象进行排序。这样的接口叫做mixin,因为它允许将可选功能”混入“到类型的主要功能。抽象类不能用于定义mixins,原因与它们无法改进现有类相同:一个类不能有一个或多个父类,并且类层次结构没有合理的位置来插入mixin。
  接口允许构造非分层类型框架。类型层次结构非常适合组织某些事物,但是其他事情并不能完全落入严格的层次结构中。例如,假设我们有一个代表歌手的接口和另个代表歌曲作者的接口:

public interface Singer {
    AudioClip sing(Song s);
}

public interface Singer {
    AudioClip sing(Song s);
}

  在现实生活中,一些歌手同时还是词曲作者.因为我们使用接口而不是抽象类来定义这些类型,所以单个类完全允许实现Singer和Songwriter类。事实上,我们可以定义第三个接口同时扩展Singer和Songwriter并添加适合组合的新方法:

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();

    void actSensitive();
}

  你并不总是需要这种灵活性,但是当你需要这么做时,接口就是你的救星。另一种方法是膨胀类层级结构,其中包含每个受支持的属性组合的单独类。如果类型系统有n个属性,那就可能需要支持2^n种可能的组合。这就是众所周知的组合爆炸。膨胀的类层次结构可能会导致具有许多方法的膨胀类,这些方法仅在其参数类型上有所不同,因为类层次结构中没有类型来捕获常见行为。
  接口实现了安全,强大的功能增强,通过包装类习惯用法(item18).如果你使用抽象类来定义类型,那就使得想要添加功能的程序员除了继承外别无选择。结果类比包装类更不强大,更脆弱。
  当根据其他接口方法明显实现接口方法时,考虑以默认方法的形式向程序员提供帮助。有关此技术的实例,参见第104页的removeIf方法。如果提供默认方法,确保使用@implSpec Javadoc标记(item19)记录它们的继承。
  通过提供实现方法的默认方法有一些限制。虽然许多接口指定了Object方法的行为,比如equals和hashCode,但是不允许为它们提供默认方法。此外,接口不被允许包含实例字段或非公有静态成员(私有静态方法除外)。最后,你不能向你无法控制的接口添加默认方法。
  但是你可以通过提供与接口一个使用的抽象骨架实现类来结合接口和抽象类的优点。接口定义类型,可能提供一些默认方法,而骨架实现类实现生于非基本接口方法。扩展骨架实现类实现需要完成大部分工作来实现接口。这就是模板方法模式[Gamma95].
  按照惯例,骨架实现类被称为AbstractInterface,interface是实现接口的名字。例如Collections框架提供了一个骨架实现,以与每个主集合接口一起使用:AbstractCollection, AbstractSet, AbstractList, 和 AbstractMap。
  可以认为,把他们称为SkeletalCollection, SkeletalSet, SkeletalList, 和 SkeletalMap是由意义的,但是已经确立了Abstract的约定了。正确设计时,骨架实现(无论是单独的抽象类还是仅包含接口上的默认方法)都可以使程序员很容易提供自己的接口方法。例如,这是一个静态工厂方法,在AbstractList上包含了一个完整,功能齐全的List实现:

Effective Java(3rd)-Item20 使用接口而不是抽象类_第1张图片
image.png

  当你考虑List实现能为你做所有事情时,此实例是对骨架实现的强大功能的令人印象深刻的演示。顺便提一下,这个例子是一个适配器 [Gamma95],允许将int数组视为Integer实例列表。由于在int值和Integer实例之间来回转换(装箱和拆箱),它的性能并不是那么好。请注意,它的实现采用了匿名类的形式 (item24) 。
  骨架实现类的优点在于它们提供了抽象类的所有实现帮助,而不会强加抽象类在用作类型定义时所施加的严格约束。对于大多数具有骨架实现类的接口的实现者,扩展此类是显而易见的选择,但是它是严格可选的。如果一个类不能扩展骨架实现类,该类始终可以直接实现该接口。该类仍然受益于接口本身存在的任何默认方法。此外,骨架实现仍然可以帮助实现者的任务。实现接口的类可以将接口方法的调用转发到扩展骨架实现的私有内部类的包含实例。这种称为模拟多重继承的技术与条目18中讨论的包装类习惯密切相关。它提供了多重继承的许多好处,同时避免了陷阱。
  编写骨架实现类是一个相对简单但有点乏味的过程。首先,研究接口并确定哪些方法是其他方法可以实现的原语。这些原语将是骨架实现中的抽象方法。接下来,在接口中可以直接在基元上实现的所有方法提供默认方法,但是记住,你可能不能为Object的方法提供默认方法(比如equals和hashCode)。如果基元和默认方法覆盖了接口,你就完成了,并且也不需要骨架实现类。否则,编写一个声明为实现接口的类,以及所有剩余接口方法的实现。该类可以包含适合该任务的任何非公有字段和方法。
  举一个简单的例子,考虑Map.Entry 接口。明显的原语是getKey,getValue,和(可选的)setValue.接口指定了equals和hashCode的行为,并在原语上有明显的toString实现。由于你不被允许为Object方法提供默认实现,所有实现都被放在骨架实现类中:

// Skeletal implementation class
public abstract class AbstractMapEntry implements Map.Entry {
    // Entries in a modifiable map must override this method
    @Override
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    // Implements the general contract of Map.Entry.equals
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Map.Entry)) return false;
        Map.Entry e = (Map.Entry) o;
        return Objects.equals(e.getKey(), getKey()) && Objects.equals(e.getValue(), getValue());
    }

    // Implements the general contract of Map.Entry.hashCode
    @Override
    public int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    @Override
    public String toString() {
        return getKey() + "=" + getValue();
    }
}

  请注意,该骨架实现无法在Map.Entry接口中实现或作为子接口实现,因为不允许使用默认方法来覆写注入equals,hashCode和toString之类的Object方法。
  因为骨架实现是为继承而设计的,你应该遵循条目19的所有设计和文档指南。为了简洁起见,前面的示例省略了文档注释,但是良好的文档在骨架实现中绝对是有必要的,无论它是由接口上的默认方法还是单独的抽象类组成的。
  骨架实现的一个小变体就是简单实现,例如AbstractMap.SimpleEntry。一个简单实现就像骨架实现,因为它实现了一个接口并且为继承而设计,但它的不同之处在于它不是抽象的:它是最简单的工作实现。你可以根据情况使用它,或根据情况保证子类。
  总而言之,接口通常是定义多个实现类型的最佳方式。如果你导出了一个不容易的接口,你可以强烈考虑提供骨架实现来配合它。在可能的范围内,你应该通过接口的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。也就是说,在接口的限制的情况下,应该使用骨架类采用抽象类的形式来实现。
本文写于2019.4.11,历时3天

你可能感兴趣的:(Effective Java(3rd)-Item20 使用接口而不是抽象类)