java中可以用来定义允许多个实现的类型有两种:接口和抽象类。
接口和抽象类的区别:
1,抽象类中可以存在某些方法的实现,接口不可以
2,如果要实现抽象类定义的类型,类必须成为抽象类的子类。而对接口来说,任何一个类,只要实现接口里面必要的方法,就可以了,而且不用管这个类处于类的层次的哪个位置(例如:内部类也可以实现)
3,java是单继承,多实现
现有的类可以很容易的被更新,以实现接口的形式。例如,现在有多个类要扩展排序功能,而咱们要做的就是对这些类实现comparable接口,并且重新接口中必要的compare方法就可以了。但是如果要用抽象类的形式扩展的话,那么就必须对抽象类所有的子类进行扩展。
接口是定义mixin(混合类型)的理想选择。Comparable这样的接口被称为mixin的原因是:它允许任何的功能被混合到类型的主要功能中(排序功能)。抽象类并不能用于定义mixin,原因在于它们不能被更新到现有类中:java是单继承,多实现 所以类不可能有一个以上的父类,类层次结构中也没有合适的地方来插入mixin,问题在于功能是可选的,如果把一个功能放入类层次中,那就将这项功能混入到了所有的子类中。
例如:我们要实现以下4种动物:
Dog - 狗狗;
Bat - 蝙蝠;
Parrot - 鹦鹉;
Ostrich - 鸵鸟。
如果按照哺乳动物和鸟类归类,我们可以设计出这样的类的层次:
animal - m b
但是如果按照“能跑”和“能飞”来归类,我们就应该设计出这样的类的层次:
animal-r f
如果要把上面的两种分类都包含进来,我们就得设计更多的层次:
哺乳类:能跑的哺乳类,能飞的哺乳类;
鸟类:能跑的鸟类,能飞的鸟类。
这么一来,类的层次就复杂了:
animal-mb-rf
如果要再增加“宠物类”和“非宠物类”,这么搞下去,类的数量会呈指数增长,很明显这样设计是不行的。
正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计:
class Animal(object):
pass
抽象类:
class Mammal(Animal):
pass
class Bird(Animal):
pass
各种动物:
class Dog(Mammal):
pass
class Bat(Mammal):
pass
class Parrot(Bird):
pass
class Ostrich(Bird):
pass
现在,我们要给动物再加上Runnable和Flyable的功能,只需要先定义好Runnable和Flyable的类:
class Runnable(object):
def run(self):
print(‘Running…’)
class Flyable(object):
def fly(self):
print(‘Flying…’)
对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog:
class Dog(Mammal, Runnable):
pass
对于需要Flyable功能的动物,就多继承一个Flyable,例如Bat:
class Bat(Mammal, Flyable):
pass
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
Mixin
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为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();
}
类似这种情况,如果咱们用创建层次类来实现的话,那么咱们每种情况都要创建一个类,就会导致组合臃肿
第16条中介绍的包装类模式,接口便利安全地增加类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增加新的功能以外没有其他的选择。这样得到的类与包装类相比,功能更差,更脆弱。
虽然接口不允许包含方法的实现,但是我们可以通过对导出的每个重要的接口都提供一个抽象的骨架实现类,这样咱们可以把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,骨架实现类接管了所有与接口实现相关的工作。按照惯例,骨架实现被称为 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的功能。由于骨架实现本身已经实现了很多通用的操作,在这里实际上只做了很少的改动就得到了一个功能良好的类。
1 :定义一个基本的接口,其中有c ,d,f 三种方法
public interface Mydemo{
public void c();
public void d();
public void f();
};
2:定义一个抽象类,其中有两个方法a和b,实现接口并实现c和d和 f
public abstract class AbrMydemo implements Mydemo{
public void a();
public void b();
public void c(){
}
public void d(){
}
public void f(){
}
};
public class maindemo extends AbrMydemo{//无需改变,继承父类实现的方法,可以直接调用
public void a(){
}
public void b(){
}
//继承AbrBase对IBase的实现
};
也可以直接用匿名内部类的方式转发AbrMydemo中的方法,在maindemo中使用
public class maindemo {
public Mydemoa(){
Mydemo demo = new AbrMydemo()
demo.f();
}
public class Nbl(){
也可以在内部类中直接调用外部类的方法
}
public void b(){
}
//继承AbrBase对IBase的实现
};
骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,又不强加抽象类作为类型定义时的限制,可以扩展骨架实现也完全可以手动实现整个接口。此外骨架实现类也有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用,转发到另一个内部私有类的实例上,这个内部私有类扩展了骨架类的实现这种方法被称为“模拟多重继承”,这项技术具有多重继承的绝大多数优点,同时又避开了相应的缺陷,实际上就是用包装与转发模拟了多个类的混合功能。
编写骨架实现类相对比较简单,但是有点单调乏味,首先,必须认真研究接口,并确定哪些方法是最为基本的,其他的方法则可以根据它们来实现。这些基本的方法将成为骨架实现类中的抽象方法。然后,必须为接口中所有其他的方法提供具体实现。例如接口Map与骨架实现AbstractMap。由于骨架实现是为了继承的目的设计的,所以应该遵守第17条中介绍的所有关于设计和文档的指导原则。
骨架实现上有个小小的不同,就是简单实现。AbstractMap.SimpleEntry就是个例子。简单实现就像个骨架实现,这是因为它实现了接口并且是为了继承则设计的,但是区别在于它不是抽象的,它就最简单的可能的有效实现,你可以原封不动地使用也可以看情况将它子类化。
public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable
{
private static final long serialVersionUID = -8499721149061103585L;
private final K key;
private V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
public SimpleEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return eq(key, e.getKey()) && eq(value, e.getValue());
}
public int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
public String toString() {
return key + "=" + value;
}
}
接口比抽象类优势很多,但是抽象类仍然有个明显的优势:抽象类的演变比接口的演变要容易很多。如果在后续的版本中需要在抽象类中增加新的方法,那么建立一个具体方法后,就可以提供默认的实现,抽象类的所有实现都将提供这个新的方法。而接口,就不能这样做。
虽然接口有骨架实现类,在接口中增加方法后,再骨架实现类也增加具体的方法不就可以解决了。可是那些不从骨架实现类继承的接口实现仍然会遭到破坏。
因此,设计公有的接口要非常谨慎。接口一旦被公开发型,并且已被广泛实现,再想改变这个接口几乎是不可能的。在发行新接口的时候,最好的做法是,在接口被“冻结”之前,尽可能让更多的程序员用尽可能多的方式来实现这个新街口,这样有助于在依然可以改正缺陷的时候就发现它们。
简言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,既当演变的容易性比灵活性和功能更为重要的时候(这种情况,应该用抽象类来定义类型)。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。