Effective Java笔记(20)接口优于抽象类

        Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从Java 8为继承引入了缺省方法( default method),这两种机制都允许为某些实例方法提供实现。主要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。因为Java只允许单继承,所以用抽象类作为类型定义受到了限制。任何定义了所有必要的方法并遵守通用约定的类,都允许实现一个接口,无论这个类是处在类层次结构中的什么位置。

        现有的类可以很容易被更新,以实现新的接口。如果这些方法尚不存在,你所需要做的就只是增加必要的方法,然后在类的声明中增加一个implements子句。例如,当Comparable、Iterable 和Autocloseable接口被引入Java平台时,更新了许多现有的类,以实现这些接口。一般来说,无法更新现有的类来扩展新的抽象类。如果你希望两个类扩展同一个抽象类,就必须把抽象类放到类型层次( type hierarchy) 的高处,这样它就成了那两个类的一个祖先。遗憾的是,这样做会间接地伤害到类层次,迫使这个公共祖先的所有后代类都扩展这个新的抽象类,无论它对于这些后代类是否合适。

        接口是定义mixin (混合类型)的理想选择。不严格地讲,mixin 类型是指:类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了某些可供选择的行为。例如Comparable是一个mixin接口,它允许类表明它的实例可以与其他的可相互比较的对象进行排序。这样的接口之所以被称为mixin,是因为它允许任选的功能可被混合到类型的主要功能中。抽象类不能被用于定义mixin,同样也是因为它们不能被更新到现有的类中:类不可能有一个以上的父类,类层次结构中也没有适当的地方来插入mixin。

        接口允许构造非层次结构的类型框架。类型层次对于组织某些事物是非常合适的,但是其他事物并不能被整齐地组织成一个严格的层次结构。例如,假设我们有一个接口代表一个singer (歌唱家),另一个接口代表一个songwriter (作曲家):
 

public interface Singer {
    AudioClip sing(Song s);
}
public interface Songwriter {
    Song compose(int chartPosition);
}

        在现实生活中,有些歌唱家本身也是作曲家。因为我们使用了接口而不是抽象类来定义这些类型,所以对于单个类而言,它同时实现Singer和Songwriter是完全允许的。实际上,我们可以定义第三个接口,它同时扩展Singer和Songwriter,并添加一些适合于这种组合的新方法:

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

        也许并非总是需要这种灵活性,但是一旦这样做了,接口可就成了救世主。另外一种做法是编写一个臃肿( bloated)的类层次,对于每一-种要被支持的属性组合,都包含一个单独的类。如果在整个类型系统中有n个属性,那么就必须支持2”种可能的组合。这种现象被称为“组合爆炸”(combinatorialexplosion)。类层次臃肿会导致类也臃肿,这些类包含许多方法,并且这些方法只是在参数的类型上有所不同而已,因为类层次中没有任何类型体现了公共的行为特征。

        通过第18条中介绍的包装类(wrapperclass)模式,接口使得安全地增强类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增加功能,再没有其他的选择了。这样得到的类与包装类相比,功能更差,也更加脆弱。

        当一个接口方法根据其他接口方法有了明显的实现时,可以考虑以缺省方法的形式为程序员提供实现协助。通过缺省方法可以提供的实现协助是有限的。虽然许多接口都定义了object方法的行为,如equals和hashCode,但是不允许给它们提供缺省方法。而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)。最后一点,无法给不受你控制的接口添加缺省方法。

        但是,通过对接口提供一个抽象的骨架实现( skeletal implementation)类,可以把接口和抽象类的优点结合起来。接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法。

        按照惯例,骨架实现类被称为AbstractInterface,这里的Interface是指所实现的接口的名字。例如,Collections Framework为每个重要的集合接口都提供了一个骨架实现,包括AbstractCollection、AbstractSet、AbstractList和AbstractMap。将它们称作SkeletalCollection、SkeletalSet、 SkeletalList 和SkeletalMap也是有道理的,但是现在Abstract的用法已经根深蒂固。如果设计得当,骨架实现(无论是单独一个抽象类,还是接口中唯-包含的缺省方法)可以使程序员非常容易地提供他们自己的接口实现。例如,下面是一个静态工厂方法,除AbstractList之外,它还包含了一个完整的、功能全面的List实现:

static List<Integer> intArrayAsList(int[] a) {
    Objects. requireNonNull(a);
    return new AbstractList<>() {
        @Override 
        public Integer get(int i) {
            return a[i];
        }
        @Override 
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;
            return oldVal;
        }
        @Override 
        public int size() {
            return a.length;
        }
    };
}

        如果想知道一个List实现应该为你完成哪些工作,这个例子就充分演示了骨架实现的强大功能。顺便提一下,这个例子是个Adapter,它允许将int数组看作Integer实例的列表。由于在int值和Integer实例之间来回转换需要开销,它的性能不会很好。注意,这个实现采用了匿名类(anonymous class)的形式。

        骨架实现类的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象.类被用作类型定义时”所特有的严格限制。对于接口的大多数实现来讲,扩展骨架实现类是个很显然的选择,但并不是必须的。如果预置的类无法扩展骨架实现类,这个类始终能手工实现这个接口。同时,这个类本身仍然受益于接口中出现的任何缺省方法。此外,骨架实现类仍然有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。这种方法被称作模拟多重继承( simulated multiple inheritance),它与第18条中讨论过的包装类模式密切相关。这项技术具.有多重继承的绝大多数优点,同时又避免了相应的缺陷。

        编写骨架实现类相对比较简单,只是过程有点乏味。首先,必须认真研究接口,并确定哪些方法是最为基本的,其他的方法则可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法。接下来,在接口中为所有可以在基本方法之.上直接实现的方法提供缺省方法,但要记住,不能为object方法(如equals和hashCode)提供缺省方法。如果基本方法和缺省方法覆盖了接口,你的任务就完成了,不需要骨架实现类了。否则,就要编写一个类,声明实现接口,并实现所有剩下的接口方法。这个类中可以包含任何非公有的域,以及适合该任务的任何方法。

        以Map.Entry接口为例,举个简单的例子。明显的基本方法是getKey、getValue和(可选的) setValue。接口定义了equals和hashCode的行为,并且有一个明显的toString实现。由于不允许给object方法提供缺省实现,因此所有实现都放在骨架实现类中:

public abstract class AbstractMapEntry<K, V>
        implements Map.Entry<K,V> {

    @Override
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    @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());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    @Override
    public String toString() {
        return getKey() + "=" + getValue();
    }
}

        注意,这个骨架实现不能在Map.Entry接口中实现,也不能作为子接口,因为不允许缺省方法覆盖object方法,如equals、hashCode和toString。因为骨架实现类是为了继承的目的而设计的,所以应该遵从第19条中介绍的所有关于设计和文档的指导原则。为了简洁起见,上面例子中的文档注释部分被省略掉了,但是对于骨架实现类而言,好的文档绝对是非常必要的,无论它是否在接口或者单独的抽象类中包含了缺省方法。

        骨架实现上有个小小的不同,就是简单实现( simple implementation), AbstractMap.SimpleEntry就是个例子。简单实现就像骨架实现一样,这是因为它实现了接口,并且是为了继承而设计的,但是区别在于它不是抽象的:它是最简单的可能的有效实现。你可以原封不动地使用,也可以看情况将它子类化。

        总而言之,接口通常是定义允许多个实现的类型的最佳途径。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。而且,还应该尽可能地通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用。也就是说,对于接口的限制,通常也限制了骨架实现会采用的抽象类的形式。

猜你喜欢

转载自blog.csdn.net/java_faep/article/details/132075562