首页 > 代码库 > Java中接口和抽象类的比较

Java中接口和抽象类的比较

Java中接口和抽象类的比较-2013年5月写的读书笔记摘要

1. 概述

        接口(Interface)和抽象类(abstract class)是 Java语言中支持抽象类的两种机制,是Java程序设计使用多态性的基础[[1]](在面向对象语言中,接口的多种不同的实现方式即为多态。多态性是允许你将父对象设置成为和一个或更多的他的子对象的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自“Delphi4编程技术内幕”)。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针[[2]])。

在那些面向对象(object)的程序设计语言的概念中,类(classss指的是一种抽象的数据类型、是客观对象在人脑中的主观反映、是对象共性的抽象、类型相同事物数据的抽象。可以说,全部的对象都需要通过类来进行描述,但是全部的类却不一定都是用来对对象来进行描述的。如果某一个类中所包含的信息不足以用来描述一个具体的对象,那么我们就称其为抽象类(abstract class)抽象类是我们在对某一问题领域进行设计和分析时所得出的抽象概念,是一系列本质上相同,而外在形象各异的具体概念的抽象反映[[3]]。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表示抽象概念的抽象类是不能够实例化对象的。

接口好比是程序之间的一个约定或合同,但它只定义了行为的协议,并没有定义履行接口协议的具体方法。接口中只指定抽象方法的方法头,但不提供抽象方法的具体实现。接口定义的仅仅是实现某种特定功能的一组对外接口和规范,而其具体功能的实现是在实现这个接口的各个类中完成的[1],一个类实现了某个接口,我们就说这个类符合了某个约定。

接口表示一种能力[4],一个类实现了某个接口,就表示这个类具备了某种能力。生活中一个人可以具有多项能力,一个类也可以实现多个接口。

抽象类强调的是“概念”,接口强调的是“能力”(或者说是“行为”)。

2.Java中的接口和抽象类的相似之处

(1)抽象类和接口都能实现对一组行为的抽象,接口和抽象类都可包含抽象方法。继承(实现)它们的类实例必须全部实现它们定义的抽象方法后才能用于实例化对象(不过,抽象类可以一个抽象方法都没有);

(2)接口和抽象类木身都不能用于对象实例化但它们的引用可以指向继承(实现)它们的类实例从而动态地使用这些类实例。

3. Java中的接口和抽象类的区别

1) 属性抽象类中可以有变量,而接口中不能定义变量,即接口中属性都会自动用public static final来修饰,都是全局静态常量,且必须在定义时指定初始值。

2) 方法抽象类中可以有具体方法和抽象方法,抽象类的子类也可以是抽象类;而接口中所有方法都是抽象方法,接口中方法都会自动用public abstract修饰。由于接口不涉及实现,从这点上看,接口在抽象化程度方面比抽象类的更高

3) 继承性:抽象类属于一种特殊的类,类的继承必须满足一个子类只能有一个父类(单继承),但可以实现若干个接口(多实现)。而接口可以实现多继承,即一个接口可以有多个父接口。

4) 设计思想[1]: 这一点也是最本质的一点,对于抽象类与接口的选择很重要。对于抽象类来说,抽象类与其子类之间存在一个is a"的继承关系,即二者形成层次结构父类和子类在概念本质上是相同的


如Shape类与其子类Circle , Square ,Triangle在本质上是相同的,明显存在一个“is a"关系,即Circle(圆形)、Square(矩形),Triangle(三角形)都属于Shape形状。对于接口来说.接口与实现它的类之间不存在仟何层次关系。也不要求接口和实现它的类之间在概念本质上一致,只要求接口的实现者实现接口规定的功能。接口可以实现毫不相关类的相同行为,比抽象类的使用更加方便灵活。比如定义一个接口open_close表示开关行为:

interface  open_close{

void open();

void close();

}

门door有开关功能,可以让Door类实现open_close接口。

class Door implements open_close {

void open() {……}

  void close() {……}

 }

