浅谈抽象类与接口

浅谈接口与抽象类

接口和抽象类是面向对象编程中的两个重要概念,它们分别对应两种继承方式——接口继承、类继承,这两种继承的含义、用途存在差异,理解这些差异能帮助我们设计出可复用性更高的代码。

一些支持面向对象的编程语言(如C++、Python)在语法上不区分接口继承和类继承,很容易混淆;另一些编程语言(如Java)的语法则会区分这两种继承方式,所以我们会以Java为例来谈谈接口继承和类继承的区别。

Java中分别使用interfaceabstract class定义接口和抽象类;implementsextends分别是接口继承和类继承的关键字。

至此大家可能会有一些疑问,当然第一个疑问就是“接口继承和类继承有什么区别?”,还有人会问“接口继承和类继承是什么东西?”、“接口和抽象类是什么东西?”、“类又是什么东西?”、“面向对象是什么东西?”。

我们先请那个问“面向对象是什么东西?”的同学出去,出门左转找个安静的角落自行百度,限于篇幅这里不作讨论。重点 来看看剩下的几个问题。


为了方便讨论,我们对方法的声明和实现做如下约定

  • 方法的声明:包括方法名、参数列表、返回值、修饰词。
  • 方法的实现:跟在方法的声明后的花括号“{}”以及里面的内容。

比如方法 public void method(String param) { ... }public void method(String param)属于声明,{ ... }属于实现。

类是什么?

类是对一组具有相同属性和行为的对象的抽象,定义了对象的状态、行为。类是对象的模板,对象是类的实例。例如

public class Phone {
    private int id;
    
    public void call(String phoneNumber) { ... }
    public void sendMessage(String phoneNumber, String message) { ... }
}
Phone myPhone = new Phone();

对象myPhone是以Phone类为模板生成的一个实例,myPhone具备Phone类定义的所有数据和方法。

抽象类是什么?

抽象类是包含了抽象方法的类,抽象方法就是只有声明没有实现的方法。例如

public abstract class AbstractPhone {
    public abstract void call(String phoneNumber);
}

在抽象类AbstractPhone中,抽象方法call(String)只有声明但没有实现。抽象类只是一种特殊的类,因为存在抽象方法,所以它不能被实例化(想想看抽象类为什么不能被实例化?)。抽象类的主要作用是把这些抽象方法的实现延迟到子类。

有同学会说“C++里没有抽象方法,你是不是在诓我?”
答:真没诓你。C++中的纯虚函数与Java中的抽象函数是同样的概念。不过Java中抽象函数必须要用关键词abstract修饰,抽象类也必须用关键词abstract修饰,而C++中则不需要用特殊的关键词修饰抽象类。这些只是语法上的差异并不影响抽象类的实质——类中存在一些只有声明没有实现的方法。至于Java中的abstract关键字,我认为只是用来提醒程序员的,Java源码编译成字节码后abstract八成无迹可寻了。事实上Python语法不支持定义抽象类,但不用关心它,因为我们可以通过编码技巧达到抽象类的作用。

也有同学会说“Java中只要是被abstract修饰的类就是抽象类,不一定要有抽象方法。”
答:那我会觉得你在抬杠,这样的抽象类有什么意义?把abstract关键词删掉它就变成一个普通的类了!

接口是什么?

接口是表示对象的能力的集合。何谓“能力”?能力表示对象可以处理什么请求,在面向对象里一个方法的声明就代表一种能力,简单来说接口是对象公有方法声明的集合。对象只有通过接口才能与外部通信,所有符合对象接口描述的请求都可以由这个对象处理。

类型是一个用来标识特定接口的名字,那么满足接口的对象就具有该接口的类型。

Phone myPhone = new Phone();

对象myPhone能处理类型Phone定义的所有请求(call(String)sendMessage(String, String)),所以myPhone满足Phone接口,那么myPhone就具有Phone类型。

注意区分类型,这是两个不同的概念

  • 类:类是对象的模板,定义了对象的数据和行为;一个对象只可能是一个类的实例;
  • 类型:类型是特定接口的名字,只和接口有关、与实现无关;一个对象可能具有多种类型。

Phone myPhone = new Phone();中,左边的Phone表示类型,用来说明myPhone是一个Phone类型的对象;右边的Phone表示类,用于提供模板来实例化一个对象。

