每当面试的时候,我总喜欢问一下应聘者这样一个问题:接口与抽象类有什么区别?这个问题看上去很简单,网上的答案也一搜一大把。然而,我根本不想要应聘者背出来的答案–一个对技术热衷的人,这个问题一定会反复思考过无数次,一定能说出自己的一些见解。然而,令我失望的是,我很少得倒令我满意的答案。写下这篇,分享一下我对接口和抽象类的认识。
表象
- 接口用interface声明,而抽象类用class声明
- 实现接口关键字 implements ,继承抽象类关键字 extends
这些都是表象的东西,是语言层面上对接口和抽象类做了一个区分
abstract 不能与 private、static、final或native并列修饰同一个方法
说到这里,我们就顺便看一下这几个关键字吧
public protected deafult(缺省)private
这几个关键字是作用域关键字
public 该方法或字段对所有用户开发
protected 该方法或字段对自己,同包下的类,子类开放
缺省 该方法或字段对自己,同包下的类开放
private 该方法或字段仅对自己开放
ps:这里有两点可能会被有些人搞混
- protected比默认缺省作用范围更广
- protected方法和字段可以对同包下的类开放,有些人认为,被protected修饰了的,仅对自己和子类开放
final
- final类不能被继承,没有子类,final类中的方法默认是final的
- final方法不能被子类的方法覆盖,但可以被继承
- final成员变量表示常量,只能被赋值一次,赋值后值不再改变
- final不能用于修饰构造方法
接口的所有方法都是抽象的,也就是说,接口的方法都是被abstract隐式修饰的。所以接口方法不能出现tatic、final或native。而且接口的访问权限都是public的。
接口的字段都是 public static final 关键字修饰
接口中的字段必须在声明的时候初始化
这是由于,接口的字段都是被final隐式修饰的。
不要用接口取代枚举
这个问题在之前的分享中提到过,这里不再赘述
接口定义类型
单继承与多实现
这个特性造成了接口的一个重要应用:用于定义类型。举例说明用抽象类定义类型的弊端。
比如我们定义一个歌手类型和一个歌曲作家类型:
public interface SongWriter {
void writeSong();
}
public interface Singer {
void singSong();
}
如果一个人,既是一个歌手,又是一个作曲家,三里屯这边这样的人并不罕见,那么这个人我们可以这样定义。
public class SingerAndWriter implements Singer,SongWriter{
@Override
public void singSong() {
}
@Override
public void writeSong() {
}
}
但是如果我们不小心把类型定义成抽象类,那么抽象类不支持多继承的规则将会限制一个歌手为自己写歌。
前段时间,和小伙伴讨论这个问题,小伙伴给出了一个解决方案。写一个SingerAndWriter,让他持有SongWriter和Singer的引用,这样想唱就唱,想写就写。但是这样的设计有一个很大的问题。本来SingerAndWriter is a SongWriter以及SingerAndWriter is a Singer。而采用小伙伴的这种方式的话,就成了SingerAndWriter has a SongWriter以及SingerAndWriter has a Singer。如果一旦这样做了,功能看上去实现了。但是某天我们要办一场音乐会,那么需要传递Singer作为入参。。。。。。。惊出一身冷汗!
接口的方法名不能重复,以防止一个类实现多个接口的时候出现问题
由于接口可以多实现,这就造成了,如果客户端实现的两个接口中有相同方法名的时候,会造成功能实现上的混乱甚至错误。而一旦发生了这样的问题,结果将是灾难性的。因为接口这种顶层的东西,往往是牵一发而动全身的。
抽象类为了复用
以下说明摘自effective java
抽象类为了复用
抽象类的一个特点就是,允许里面有非抽象方法。
继承是一种实现代码重用的有力手段。但他并非永远是完成这项工作的最佳手段。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。防止继承滥用,请使用装饰模式
与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,子类可能会遭到破坏,即使它的代码完全没有改变。因而,子类必须跟着其超类的更新二演变,除非超类是专门为了扩展而设计的,并且有很好的文档说明。
解决办法:装饰模式。(“复合”)
新类与旧类实现同一个接口,新类持有旧类的一个引用。
新类中每个实例方法都可以调用包含现有类实例中对应的方法,并返回他的结果。这样得到的类将会非常稳固,他不依赖于现有类的实现细节。即使现有类增加了新的方法,也不会影响新的类。
包装类几乎没什么缺点。需要注意的一点是,包装类不适合用在回调框架中;对象把自身的引用传递给其他对象,用于后续的调用“回调”。因为被包装起来的对象并不知道他外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。这被称为SELF问题。
有些人担心转发方法调用所带来的性能影响,或者包装对象导致的内存占用。在实践中,这两者都不会造成很大的影响。编写转发方法倒是有些琐碎,但是只需要给每个接口编写一次构造器,转发类则可以通过包含接口的包替你提供。