Java从入门到精通(微视频精编版)
上QQ阅读APP看书,第一时间看更新

第8章 类的继承与多态特性

视频讲解:1小时24分钟

继承、多态和封装是Java面向对象的3大特征,它们是面向对象程序开发的重要环节,如果在程序中使用得当,能将整个程序的架构变得非常有弹性,同时可以减少代码的冗余性,继承机制的使用可以重复用一些定义完好的类,减少重复代码的编写。多态机制可以动态调整对象的调用,降低对象之间的依存关系。同时为了优化继承与多态,一些类除了继承父类还使用接口的形式,Java中的类可以同时实现多个接口,接口被用来建立类与类之间关联的标准。正因为使用了这些机制使Java语言更具有生命力。

学习摘要:

 类的继承

 方法的重写与Super关键字

 修饰符

 封装的使用

 Object类

 抽象类与接口

8.1 类的继承

视频讲解

继承在面向对象开发思想中是一个非常重要的概念,通过继承可以使用以前定义的类的成员方法和成员变量,经过简单的程序编码就可以构建功能强大的新类。节约了很多编程时间,同时减少了出错的机会,也可以提高软件的可维护性、可扩展性。本堂课节将详细讲解类的继承。

继承可以理解为现实世界中的“是一种(is-a)”关系,例如轿车是一种汽车,而汽车又是一种交通工具,如图8.1所示。

图8.1 is-a关系

我们应用“is-a”关系可以通过已知事物去认识未知事物,从而提高认知新事物的效率。Java程序设计语言同样可以通过继承机制,在现有类的基础上去定义一个新的类,并在原有类的基础上添加新的方法或修改原有方法,从而提高编程效率、减少错误概率。

实现继承是通过extends关键字在声明类的时候指定其父类,其声明格式如下。

语法:[修饰符] class类名extends父类名

参数:修饰符是可选参数,它是权限修饰符,用于限定类的访问级别,extends父类名用于指定继承的父类,其中父类又可以叫作超类,它可以是任何可继承的类。例如:

对于这段代码,我们应该理解为SaloonCar类继承了Car类,所以Car是SaloonCar类的直接父类(或直接超类),而SaloonCar是Car类的直接子类。

【例8.1】 在项目中先分别创建Car类和SaloonCar类,在Car类中编写汽车类的部分变量与方法。SaloonCar类使用extends关键字继承Car类,不用编写类体代码,即是一个空类;然后编写Program类,在该类的主方法中创建SaloonCar类的实例对象,注意不是Car类而是空的SaloonCar类;最后设置该实例对象的属性,并执行相应的方法,看空的SaloonCar类是否继承并拥有父类的成员。(实例位置:资源包\源码\08\8.01)

(1)编写Car类,它将作为SaloonCar类的父类,即被继承的一方。

(2)使用Eclipse“新建Java类”向导,创建子类。在这个向导中指定包、类名的同时,还可以指定继承的超类(即父类)和实现的接口。如图8.2所示。而继承超类正是子类SaloonCar要实现的,所以使用向导可以直接为我们生成继承的代码。

图8.2 新建类向导窗口部分截图

向导生成的SaloonCar类代码如下,本例不做任何修改,因为我们要证明空的子类是否能继承父类的成员变量与成员方法。

package com.mrsoft;

public class SaloonCar extends Car {

}

建议读者使用代码输入方式把该步骤做几次练习。巩固继承的编写格式。

(3)编写程序的Program类,它是本实例的主类,在该类的主方法中,先创建SaloonCar类的实例对象,注意这里创建的不是父类Car的实例对象;然后设置该对象的属性,并执行相应的方法,而这些都是父类中定义的;最后运行程序,查看子类是否继承了这些成员变量与成员方法。

实例的运行效果如图8.3所示。

图8.3 实例运行效果

从实例的运行结果,可以看出,SaloonCar类虽然没有在类体中定义任何成员变量与成员方法,但是它还是从父类继承了它应该拥有的成员。

8.2 方法的重写与Super关键字

视频讲解

继承某个父类而生成新的子类,不但拥有父类的变量与方法,我们还可以为子类添加新的成员变量和成员方法,增强父类的功能,即所谓的扩展。甚至还可以在子类中为父类的某个方法定义多个重载方法,增加该类的灵活性。但是有时候,父类的方法并不能完全适应于子类,或者子类需要有不同的行为。例如,父类的drive()方法是输出开车的描述信息,而子类有可能要添加更多的功能,或者要丰富drive()方法,在开车的时候要检测油箱的油量是否充足,如果油箱是空的,那么是无法开车的。

这种情况下,子类可以重写父类的某个方法,或者说是覆盖父类的某个方法。只要在子类中定义与父类相同的方法即可。但是要注意方法的声明一定要和父类的方法声明一样,或者把父类某方法的声明代码直接复制过来,去掉方法体,重新编写业务代码。