接口实际上定义了对象与外部的通信方式

  • 声明了对象应该具备的方法;对象具备某个接口类型,就必须提供这个类型定义的所有方法的实现。
  • 外部模块只能通过接口了解对象提供了哪些方法可以被调用。

我们把上面的Phone类抽象成接口(把所有方法的实现都删掉,只留下方法的声明)

public interface Phone {
    void call(String phoneNumber);
    void sendMessage(String phoneNumber, String message);
}

可能有同学坐不住了“C++里没有接口这个概念!”
答:C++确实没有类似Java的interface一样的关键字专门用来标识接口,但不妨碍C++使用接口的概念——C++中只包含纯虚函数的类与Java的“接口”是一样的效果。

具备类型Phone的对象必须提供call(String)sendMessage(String, String)的实现。接口不能实例化(想想看接口为什么不能实例化?),所以我们需要一个类来实例化能满足Phone接口的对象,这个类会给类型Phone定义的每个方法都添加实现。

public class Nokia implements Phone {
    public void call(String phoneNumber) { ... }
    public void sendMessage(String phoneNumber, String message) { ... }
}

对于客户代码

...
Phone myPhone = new Nokia();
myPhone.sendMessage("1234567890", "我有一个电话,我正在发短信...");
myPhone.call("1234567890");
...

严格来说myPhone不是对象而是一个Phone引用类型的变量,这个变量引用了一个Nokia对象,为了表述方便,我们暂且认为myPhone就是那个Nokia对象。

Nokia保证了对象myPhone一定有Phone类型定义的所有方法的实现;客户代码知道类型Phone有方法call(String)sendMessage(String, String)可供调用,而myPhone又具备类型Phone,所以客户代码可以直接调用那两个方法,而不用关注其他信息。

加深理解类型和类的区别——Phone myPhone= new Nokia();中的Phone表示的是类型,Nokia表示的是类。

接口有哪些特点?

支持多态

我们知道,客户代码只要知道对象具备什么类型,就可以直接对该对象调用那个类型定义的方法;我们还知道,接口其实就是一些方法的声明,这些方法的实现放在实现了接口的类中。这就是说明接口和实现其实是分离的,但…那又怎样?

了解C++的同学可能想起了头文件和源文件,确实,接口和实现的关系与头文件与源文件的关系很像——声明在一个地方,而定义在另一个地方;

既然接口和实现是分离的,就意味着实现可以独立于接口变化,也意味着接口可以有多种不同的实现。比如前面的Nokia类为Phone接口定义的方法提供了实现,但无论Nokia怎么改变方法的实现都不会影响Phone接口本身的定义,也不会对客户代码造成影响。此外,其他类也可以为Phone接口定义的方法提供实现,比如

public class XiaoMi implements Phone {
    public void call(String phoneNumber) { ... }
    public void sendMessage(String phoneNumber, String message) { ... }
}

客户代码中将myPhone替换成XiaoMi的实例,仍然不影响对myPhone调用方法call(String)sendMessage(String, String)

...
Phone myPhone = new XiaoMi();    // 原本为 Phone myPhone = new Nokia(); 
myPhone.sendMessage("1234567890", "我有一个电话,我正在发短信...");
myPhone.call("1234567890");
...

因此,具有相同接口的对象可以对请求做不同的实现,但对客户代码而言没有任何影响。示例中Nokia对象和XiaoMi对象对方法call(String)sendMessage(String, String)的实现就不同,而客户代码对myPhone的调用方式却没变。

所以接口支持了动态绑定,也就是发送给对象的请求和它的实现可以在运行时绑定。动态绑定允许在运行时替换具有相同接口的对象,这种可替换性就是面向对象的核心概念之一——多态。使用多态可以松耦合甚至接耦,支持在运行时替换服务提供方。

一个对象可以具备多种类型

一个对象可以是同时具备多种类,支持多种接口。就像我们的手机既可以电话,也可以是相机、音乐播放器、视频播放器。考虑上面的例子,现在又有一个Camera接口

public interface Camera {
    void photograph();
}

XiaoMi同时满足Phone接口和Camera接口

public class XiaoMi implements Phone, Camera {
    public void call(String phoneNumber) { ... }
    public void sendMessage(String phoneNumber, String message) { ... }
  
    public void photograph() { ... }
}

XiaoMi类的实例既具有Phone类型,还具有Camera类型(实际上还具有XiaoMi类型,因为XiaoMi本身也是一种类型)。

