接口和类

This commit is contained in:
尉勇强2 2019-10-24 20:11:14 +08:00
parent 9aded46fe5
commit 52e5d7165a
13 changed files with 588 additions and 0 deletions

View File

@ -1,2 +1,45 @@
## 最小化可变性
不可变类简单来说是它的实例不能被修改的类。
包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。
Java 平台类库包含许多不可变的类,包括 `String` 类,基本类型包装类以及 `BigInteger` 类和 `BigDecimal` 类。
有很多很好的理由:不可变类比可变类更容易设计,实现和使用。 他们不太容易出错,更安全。
要使一个类不可变,请遵循以下五条规则:
1. **不要提供修改对象状态的方法(也称为 mutators。**
2. **确保这个类不能被继承。** 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化通常是通过 `final` 修饰类,但是我们稍后将讨论另一种方法。
3. **把所有属性设置为 final。** 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型所述。
4. **把所有的属性设置为 private。** 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 `final` 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(详见第 15 和 16 条)。
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。
**示例代码**[Item17Example01.java](ClassesAndInterfaces/src/main/java/com/jueee/item17/Item17Example01.java):代表了一个复数(包含实部和虚部的数字)。 除了标准的 `Object` 方法之外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何创建并返回一个新的 `Complex` 实例,而不是修改这个实例。 这种模式被称为函数式方法,因为方法返回将操作数应用于函数的结果,而不修改它们。
**不可变对象很简单。** 一个不可变的对象可以完全处于一种状态,也就是被创建时的状态。 如果确保所有的构造方法都建立了类不变量,那么就保证这些不变量在任何时候都保持不变,使用此类的程序员无需再做额外的工作。 另一方面,可变对象可以具有任意复杂的状态空间。 如果文档没有提供由设置mutator方法执行的状态转换的精确描述那么可靠地使用可变类可能是困难的或不可能的。
**不可变对象本质上是线程安全的; 它们不需要同步。** 被多个线程同时访问它们时并不会被破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以**不可变对象可以被自由地共享。** 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。
```java
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
```
不可变对象可以自由分享的结果是你永远不需要做出防御性拷贝defensive copies详见第 50 条)。 事实上,永远不需要做任何拷贝,因为这些拷贝永远等于原始对象。 因此,你不需要也不应该在一个不可变的类上提供一个 clone 方法或拷贝构造方法copy constructor详见第 13 条)。 这一点在 Java 平台的早期阶段还不是很好理解,所以 `String` 类有一个拷贝构造方法,但是它应该尽量很少使用(详见第 6 条)。
**不仅可以共享不可变的对象,而且可以共享内部信息。** 例如,`BigInteger` 类在内部使用符号数值表示法。 符号用 `int` 值表示,数值用 `int` 数组表示。 `negate` 方法生成了一个数值相同但符号相反的新 `BigInteger` 实例。 即使它是可变的,也不需要复制数组;新创建的 `BigInteger` 指向与原始相同的内部数组。
**不可变对象为其他对象提供了很好的构件building blocks**,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成 `Map` 对象的键和 `Set` 的元素,一旦不可变对象作为 `Map` 的键或 `Set` 里的元素,即使破坏了 `Map``Set` 的不可变性,但不用担心它们的值会发生变化。
**不可变对象提供了免费的原子失败机制(详见第 76 条)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是如果是大型的对象下。
**示例代码**[Item17Example02.java](ClassesAndInterfaces/src/main/java/com/jueee/item17/Item17Example02.java):创建一个不可改变类,可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法。
**如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性** 。减少对象可以存在的状态数量,可以更容易地分析对象,以及降低出错的可能性。因此,除非有足够的理由把属性设置为非 `final` 的情况下,否则应该每个属性都设置为 `final` 的。
把本条目的建议与条目 15 的建议结合起来,你自然的倾向就是:**除非有充分的理由不这样做,否则应该把每个属性声明为私有 final 的。**

View File

