4.0 第4章 类的继承 4.1 基本概念

第4章 类的继承

上一章,我们谈到了如何将现实中的概念映射为程序中的概念,我们谈了类以及类之间的组合,现实中的概念间还有一种非常重要的关系,就是分类。分类有个根,然后向下不断细化,形成一个层次分类体系,这种例子是非常多的。

1)在自然世界中,生物有动物和植物,动物有不同的科目,食肉动物、食草动物、杂食动物等,食肉动物有狼、豹、虎等,这些又细分为不同的种类。
2)打开电商网站,在显著位置一般都有分类列表,比如家用电器、服装,服装有女装、男装,男装有衬衫、牛仔裤等。

计算机程序经常使用类之间的继承关系来表示对象之间的分类关系。在继承关系中,有父类子类,比如动物类Animal和狗类Dog, Animal是父类,Dog是子类。父类也叫基类,子类也叫派生类。父类、子类是相对的,一个类B可能是类A的子类,但又是类C的父类。

之所以叫继承,是因为子类继承了父类的属性和行为,父类有的属性和行为子类都有。但子类可以增加子类特有的属性和行为,某些父类有的行为,子类的实现方式可能与父类也不完全一样。

使用继承一方面可以复用代码,公共的属性和行为可以放到父类中,而子类只需要关注子类特有的就可以了;另一方面,不同子类的对象可以更为方便地被统一处理。

本章详细介绍继承。我们先介绍继承的基本概念,然后详述继承的一些细节,理解了继承的用法之后,我们探讨继承实现的基本原理,最后讨论继承的注意事项,解释为什么说继承是把双刃剑,以及如何正确地使用继承。

4.1 基本概念

本节介绍Java中继承的基本概念,在Java中,所有类都有一个父类Object,我们先来看这个类,然后主要通过图形处理中的一些简单例子来介绍继承的基本概念。

4.1.1 根父类Object

在Java中,即使没有声明父类,也有一个隐含的父类,这个父类叫Object。Object没有定义属性,但定义了一些方法,如图4-1所示。

epub_923038_39

图4-1 类Object中的方法

本节我们会介绍toString()方法,其他方法我们会在后续章节中逐步介绍。toString()方法的目的是返回一个对象的文本描述,这个方法可以直接被所有类使用。

比如,对于我们上一章介绍的Point类,可以这样使用toString方法:

1
2
Point p = new Point(2,3);
System.out.println(p.toString());

输出类似这样:

1
Point@76f9aa66

这是什么意思呢?@之前是类名,@之后的内容是什么呢?我们来看下toString()方法的代码:

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

getClass().getName() 返回当前对象的类名,hashCode()返回一个对象的哈希值,哈希我们会在后续章节进一步介绍,这里可以理解为是一个整数,这个整数默认情况下,通常是对象的内存地址值,Integer.toHexString(hashCode())返回这个哈希值的十六进制表示。

为什么要这么写呢?写类名是可以理解的,表示对象的类型,而写哈希值则是不得已的,因为Object类并不知道具体对象的属性,不知道怎么用文本描述,但又需要区分不同对象,只能是写一个哈希值。

但子类是知道自己的属性的,子类可以重写父类的方法,以反映自己的不同实现。所谓重写,就是定义和父类一样的方法,并重新实现。

4.1.2 方法重写

上一章,我们介绍了一些图形处理类,其中有Point类,这次我们重写其toString()方法,如代码清单4-1所示。

代码清单4-1 Point类:重写toString()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public double distance(Point point){
return Math.sqrt(Math.pow(this.x-point.getX(),2)
+Math.pow(this.y-point.getY(), 2));
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public String toString() {
return "("+x+", "+y+")";
}
}

toString()方法前面有一个@Override,这表示toString()这个方法是重写的父类的方法,重写后的方法返回Point的x和y坐标的值。重写后,将调用子类的实现。比如,如下代码的输出就变成了(2,3)。

1
2
Point p = new Point(2,3);
System.out.println(p.toString());

4.1.3 图形类继承体系

