接口和抽象类
接口 | 抽象类 | |
为什么需求 | 很多时候,我们实际上关心的,并不是对象的类型,而是对象的能力,只要能提供这个能力,类型并不重要。比如要拍照,很多时候,只要能拍出符合需求的照片就行,至于是用手机拍,还是用Pad拍,或者是用单反相机拍,并不重要,即关心的是对象是否有拍出照片的能力,而并不关心对象到底是什么类型,手机、Pad或单反相机都可以。 这种情况下,类型并不重要,重要的是能力。那如何表示能力呢?接口。 接口这个概念在生活中并不陌生,电子世界中一个常见的接口就是USB接口。计算机往往有多个USB接口,可以插各种USB设备,如键盘、鼠标、U盘、摄像头、手机等。接口声明了一组能力,但它自己并没有实现这个能力,它只是一个约定。接口涉及交互两方对象,一方需要实现这个接口,另一方使用这个接口,但双方对象并不直接互相依赖,它们只是通过接口间接交互。 针对接口而非具体类型进行编程,是计算机程序的一种重要思维方式。 接口很多时候反映了对象以及对对象操作的本质。 接口的好处:
| 抽象方法和抽象类看上去是多余的,对于抽象方法,不知道如何实现,定义一个空方法体不就行了吗?而抽象类不让创建对象,看上去只是增加了一个不必要的限制。 引入抽象方法和抽象类,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少误用。使用抽象方法而非空方法体,子类就知道它必须要实现该方法,而不可能忽略,若忽略Java编译器会提示错误。使用抽象类,类的使用者创建对象的时候,就知道必须要使用某个具体子类,而不可能误用不完整的父类。 无论是编写程序,还是平时做其他事情,每个人都可能会犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。 抽象类就是Java提供的这样一种机制,使程序更为清晰,可以减少误用。 |
简介 | Java中使用接口的一个典型例子就是“比较”,很多对象都可以比较,对于求最大值、求最小值、排序的程序而言,它们其实并不关心对象的类型是什么,只要对象可以比较就可以了,或者说,它们关心的是对象有没有可比较的能力。Java API中提供了Comparable接口,以表示可比较的能力。
定义接口:
实现接口:
使用接口:
| 抽象类就是抽象的类。抽象是相对于具体而言的,一般而言,具体类有直接对应的对象,而抽象类没有,它表达的是抽象概念,一般是具体类的比较上层的父类。比如,狗是具体对象,而动物则是抽象概念;樱桃是具体对象,而水果则是抽象概念;正方形是具体对象,而图形则是抽象概念。 只有子类才知道如何实现的方法,一般被定义为抽象方法。抽象方法是相对于具体方法而言的,具体方法有实现代码,而抽象方法只有声明,没有实现。接口中的方法(非Java 8引入的静态和默认方法)就都是抽象方法。 抽象方法和抽象类都使用abstract这个关键字来声明:
|
一些细节 | 接口中可以定义变量:
这里定义了一个变量int a,修饰符是public static final,但这个修饰符是可选的,即使不写,也是public static final。这个变量可以通过“接口名.变量名”的方式使用,如Interface1.a。 接口也可以继承,一个接口可以继承其他接口,继承的基本概念与类一样,但与类不同的是,接口可以有多个父接口:
IChild有IBase1和IBase2两个父类,接口的继承同样使用extends关键字,多个父接口之间以逗号分隔。 类的继承与接口可以共存,换句话说,类可以在继承基类的情况下,同时实现一个或多个接口:
关键字extends要放在implements之前。 接口也可以使用instanceof关键字,用来判断一个对象是否实现了某接口。 静态方法和默认方法:
|
|
对比和关系 | 相同之处:
不同之处:
抽象类和接口是配合而非替代关系,它们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。比如,在Java类库中:
对于需要实现接口的具体类而言,有两个选择:
继承的好处是复用代码,只重写需要的部分即可,需要编写的代码比较少,容易实现。不过,如果这个具体类已经有父类了,那就只能选择实现接口了。 抽象类和接口经常相互配合,接口定义能力,而抽象类提供默认实现,方便子类实现接口。 | |
继承是把双刃剑
一方面继承非常强大,至少有两个好处:
- 复用代码;
- 利用多态和动态绑定统一处理多种不同子类的对象。
封装就是隐藏实现细节,提供简化接口。使用者只需要关注怎么用,而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。函数是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题。可以说,封装是程序设计的第一原则,没有封装,代码之间会到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的。
另一方面继承的破坏力也很强:
- 继承的破坏力体现在不恰当的继承会破坏封装,主要是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。可以看这个问题来帮助理解这个问题。
- 继承关系是设计用来反映is-a关系的,但很难完全保证这点:
- 现实中,设计完全符合is-a关系的继承关系是困难的。比如,绝大部分鸟都会飞,可能就想给鸟类增加一个方法fly()表示飞,但有一些鸟就不会飞,比如企鹅。
- Java没有办法约束这种关系,父类有的属性和行为,子类并不一定都适用,子类还可以重写方法,实现与父类预期完全不一样的行为。
对于通过父类引用操作子类对象的程序而言,它是把对象当作父类对象来看待的,期望对象符合父类中声明的属性和行为。如果不符合,结果将会非常混乱。
如何应对继承的双面性呢?
正确使用继承 | 使用继承大概主要有三种场景: 第一种:基类是别人写的,我们写子类 基类主要是Java API、其他框架或类库中的类,在这种情况下,我们主要通过扩展基类,实现自定义行为,这种情况下需要注意的是:
第二种:我们写基类,别人可能写子类 这种情况需要注意:
第三种:基类、子类都是我们写的 这种情况需要同时注意情况一和二中的事项。 |
避免使用继承 | 三种方法可以避免使用继承: 第一种:使用final关键字
第二种:优先使用组合而非继承 使用组合可以抵挡父类变化对子类的影响,从而保护子类,应该优先使用组合。这样,子类就不需要关注基类是如何实现的了,基类修改实现细节,增加公开方法,也不会影响到子类了。但组合的问题是,子类对象不能当作基类对象来统一处理了。解决方法是使用接口。
改为
第三种:使用组合和接口结合起来替代继承
将组合和接口结合起来替代继承,既可以统一处理,又可以复用代码,还不用担心破坏封装。
|
知其然知其所以然,只有掌握了底层原理,借助第一性原理,才可以在日常开发和项目中运用自如,潇洒走江湖。

查看24道真题和解析