java 抽象类和接口的区别

我一直习惯使用接口,基本上没有使用过抽象类。那么问题来了,两者的区别在哪里?抽象类有什么用?

通过这篇文章,我知道接口和抽象类是无法相互取代的。使用时需要具体分析。

一、一些概念

(1)interface

接口的概念详见:

java中接口有什么作用?

(2)abstract

abstract 关键字可以修饰类或方法。
abstract 类可以扩展(增加子类),但不能直接实例化。

abstract 方法不在声明它的类中实现,但必须在某个子类中重写。

在我的理解中,抽象类abstract更像是辅助继承的实现。

abstract修饰的类只能拓展,不能直接实例化,有点类似于interface只能通过实现类去实现。abstract修饰的方法必须在某个子类中重写,有点类似于实现接口的类必须实现接口中的所有方法。其实这两个东西感觉是差不多的…

在一般的应用里,最顶级的是接口,然后是抽象类实现接口,最后才到具体类实现。

Human.java

Human接口,人需要生活,所以定义一个live方法。

Programmer.java

Programmer抽象类,程序员也是人,所以实现Human接口,实现live方法。程序员需要不停地学习,但是不同领域的程序员所学不同,所以定义抽象方法study,强制要求所有子类重写study方法。程序员全都需要编程,所以定义write方法。

Javaer.java

Javaer必须重写study方法,但是可以直接使用父类中继承的write方法。

顺带一提:接口里的方法都是public abstract的(无论你是否显示的用这两个修饰符修饰),而抽象类中可以有非抽象方法。在设计程序的时候,可以把方法声明在一个接口里,然后用一个抽象类实现这个接口,把其中公用的方法实现了,再把更具体的方法留给抽象类的子类去实现,从而形成不错的结构。

二、区别

抽象类和接口之间存在较大的相同点,甚至有时候还可以互换,但是始终存在一些区别,所以有必要搞清楚区别在哪里。

参考:http://www.cnblogs.com/chenssy/p/3376708.html

这篇文章比我之前总结的好多了…我参考了这篇文章的各种观点,并且补充个人理解。

(1)语法层次

抽象类可以拥有私有成员数据,同时也可以拥有自己的非抽象方法。

接口仅能够有静态、不能修改的成员数据(但是一般不会在接口中使用成员数据),接口所有的方法都必须是抽象的。

在某种程度上来说,接口是抽象类的特殊化。

java中一个子类只能继承一个抽象类,但是却可以实现多个接口。这里涉及多重继承的问题,请参照:

http://www.xie4ever.com/2017/05/18/java-%E5%AE%9E%E7%8E%B0%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF/

(2)设计层次

上面只是从语法层次和具体实现角度来区分它们之间的关系,这些都是低层次的区别。要真正使用好抽象类和接口,必须从设计理念的角度才能看出本质所在。

1、抽象层次不同

抽象类对类抽象,而接口对行为抽象。

抽象类对整个类整体进行抽象,包括属性、行为。举个例子:动物会进食,吼叫(行为),都有名字(属性),可以设计以下抽象类:

Animal.java

这里的Animal类是“动物”,继承这个Animal类的子类也应该是“动物”。这里的Animal是对“动物类”的抽象。

题外话:很多人在这一步就会懵逼了,这个抽象类和普通类也太相似了吧!Animal类写成普通类也无伤大雅,为什么要使用抽象类呢?

  1. 抽象类不能被实例化。
  2. 抽象类可以有构造函数,被继承时子类必须继承父类一个构造方法,抽象方法不能被声明为静态。
  3. 抽象方法只需申明,而无需实现,抽象类中可以允许普通方法有主体。
  4. 含有抽象方法的类必须申明为抽象类。
  5. 抽象的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类。

个人认为,主要区别在于抽象类不能被实例化,所以只能和接口一样起一个“指导”、“模板”的作用。

如果Animal是一个普通类,就可以这样实例化了:

个人理解:一般情况下,我们需要的是一只具体的动物,是Animal类的延伸,而不是Animal类本身。Animal类在设计上应该是一种“模板”,不需要被实例化,所以设计为抽象类更加合适。

回到正题:接口是对类局部(行为)进行抽象,更加近似于某种“规范”。如果上面的例子使用接口实现,应该写成这样:

Animal.java

但是,这个Animal接口不应该用name这个属性,因为接口是一类行为,不应该出现属性定义。

