pursue wind pursue wind
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
  • EffectiveJava

    • 考虑使用静态工厂方法替代构造方法
    • 当构造方法参数过多时使用builder模式
    • 使用私有构造方法或枚类实现Singleton属性
    • 使用私有构造方法执行非实例化
    • 依赖注入优于硬连接资源(hardwiring resources)
    • 避免创建不必要的对象
    • 消除过期的对象引用
    • 避免使用Finalizer和Cleaner机制
    • 使用try-with-resources语句替代try-finally语句
    • 重写equals方法时遵守通用约定
    • 重写equals方法时同时也要重写hashcode方法
    • 始终重写 toString 方法
    • 谨慎地重写 clone 方法
    • 考虑实现Comparable接口
    • 使类和成员的可访问性最小化
    • 在公共类中使用访问方法而不是公共属性
    • 最小化可变性
    • 组合优于继承
    • 要么设计继承并提供文档说明,要么禁用继承
    • 接口优于抽象类
    • 为后代设计接口
    • 接口仅用来定义类型
    • 类层次结构优于标签类
    • 支持使用静态成员类而不是非静态类
    • 将源文件限制为单个顶级类
    • 不要使用原始类型
    • 消除非检查警告
    • 列表优于数组
    • 优先考虑泛型
    • 优先使用泛型方法
    • 使用限定通配符来增加API的灵活性
    • 合理地结合泛型和可变参数
    • 优先考虑类型安全的异构容器
    • 使用枚举类型替代整型常量
    • 使用实例属性替代序数
    • 使用EnumSet替代位属性
    • 使用EnumMap替代序数索引
    • 使用接口模拟可扩展的枚举
    • 注解优于命名模式
    • 始终使用Override注解
    • 使用标记接口定义类型
    • lambda表达式优于匿名类
    • 方法引用优于lambda表达式
    • 优先使用标准的函数式接口
    • 明智审慎地使用Stream
    • 优先考虑流中无副作用的函数
    • 优先使用Collection而不是Stream来作为方法的返回类型
    • 谨慎使用流并行
    • 检查参数有效性
    • 必要时进行防御性拷贝
    • 仔细设计方法签名
    • 明智审慎地使用重载
    • 明智审慎地使用可变参数
    • 返回空的数组或集合,不要返回 null
    • 明智审慎地返回 Optional
    • 为所有已公开的 API 元素编写文档注释
    • 最小化局部变量的作用域
    • for-each 循环优于传统 for 循环
    • 了解并使用库
    • 若需要精确答案就应避免使用 float 和 double 类型
    • 基本数据类型优于包装类
    • 当使用其他类型更合适时应避免使用字符串
    • 当心字符串连接引起的性能问题
    • 通过接口引用对象
    • 接口优于反射
    • 明智审慎地本地方法
    • 明智审慎地进行优化
    • 遵守被广泛认可的命名约定
    • 只针对异常的情况下才使用异常
    • 对可恢复的情况使用受检异常,对编程错误使用运行时异常
    • 避免不必要的使用受检异常
    • 优先使用标准的异常
    • 抛出与抽象对应的异常
    • 每个方法抛出的异常都需要创建文档
    • 在细节消息中包含失败一捕获信息
    • 保持失败原子性
    • 不要忽略异常
    • 同步访问共享的可变数据
    • 避免过度同步
    • executor 、task 和 stream 优先于线程
    • 并发工具优于 wait 和 notify
    • 文档应包含线程安全属性
    • 明智审慎的使用延迟初始化
    • 不要依赖线程调度器
    • 优先选择 Java 序列化的替代方案
    • 非常谨慎地实现 Serializable
    • 考虑使用自定义的序列化形式
    • 保护性的编写 readObject 方法
    • 对于实例控制,枚举类型优于 readResolve
    • 考虑用序列化代理代替序列化实例
  • On Java 8

  • 书
  • EffectiveJava
pursuewind
2020-11-22

接口优于抽象类

# 20. 接口优于抽象类

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

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

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

接口允许构建非层级类型的框架。 类型层级对于组织某些事物来说是很好的,但是其他的事物并不是整齐地落入严格的层级结构中。 例如,假设我们有一个代表歌手的接口,和另一个代表作曲家的接口:

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

public interface Songwriter {
    Song compose(int chartPosition);
}
1
2
3
4
5
6
7

在现实生活中,一些歌手也是作曲家。 因为我们使用接口而不是抽象类来定义这些类型,所以单个类实现歌手和作曲家两个接口是完全允许的。 事实上,我们可以定义一个继承歌手和作曲家的第三个接口,并添加适合于这个组合的新方法:

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

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

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

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

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

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

按照惯例,骨架实现类被称为 AbstractInterface,其中 Interface 是它们实现的接口的名称。 例如,集合框架( Collections Framework)提供了一个框架实现以配合每个主要集合接口:AbstractCollection,AbstractSet,AbstractList 和 AbstractMap。 可以说,将它们称为 SkeletalCollection,SkeletalSet,SkeletalList 和 SkeletalMap 是有道理的,但是现在已经确立了抽象约定。 如果设计得当,骨架实现(无论是单独的抽象类还是仅由接口上的默认方法组成)可以使程序员非常容易地提供他们自己的接口实现。 例如,下面是一个静态工厂方法,在 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;
        }
    };

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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

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

编写一个骨架的实现是一个相对简单的过程,虽然有些乏味。 首先,研究接口,并确定哪些方法是基本的,其他方法可以根据它们来实现。 这些基本方法是你的骨架实现类中的抽象方法。 接下来,为所有可以直接在基本方法之上实现的方法提供接口中的默认方法,回想一下,你可能不会为诸如 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();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

请注意,这个骨架实现不能在 Map.Entry 接口中实现,也不能作为子接口实现,因为默认方法不允许重写诸如 equals,hashCode 和 toString 等 Object 类方法。

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

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

总而言之,一个接口通常是定义允许多个实现的类型的最佳方式。 如果你导出一个重要的接口,应该强烈考虑提供一个骨架的实现类。 在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。 也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式。

Last Updated: 2023/01/30, 11:01:00
要么设计继承并提供文档说明,要么禁用继承
为后代设计接口

← 要么设计继承并提供文档说明,要么禁用继承 为后代设计接口→

Theme by Vdoing | Copyright © 2019-2023 pursue-wind | 粤ICP备2022093130号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜
  • 网络DJ