加深理解类和类型的区别

  • XiaoMi类的实例具有XiaoMi类型、Phone类型、Camera类型。
  • 但只可能是类XiaoMi的实例,绝不会是其他类的实例。

表现在客户代码里就是这样

...
XiaoMi xiaoMi = new XiaoMi();
xiaoMi.sendMessage("1234567890", "我有一个电话,我正在发短信...");
xiaoMi.call("1234567890");
xiaoMi.photograph();
...
Phone myPhone = xiaoMi;
myPhone.sendMessage("1234567890", "我有一个电话,我正在发短信...");
myPhone.call("1234567890");
...
Camera myCamera = xiaoMi;
myCamera.photograph();
...

接口继承和类继承是什么?

类继承

类继承根据一个对象的实现定义了另一个对象的实现。类继承意味着被继承的是实现,子类的实例天然具备父类的实现。例如

public class XiaoMi {
    public void call(String phoneNumber) { ... }
}
public class XiaoMiPlus extends XiaoMi {
    ...
}

XiaoMiPlus看上去没有实现call(String)方法,但因为类XiaoMiPlus继承自XiaoMi,也就继承了对call(String)的实现,因此XiaoMiPlus的实例具有call(String)的实现。

...
XiaoMiPlus myPhone = new XiaoMiPlus();
myPhone.call("1234567890");
...

客户代码对myPhone调用call(String)是合法的。

接口继承

接口继承描述了一个对象什么时候可以用来替代另一个对象。接口继承意味着被继承的是方法的声明。

一些语言看起来没有接口继承的必要(如Python等动态语言),我们正好借此来看看接口继承的真相。

1)动态类型的语言不需要显示声明变量类型,因此只要对象有某个方法的实现,客户代码就可以直接调用这个方法,而不理会这个对象是不是具备期望的类型。例如

class Nokia:
    def call(phoneNumber):
        ...
class XiaoMi:
    def call(phoneNumber):
        ...

Nokia的和类Xiaomi的都实现了call(str)方法,因此两个类的实例都具有方法call(str)实现,客户代码可以直接调用这个方法

...
myPhone = Nokia() // 或者 myPhone = XiaoMi()
myPhone.call("1234567890")
...

客户代码向myPhone发起调用call(str)的请求,myPhone发现自己恰好有call(str)的实现,就用这个实现响应了客户代码的请求(当然,如果没有这个实现的话就只能在运行时报错了)。注意类Nokia的对象和类Xiaomi的对象之间没有任何关联,只是恰巧都支持call()方法。

2)Java或C++等静态类型的语言为了防止在运行时因为找不到实现而报错,会在编译期间做检查,要求对象必须具备指定的类型,否则编译器就认为对象没有相关方法的实现,编译无法通过。实际上这种限制只是把找不到实现的错误由运行时提前到了编译期间,极大地降低了排查问题的难度。

因此在Java或C++中,客户代码调用call(String)方法时要求myPhone必须具备Phone类型(或其子类型),而类Nokia和类XiaoMi也必须继承自类型Phone,否则编译器会认为程序员没有为类添加方法call(String)的实现而报错。

Python没有专门管理接口的语法,需要程序员自己管理接口;
C++通过类管理接口;
Java用一个专用的关键字interface管理接口;

接口和抽象类有哪些区别?

意义不同

  • 抽象类是一种特殊的类,为子类定义公共实现,存在只有声明没有实现的方法;
  • 接口是表示能力的集合,只包含方法的声明不包含实现。

复用机制不同

  • 抽象类:复用实现。子类共用抽象类的代码。
  • 接口:复用接口,提高可替代性。接口相同实现不同的系统,可以共用客户代码。

继承不同

  • 类继承强调的是继承方法的实现。
  • 接口继承强调的是继承方法的描述。

其他问题

为什么接口不能实例化?

接口只包含了方法的声明不包含实现,只有声明而没有指定方法的实现,即便是可以实例化,对象也无法处理请求。

为什么抽象类不能实例化?

抽象类包含抽象方法,抽象方法只是被声明了而没有被实现。同样,即便是可以实例化,对象也无法处理对抽象操作的请求。

熟悉C语言的同学可能会有更深刻的理解,对于一个只有声明、没有定义的函数,编译阶段很顺利但链接阶段会报错,因为编译器不知道应该让哪段代码回应对这个函数的调用操作。

你可能感兴趣的:(编码设计,java,java,开发语言,设计模式,设计规范)