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<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);
        // 菱形操作符只在JAVA9之后可用,如果你在之前的版本请使用 <Integer>
        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<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接口内部,也不能成为Map.Entry的子接口,因为不允许覆盖Object的默认方法如equals, hashCode, 和 toString。

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容