接口和抽象类的区别
1.抽象类允许包含某些方法的实现,但是接口不允许。
2.为了实现由抽象类定义的类型,类必须成为抽象类的一个子类;只要类定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口。
现有的类可以很容易被更新,以实现新的接口。
接口是定义mixin(混合类型)的理想选择。
接口允许我们构造非层次结构的类型框架。
现在有个接口代表歌手和作曲家
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(boolean hit);
}
现实的情况中,有些歌手本身也是作曲家。因为使用的是接口而不是抽象类,所以对于单个类而言,它可以同时实现Singer和Songwriter两个接口。
public interface SingerSonwriter extends Singer,Songwriter {
AudioClip strum();
void actSensitive();
}
这种情况不是很常见,但却能解决大问题。
如果是采用层次结构的做法,那么每一个要被支持的类的属性组合,都要包含一个单独的类。如果整个类型系统有N个属性,那么就必须支持2ⁿ种可能的组合。这种现象称为“组合爆炸”。之所以会产生这种问题,是因为类层次中没有任何类型体现了公共的行为特征。
接口使得安全地增强类的功能成为可能(包装类模式)。如果使用抽象类,除了使用继承来增加功能,没有其他选择。与包装类相比功能更差、也更脆弱。
虽然接口不允许包含方法的实现,可以为每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,这样就把接口和抽象类的优点结合起来。接口的作用依然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。习惯把骨架实现称为AbstractInterface。
骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的限制。对于大多数接口来说,扩展骨架实现类不是必需的。
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
//Primitive operations
public abstract K getKey();
public abstract V getValue();
//Entries in modifiable maps must override this method
public V setValue(V value) {
throw new UnsupportedOperationException();
}
//Implements the general contract of Map.Entry.equals
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Map.Entry)) return false;
Map.Entry<?, ?> arg = (Map.Entry<?, ?>) obj;
return equals(getKey(), arg.getKey())
&& equals(getValue(), arg.getValue());
}
private static boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
//Implements the general contract of Map.Entry.equals
@Override
public int hashCode() {
return hashCode(getKey()) ^ hashCode(getValue());
}
private static int hashCode(Object obj) {
return obj == null ? 0 : obj.hashCode();
}
}
因为骨架实现类是为了继承的目的而设计的,所以应该遵守
第17条的准则。
与骨架实现有个小小不同的简单实现(simple implementation),看起来很像骨架实现,因为它实现了接口,并且是为继承而设计的,区别在于它不是抽象的:它是最简单的可能的有效实现。你可以原封不动的使用,也可以看情况将它子类化。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;
}
}
接口比抽象类优势很多,但是抽象类仍然有个明显的优势: 抽象类的演变比接口的演变要容易很多。如果在后续的版本中需要在抽象类中增加新的方法,那么建立一个具体方法后,就可以提供默认的实现,抽象类的所有实现都将提供这个新的方法。而接口,就不能这样做。
虽然接口有骨架实现类,在接口中增加方法后,再骨架实现类也增加具体的方法不就可以解决了。可是那些不从骨架实现类继承的接口实现仍然会遭到破坏。
因此,设计公有的接口要非常谨慎。接口一旦被公开发型,并且已被广泛实现,再想改变这个接口几乎是不可能的。在发行新接口的时候,最好的做法是,在接口被“冻结”之前,尽可能让更多的程序员用尽可能多的方式来实现这个新街口,这样有助于在依然可以改正缺陷的时候就发现它们。
简言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,既当演变的容易性比灵活性和功能更为重要的时候(这种情况,应该用抽象类来定义类型)。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。