Effective Java 第三版——20. 接口优于抽象类

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。html

Effective Java, Third Edition

20. 接口优于抽象类

Java有两种机制来定义容许多个实现的类型:接口和抽象类。 因为在Java 8 [JLS 9.4.3]中引入了接口的默认方法(default methods ),所以这两种机制都容许为某些实例方法提供实现。 一个主要的区别是要实现由抽象类定义的类型,类必须是抽象类的子类。 由于Java只容许单一继承,因此对抽象类的这种限制严格限制了它们做为类型定义的使用。 任何定义全部必需方法并服从通用约定的类均可以实现一个接口,而无论类在类层次结构中的位置。java

现有的类能够很容易地进行改进来实现一个新的接口。 你只需添加所需的方法(若是尚不存在的话),并向类声明中添加一个implements子句。 例如,当Comparable, Iterable, 和Autocloseable接口添加到Java平台时,不少现有类须要实现它们来加以改进。 通常来讲,现有的类不能改进以继承一个新的抽象类。 若是你想让两个类继承相同的抽象类,你必须把它放在类型层级结构中的上面位置,它是两个类的祖先。 不幸的是,这会对类型层级结构形成很大的附带损害,迫使新的抽象类的全部后代对它进行子类化,不管这些后代类是否合适。程序员

接口是定义混合类型(mixin)的理想选择。 通常来讲,mixin是一个类,除了它的“主类型”以外,还能够声明它提供了一些可选的行为。 例如,Comparable是一个类型接口,它容许一个类声明它的实例相对于其余可相互比较的对象是有序的。 这样的接口被称为类型,由于它容许可选功能被“混合”到类型的主要功能。 抽象类不能用于定义混合类,这是由于它们不能被加载到现有的类中:一个类不能有多个父类,而且在类层次结构中没有合理的位置来插入一个类型。设计模式

接口容许构建非层级类型的框架。 类型层级对于组织某些事物来讲是很好的,可是其余的事物并非整齐地落入严格的层级结构中。 例如,假设咱们有一个表明歌手的接口,另外一个表明做曲家的接口:数组

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

public interface Songwriter {
    Song compose(int chartPosition);
}

在现实生活中,一些歌手也是做曲家。 由于咱们使用接口而不是抽象类来定义这些类型,因此单个类实现歌手和做曲家两个接口是彻底容许的。 事实上,咱们能够定义一个继承歌手和做曲家的第三个接口,并添加适合于这个组合的新方法:安全

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

    void actSensitive();
}

你并不老是须要这种灵活性,可是当你这样作的时候,接口是一个救星。 另外一种方法是对于每一个受支持的属性组合,包含一个单独的类的臃肿类层级结构。 若是类型系统中有n个属性,则可能须要支持2n种可能的组合。 这就是所谓的组合爆炸(combinatorial explosion)。 臃肿的类层级结构可能会致使具备许多方法的臃肿类,这些方法仅在参数类型上有所不一样,由于类层级结构中没有类型来捕获通用行为。框架

接口经过包装类模式确保安全的,强大的功能加强成为可能(条目 18)。 若是使用抽象类来定义类型,那么就让程序员想要添加功能,只能继承。 生成的类比包装类更弱,更脆弱。ide

当其余接口方法有明显的接口方法实现时,能够考虑向程序员提供默认形式的方法实现帮助。 有关此技术的示例,请参阅第104页的removeIf方法。若是提供默认方法,请确保使用@implSpec Javadoc标记(条目19)将它们文档说明为继承。性能

使用默认方法能够提供实现帮助多多少少是有些限制的。 尽管许多接口指定了Object类中方法(如equalshashCode)的行为,但不容许为它们提供默认方法。 此外,接口不容许包含实例属性或非公共静态成员(私有静态方法除外)。 最后,不能将默认方法添加到不受控制的接口中。学习

可是,你能够经过提供一个抽象的骨架实现类(abstract skeletal implementation class)来与接口一块儿使用,将接口和抽象类的优势结合起来。 接口定义了类型,可能提供了一些默认的方法,而骨架实现类在原始接口方法的顶层实现了剩余的非原始接口方法。 继承骨架实现须要大部分的工做来实现一个接口。 这就是模板方法设计模式[Gamma95]。

