Effective-java 3 中文翻译系列 (Item 20 接口优先于抽象类)

原文链接

文章也上传到

github

(欢迎关注,欢迎大神提点。)


ITEM 20 接口优先于抽象类


Java有两种机制允许多种实现:interface和abstract类。因为在Java8中提到接口也可以写默认方法了,所以这两种机制都允许你提供一些实例方法的实现了。这两种机制最主要的不同点是:通过一个抽象类实现一个类,那么这个类必须是这个抽象类的子类。而在Java平台中只允许单继承,所以抽象类作为类型的使用被这种约束严格的限制住了。然而任何正常的类(定义了所要求的方法和遵循一般的类定义规则的类),不论它处于哪个类层级都可以实现任何一个接口。

现有的类可以很容易的通过实现一个接口来更新它的能力。你需要做的仅仅是实现接口的方法,并在类声明的地方添加implements接口的句子。例如,很多现有的类都是通过实现Comparable, Iterable, 和 Autocloseable接口添加相应的功能。而一般现有的类不能通过继承一个新的抽象类来更新功能。如果你想要让两个类都继承自同一个抽象类,你就必须调整抽象类的层级成为比这两个类更高一层的类。但是这样做可能会造成对类层级的危害,因为这样无论是否合适都需要强制把所有的类都设置成这个新抽象类的子类。

接口是定义mixins类型的理想选择。不严格地说,一个minxin类型指的是一个类在它主要的功能之外可以添加一些其他可选的行为。例如,Comparable允许一个类使用自己的类对象和其他的遵守comparable接口的对象进行排序,像这样的接口被称为一个mixin类型,因为它允许可选的功能被“mixed in”到一个主要的功能上。抽象类不能被用于定义mixins,因为它们不能被添加到一个现有的类上:一个类不能有多个父类,而且在类层级上也没有恰当的地方安放这样的类。

接口允许我们创建一个非层级结构的框架。层级结构对于一些事情是好的,但是严格的层级化结构不利于其他的东西加入进来。例如,假设我们有一个接口代表歌唱家,另一个接口代表作曲家:

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

在现实中,一些歌唱家也是作曲家。因为我们使用的是interface而不是抽象类,所以很容易可以做到使一个类同时实现Singer和Songwriter两个接口。事实上,我们可以定义第三个接口同时继承自Singer和Songwriter两个接口,并添加适合的新方法到这个这个组合上:

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

你可能不需要这种灵活性,但是一旦你需要的时候,接口就能够为你提供。
而使用抽象类的话,想要实现上面说的那些组合就会造成类的层级臃肿。如果有n种类型,那么就可能需要2的n次方种组合类出现,这被称为组合爆炸。这种臃肿的类结构会导致这些类存在很多仅仅是参数不同的方法,因为在这些类层次中没有能够捕获公共行为的类。
通过使用(Item18)中的包装类,接口能提供更加安全和有利的功能。如果你使用抽象类定义类型,那么程序猿只能通过继承重写或者添加新的功能,这就比包装类多了一些限制,少了很多可提供的功能。
当有接口为其他接口实现默认的方法时,考虑为使用者提供帮助文档,使用Item19中的@implSpec文档标签。
接口中使用默认方法有以下一些限制:

  • 即使很多接口需要使用object的方法,例如equals和hashCode,你也不允许自己实现这些默认方法。
  • 接口不允许包含实例属性和非公共的静态成员(私有静态方法除外)。
  • 不能给不受你控制的接口添加默认方法。

但是你可以结合使用接口和抽象类的优点,使用接口实现抽象的骨架实现类。这里接口定义类型,也可能实现了一些默认方法,骨架实现类在原有的接口方法之上保留了非原始的接口方法。扩展骨架的实现把大多数的工作从实现接口中剥离了出来。这被称为模版方法模式。

按照惯例,骨架实现类被称为Abstract+接口名字,这里的接口是要实现的接口。例如Collections框架为每一个主要的collection接口都这么做了:AbstractCollection, AbstractSet, AbstractList和 AbstractMap.在它们的命名上有一些争议或许应该叫SkeletalCollection, SkeletalSet, SkeletalList, 和 SkeletalMap,但是Abstract的习惯已经根深蒂固了。
当被恰当的设计时,骨架实现(无论是单独的抽象类还是和接口的默认方法组合)都可以使程序员很容易的提供它们自己的接口实现。例如,这里有一个位于AbstractList之上的静态工厂方法,它包含一个完整的、功能齐全的List实现:

static List intArrayAsList(int[] a) {
        Objects.requireNonNull(a);
        // 菱形操作符只在JAVA9之后可用,如果你在之前的版本请使用 
        return new AbstractList<>() {
            @Override public Integer get(int i) {
                return a[i]; // Autoboxing (Item 6)
            }
            @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,可以将int数组作为一个Integer列表来使用。而不用将int和Integer来回转换(因为转换的性能并不高)。注意这个实现是匿名类的形式(Item24)。骨架实现类可以为抽象类提供实现,而不受抽象类做为类型使用时的约束。大多情况下,实现一个骨架实现类的接口是继承于它,但这也是可选的。一个类如果不能继承于一个骨架实现类,但是可以直接实现接口。这个类还是能从接口的默认方法中继续继承一些实现。此外,骨架实现类还是有办法帮助到调用者完成工作。实现接口的类可以转发一个接口方法的实现到一个类内部私有的对象上,这个对象可能是一个骨架实现类的子类对象。这个技术被称为模拟多重继承,关于此最近的讨论是在Item18中。它有很多多重继承的优点而且避免了很多缺陷。

写一个骨架实现类相对来说是比较简单的,但是过程稍微有点乏味。

  • 首先,研究接口,决定哪些方法是最基础的,可以被写成骨架的抽象方法,被别人实现。
  • 然后,为可以在原始方法之上直接实现的方法提供默认实现。但是你不能为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接口内部,也不能成为Map.Entry的子接口,因为不允许覆盖Object的默认方法如equals, hashCode, 和 toString。

因为骨架实现是为继承而设计的,所以你要遵循Item19中说的文档和设计原则。为了简单起见,前面的例子中文档有些被省略了,但是好的文档在骨架实现中绝对是有必要的

有一点不同的是简单实现,例如AbstractMap.SimpleEntry。它也实现了接口并也是为继承而设计的,但是它不是抽象的,可以被单独的使用。

总结一下:接口是允许多种实现的定义类型最好的方法。如果你导出一个重要的接口,你应该为它提供一个骨架实现。在可能的范围内,你应该尽可能通过提供默认的接口方法实现,以便所有实现这个接口的类都可以调用。也就是说,对接口的限制通常要求骨架实现采用抽象类的形式。

你可能感兴趣的:(Effective-java 3 中文翻译系列 (Item 20 接口优先于抽象类))