术语:
mixin类型:不严格的讲,mixin是指这样的类型,类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了些可供选择的行为。例如,comparable是一个mixin接口,它允许类表明它的实例可以与其他的可相互比较的对象进行排序。
Java提供了两种机制用来定义允许多个实现的类型:接口和抽象类。这两种机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。一个更为重要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类,任何一个类,只要它定义了所必要的方法,并且遵守了通用的约定,它就被允许实现一个接口,而不管这个类是处于类层次的哪个位置。因为Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。
现有的类很容易被更新,以实现新的接口。如果接口中声名的方法并不存在,那么所需要做的就是增加所必要的方法,然后在类声名中增加一个implements子句。一般来说,无法更新现有的类来扩展新的抽象类。如果希望让两个类扩展同一个抽象类,就必须把抽象类放到类层次的高处,以便这两个类的一个祖先成为它的子类。遗憾的是这样做会间接伤害到类层次,迫使这个公共祖先的所有后代类都扩展这个新的抽象类,无论它对于这些后代是否合适,而且还有一个问题在于,这样加一个抽象类到类层次中,可能会导致继承中出现的一系列问题,比如方法冲突。
接口是定义mixin(混合类型)的理想选择。像Comparable这样的接口之所以被称为mixin,是因为它允许任何的功能被混合到类型的主要功能中。抽象类并不能用于定义mixin,原因在于它们不能被更新到现有类中:类不可能有一个以上的父类,类层次结构中也没有合适的地方来插入mixin,这里的问题在于功能是可选的,如果把它插入到类层次中,那就将这项功能混入到了所有的子类中,这并不是所期望的,但是由接口来完成这项工作则再合适不过了。
接口允许我们构造非层次结构的类型框架。类型层次对于组织某些事物是非常合适的,但是其他有些事物并不能被整齐地组织成一个严格的层次结构。例如,有一个接口代表singer,一个接口代表songwriter,这两者之间可能并不存在父子关系,在现实生活中有些歌唱家本身就是作曲家。如果我们使用接口来定义这些类型,对于单个类而言,它同时实现Singer和songwriter是完成可以的,实际上,甚至可以定义第三个接口,让它同时扩展singer和songwriter,并添加一些适合这组人的新方法。
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(boolean hit);
}
public interface SingerSongWriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
上面的代码有几处需要注意一下:
1、虽然类是不允许有多重继承的,但是一个接口可以从多个接口继承而来。
2、接口内部的方法返回类型也可以是另一个接口,比如AudioClip本身就是个接口,在这里作为一个类型来使用。
3、接口的声名中,所有的方法都默认为public权限的。
4、对于从其他接口继承来而来的接口,其父接口的方法也被继承了下来。
第16条中介绍的包装类模式,接口便利安全地增加类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增加新的功能以外没有其他的选择。这样得到的类与包装类相比,功能更差,更脆弱。
虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序员提供实现上的帮助。通过对导出的每个重要的接口都提供一个抽象的骨架实现(skeletal implementation)类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。按照惯例,骨架实现被称为 AbstractInterface,这里的Interface是指所实现的接口的名子。如果设计得当,骨架实现可以使程序员很容易提供他们自己的接口实现。例如,下面是一个静态工厂方法,它包含一个完整的,功能全面的List实现。
// Concrete implementation built atop skeletal implementation
static List<Integer> intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList<Integer>() {
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;
}
public int size() {
return a.length;
}
};
}
上面的代码中利用了骨架实现AbstractList来实现一个完整的List,实际上,骨架是做为一个被匿名内部类的扩展类存在的,在这个内部类中对骨架实现新增加了两个方法并重写了一个方法以定制我们所需要的List的功能。由于骨架实现本身已经实现了很多通用的操作,在这里实际上只做了很少的改动就得到了一个功能良好的类。
骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,又不强加抽象类作为类型定义时的限制,可以扩展骨架实现也完全可以手动实现整个接口。此外骨架实现类仍然能够有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用,转发到另一个内部私有类的实例上,这个内部私有类扩展了骨架类的实现这种方法被称为“模拟多重继承”,这项技术具有多重继承的绝大多数优点,同时又避开了相应的缺陷,实际上就是用包装与转发模拟了多个类的混合功能。
编写骨架实现类相对比较简单,但是有点单调乏味,首先,必须认真研究接口,并确定哪些方法是最为基本的,其他的方法则可以根据它们来实现。这些基本的方法将成为骨架实现类中的抽象方法。然后,必须为接口中所有其他的方法提供具体实现。例如接口Map与骨架实现AbstractMap。由于骨架实现是为了继承的目的设计的,所以应该遵守第17条中介绍的所有关于设计和文档的指导原则。
骨架实现上有个小小的不同,就是简单实现。AbstractMap.SimpleEntry就是个例子。简单实现就像个骨架实现,这是因为它实现了接口并且是为了继承则设计的,但是区别在于它不是抽象的,它就最简单的可能的有效实现,你可以原封不动地使用也可以看情况将它子类化。
使用抽象类定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变比接口的演变要容易的多。如果在后续的发行版本中,希望在抽象类中增加新的方法,始终可以增加具体的方法,它包含合理的默认实现,然后该抽象类的所有实现都将提供这个新方法。对于接口这样做是行不通的。要想在公有接口中增加方法而不破坏实现这个接口的所有类,一般是不可能的。之前实现该接口的类会漏掉新增加的方法,并且无法再编译通过。在为接口增加新方法的同时,也为骨架实现类增加同样的新方法,这样可以在一定程序上减小由此带来的破坏,但是,这样做并没有真正的解决问题。所有不从骨架实现类继承的接口实现仍然会被破坏。
因此,在设计公有的接口要非常小心。接口一旦被公开,并且已被广泛实现,再想改变这个接口几乎是不可能的。必须在初次设计的时候就保证接口是正确的。在发行新接口的时候,最好的做法是在接口被冻结前尽可能让更多的程序员用尽可能多的方式来实现这个接口。这样有助于在依然可以改正缺陷的时候就发现它们。
总之,接口通常是定义允许多个实现类型的最佳途径。这个规则有个例外,就是当演变的容易性比灵活性和功能性更重要的时候,这时应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果导出了一个重要的接口。就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口, 并通过编写多个实现来对它们进行全面的测试。