@ -0,0 +1,39 @@
## 组合优于继承
继承是实现代码重用的有效方式,但并不总是最好的工具。
从普通的具体类跨越包级边界继承,是危险的。
**与方法调用不同,继承打破了封装。**
换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
查询 `HashSet` ,从创建它之后已经添加了多少个元素:
- [Item18Example01.java](ClassesAndInterfaces/src/main/java/com/jueee/item18/Item18Example01.java):不满足预期
`HashSet` 类包含两个添加元素的方法,分别是 `add``addAll`,所以重写这两个方法。
- [Item18Example02.java](ClassesAndInterfaces/src/main/java/com/jueee/item18/Item18Example02.java):满足预期
因为 `HashSet``addAll` 方法是在其 `add` 方法之上实现的。通过消除 `addAll` 方法的重写来“修复”子类。
导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。假设一个程序的安全性依赖于这样一个事实:所有被插入到集中的元素都满足一个先决条件。可以通过对集合进行子类化,然后并重写所有添加元素的方法,以确保在添加每个元素之前满足这个先决条件,来确保这一问题。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有可能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。在把 `Hashtable``Vector` 类加入到 `Collections` 框架中的时候,就修复了几个类似性质的安全漏洞。
这两个问题都源于重写方法。 如果仅仅添加新的方法并且不要重写现有的方法,可能会认为继承一个类是安全的。 虽然这种扩展更为安全,但这并非没有风险。 如果父类在后续版本中添加了一个新的方法,并且你不幸给了子类一个具有相同签名和不同返回类型的方法,那么你的子类编译失败。 如果已经为子类提供了一个与新的父类方法具有相同签名和返回类型的方法,那么你现在正在重写它,因此将遇到前面所述的问题。 此外,你的方法是否会履行新的父类方法的约定,这是值得怀疑的,因为在你编写子类方法时,这个约定还没有写出来。
幸运的是,有一种方法可以避免上述所有的问题。不要继承一个现有的类,而应该给你的新类增加一个私有属性,该属性是 现有类的实例引用这种设计被称为组合composition因为现有的类成为新类的组成部分。新类中的每个实例方法调用现有类的包含实例上的相应方法并返回结果。这被称为转发forwarding而新类中的方法被称为转发方法。由此产生的类将坚如磐石不依赖于现有类的实现细节。
通过组合方式实现:
- [ForwardingSet.java](ClassesAndInterfaces/src/main/java/com/jueee/item18/ForwardingSet.java):新类中的每个实例方法调用现有类的包含实例上的相应方法并返回结果。
- [InstrumentedSet.java](ClassesAndInterfaces/src/main/java/com/jueee/item18/InstrumentedSet.java):包装类,因为每个 `InstrumentedSet` 实例都包含(“包装”)另一个 Set 实例。
这也被称为装饰器模式,因为 `InstrumentedSet` 类通过添加计数功能来“装饰”一个集合。
- [Item18Example03.java](ClassesAndInterfaces/src/main/java/com/jueee/item18/Item18Example03.java):测试组合方式实现的。
如果在合适组合的地方使用继承,则会不必要地公开实现细节。由此产生的 API 将与原始实现联系在一起,永远限制类的性能。更严重的是,通过暴露其内部,客户端可以直接访问它们。至少,它可能导致混淆语义。
总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。

View File

@ -0,0 +1,29 @@
## 要么设计继承并提供文档说明,要么禁用继承
对于专门为了继承而设计并且具有良好文档说明的类而言,这个类必须准确地描述重写每个方法带来的影响。
换句话说该类必须文档说明可重写方法的自用性self-use。 对于每个 public 或者 protected 的方法,文档必须指明方法调用哪些可重写方法,以何种顺序调用的,以及每次调用的结果又是如何影响后续处理。更一般地说,一个类必须文档说明任何可能调用可重写方法的情况。
关于程序文档有句格言:**好的 API 文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。**
为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无须承受不必要的痛苦,**类必须以精心挑选的 protected 方法的形式提供适当的钩子hook以便进入其内部工作中**。或者在罕见的情况下,提供受保护的属性。
**测试为继承而设计的类的唯一方法是编写子类。** 如果你忽略了一个关键的受保护的成员,试图编写一个子类将会使得遗漏痛苦地变得明显。 相反,如果编写的几个子类,而且没有一个使用受保护的成员,那么应该将其设为私有。 经验表明,三个子类通常足以测试一个可继承的类。 这些子类应该由父类作者以外的人编写。
当你为继承设计一个可能被广泛使用的类的时候,要意识到你永远承诺你文档说明的自用模式以及隐含在其保护的方法和属性中的实现决定。 这些承诺可能会使后续版本中改善类的性能或功能变得困难或不可能。 因此, **在发布它之前,你必须通过编写子类来测试你的类。**
另外,请注意,继承所需的特殊文档混乱了正常的文档,这是为创建类的实例并在其上调用方法的程序员设计的。 在撰写本文时,几乎没有工具将普通的 API 文档从和仅仅针对子类实现的信息,分离出来。
还有一些类必须遵守允许继承的限制。 **构造方法绝不能直接或间接调用可重写的方法。** 如果违反这个规则,将导致程序失败。 父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类中的重写方法被调用。 如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。
- [Item19Example01.java](ClassesAndInterfaces/src/main/java/com/jueee/item19/Item19Example01.java):违反这个规则的类。
期望这个程序打印两次 `instant` 实例,但是它第一次打印出 `null`,因为在 `Sub` 构造方法有机会初始化 `instant` 属性之前,`overrideMe` 被 `Super` 构造方法调用。
请注意,这个程序观察两个不同状态的 `final` 属性! 还要注意的是,如果 `overrideMe` 方法调用了 instant 实例中任何方法,那么当父类构造方法调用 `overrideMe` 时,它将抛出一个 `NullPointerException` 异常。
这个程序不会抛出 `NullPointerException` 的唯一原因是 `println` 方法容忍 `null` 参数。
在修改非 `final` 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继承而设计和文档说明。**解决这个问题的最好办法是,在没有想要安全地子类化的设计和文档说明的类中禁止子类化。 有两种方法禁止子类化。** 两者中较容易的是声明类为 `final`。 另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。
简而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有的自用模式,并且一旦建立了文档,在这个类的整个生命周期中都必须遵守。如果没有做到,子类就会依赖父类的实现细节,如果父类的实现发生了变化,它就有可能遭到破坏。为了允许其他人能编写出高效的子类,还你必须导出一个或者多个受保护的方法。除非知道真正需要子类,否则最好通过将类声明为 `final`,或者确保没有可访问的构造器来禁止类被继承。

