Java中的抽象类和接口

写这篇文章,是想从面向对象设计的角度出发,去探讨抽象类和接口。也是对抽象类和接口要点的总结。

抽象类

我们知道,抽象是面向对象的四大特性之一。我们从现实世界的事物中可以抽象出事物的属性和功能。通过分析不同的事物,发现彼此之间可能存在很多不同的关系,比如层次的继承关系、部分与整体强相关的组合关系、部分与整体弱相关的聚合关系等等。其中,将事物的共性(属性和方法)抽取出来,就形成父子继承关系。在这个过程中,有些方法是有默认实现的,就可以在父类中定义方法体(尽管子类可以覆盖)。但有些只知道有此方法,根本不知道子类会如何实现。这时候,抽象类就派上用场了。注意:抽象类描述的仍是继承关系。

抽象类用于定义所有子类共同的、但不确定如何实现的成员方法。Java中抽象类的使用有如下要点:

  • 抽象方法必须定义在抽象类中;反过来,如果一个类含有抽象方法,则该类必须定义为抽象类。抽象类和抽象方法都要用abstract关键字修饰;
  • 抽象类不能被初始化。解释的话,知乎上的一个回复也很贴切:周星驰的电影中那个去水果店,揪住老板说要一斤水果!老板问他你要什么水果,他说他就要水果!这个水果就是抽象类,你如果能在水果店买到一斤叫水果的水果,那就能实例化一个抽象类。
  • 抽象类的方法必须在子类中实现,如果一个子类只实现了父类的一部分抽象方法,那么该子类仍然是抽象类。
  • 抽象类中既可以有抽象方法,也可以定义具体的成员变量和成员方法。除了具有抽象方法之外,其它的跟普通类一样定义;
  • 一旦子类继承了某个抽象类,则抽象类中的抽象方法必须在子类中被覆盖;
  • 若某个类要继承抽象类,同样使用extends关键字,而且是单继承。即一个类只能继承一个抽象类。

一个抽象类的例子:

1
2
3
4
5
6
7
abstract class Person{
String name;
abstract void eat();
void speak(){
System.out.println("my name:" + name);
}
}

接口

接口是对外暴露的一系列方法的声明,用于对类进行扩展。接口的本质是一系列规范,在Objective-C中甚至没有接口(Interface),而是叫协议(protocol)。理解这一点,就好像主板上对外提供各种接口,有USB接口、SATA硬盘接口、PCIE接口等。尽管不同的主板可以将接口的颜色换一下,位置换一下。但是各类接口的尺寸、针脚等都是固定的,是早先定义的规范。

在面向对象设计中,接口就是预先定义的规范。当定义了一个接口,仅仅说明了此接口能做什么,没有定义如何做。如果一个类实现此接口,就立刻知道了这个类能做什么,至于如何做,是在类内部定义的。

Java中接口用得非常广泛,比如Comparable接口定义的规范是:两个对象可以比较。但如何比较取决于对象内部的实现。比较String和Number都不是问题,但比较两个自定义对象时,是按照哪个字段进行比较,就要在类内部实现了。如Student类可以按照姓名比较,也可以按照学号比较等。

Java中接口的使用有如下要点:

  • 接口是用于对类进行扩展的。若某个类实现了一个接口,就证明该类具有此接口定义的所有方法。
  • 接口中可以定义两类元素:常量和抽象方法;注意:接口中的方法都是public的。
  • 接口中的常量默认都是public static final的。尽管如此,显式声明可以提高代码可读性;
  • 接口中的方法默认都是public abstract的,尽量显式声明;
  • 接口不能被初始化,但可以通过接口名调用接口中的常量;
  • java中支持多实现。即一个类可以实现多个接口。
  • 若某个类要实现接口,使用implements关键字。
1
2
3
4
5
6
interface Pocket {
public static final SIZE = 20;
public abstract String getSome();
int MAX_COUNT = 5; //合法,但不友好,尽量添加public static final修饰
void pubSome();
}

另外,接口也可以被继承,但只能被接口继承,甚至可以在接口之间实现多继承。因为接口中的方法只是声明,而具体的实现是在实现此接口的类中。若两个接口中有签名完全相同的方法,一个类实现了这两个接口,只要将接口中声明的这个方法实现了就行,无所谓这个方法是来自哪个接口。注意,不要在不同的接口定义方法名和参数列表都相同,但返回值不相同的方法,这样的设计很糟糕。

1
2
3
4
5
6
7
8
9
10
interface A {
public void showA();
}
interface B extends A{
public void showB();
}
class C implements B{ //必须实现接口A和接口B的所有方法
public void showA(){}
public void showB(){}
}

抽象类和接口的比较

因为抽象类仍然是一个类,所以抽象类可以定义构造方法、成员变量、普通方法以及静态方法,而且可以使用public、protected等修饰符。但是这些在接口中都不可以。接口默认修饰符为public static ,不需要在接口中显式声明。比较特别的一点是,接口和抽象类都可以包含静态变量,只是接口中静态变量只能是public static final类型,相当于常量。而抽象类中可以用任意修饰符进行修饰。

不过,有的人觉得既然抽象类已经可以定义抽象方法,类可以继承并实现此方法。接口也是定义抽象方法的,而且接口还不能定义成员变量和有实现的成员方法,比抽象类的功能还弱。为什么有抽象类了还需要接口呢?就说接口可以多实现,抽象类只能单继承,但如果Java某一天支持多继承了,是不是接口就没有存在的意义了呢?

其实,这样的问题只是从两者功能实现的相似性考虑问题了。然而,看到本质,还是面向对象设计思想。抽象类描述的是继承的关系,而接口则是对类的扩展,可以描述聚合组合的关系。这个是有很大区别的。比如知乎的鱼鱼鱼举的一个例子就很贴切:

苹果和鸡都可以吃,但是苹果是水果,鸡是动物,这个时候可以通过实现 edible 这个接口。所以说,“接口比类带来更多的灵活性”。

尽管实际开发中很多功能两者都可以实现,但是良好的实现是取决于良好的面向对象设计思想。这一点也很重要。一般情况下,继承的耦合性太强,而良好的设计往往是高内聚低耦合。因此,开发中优先选用接口,尽量少用抽象类

如果对于抽象类和接口比较迷糊,可以到知乎的一个问题上看看,瞬间会清晰很多。Java 中的接口有什么作用?

若感觉文章中有什么问题,欢迎一起讨论,或者可以帮我指正。非常感谢~