面向对象之抽象类和接口
前言
二胖知道"抽象"是面向对象的一大特征,而且他还听说过"面向接口编程"这一概念,Bill大神曾经还告诉他这一概念在后面的开发中很重要。所以二胖暗下决心一定要好好学习这一知识点。二胖感觉到抽象类和接口都可以体现出抽象这一特征,那么它们各自的特点以及两者的区别是怎样的?
正文
抽象类
什么是抽象类?它具有哪些特点?
考虑如下情景:一个互联网公司有程序员也有项目经理,程序员有姓名、工号、薪水、工作内容,项目经理除了姓名、工号、薪水、工作内容还有奖金。那么现在我们如何对这一情景进行建模并用Java语言来描述呢?
- 分析:首先我们知道Java是面向对象的语言,那么我们肯定需要先找出该情景所涉及的对象,所以我们可以得到下面的数据结构:
程序员:
属性:姓名、工号、薪水
行为:工作
项目经理:
属性:姓名、工号、薪水、奖金
行为:工作
- 编码:然后就是用Java语言将它们描述出来,如下:
// 描述程序员
class Programmer
{
private String name; // 姓名
private String id; // 工号
private double pay; // 薪水
Programmer(String name, String id, double pay)
{
this.name = name;
this.id = id;
this.pay = pay;
}
// 工作内容
public void work()
{
System.out.println("code...");
}
}
// 描述项目经理
class Manager
{
private String name; // 姓名
private String id; // 工号
private double pay; // 薪水
private int bonus; // 奖金
Manager(String name, String id, double pay, int bonus)
{
this.name = name;
this.id = id;
this.pay = pay;
this.bonus = bonus;
}
// 工作内容
public void work()
{
System.out.println("manage...");
}
}
通过观察我们其实可以发现大量的重复代码,代码的可扩展性和可维护性都很差。因为程序员和项目经理具有很多共性内容:姓名、工号、薪水、工作内容。而这些内容却写了两遍。在这种情况下,我们就可以通过定义抽象类来解决这个问题,如下:
// 描述雇员
abstract class Employee
{
private String name;
private String id;
private double pay;
Employee(String name, String id, double pay)
{
this.name = name;
this.id = id;
this.pay = pay;
}
public abstract void work();
}
// 描述程序员
class Programmer extends Employee
{
Programmer(String name, String id, double pay)
{
super(name, id, pay);
}
public void work()
{
System.out.println("code...");
}
}
// 描述项目经理
class Manager extends Employee
{
private int bonus;
Manager(String name, String id, double pay, int bonus)
{
super(name, id, pay);
this.bonus = bonus;
}
public void work()
{
System.out.println("manage...");
}
}
由于Programmer类与Manager类并不存在继承关系,所以我们只能将它们具有的共性内容全部提取出来放到另外一个单独的类中,这样的类我们就可以定义为抽象类。我们还可以注意到:Programmer类和Manager类都有work方法但方法体却不一样,这样的方法我们就可以定义为抽象方法。该方法在Programmer类中并不实现,而是由具体的实现类Programmer类和Manager类去实现。于是我们可以说:含有抽象方法的类就是抽象类。
通过上面这个例子我们其实可以总结出抽象类所具有的一些特点:
-
抽象类和抽象方法是用abstract关键字来修饰的。
-
抽象类中不一定有抽象方法,但是有抽象方法的类必须被定义为抽象类。(这一点后面还会提到)
-
因为抽象类中含有不具有方法实现的抽象方法,所以抽象类不能被实例化。
-
抽象类的子类如果不重写所有的抽象方法,那么该子类也是一个抽象类;如果重写了所有的抽象方法,那么该子类就是一个具体的类。
-
抽象类的实例化其实是靠具体的子类来实现的(这其实也是多态的一种表现形式[关于多态的详细说明可以参考这一篇文章面向对象之多态])。
有关抽象类的一些注意?
-
上面说到:抽象类不能被实例化,但抽象类也是有构造函数的,它的作用是用来给子类对象进行初始化。因为我们知道子类的构造函数会调用父类的构造函数。(关于这一点的详细说明可以参考这一篇文章面向对象之继承)
-
abstract关键字不能与private、static、final关键字共存。不能与private、final共存的原因是被abstract修饰的类或方法需要被覆盖;不能与static共存的原因是通过abstract修饰的类直接调用方法是没有意义的(关于static关键字的详细说明可以参考这一篇文章面向对象之static关键字)。
-
抽象类由于需要被其他类继承然后覆盖方法,所以抽象类一定是个父类。
-
抽象类中可以不定义抽象方法,并且通常这个类中的方法有方法体却没有内容。这样做的目的就是为了不让该类创建对象。(AWT的适配器类就是这种类)
抽象类和普通类的区别?
-
一般类有足够的信息描述事物;而抽象类描述事物的信息有可能不足。
-
一般类中不能定义抽象方法,只能定义非抽象方法;而抽象类中可定义抽象方法,同时也可以定义非抽象方法。
-
一般类可以被实例化;而抽象类不可以被实例化。
接口
什么是接口?它具有哪些特点?
我们知道接口这个概念并不是Java中才有,而且也不是在软件中才有。它在生活中可谓是无处不在。那么接口到底是什么呢?其实我们可以简单理解:它就是一种规则。比如说我们现在使用的笔记本电脑,我们如何能够在不知道以后会出现什么功能的前提下来扩展笔记本电脑呢?其实我们可以从生活中找到答案:插板,不管我们是在这个地球上哪一个地方买到的插头都是可以插在对应的插板上的。这个插板其实就是一个规则,只要符合这个规则的插头就是可以插的。那么这一思想也是可以应用在这里的。我们只需定义一个规则,日后出现的设备只要符合这个规则就可以在电脑上应用起来。规则在java中的体现就是接口。就像下面这样:
interface USB // 暴露的规则
{
public void open();
public void close();
}
class UPan implements USB
{
public void open()
{
System.out.println("upan open");
}
public void close()
{
System.out.println("upan close");
}
}
class UsbMouse implements USB
{
public void open()
{
System.out.println("UsbMouse open");
}
public void close()
{
System.out.println("UsbMouse close");
}
}
通过上面的代码,我们可以明显感觉到接口的使用使这些设备和笔记本电脑的耦合性降低了。
接口具有哪些特点?
-
我们其实可以从另一个方面来看接口:它其实是一种极度抽象,因为接口中的方法全是抽象的。
-
上面说到:接口是规则,那么接口中的成员也是比较特殊的,它们具有固定的修饰符:全局常量默认被public、static和final修饰,抽象方法默认被public和abstract修饰。
-
实现了接口的子类必须要覆盖接口中所有的抽象方法之后才能被实例化,否则该类是抽象类。
-
由于在java中不支持多继承,但是在java中支持多实现:即一个类可以实现多个接口。接口的出现就避免了单继承的局限性。
关于接口中有些概念jdk7-jdk9有些变化,具体有哪些变化可以参考相关阅读
抽象类和接口的异同点?
相同点:
- 相同点:它们都是通过不断向上抽取共性内容而来的。
不同点:
从语法角度上讲:
-
抽象类只能单继承;而接口可以多实现或多继承(接口与接口之间就是继承的关系)。
-
抽象类中可以定义抽象方法和非抽象方法,子类继承后可以直接使用非抽象方法;接口中只能定义抽象方法并且必须由子类去实现。
从设计角度上讲:
- 抽象类的继承,是一种is a关系,是在定义该体系的基本共性内容;接口的实现是一种with a(组合)关系,在在定义该体系的额外功能。
针对这一点,有必要再进行说明。考虑如下情景:鲸鱼、海豚都会游泳,并且它们都是海底动物。我们如何设计这些对象之间的关系?根据我们的实际生活经验其实可以轻松判断出鲸鱼、海豚与游泳之间是with a的关系:即鲸鱼、海豚具有游泳的能力,而它们与海底动物之间是is a的关系:即鲸鱼、海豚是海底动物。所以很明显,with a关系比is a关系更加灵活。于是:
// 游泳
interface Swim {
void swim();
}
// 海底动物类
abstract class Underseaanimals {
}
// 鲸鱼类
class Whale extends Underseaanimals implements Swim {
public void swim() {
System.out.println("Whale swim...");
}
}
// 海豚类
class Dolphin extends Underseaanimals implements Swim {
public void swim() {
System.out.println("Dolphin swim...");
}
}
现在考虑另外一个情景:门都具备开和关的功能,但有的门可以指纹开锁,有的门可以报警,这个时候又该怎样设计这些对象之间的关系呢?很明显开和关是所有门的基本共性内容,而指纹开锁和报警是额外功能。于是:
// 指纹解锁
interface Fingerprint {
void unlock();
}
// 报警
interface Alarm {
void alarm();
}
// 门类
abstract class Door {
void open(){};
void close(){};
}
// 具备指纹开锁功能的门
class FingerprintDoor extends Door implements Fingerprint {
public void unlock() {
System.out.println("我是一个可以指纹开锁的门...");
}
}
// 具备报警功能的门
class Dolphin extends Door implements Alarm {
public void alarm() {
System.out.println("我是一个可以报警的门...");
}
}