例如在子类SaloonCar中重写父类的drive()方法。

另外,还可以使用super关键字引用父类的方法,然后再添加新的业务代码。例如:

上面段代码重写了父类的drive()方法,但是在oil变量大于0的时候,又需要执行父类的drive()方法,所以使用了super关键字去访问父类的drive()方法,这样,子类就不必关心父类的drive()方法是如何实现的,这段代码等价于如下。

【例8.2】 修改实例8.01,在空的SaloonCar类中重写父类的drive()方法。然后再次运行程序。(实例位置:资源包\源码\08\8.02)

(1)在Java编辑器中,把光标定位在类体中,右击,选择“源码”→“覆盖/实现方法”命令,如图8.4所示。

图8.4 编辑器中的弹出菜单

(2)将弹出的“覆盖/实现方法”对话框中列出了父类的方法,选择我们要重写的父类方法,然后选择一个插入点,并选中“生成方法注释”复选框,如图8.5所示。单击“确定”按钮。

图8.5 覆盖/实现方法选项卡

(3)重写父类的drive()方法,并在SaloonCar类中添加成员变量oil,它在drive()方法中用于判断油量多少。

还可以在子类的构造方法中,使用super关键字执行父类的构造方法,其语法格式如下。

语法:super([构造参数列表])

参数:构造参数列表是可选参数,根据调用构造方法的形参声明来填充。例如:

Car类没有定义默认的构造方法,编译器会自动为它创建一个隐藏的无参数的默认构造方法,这段代码在子类的构造方法中重写了这个构造方法。

8.3 修饰符

视频讲解

Java中定义了private、public、protected和默认的权限修饰符,这些修饰符控制着对类和类的成员变量以及成员方法的访问规则。另外,还可以辅助static和final关键字定义特殊规则。本堂课将介绍这4种权限修饰符和它们的访问权限。

表8.1中描述了修饰符的访问权限。

表8.1 Java语言中的修饰符

8.3.1 public修饰符

public是公有类型的权限修饰符,即使用public修饰的类、成员变量和成员方法,其他类都可以访问,包括任意包中的任意类、以及子类。

例如,之前章节的所有实例都使用到了main()主方法,该方法的权限修饰符就使用了public,其声明代码如下。

public static void main(String[] args)

该方法必须声明为public权限,因为这个方法必须能够被虚拟机访问以执行程序。

之前的实例中所有成员变量和成员方法都是使用public修饰符定义的,这里不再以新的实例做介绍。

8.3.2 private修饰符

private是私有权限修饰符,只有本类,即定义private私有成员的类,能够访问,对于其他方式的访问,它都会拒绝“不要碰我”。

如果一个类的成员变量或成员方法被修饰为private,该成员变量只能在本类中被使用,在子类中不可见,并且对其他包的类也不可见。

【例8.3】 定义一个Superclass类,在该类中定义private修饰的私有方法print()。然后创建子类与其他类进行访问,并把鼠标停留在有错误的代码位置,查看Eclipse的Java编译器提示的错误信息。(实例位置:资源包\源码\08\8.03)

(1)首先创建Superclass类。

(2)再创建SubClass子类,在该类中访问超类的print()方法,Eclipse的Java编辑器提示错误,如图8.6所示。

图8.6 子类访问超类private成员

(3)再创建OtherClass类,在该类中创建Superclass类的实例对象,并访问该对象的print()方法,Eclipse的Java编辑器提示错误,如图8.7所示。

图8.7 其他类访问类中private成员

8.3.3 protected修饰符

protected是保护级别的权限修饰符,它保护成员不会被其他包或非子类访问。即protected修饰的成员只能被子类(可以不是直接子类,即间接继承的子类也可以)和同一个包中定义的其他类访问。

【例8.4】 定义一个Superclass类,在该类中定义protected修饰的方法print()。然后创建子类与其他类进行访问,并把鼠标光标停留在有错误的代码位置,查看Eclipse的Java编译器提示的错误信息,或者查看正确的运行结果。(实例位置:资源包\源码\08\8.04)

(1)首先创建Superclass类。

(2)再创建SubClass子类,它继承Superclass类,在该类中访问超类的print()方法。

实例的运行效果如图8.8所示。

图8.8 实例运行效果

(3)再创建OtherClass类,在该类中创建Superclass类的实例对象,并访问该对象的print()方法。

实例的运行效果如图8.9所示。

图8.9 实例运行效果

(4)在其他包中创建一个OtherPackageClass类,在该类中创建Superclass类的实例对象,并访问该对象的print()方法,Eclipse的Java编辑器提示错误,如图8.10所示。

图8.10 其他包的类访问Superclass类成员的错误信息

8.3.4 默认权限修饰符