接下来,我们以一些图形处理中的例子来进一步解释。先来看一些图形的例子,如图4-2所示。

这都是一些基本的图形,图形有线、正方形、三角形、圆形等,图形有不同的颜色。接下来,我们定义以下类来说明关于继承的一些概念:

  • 父类Shape,表示图形。
  • 类Circle,表示圆。
  • 类Line,表示直线。
  • 类ArrowLine,表示带箭头的直线。

1.图形

所有图形(Shape)都有一个表示颜色的属性,有一个表示绘制的方法,如代码清单4-2所示。

epub_923038_40

图4-2 一些图形的例子
代码清单4-2 类Shape
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Shape {
private static final String DEFAULT_COLOR = "black";
private String color;
public Shape() {
this(DEFAULT_COLOR);
}
public Shape(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public void draw(){
System.out.println("draw shape");
}
}

以上代码非常简单,实例变量color表示颜色,draw方法表示绘制,我们没有写实际的绘制代码,主要是演示继承关系。

2.圆

圆(Circle)继承自Shape,但包括了额外的属性:中心点和半径,以及额外的方法area,用于计算面积,另外,重写了draw方法,如代码清单4-3所示。

圆(Circle)继承自Shape,但包括了额外的属性:中心点和半径,以及额外的方法area,用于计算面积,另外,重写了draw方法,如代码清单4-3所示。

代码清单4-3 类Circle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Circle extends Shape {
//中心点
private Point center;
//半径
private double r;
public Circle(Point center, double r) {
this.center = center;
this.r = r;
}
@Override
public void draw() {
System.out.println("draw circle at " +center.toString()+" with r "+r
+", using color : "+getColor());
}
public double area(){
return Math.PI*r*r;
}
}

说明:

1)Java使用extends关键字表示继承关系,一个类最多只能有一个父类;
2)子类不能直接访问父类的私有属性和方法。比如,在Circle中,不能直接访问Shape的私有实例变量color;
3)除了私有的外,子类继承了父类的其他属性和方法。比如,在Circle的draw方法中,可以直接调用getColor()方法。

使用它的代码如下:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Point center = new Point(2,3);
//创建圆,赋值给circle
Circle circle = new Circle(center,2);
//调用draw方法,会执行Circle的draw方法
circle.draw();
//输出圆面积
System.out.println(circle.area());
}

程序的输出为:

1
2
draw circle at (2,3) with r 2.0, using color : black
12.566370614359172

这里比较奇怪的是,color是什么时候赋值的?在new的过程中,父类的构造方法也会执行,且会优先于子类执行。在这个例子中,父类Shape的默认构造方法会在子类Circle的构造方法之前执行。关于new过程的细节,我们会在4.3节进一步介绍。

3.直线

线(Line)继承自Shape,但有两个点,以及一个获取长度的方法,并重写了draw方法,如代码清单4-4所示。

代码清单4-4 类Line
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Line extends Shape {
private Point start;
private Point end;
public Line(Point start, Point end, String color) {
super(color);
this.start = start;
this.end = end;
}
public double length(){
return start.distance(end);
}
public Point getStart() {
return start;
}
public Point getEnd() {
return end;
}
@Override
public void draw() {
System.out.println("draw line from "
+ start.toString()+" to "+end.toString()
+ ", using color "+super.getColor());
}
}

这里我们要说明的是super这个关键字,super用于指代父类,可用于调用父类构造方法,访问父类方法和变量。

1)在Line构造方法中,super(color)表示调用父类的带color参数的构造方法。调用父类构造方法时,super必须放在第一行。
2)在draw方法中,super.getColor()表示调用父类的getColor方法,当然不写super.也是可以的,因为这个方法子类没有同名的,没有歧义,当有歧义的时候,通过super.可以明确表示调用父类的方法。
3)super同样可以引用父类非私有的变量。

4.带箭头直线

带箭头直线(ArrowLine)继承自Line,但多了两个属性,分别表示两端是否有箭头,也重写了draw方法,如代码清单4-5所示。