个人理解:这里的Animal接口是“动物的行为”,即实现这个Animal接口的实现类应该具备“动物的这些行为”,是“具有动物的这些行为的某些东西”,而不一定要是某种“动物”。

2、跨域不同

抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。抽象类一般是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可。但是接口不同,实现它的子类之间可以不存在任何关系。

我认为这只是由类的实际意义决定的。

现在有个情景:猫和老虎都有不同的名字(属性),都会进食和吼叫(方法),但是叫声不同,怎么进行设计呢?

首先猫和老虎就是两个类Cat和Tiger,两者都属于动物,所以可以把成员变量和方法都抽出来写成一个Animal抽象类,然后让Cat和Tiger继承Animal。

Animal.java

因为两种动物的叫声不同,所以howl需要设计成抽象方法,在子类中重写。至于eat方法,暂时不需要太讲究,直接在Animal类中定义默认实现。

于是,可以写出两个子类:

Cat.java

Tiger.java

补充一点:因为每种动物的name都不同,所以设计时完全没有必要把在Animal类中把name属性抽出来,让子类自己去实现更好(用得到就定义,用不到就不定义,没必要在父类中强行定义这个属性)。

Cat.java

那么问题来了,其实我完全可以把Animal类设置成一个接口,把相同的方法都抽出来成为接口方法,然后让Cat和Tiger去实现Animal接口:

Animal.java

Cat.java

Tiger.java

区别在哪里呢?看上去就在eat方法。

如果继承Animal抽象类,eat方法是不需要单独实现的,子类自动继承抽象类中的默认方法。如果实现Animal接口,eat方法要在各自的实现类中单独实现了。

到这里我们可以发现:接口不能实现“重用”。

但是,不要轻易下这个结论。这种说法在jdk1.8以前是适用的,但在1.8中,java引入了interface中的default方法,导致Animal接口可以这样实现:

Animal.java

这下尴尬了,eat接口可以重用,抽象类和接口的实现效果太相近了…

个人认为:Animal比起行为,更像是一个种类,所以应该设计为抽象类而不是接口。抽象类和接口实现的效果可以差不多,但是要分清楚哪种设计更符合语义。

个人认为:如果需要使用继承体系(比如Tiger和Cat都是Animal,都有相同的行为),而且父类不适合实例化,选择抽象类(父类也需要实例化就使用普通父类)。

现在有另外一个情景,飞机和鸡都会飞,怎么进行设计呢?

飞机和鸡除了会飞以外(一个相同方法),不存在任何的共同点。所以只能设计一个名为Fly的接口,然后Plane和Chicken都去实现这个Fly接口,让他们都具有飞的方法。

这里用接口无疑更加合适。也就是说,抽象类不适合用于表现行为。

我认为:如果类之间不存在相同意义(比如Plane和Chicken之间除了都会飞以外几乎不存在共同点)并且存在需要共同遵守的规范(都会飞),选择接口。

在进行设计时,需要具体情况具体分析。

3、 设计流程不同

(1)抽象类

一般而言,抽象类是自下而上来设计的,我们要先知道子类才能抽象出父类。

比如我们只有一个Cat类,如果马上就抽象成一个动物类,是不是设计有点儿过度呢?起码要有两个动物类,比如Cat和Tiger,才会去抽出两者的共同特点,形成一个Animal抽象类。

也就是说,在设计过程中,我们不一定从一开始就知道要有一堆近似的对象。只有在开发中发现一堆相似的需要后,才会考虑抽出抽象类。所以说抽象类往往都是通过重构而来的。

举个例子:现在我要开发一个养宠物游戏,1.0版本只有猫这种宠物,那么我写一个Cat类就足够了。迭代的第二个版本需要增加养狮子,那么我可以写多一个Lion类。迭代到第十个版本时,我终于决定优化代码,抽出一个Animal模板,这时候就要考虑使用抽象类了。

(2)接口

一般而言,接口是从上至下来设计的,我一开始就知道要有一堆对象需要具有某种能力。

比如说我们想让一系列的东西飞,那就实现一个Fly接口好了。什么时候会有什么东西怎么实现这个Fly接口,我们不需要关心。

所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。

三、总结

实际上,好的设计不一定通过重构而来,可以事先就进行全面的考虑。

我抽像类用得还是太少了,主要还是使用接口。这里要特别注意不要“滥用”接口(特别是在某些框架下),以后再讨论。

发表评论

电子邮件地址不会被公开。 必填项已用*标注