当不添加任何权限修饰符时,编译器会使用默认的修饰符,该修饰符的权限级别与protected类似,不同之处在于,在其他包定义的子类无法访问父类默认权限修饰的成员。

【例8.5】 定义一个Superclass类,在该类中定义没有修饰符的print()方法。然后在其他包中创建该类的子类,在子类中访问父类的print()方法,并把鼠标停留在有错误的代码位置,查看Eclipse的Java编译器提示的错误信息,或者查看运行结果。(实例位置:资源包\源码\08\8.05)

(1)首先创建Superclass类。注意包的位置是com.mrsoft。

(2)再创建SubClass子类,它继承Superclass类,并且定义在与父类不同的包中。在该类中访问超类的print()方法。Eclipse的Java编辑器提示错误,如图8.11所示。

图8.11 不同包的子类不能访问父类默认权限修饰的成员

8.4 封装

视频讲解

Java是面向对象的编程语言,而继承、多态与封装是面向对象的3大特性,本节将介绍的封装是指把对象的属性隐藏在对象的内部,不允许外部直接访问和修改,例如之前的实例就直接使用“whiteCar.color = "红色";”之类的代码直接修改color属性的值,我们应该避免这种情况,而使用该类定义的其他方法来实现内部属性的访问与修改。Java规范中的设置器与访问器就是实现封装的标准方法,它们用于获取和设置对象的属性值。

8.4.1 把属性隐藏

要实现封装,第一步就是设置类的成员变量(即对象的属性)使用private修饰符,使其他类无法直接访问该成员变量,以防止外部直接访问和修改。例如:

8.4.2 定义设置器

把成员变量设置为private私有权限之后,其他类就不能访问,必须通过本类定义的设置器方法来设置和修改该成员变量的值,设置器方法的命名一般以set作为前缀,属性名做后缀,setColor()方法从名称上就能够理解,它用于设置color属性值。例如:

public是方法的权限修饰符,对于设置器来说,它需要暴露给其他类,使其可以访问,所以使用public修饰符。参数列表定义了String类型的color参数,在方法体中为区别参数color与成员变量color,使用了this关键字引用成员变量,并赋值为color参数的值,也可以使用与成员变量不同名称的参数,上述代码为了演示重名情况而使用了this关键字。

以后设置对象的color属性就可以使用setColor()方法完成。例如:

说明

如果不按规范做的话,可以定义任何方法实现属性的设置与修改,但是建议大家采用统一的规范编程,使用设置器实现属性的修改,增加程序的可读性。

8.4.3 定义访问器

访问器和设置器一样,因为成员变量被设置为private私有权限,相对于其他类就隐藏了这个成员变量,所以要使用访问器方法读取对象的属性值,访问器以get或is作为方法名称的前缀,以属性名作为后缀。例如color属性的访问器方法代码如下。

对于boolean类型的属性,应该使用is前缀定义访问器方法,例如running属性访问器方法的代码如下。

注意

请注意is和get作为前缀的访问器的用法,它们分别用于布尔类型和其他类型属性的访问。

8.4.4 使用Eclipse完成封装

Eclipse拥有最强的代码辅助功能,使用向导可以自动为选择的成员变量添加设置器与访问器(或者说get/set方法),具体步骤如下。

【例8.6】 定义Car类,在该类中定义字符串color和布尔类型的running两个成员变量,并设置它们的访问器与设置器方法。(实例位置:资源包\源码\08\8.06)

(1)在该类的编辑器中,右击,选择“源码”→“生成Getter和Setter”命令,或者先按Alt+Shift+S快捷键不释放,再按R键。将弹出“生成Getter和Setter”对话框,该对话框就是生成访问器与设置器的向导。在对话框中首先需要选择要创建的访问器与设置器,也可以单击右侧的“全部选中”按钮选择生成所有成员变量的访问器与设置器方法;然后需要选择生成方法的插入点和修饰符;最后选择是否添加方法注释,如图8.12所示。

如果没有特殊要求,使用默认值生成访问器与设置器便可,笔者只单击“全部选中”按钮和“确定”按钮就完成了访问器与设置器代码的生成。生成的程序代码如下所示,我们可以完善程序注释,或修改代码。

图8.12 访问器与设置器生成向导

可以不修改英文注释,但是本书为方便初学者,对生成的代码添加中文注释,最终修改完成的程序代码如下。

(2)编写实例的主类,演示访问器与设置器的使用。

实例运行结果如图8.13所示。

图8.13 实例运行效果

8.5 Object类

视频讲解

在Java中,所有的类都直接或间接继承了java.lang.Object类。Object类是比较特殊的类,它是所有类的父类,即Java语言中任何一个类都是Object类的子类。当创建一个类时,如果没有使用extends关键字继承指定的类,那么编译器总是默认直接继承Object类,如果指定继承其他类,那么也会间接继承Object类。