代码清单4-5 类ArrowLine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ArrowLine extends Line {
private boolean startArrow;
private boolean endArrow;
public ArrowLine(Point start, Point end, String color,
boolean startArrow, boolean endArrow) {
super(start, end, color);
this.startArrow = startArrow;
this.endArrow = endArrow;
}
@Override
public void draw() {
super.draw();
if(startArrow){
System.out.println("draw start arrow");
}
if(endArrow){
System.out.println("draw end arrow");
}
}
}

ArrowLine继承自Line,而Line继承自Shape, ArrowLine的对象也有Shape的属性和方法。

注意draw()方法的第一行,super.draw()表示调用父类的draw()方法,这时候不带super.是不行的,因为当前的方法也叫draw()。

需要说明的是,这里ArrowLine继承了Line,也可以直接在类Line里加上属性,而不需要单独设计一个类ArrowLine,这里主要是演示继承的层级性。

5.图形管理器

使用继承的一个好处是可以统一处理不同子类型的对象。比如,我们来看一个图形管理者类,它负责管理画板上的所有图形对象并负责绘制,在绘制代码中,只需要将每个对象当作Shape并调用draw方法就可以了,系统会自动执行子类的draw方法。如代码清单4-6所示。

代码清单4-6 图形管理器类ShapeManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ShapeManager {
private static final int MAX_NUM = 100;
private Shape[] shapes = new Shape[MAX_NUM];
private int shapeNum = 0;
public void addShape(Shape shape){
if(shapeNum<MAX_NUM){
shapes[shapeNum++] = shape;
}
}
public void draw(){
for(int i=0; i<shapeNum; i++){
shapes[i].draw();
}
}
}

ShapeManager使用一个数组保存所有的shape,在draw方法中调用每个shape的draw方法。ShapeManager并不知道每个shape具体的类型,也不关心,但可以调用到子类的draw方法。

我们来看下使用ShapeManager的一个例子:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
ShapeManager manager = new ShapeManager();
manager.addShape(new Circle(new Point(4,4),3));
manager.addShape(new Line(new Point(2,3), new Point(3,4), "green"));
manager.addShape(new ArrowLine(new Point(1,2),
new Point(5,5), "black", false, true));
manager.draw();
}

新建了三个shape,分别是一个圆、直线和带箭头的线,然后加到了shapemanager中,然后调用manager的draw方法。

需要说明的是,在addShape方法中,参数Shape shape,声明的类型是Shape,而实际的类型则分别是Circle、Line和ArrowLine。子类对象赋值给父类引用变量,这叫向上转型,转型就是转换类型,向上转型就是转换为父类类型。

变量shape可以引用任何Shape子类类型的对象,这叫多态,即一种类型的变量,可引用多种实际类型对象。这样,对于变量shape,它就有两个类型:类型Shape,我们称之为shape的静态类型;类型Circle/Line/ArrowLine,我们称之为shape的动态类型。在ShapeManager的draw方法中,shapes[i].draw()调用的是其对应动态类型的draw方法,这称之为方法的动态绑定

为什么要有多态和动态绑定呢?创建对象的代码(ShapeManager以外的代码)和操作对象的代码(ShapeManager本身的代码),经常不在一起,操作对象的代码往往只知道对象是某种父类型,也往往只需要知道它是某种父类型就可以了。

可以说,多态和动态绑定是计算机程序的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为。在4.3节,我们会进一步介绍动态绑定的实现原理。

4.1.4 小结

本节介绍了继承和多态的基本概念。

1)每个类有且只有一个父类,没有声明父类的,其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,以及重写父类的方法实现。
2)new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法。
3)子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。
4)子类对象可以赋值给父类引用变量,这叫多态;实际执行调用的是子类实现,这叫动态绑定。

继承和多态的基本概念是比较简单的,子类继承父类,自动拥有父类的属性和行为,并可扩展属性和行为,同时,可重写父类的方法以修改行为。但关于继承,还有很多细节,我们下一节继续讨论。