手机MobileTelephone有开关功能,可以让mobileTelephone类实现open_close接口。

class MobileTelephone implements open_close {

void open() {……}

void close() {……}

 }

电脑computer有开关功能,可以让Computer类实现open_close接口。

   class Computer implements open_close {

void open() {……}

void close() {……}

 }

从该例可以看出,类Door, MobileTelephone,Computer与接口open_close之间不存在任何层次关系,它们之间是“has”关系,即Door, MobileTelephone,Computer都具有接口open_close指定的功能。通过接口open_close实现了毫不相关类Door, MobileTelephone,Computer的共同行为。再举个不太精确,但是好理解的例子,在学校的学生信息管理软件中“我是一个能打篮球、踢足球的学生”可以定义为:

class Me extends Student implementsIPlayBasketeball, IPlayFootball { }

我是学生,因此我extends Student,我有打篮球,踢足球的能力,因此我实现IPlayBasketeball, IPlayFootball,我就拥有了IPlayBasketeball, IPlayFootball接口提供的各种行为(如,传球,头球,射门等),并且我自己实现它们。


4.接口与抽象类的选择[4]

考虑这样一个例子,假设我们在为一个电器生产厂家开发手机软件,问题领域中有一个关于手机的抽象概念,该手机具一些动作如开机,关机等,此时我们可以通过接口或者抽象类来定义一个表示该抽象概念的类型,定义方式分别如下所示:

使用接口方式定义手机:

interface Mobile_phone{

void open();

void close();

}

使用抽象类方式定义手机:

abstract class Mobile_phone{

abstract void open();

abstract void close();

}

而具体的手机类型(如A18型,B30型)可以继承抽象类方式定义,或者使用实现接口的方式定义。看起来好像使用接口和抽象类没有大的区别。但随着技术的发展,如果现在要求手机要集成有信用卡的功能。我们应该如何设计针对该例子的类结构呢?信用卡的一些基木功能有,电子钱包,消费等,这些功能和手机的开机关机功能属于两个不同的概念,根据接口隔离原则(ISP,Interface Segregation Principle)应该把它们分别定义在代表这两个概念的抽象表示(接口或者抽象类)中。这时“信用卡”这个抽象概念的定义可能是这两种情况:

interface Creditcard {

void e_wallet();

void consume();

}

abstract class Creditcard {

abstract void a_ wallet();

abstract void consume();

}

这时手机和银行卡这两个概念的定义方式就有了四种可能的组合,如下表:

 

手机

信用卡

方案A

定义为抽象类

定义为抽象类

方案B

定义为抽象类

定义为接口

方案C

定义为接口

定义为抽象类

方案D

定义为接口

定义为接口

方案A(两个概念都定义为抽象类)可以马上排除,因为Java不支持多继承,实现类这无法同时继承这两个抽象类。在这里可以看到抽象类在通过概念的组合来扩展功能的时候是

不方便的

其他的方式目前从语法上都是可行的,但谁更合理是值得考究的。

先研究一下方案C(“手机”定义为接口;“信用卡”定义为抽象类),这时实现这两个概念的类和这两个概念之间的关系就是,“like”手机,“is”信用卡,也就是说实现手机接口、继承信用卡抽象类的子类具有手机的功能,但是本质上是信用卡。显然这和我们对问题领域的理解不符。因为带信用卡功能的“手机”在概念本质上是手机,同时具有信用卡的功能可以像信用卡一样使用)。所以这个方案是不合理的。除非我们是在为信用卡的制造商写软件,他们希望加入手机的功能。而且,如果手机再扩展功能,如电子地图,导航器等等,把这种扩展的功能概念像信用卡”一样定义为抽象类的话,由于Java的单继承机制,这是无法实现的。这道理和方案A一样,把用来扩展功能的概念定义为抽象类并不合适。

方案B(“手机”定义为抽象类;“信用卡”定义为接口)应该是目前最合理的设计了,这时反映出的概念是is手机,like“信用卡”。如果有扩展功能的话,可以再定义成接口,成为“is手机,like信用卡like电子地图”,从而正确的反应我们面对的问题域。  