按照惯例,骨架实现类被称为AbstractInterface,其中Interface是它们实现的接口的名称。 例如,集合框架( Collections Framework)提供了一个框架实现以配合每一个主要集合接口:AbstractCollectionAbstractSetAbstractListAbstractMap。 能够说,将它们称为SkeletalCollectionSkeletalSetSkeletalListSkeletalMap是有道理的,可是如今已经确立了抽象约定。 若是设计得当,骨架实现(不管是单独的抽象类仍是仅由接口上的默认方法组成)可使程序员很是容易地提供他们本身的接口实现。 例如,下面是一个静态工厂方法,在AbstractList的顶层包含一个完整的功能齐全的List实现:

// Concrete implementation built atop skeletal implementation

static List<Integer> intArrayAsList(int[] a) {

    Objects.requireNonNull(a);

    // The diamond operator is only legal here in Java 9 and later

    // If you're using an earlier release, specify <Integer>

    return new AbstractList<>() {

        @Override public Integer get(int i) {

            return a[i];  // Autoboxing ([Item 6](https://www.safaribooksonline.com/library/view/effective-java-third/9780134686097/ch2.xhtml#lev6))

        }

        @Override public Integer set(int i, Integer val) {

            int oldVal = a[I];

            a[i] = val;     // Auto-unboxing

            return oldVal;  // Autoboxing

        }

        @Override public int size() {

            return a.length;

        }

    };

}

当你考虑一个List实现为你作的全部事情时,这个例子是一个骨架实现的强大的演示。 顺便说一句,这个例子是一个适配器(Adapter )[Gamma95],它容许一个int数组被看做Integer实例列表。 因为int值和整数实例(装箱和拆箱)之间的来回转换,其性能并非很是好。 请注意,实现采用匿名类的形式(条目 24)。

骨架实现类的优势在于,它们提供抽象类的全部实现的帮助,而不会强加抽象类做为类型定义时的严格约束。对于具备骨架实现类的接口的大多数实现者来讲,继承这个类是显而易见的选择,但它不是必需的。若是一个类不能继承骨架的实现,这个类能够直接实现接口。该类仍然受益于接口自己的任何默认方法。此外,骨架实现类仍然能够协助接口的实现。实现接口的类能够将接口方法的调用转发给继承骨架实现的私有内部类的包含实例。这种被称为模拟多重继承的技术与条目 18讨论的包装类模式密切相关。它提供了多重继承的许多好处,同时避免了缺陷。

编写一个骨架的实现是一个相对简单的过程,虽然有些乏味。 首先,研究接口,并肯定哪些方法是基本的,其余方法能够根据它们来实现。 这些基本方法是你的骨架实现类中的抽象方法。 接下来,为全部能够直接在基本方法之上实现的方法提供接口中的默认方法,回想一下,你可能不会为诸如Object类中equalshashCode等方法提供默认方法。 若是基本方法和默认方法涵盖了接口,那么就完成了,而且不须要骨架实现类。 不然,编写一个声明实现接口的类,并实现全部剩下的接口方法。 为了适合于该任务,此类可能包含任何的非公共属性和方法。

做为一个简单的例子,考虑一下Map.Entry接口。 显而易见的基本方法是getKey,getValue和(可选的)setValue。 接口指定了equalshashCode的行为,而且在基本方面方面有一个toString的明显的实现。 因为不容许为Object类方法提供默认实现,所以全部实现均放置在骨架实现类中:

// Skeletal implementation class

public abstract class AbstractMapEntry<K,V>

        implements Map.Entry<K,V> {

    // 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接口中实现,也不能做为子接口实现,由于默认方法不容许重写诸如equalshashCodetoStringObject类方法。

因为骨架实现类是为了继承而设计的,因此你应该遵循条目 19中的全部设计和文档说明。为了简洁起见,前面的例子中省略了文档注释,可是好的文档在骨架实现中是绝对必要的,不管它是否包含 一个接口或一个单独的抽象类的默认方法。

与骨架实现有稍许不一样的是简单实现,以AbstractMap.SimpleEntry为例。 一个简单的实现就像一个骨架实现,它实现了一个接口,而且是为了继承而设计的,可是它的不一样之处在于它不是抽象的:它是最简单的工做实现。 你能够按照状况使用它,也能够根据状况进行子类化。

总而言之,一个接口一般是定义容许多个实现的类型的最佳方式。 若是你导出一个重要的接口,应该强烈考虑提供一个骨架的实现类。 在可能的状况下,应该经过接口上的默认方法提供骨架实现,以便接口的全部实现者均可以使用它。 也就是说,对接口的限制一般要求骨架实现类采用抽象类的形式。

相关文章
相关标签/搜索