View File

@ -0,0 +1,16 @@
## 接口优于抽象类
Java 有两种机制来定义允许多个实现的类型:**接口** 和 **抽象类**
由于在 Java 8 中引入了接口的默认方法default methods ),因此这两种机制都允许为某些实例方法提供实现。 一个主要的区别是要实现由抽象类定义的类型,类必须是抽象类的子类。 因为 Java 只允许单一继承,所以对抽象类的这种限制严格限制了它们作为类型定义的使用。 任何定义所有必需方法并服从通用约定的类都可以实现一个接口,而不管类在类层次结构中的位置。
一般来说,现有的类不能改进以继承一个新的抽象类。 如果你想让两个类继承相同的抽象类,你必须把它放在类型层级结构中的上面位置,它是两个类的祖先。 不幸的是,这会对类型层级结构造成很大的附带损害,迫使新的抽象类的所有后代对它进行子类化,无论这些后代类是否合适。
#### 接口是定义混合类型的理想选择
接口是定义混合类型mixin的理想选择。一般来说mixin 是一个类,除了它的“主类型”之外,还可以声明它提供了一些可选的行为。 例如,`Comparable` 是一个类型接口,它允许一个类声明它的实例相对于其他可相互比较的对象是有序的。 这样的接口被称为类型,因为它允许可选功能被“混合”到类型的主要功能。 抽象类不能用于定义混合类,这是因为它们不能被加载到现有的类中:一个类不能有多个父类,并且在类层次结构中没有合理的位置来插入一个类型。
接口允许构建非层级类型的框架。 类型层级对于组织某些事物来说是很好的,但是其他的事物并不是整齐地落入严格的层级结构中。
**示例代码**[Item20Example01.java](ClassesAndInterfaces/src/main/java/com/jueee/item20/Item20Example01.java):定义一个继承歌手和作曲家的接口,并添加适合于这个组合的新方法。

View File