那方案D两者都定义为接口,是不是就不行呢?相对方案C来说,方案D的设计没有反映出手机”是问题领域的本质的主体,使人有到底我们在搞手机还是信用卡还是别的什么东西?”这个疑问。这个缺点是不容置疑的。但从另一方面来说:“手机”这个概念定义成接口,在软件规模扩大的前提下,也许为以后其他的组件的使用提供了方便。比如说,假设厂家又有一个遥控器”的概念要我们设计,要把手机的功能设计进去,这时时候手机”如果是接口就方便了,implements他就行。所以说,方案D是牺牲了概念的清晰性,得到了扩展

如果预见到问题领域以后没有太大变化,方案B是最好的。方案D在目前是不合适的,但在以后的扩展中也许很方便。这里得到的结论就是:如果只是在定义一组行为框架的话,抽象类合适用来定义问题领域中的本质的抽象概念,接口合适用来定义扩展功能的抽象概念。  

 

  在刚刚这个例子中“手机”,“信用卡”仅仅是一组抽象方法,也就是概念中含有的只是行为框架没有实现,这时候定义成抽象类或接口都有自己的道理。如果概念中己经含有了实现,这时候就把该概念定义成抽象类了。

  比如一个“A系列打印机”的抽象类,由他定义不同类型的打印机,那一系列的打印机打印页头,页脚的方案都是一样的,但打印页面主体比较复杂,各种具体型号的打印机的各有它们不同的打印方法,这时可以这么设计:   

方案一:按照打印机应该打印完整页面的自然逻辑, PrintBody()抽象方法是打印机这个概念的一部分,设计为抽象类:

abstract class A_SeriesPrinter{

abstract protected void PrintBody();

public void OutReport() {

PrintHeader();

PrintBody();

PrintFooter();

}

protected void Draw(String str) {  /*实现的代码*/ }

protected void PrintHeader() {         Draw("Head");/*实现的代码*/      }

protected void PrintFooter() {           Draw("Footer");/*实现的代码*/   }

}

}

继承抽象类的代码:

classXXPrinter extends A_ SeriesPrinter {

protected void PrintBody() {    /*实现的代码‘/ }

}

classYYPrinter extends A_SeriesPrinter  {

protected void PrintBody() {    /*实现的代码‘/ }

}

运用的代码:

XXPrinter xx = new XXPrinter();

xx.OutReport();

YYPrinter yy = new YYPrinter();

yy.OutReport();

显然这个方案是简单而清楚的。

方案二:为了扩展性,硬把PrintBody()抽象方法取出来成为一个接口IBody,代码如下:

abstractclass A_SeriesPrinter {  //思考一下,还用abstract么?

protected void Draw( String str) {    /*实现的代码*/      }

protected void PrintHeader() {   Draw("Head");/*实现的代码*/        }

protected void PrintFooter() {        Draw("Footer");/*实现的代码*/ }

}

interface IBody{

void PrintBody();//多了一个IBody接口的概念

}

在这里先解决一个问题,如果Printer去掉了PrintBody()抽象方法,都是实现了的方法,是不是就应该把它定义为普通的类呢?

答案是否定的,设计一个抽象概念为抽象类的意义,不是因为它含有抽象方法,而主要因为是他表示的概念不应该被实例化,即使它里头的方法全部是实现了的,只是想让子类继承的代码。在上面这个例子中,“A系列打印机”这个概念,是不应该有实例的,有实例的应该是具体型号的打印机。所以,即便是全部是实现了的方法,方案二中的A_SeriesPrinter还是定义成抽象类更好。  

继续看继承类并实现接口的代码:

classXXPrinter extends  A_SeriesPrinterimplements IBody {

public void PrintBody()    {       ;/*实现的代码*/     }

public void OutReport() {  // OutReport()被迫移到了实现类

PrintHeader();

PrintBody();

PrintFooter(); 

}

}

