
4.2 抽象类
在面向对象的概念中,所有的对象都是通过类来描述的,但并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。如果我们要开发一个作图软件包,就会发现问题领域存在着点、线、三角形和圆等这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念。形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的,抽象类必须被继承。
4.2.1 抽象方法
在讨论抽象类之前,我们首先来了解什么是抽象方法。抽象方法(abstract method)在形式上就是包含abstract修饰符的方法声明,它没有方法体,也就是没有实现方法。抽象方法的声明格式如下:
abstract returnType abstractMethodName([paramlist]);
抽象方法只能出现在抽象类中。如果一个类中含有抽象方法,那么该类也必须声明为抽象的,否则在编译时编译器会报错,例如:
class Test{ abstract int f(); }
编译时的错误信息为:
Test.java:1: Test should be declared abstract; it does not define f() in Test class Test{ ^ 1 error
4.2.2 抽象类
在现实世界中存在的一些概念通常用来泛指一类事物,比如家具,它用来指桌子、凳子、柜子等一系列具体的实物。就家具本身而言,并没有确定的对应实物。在Java中,我们可以定义一个抽象类,来表示这样的概念。
定义一个抽象类需要关键字abstract,其基本格式如下:
abstract class ClassName{ ... }
注意:作为类的修饰符,abstract和final不可同时出现在类的声明中,因为final将限制一个类被继承,而抽象类却必须被继承。
抽象类不能被实例化,在程序中如果试图创建一个抽象类的对象,编译时Java编译器会提示出错。抽象类中最常见的成员就是抽象方法。抽象类中也可以包含供所有子类共享的非抽象的成员变量和成员方法。继承抽象类的非抽象子类只需要实现其中的抽象方法,对于非抽象方法,既可以直接继承,也可以重新覆盖。
下面我们通过一个具体的例子来说明抽象类的使用。在一个有关各种图形的应用程序中,我们可以将各种图形的共有的、相似的状态和行为提取出来,放在一个抽象类(Graphic)中,那些具体的图形,例如点、线、圆等都继承这个类。
在类Graphic中我们定义了一个方法area(),用来返回一个图形的面积。在Graphic中,这个方法只是简单地返回一个值0,对于点和线这样的对象来说,直接继承这个方法是合适的;而对于一个圆来说,直接继承该方法显然是错误的,所以在类Circle中需要重新实现该方法。
在类Graphic中还声明了一个抽象方法draw(),该方法用来绘制一个图形。每个图形都具有这个行为,但它们的具体绘制方式却各不相同,所以在Graphic中将draw()方法声明为抽象的,留待在各个继承类中去实现,见例4.4。
例4.4 GraphicDemo.java
abstract class Graphic{ public static final double PI = 3.1415926; double area(){ return 0; }; abstract void draw(); } class Point extends Graphic{ protected double x, y; public Point(double x, double y) { this.x = x; this.y = y; } void draw(){ //在此实现绘制一个点 System.out.println("Draw a point at ("+x+","+y+")"); } public String toString(){ return "("+x+","+y+")"; } } class Line extends Graphic{ protected Point p1, p2; public Line(Point p1, Point p2){ this.p1 = p1; this.p2 = p2; } void draw(){ //在此实现绘制一条线 System.out.println("Draw a line from "+p1+" to "+p2); } } class Circle extends Graphic{ protected Point o; protected double r; public Circle(Point o, double r) { this.o = o; this.r = r; } double area() { return PI * r * r; } void draw() { //在此实现绘制一个圆 System.out.println("Draw a circle at "+o+" and r="+r); } } public class GraphicDemo{ public static void main(String []args){ Graphic []g=new Graphic[3]; g[0]=new Point(10,10); g[1]=new Line(new Point(10,10),new Point(20,30)); g[2]=new Circle(new Point(10,10),4); for(int i=0;i<g.length;i++){ g[i].draw(); System.out.println("Area="+g[i].area()); } } }
4.2.3 抽象类和接口的比较
抽象类在Java语言中体现了一种继承关系,要想使得继承关系合理,抽象类和继承类之间必须存在“是一个(is a)”关系,即抽象类和继承类在本质上应该是相同的。而对于接口来说,并不要求接口和接口实现者在本质上是一致的,接口实现者只是实现了接口定义的行为而已。
在Java中,按照继承关系,所有的类形成了一个树型的层次结构,抽象类位于这个层次中的某个位置。接口不存在于这种树型的层次结构中,位于树型结构中任何位置的任何类都可以实现一个或者多个不相干的接口。
注意:在抽象类的定义中,我们可以定义方法,并赋予其默认行为。而在接口的定义中,只能声明方法,不能为这些方法提供默认行为。抽象类的维护要比接口容易一些,在抽象类中,增加一个方法并赋予其默认行为,并不一定要修改抽象类的继承类。而接口一旦修改,所有实现该接口的类都被破坏,需要重新修改。
下面我们通过一个应用案例来说明抽象类和接口的使用。在一个超市的管理软件中,所有的商品都具有价格,我们可以把商品的价格、设置和获取商品价格的方法,定义成一个抽象类Goods:
abstract class Goods{ //商品价格 protected double cost; //设置商品价格 abstract public void setCost(); //获取商品价格 abstract public double getCost(); ... }
某些商品,例如食品,具有一定保质期,我们需要为这类商品设置过期日期,并希望在过期时,能够通知过期消息。对于这样的行为,我们是否可以把它们也整合在类Goods中呢?显然这并不合适,因为对于其他商品来说,并不存在这样的行为,比如服装,而Goods中的方法,应该是所有子类共有的行为。我们可以将过期这样的行为,设计在一个接口Expiration中,Goods的子类可以选择是否要实现Expiration接口。接口代码如下:
interface Expiration{ //设置过期日期 void setExpirationDate(); //通知过期 void expire(); }
对于服装这类商品,我们需要继承抽象类Goods中的属性和方法,对其中的抽象方法必须提供具体的实现,至于Expiration接口可以完全不管。而食品这样的商品,我们既要继承Goods抽象类,又要实现Expiration接口。代码如下:
class Clothes extends Goods{ public void setCost(){ ... } public double getCost(){ ... return cost; } ... } class Food extends Goods implements Expiration{ public void setCost(){ ... } public double getCost(){ ... return cost; } public void setExpirationDate() { ... } public void expire() { ... } ... }
仔细体味一下个中关系,抽象类Goods(商品)和类Clothes(服装)及Food(食品)存在着“is a”的关系;而接口Expiration和Food具有联系,与Clothes就不存在联系。