面向对象编程的重要知识:继承和多态。通过类的继承机制,可以使用已有的类为基础派生出新类,无需编写重复的程序代码,很好地实现程序代码复用。多态是面向对象编程中继封装和继承之后的另一大特征,它具体是指同一个行为具有多个不同表现形式或形态的能力。使用多态机制,可以提高程序的抽象程度和简洁性,最大程度地降低类和程序模块间的耦合性,并提高程序的可扩展性和可维护性。从Java语言的底层逻辑上看,封装和继承是为实现多态做准备的。
6.1 类的继承
继承描述了类的所属关系,多个类通过继承可形成一个关系体系,进而在原有类的基础上派生出新的类,扩展新的功能,从而实现了代码的复用。采用继承机制来设计软件系统中的类,可以提高程序的抽象程度,降低程序维护工作量,提高开发效率。
6.1.1 继承的概念
在现实生活中,“继承”是指一个对象直接使用另一对象的属性和方法,也可以指按照法律或遵照遗嘱接受死者的财产、职务、头衔、地位等。我们先利用现实生活中的例子来说明继承的含义。如图6.1所示,卡车(Truck类)和公交车(Bus类)都属于汽车(Car类),它们都有发动机(engine)和轮子(wheel),都可以行驶(run)和刹车(stop),但是卡车类新增了载重量(capacity)、拉货(load)和卸货(unload)方法,公共车类新增了载客量(capacity)、报站(busstop)和停靠(dock)方法。
在面向对象程序设计中,一个新类从已经存在的类中获得成员变量或成员方法,这种现象称为继承。提供继承信息的类被称为父类(超类、基类),得到继承信息的类被称为子类(派生类)。一个父类可以同时拥有多个子类,但Java语言不支持多重继承,所以一个类只能有一个父类。父类是所有子类的公共成员变量和成员方法的集合,而子类是父类的特例,子类继承父类的成员变量和成员方法,可以修改父类的成员变量或重写父类的方法,也可以增加新的成员变量或成员方法。采用继承机制来设计系统中的类,可以提高程序的抽象程度,更加接近于人类的思维方式。
继承机制的显著特点之一是可以实现代码复用。以第5章的Dog类为例,我们能够以它为样板而设计新的Cat类,代码如下:
1 public class Cat {
2 private String name;
3 private int age;
4 public void show(){
5 System.out.println("猫咪"+name+"在吃咸鱼");
6 }
7 }
仔细观察这段代码我们会发现,在Dog类和Cat类中有很多重复代码, 假设我们后续增加Pig类、Monkey类等类,重复代码还不断增加。这时候,我们就可以提取这些类的相同的成员变量和成员方法,设计一个Anima(动物类),使Dog类、Cat类等继承Animal类。
6.1.2 继承的使用
在Java语言中,类的继承通过extends关键字来实现,在定义类时通过extends关键字指出新定义类的父类,表示在两个类之间建立了继承关系。新定义的类称为子类,它可以从父类那里继承所有非private(私有)的成员。Java继承的语法格式如下:
class 子类A extends 父类B{
// 代码块
}
该语法表示子类A派生于父类B,如果类B又是某个类的子类,则类A同时也是该类的间接子类。如果没有extends关键字,则该类默认为java.lang.Object类的子类。Java语言中的所有类都是直接或间接地继承java.langObject类得到的,所以之前所有例子中的类均是java.lang.Object类的子类。
接下来,通过案例来演示如何通过继承Animal类来派生Dog类、Cat类,从而形成类的继承体现,实现代码复用,如例6-1所示。
例6-1 Demo0601.java
1 package com.aaa.p0601; // 需要先建立包
2
3 class Animal { // 定义父类Animal类
4 public String name;
5 public int age;
6 public void show() {
7 System.out.println("名字是" + name + ",年龄:" + age);
8 }
9 }
10 class Dog extends Animal { // 定义子类Dog类
11 String color;
12 public void eat(){
13 System.out.println(color + "色的狗狗在啃骨头");
14 }
15 }
16 class Cat extends Animal{ // 定义子类Cat类
17 }
18 public class Demo0601{
19 public static void main(String[] args) {
20 Dog d = new Dog();
21 d.name = "旺财";
22 d.age = 3;
23 d.color = "黑色";
24 d.show();
25 d.eat();
26 }
27 }
程序的运行结果如下:
名字是旺财,年龄:3
黑色的狗狗在啃骨头
例6-1中,Dog类通过extends关键字继承了Animal类,它就是Animal的子类,Cat类同样如此。从程序运行结果中可发现,Dog类虽然没有定义name、age成员变量和show()成员方法,但却能访问这些成员,说明子类可以继承父类所有的成员。
编程技巧: 使用继承时,先定义父类(超类、基类),再定义子类(派生类),很好地体现了面向对象的思想。
6.2 方法重写
在继承机制中,当父类中的方法无法满足子类需求或子类具有特有的功能的时候,就需要在子类中重写父类的方法(也称为方法覆盖)。准确地说,方法重写建立在继承关系之上,具体是指子类从父类中继承方法时,如果在子类中有定义名称、参数个数、参数类型均与父类中的方法完全一致,但方法内容不同,即子类修改了父类中方法的实现,此时创建的子类对象调用这个方法时,程序会调用子类的方法来执行,即子类的方法重写了从父类继承过来的同名方法。
当子类重写父类方法的时候,可以使用与父类相同的方法名及参数列表,也可以执行不同的功能。子类既可以隐藏和访问父类的方法,也可以覆盖继承父类的方法,体现了Java语言的优越性和灵活性。
接下来,通过案例来演示方法重写。以Animal为父类,Dog类、Cat类继承Animal类,Dog类重写了父类的show()方法,如例6-2所示。
例6-2 Demo0602.java
1 package com.aaa.p0602;
2
3 class Animal { // 定义父类Animal类
4 public String name;
5 public int age;
6 public Animal(){
7 System.out.println("调用了动物类的构造方法Animal()");
8 }
9 public void show() {
10 System.out.println("父类名字是" + name + ",年龄:" + age);
11 }
12 }
13 class Dog extends Animal { // 定义子类Dog类
14 String color;
15 public Dog(){
16 System.out.println("调用了狗类的构造方法Dog()");
17 }
18 public void eat(){
19 System.out.println(color + "色的狗狗在啃骨头");
20 }
21 public void show() {
22 System.out.println("狗狗名字是" + name + ",颜色是" + color);
23 }
24 }
25 class Cat extends Animal { // 定义子类Cat类
26 }
27 public class Demo0602{
28 public static void main(String[] args) {
29 Dog d = new Dog();
30 d.name = "旺财";
31 d.age = 2;
32 d.color = "黑色";
33 d.show();
34 d.eat();
35 Cat c = new Cat();
36 c.show();
37 }
38 }
程序的运行结果如下:
调用了动物类的构造方法Animal()
调用了狗类的构造方法Dog()
名字是旺财,颜色是黑色
黑色色的狗狗在啃骨头
调用了动物类的构造方法Animal()
父类名字是null,年龄:0
例6-2中,Dog类继承了Animal类的show()方法,但在子类Dog中对父类的show()进行了重写,Cat类只是继承了Animal类。从程序运行结果中可发现,第29行代码直接调用Dog()构造方法,理应输出“调用了狗类的构造方法Dog()”,但是先输出“调用了动物类的构造方法Animal()”,这是因为在执行子类的构造方法前,会首先调用父类中无参的构造方法,其目的是为了继承自父类的成员进行初始化操作。 第33行代码在调用Dog类对象的show()方法时,只会调用子类重写的方法,并不会调用父类的show()方法。Cat类中没有重写父类的show()方法,第36行代码中的Cat类对象仍然调用的是父类的show()方法,同时由于没有给Cat类对象的成员变量name和age赋值,所以显示的名字是默认值“null”,年龄也是默认值“