@ -0,0 +1,83 @@
package com.jueee.item17;
public class Item17Example01 {
// 公共的静态 final 常量
public static final Complex1 ZERO = new Complex1(0, 0);
public static final Complex1 ONE = new Complex1(1, 0);
public static final Complex1 I = new Complex1(0, 1);
public static void main(String[] args) {
Complex1 complex1 = new Complex1(3, 5);
Complex1 complex2 = new Complex1(-1, -8);
System.out.println(complex1.plus(complex2));
System.out.println(complex1.minus(complex2));
System.out.println(complex1.times(complex2));
System.out.println(complex1.dividedBy(complex2));
}
}
// 不变复数类
final class Complex1 {
private final double re;
private final double im;
public Complex1(double re, double im) {
this.re = re;
this.im = im;
}
public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
public Complex1 plus(Complex1 c) {
return new Complex1(re + c.re, im + c.im);
}
public Complex1 minus(Complex1 c) {
return new Complex1(re - c.re, im - c.im);
}
public Complex1 times(Complex1 c) {
return new Complex1(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex1 dividedBy(Complex1 c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex1((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Complex1)) {
return false;
}
Complex1 c = (Complex1)o;
// See page 47 to find out why we use compare instead of ==
return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
}
@Override
public int hashCode() {
return 31 * Double.hashCode(re) + Double.hashCode(im);
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}

View File

@ -0,0 +1,87 @@
package com.jueee.item17;
public class Item17Example02 {
// 公共的静态 final 常量
public static final Complex2 ZERO = Complex2.valueOf(0, 0);
public static final Complex2 ONE = Complex2.valueOf(1, 0);
public static final Complex2 I = Complex2.valueOf(0, 1);
public static void main(String[] args) {
Complex2 complex1 = Complex2.valueOf(3, 5);
Complex2 complex2 = Complex2.valueOf(-1, -8);
System.out.println(complex1.plus(complex2));
System.out.println(complex1.minus(complex2));
System.out.println(complex1.times(complex2));
System.out.println(complex1.dividedBy(complex2));
}
}
// 不变复数类
class Complex2 {
private final double re;
private final double im;
private Complex2(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex2 valueOf(double re, double im) {
return new Complex2(re, im);
}
public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
public Complex2 plus(Complex2 c) {
return new Complex2(re + c.re, im + c.im);
}
public Complex2 minus(Complex2 c) {
return new Complex2(re - c.re, im - c.im);
}
public Complex2 times(Complex2 c) {
return new Complex2(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex2 dividedBy(Complex2 c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex2((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Complex2)) {
return false;
}
Complex2 c = (Complex2)o;
// See page 47 to find out why we use compare instead of ==
return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
}
@Override
public int hashCode() {
return 31 * Double.hashCode(re) + Double.hashCode(im);
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}

View File

@ -0,0 +1,82 @@
package com.jueee.item18;
// Reusable forwarding class
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public void clear() {
s.clear();
}
public boolean contains(Object o) {
return s.contains(o);
}
public boolean isEmpty() {
return s.isEmpty();
}
public int size() {
return s.size();
}
public Iterator<E> iterator() {
return s.iterator();
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}

View File

@ -0,0 +1,30 @@
package com.jueee.item18;
// Wrapper class - uses composition in place of inheritance
import java.util.Collection;
import java.util.Set;
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}

View File

@ -0,0 +1,48 @@
package com.jueee.item18;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
public class Item18Example01 {
public static void main(String[] args) {
InstrumentedHashSet<String> set1 = new InstrumentedHashSet<String>();
set1.add("a");
set1.add("b");
set1.add("a");
System.out.println(set1.getAddCount()); // 3 满足预期
InstrumentedHashSet<String> set2 = new InstrumentedHashSet<>();
set2.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(set2.getAddCount()); // 6 不满足预期
}
}
// 查询 HashSet 从创建它之后已经添加了多少个元素
class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}

View File

@ -0,0 +1,41 @@
package com.jueee.item18;
import java.util.Arrays;
import java.util.HashSet;
public class Item18Example02 {
public static void main(String[] args) {
InstrumentedHashSet2<String> set1 = new InstrumentedHashSet2<String>();
set1.add("a");
set1.add("b");
set1.add("a");
System.out.println(set1.getAddCount()); // 3 满足预期
InstrumentedHashSet2<String> set2 = new InstrumentedHashSet2<>();
set2.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(set2.getAddCount()); // 3 满足预期
}
}
// 查询 HashSet 从创建它之后已经添加了多少个元素
class InstrumentedHashSet2<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet2() {}
public InstrumentedHashSet2(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
public int getAddCount() {
return addCount;
}
}

View File

@ -0,0 +1,25 @@
package com.jueee.item18;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class Item18Example03 {
public static void main(String[] args) {
InstrumentedSet<String> set = new InstrumentedSet<String>(new TreeSet<String>());
set.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(set.getAddCount()); // 3
System.out.println(set); // [Crackle, Pop, Snap]
// 用于临时替换没有计数功能下使用的集合实例
Set<String> s = new HashSet<String>();
s.add("a");
s.add("b");
s.add("a");
InstrumentedSet<String> set2 = new InstrumentedSet<String>(s);
System.out.println(set2.getAddCount()); // 3
System.out.println(set2); // [a, b]
}
}

View File

@ -0,0 +1,36 @@
package com.jueee.item19;
import java.time.Instant;
/**
* 构造方法绝不能直接或间接调用可重写的方法
* @author hzweiyongqiang
*/
public final class Item19Example01 extends Super {
// Blank final, set by constructor
private final Instant instant;
Item19Example01() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override
public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Item19Example01 sub = new Item19Example01();
sub.overrideMe();
}
}
class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {}
}

View File

@ -0,0 +1,29 @@
package com.jueee.item20;
public class Item20Example01 {
// 代表歌曲的类
public class Song {}
// 代表音频的类
public class AudioClip {}
// 代表歌手的接口
public interface Singer {
AudioClip sing(Song s);
}
// 代表作曲家的接口
public interface Songwriter {
Song compose(int chartPosition);
}
// 在现实生活中一些歌手也是作曲家
// 因为我们使用接口而不是抽象类来定义这些类型所以单个类实现歌手和作曲家两个接口是完全允许的
// 事实上我们可以定义一个继承歌手和作曲家的第三个接口并添加适合于这个组合的新方法
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum(); // 弹奏
void actSensitive();
}
}