classYYPrinter extends  A_SeriesPrinterimplements Body {

public void PrintBody() { ;/*实现的代码*/}

public void OutReport(){  // OutReportQ被迫移到了实现类

PrintHeader();

PrintBody();

PrintFooter();

}

}

运用的代码:

XXPrinter xx = new XXPrinterQ;

xxDutReportQ;

YYPrinter yy = new YYPrinterQ;

yy.OutReportQ;  

这样做会显得很奇怪和复杂: class XXPrinter extends Printer implementsIBody?好像打印Body竟然是打印机的附加功能?(这太让人难以理解了),还无端的多出了一个IBody接口的概念。而且,OutReport()被迫移到了各个实现类,代码变长而且复杂了。所以这时抽象类是最好的选择。除非有业务要求需要把Body的打印从打印机分离出来。套到别的概念中去。这时才有考虑使它成为接口的可能,但再次提醒大家,代码会变得复杂。追溯问题出现的源头,是因为PrintBody()这个抽象方法和打印机这个概念结合的太紧密了,它本身就是打印机功能的不可缺少的一部分。贪图接口语法上的灵活性,自目的追求扩展性开放性,而不顾对问题领域的理解而建模,只要某一个概念(A_SeriesPrinter)中含有的行为框架(PrintBody())都分离出来搞成接口,就会有一系列的编码上和理解上的麻烦,反而增加了代码的复杂性。

然而,即使在使用抽象类的场合,也不要忽视通过接口定义行为模型的原则。如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。比如我扩展A_ SeriesPrinter类,在打印后加个日志信息,如viod outLog()方法,那么我就不应该把它定义成A_SeriesPrinter类的抽象方法了,而是日志接口的抽象方法。因为“日志”这概念不属于打印机的专有范畴。这样以后其他模块用到关于日志的操作规范时可以方便地用到这个日志接口。

所以,关键在于能否出色地结合业务要求对问题域进行理解分析。如果你没有做好这点,你就不能建立合理的模型,这时要不就是增加编码的复杂性,可理解性,要不就是代码难以随着业务扩展而维护和修改。

 

综上,

1) 如果只是在定义一组行为框架的话(抽象类和接口都可以实现),抽象类合适用来定义

问题领域中的本质的抽象概念,接口合适用来定义扩展功能的抽象概念。

2) 当需要为一些相关的类提供公共的实现代码时,应该优先考虑用抽象类来实现,因为抽

象类中的非抽象方法可以被子类继承下来,使实现功能的代码更简单

3) 当注重代码的扩展型和可维护性时,应该优先考虑使用接口,原因有:①接口与实现它

的类之间可以不存在任何层次关系,接口可以实现毫不相关类的相同行为,比抽象类的使用更加方便灵活;②接口只关心对象之间的交互的方法,而不关心对象所对应的具体类。接口是程序之间的一个协议,比抽象类的使用更安全、清晰。一般使用接口的情况更多[1]。

简单说的说“在只是定义一组行为框架时,对于与问题域中的抽象概念的定义应该优先考虑采用抽象类;要为一些相关类提供公共的实现代码时,应该优先考虑采用抽象类;其它情况都应该优先考虑采用接口”。——zhouyong 


补充:

问题域[[5]](Problemdomain):指提问的范围、问题之间的内在的关系和逻辑可能性空间。

在软件工程中,问题域是指被开发系统的应用领域,即在客观世界中由该系统处理的业务范围。 



[[1]]阳小兰 钱程 赵海廷. Java中抽象类和接口的使用研究.Software Guide, Vo1.9 No.10 Oct.2010

[[2]]百度百科:http://baike.baidu.com/view/126521.htm

[[3]]颜瑞江. Java中抽象类和接口的区别与联系.

[[4]]刘虢俊. 浅析Java的接口和抽象类. 计算机与信息技术,2007年11月,第11期总第108期)

[[5]]http://baike.baidu.com/view/1128785.htm


Java中接口和抽象类的比较