例如String、Integer等类都是继承Object类,除此之外自定义的类也都继承Object类,由于所有类都是Object子类,所以在定义类时,省略了extends Object关键字,图8.14中描述了这一原则。

图8.14 定义类时可以省略extends Object关键字

在Object类中主要包括clone()、finalize()、euqals()、toString()等方法。其中常用的两个方法为equals()和toString()方法。由于所有的类都是Object类的子类,所以任何类都可以重写Object类中的方法。

注意

Object类中的getClass()、notify()、notifyAll()、wait()等方法不能被重写,因为这些方法被定义为final类型。

8.5.1 equals方法

equals方法是在Object类中定义的,用于判断两个对象是否相同的方法,大部分子类都重写了该方法,用于比较指定类型的对象,例如String、Integer等都重写了该方法。如果Object的子类没有重写equals()方法,该方法将默认使用“==”运算符判断两个对象。

在Object类中equals()方法的原始定义代码如下。

【例8.7】 创建自定义的类TempClass,用于测试,再创建TestEquals类,在该方法中创建String类的两个实例对象s1和s2,再创建TempClass类的两个实例对象v1和v2,分别输出s1与s2、v1与v2的equals()方法调用结果。(实例位置:资源包\源码\08\8.07)

(1)创建TempClass类,它是一个空类,以默认方式继承Object,而且没有重写equals方法。

package com.mrsoft;
public class TempClass {
}

(2)创建TestEquals类,该类拥有主方法。

在Eclipse中运行本实例,运行结果如图8.15所示。

图8.15 使用equals()方法比较两个对象

从本实例结果可以看出,在自定义的类中使用equals()方法进行比较时,将返回false,因为equals()方法的默认实现是使用“==”运算符比较两个对象的引用地址,而不是比较对象的内容,所以要想真正做到比较两个对象的内容,需要在自定义类中重写equals()方法。

8.5.2 toString()方法

Object类中toString()方法的功能将一个对象转换为字符串形式,当一个对象参与字符串的“+”连接符操作时,也会调用该方法把对象转换成字符串,然后再和另一个字符串操作数连接。toString()方法会返回一个String实例,在实际的应用中通常重写toString()方法,为对象提供一个特定的输出模式。当这个类转换为字符串或与字符串连接时,将自动调用重写的toString()方法。

【例8.8】 在项目中创建ObjectInstance类,在类中重写Object类的toString()方法,并在主方法中输出该类的实例对象。(实例位置:资源包\源码\08\8.08)

在Eclipse中运行本实例,运行结果如图8.16所示。

图8.16 在类中重写toString()方法

在本实例中重写父类Object类的toString()方法,定义一段输出字符串,当用户打印ObjectInstance类对象时,将自动调用toString()方法。

8.6 实战

视频讲解

8.6.1 抽象类实现几何面积计算

对于每个几何图形而言,都有一些共同的属性,例如名字、面积等,而其计算面积的方法却各不相同。为了简化开发,可以定义一个超类来实现输出名字的方法,并使用抽象方法来计算面积,运行效果如图8.17所示。(实例位置:资源包\源码\08\实战\01)

图8.17 实例运行效果

8.6.2 通过接口实现多态

本实例将通过接口来完成多态数组的应用。实例中创建了DrawTest接口,并创建该接口的两个实现类Parallelogram平行四边形类与Square正方形类。然后在主方法中分别调用这两个类的draw()方法,运行结果如图8.18所示。(实例位置:资源包\源码\08\实战\02)

图8.18 多态与接口结合

8.6.3 简单的汽车销售商场

当顾客在商场购物时,卖家需要根据顾客的需求提取商品。对于汽车销售商场也是如此。用户需要先指定购买的车型,然后商家去提取该车型的汽车。本实例将实现一个简单的汽车销售商场,用来演示多态的用法,运行效果如图8.19所示。(实例位置:资源包\源码\08\实战\03)

图8.19 实例运行效果

8.6.4 重新定义对象的等式判断

由于生命的复杂性,寻找两只完全相同的宠物是不可能的。在Java语言中,却简单很多。可以通过比较感兴趣的属性来判断两个对象是否相同。本实例将创建3个宠物猫对象,通过比较它们的名字、年龄、重量和颜色属性来看它们是否相同,运行效果如图8.20所示。(实例位置:资源包\源码\08\实战\04)

图8.20 实例运行效果

8.6.5 重新计算对象的哈希码

Java中创建的对象是保存在堆中的,为了提高查找的速度而使用了散列查找。散列查找的基本思想是定义一个键来映射对象所在的内存地址。当需要查找对象时,直接查找键即可。这样就不用遍历整个堆来查找对象。本实例将重新定义不同对象的散列值(即哈希码),运行效果如图8.21所示。(实例位置:资源包\源码\08实战\05)

图8.21 实例运行效果