6.1 抽象方法和抽象类

img

扫码看视频

在设计动物基类Animal时,考虑到不同的动物的睡觉方式不一样,比如一般动物躺着睡觉,而马站着睡觉,所以在设计sleep方法时,我们给sleep方法添加一个abstract关键字来声明该方法是一个抽象的方法,如代码6.1所示。

img

可以看到,抽象方法就是使用abstract关键字声明的没有方法体的方法。

编译Animal.java,编译器提示如图6-1所示的错误。

img

图6-1 具体类中包含了抽象方法编译报错

Horse类继承自Animal类,并且也覆盖了抽象方法sleep,那为什么还会报错呢?错误的真正原因是:sleep是抽象方法,因此该方法所在的类必须声明为抽象类,即用关键字abstract声明类。为什么要这样做呢?这是为了避免当实例化一个含有抽象方法的类的对象时,若类中含有抽象方法,则意味着该种行为是不确定的。如果允许你实例化该类的对象,那么当你调用对象的抽象方法时,应该给出什么样的表现行为呢?

修改代码6.1,将Animal类声明为抽象的,如代码6.2所示。

img

注意,abstract关键字,要放在class关键字之前,一般是放在public访问说明符之后。若将类声明为抽象的,则该类将无法被实例化。

再次编译Animal.java并执行Horse,可以看到一切正常。

修改代码6.2,将覆盖的方法sleep注释起来,如代码6.3所示。

img
img

编译Animal.java,编译器提示如图6-2所示的错误。

img

图6-2 子类未重写抽象父类中的抽象方法而报错

由于Horse类继承自Animal类,但未覆盖Animal类的抽象方法sleep,因此Horse类的实现也是不完整的,不能用于实例化对象。为了保证非完整实现类不能产生对象,Java编译器要求将类声明为抽象的,也就是说,如果一个子类没有实现抽象基类中所有的抽象方法,那么子类也成为一个抽象类。

抽象类通常都是作为基类来使用的,可以在抽象类中定义派生类的公共行为(抽象方法),并提供一些基本的方法实现。

我们看一个类设计的例子,定义一个形状基类Shape,它有一个绘制方法draw,绘制什么形状呢?不确定,所以draw方法声明为abstract,Shape声明为抽象基类。从Shape派生出具体的形状子类:点(Point)、线(Line)、矩形(Rectangle)。一条线可以由两个点来确定,一个矩形可以由左上角和右下角的两个点来确定,因此Line类和Rectangle类还会用到Point类的对象,最终代码如6.4所示。

img
img

希望读者能够仔细阅读上述代码,这对掌握面向对象的类设计很有帮助。

上述程序的输出结果如下:

img

我们可以将一个没有任何抽象方法的类声明为abstract,避免由这个类产生对象。有时候,一个接口中声明了很多方法,但实现类往往只会用到其中很少的方法,为了减轻实现类的负担,可以编写一个基类对接口中声明的所有方法给出空实现(即只有代表方法体的一对花括号),由于这个类对接口中的方法都是空实现,直接使用该类的对象毫无意义,所以可以将该类声明为抽象的,以避免由该类直接产生对象。需要用到接口的类可以直接从这个基类派生,然后根据需要重写相应的方法即可。虽然基类是抽象的,但其中的方法都是有实现的(虽然是空实现),所以并不需要去覆盖所有的方法。可参看Java类库中的java.awt.event.WindowListener接口和java.awt.event.WindowAdapter类。关于接口,请参看下一节。

构造方法、静态方法、私有方法、final方法不能被声明为抽象的。这些方法有一个共同点,就是不能被覆盖,如果允许它们声明为抽象的,但是子类又不能覆盖这些方法,那不就矛盾了吗?

题外话 熟悉C++的读者可以把Java的抽象方法视为C++类中的纯虚函数。