目录
指导——修改Pet类为抽象类,强迫子类实现print()方法... 64
1.final修饰引用型变量,变量所指对象的属性值是否能改变... 65
2.abstract是否可以和private、static、final共用... 66
3.4 综合练习:使用多态完善汽车租赁系统计价功能... 95
阶段1:指导——设计猫和鸭的类结构,画出类图并写出代码... 146
阶段2:指导——增加新成员海豚,重新设计类结构... 147
任务二:制作PPT,讲解面向对象设计的基本原则... 152
8.2.3 try-catch-finally块... 163
练习——使用Iterator迭代显示存储在List中的企鹅信息... 203
10.2.2 使用JDBC-ODBC桥方式连接数据库... 216
练习——使用纯Java方式连接数据库,并进行异常处理... 220
10.3 Statement接口和ResultSet接口... 220
10.3.3 使用Statement和ResultSet查询所有宠物... 224
10.4 PreparedStatement接口... 228
10.4.1 为什么要使用PreparedStatement 228
10.4.2 使用PreparedStatement更新宠物信息... 230
指导——使用PreparedStatement插入宠物信息... 232
指导——主人登录成功后,显示主人的所有宠物信息... 275
12.4.2 使用PL/SQL Developer导入导出数据... 286
练习——定义MasterDao接口和MasterDaoJabcOracleImpl实现类... 299
指导——根据DTD定义编写XML文档,存放宠物初始信息... 323
指导——使用DOM解析存储宠物初始信息的XML文档... 324
阶段3:指导——从数据库读取新闻信息,保存在泛型集合中... 353
第1章
抽象和封装
◇本章工作任务
Ø 用类图描述电子宠物系统的设计
Ø 编写代码实现领养宠物功能
◇本章技能目标
Ø 使用类图描述设计
Ø 掌握面向对象设计的基本步骤
Ø 掌握类和对象的概念
Ø 掌握构造方法及其重载
Ø 掌握封装的概念及其使用
本章单词
请在预习时学会下列单词的含义和发音,并填写在横线处。
- class:_____________________________
- object:____________________________
- static:_____________________________
- final:______________________________
- private:____________________________
- public:_____________________________
- protect:____________________________
- overloading:________________________
- constructor:________________________
- encapsulation:______________________
相关课程回顾
在学习《使用Java实现面向对象编程》这门课程之前,我们先一起来回顾与这门课程密切相关的课程:《使用Java语言理解程序逻辑》和《深入.ENT平台和C#编程》。
在《使用Java语言理解程序逻辑》中我们学习了一下内容。
Ø Java的基本数据类型以及各种运算符。
Ø 各种程序逻辑控制语句。
Ø 对象和类的区别与联系。
Ø 定义类的方法。
Ø Java中的数组和字符串。
在《深入.ENT平台和C#编程》中我们学习了以下内容。
Ø 类和对象的定义、区别和联系。
Ø 使用集合组织相关数据。
Ø 面向对象的三个特征:封装、继承和多态。
Ø 文件读写和XML。
Ø 序列化和反序列化。
在《使用Java语言理解程序逻辑》中我们掌握了Java语言的一些基础知识,并且掌握了如何运用Java语言实现各种程序逻辑控制,这为本门课程的学习打下了良好的基础。在《深入.ENT平台和C#编程》中我们学习了面向对象的基本思想、基本概念、集合与文件的操作等,与我们这门课的许多内容都是对应的,学习过程中要特别注意C#与Java中同一技能在概念和语法上的不同,通过对比可以更牢固地掌握。
就业技能结构图
本门课程对应的就业技能结构图如图1.1所示。
图1.1 Java面向对象技术就业技能结构图
图1.1展示了本门课程要学习的主要技能。通过学习不但需要掌握面向对象的封装性、继承性和多态性在Java中的体现,掌握面向对象中另一重要概念——接口。还需要进一步提高灵活运用面向对象技术解决问题的能力,有了扎实的面向对象基础后,我们将继续学习Java的集合类型、异常、使用JDBC操作数据库、XML和文件操作,这些内容的学习将为进一步的JSP技术和框架技术学习做好准备。
本章简介
学习面向对象,理解其中的概念只是前提,灵活应用才是目的。在本门课程中,我们将通过一个电子宠物系统的设计和开发来展示面向对象的魅力。该案例贯穿书中大多数章节,让我们在完成案例的过程中轻松学会技能,深刻体会技能的应用场合,切实提高开发水平,缩短从技能到应用转化的时间。
本章是本门课程的第1章,首先学习面向对象设计的过程,也就是从现实世界中抽象出程序开发中的类,实现从现实到虚拟的转化;然后对抽象出的类进行操作,实现对现实世界中行为的模拟;第三部分对抽象出的类进行优化,通过封装隐藏类内部的信息以提高安全性;最后通过综合练习来巩固所学的技能。
1.1用面向对象设计电子宠物系统
1.1.1为什么使用面向对象
现实世界就是”面向对象的”。现实世界中的任何事物都可以看作是”对象”,比如人、建筑、交通工具、学习用品等。而事物都有自己的属性和行为。比如人,它具有各种属性:姓名、性别、身高、体重、籍贯等,还可以做很多事情:吃饭、睡觉、劳动、锻炼等。各个事物之间还会发生各种联系,人用木材可以做成一套家具,人用笔可以写出文章等。
面向对象就是采用”现实模拟”的方法设计和开发程序。计算机软件开发规模越来越大,解决的问题也越来越复杂,导致软件开发时间、软件开发成本、软件维护费用甚至软件开发质量等日益难以控制。而面向对象技术利用”面向对象的思想”去描述”面向对象的世界”,实现了虚拟世界和现实世界的一致性,符合人们的思维习惯,使得客户和软件设计开发人员之间,软件设计开发人员内部交流更加顺畅,同时还带来了代码重用性高、可靠性高等优点,大大提高了软件尤其是大型软件的设计和开发效率。
——问答———————————————————————————————————
问题:面向过程和面向对象有什么区别?
解答:我们要举办一次南京的联欢晚会。如果采用面向过程实现的话,就是全体人员合唱某某之歌→主持人宣布晚会开始→领导讲话→主持人过场→演员一表演→主持人过场→演员二表演······→最后一位演员表演→主持人宣布晚会结束,即从头至尾、自上而下的实现功能。而如果采用面向对象实现的话,首先分析晚会需要
———————————————————————————————————————
——问答———————————————————————————————————
哪些类:领导、主持人和演员。然后分析各种类的行为:主持人有宣布晚会开始、过场、宣布晚会结束,当然也有唱某某之歌。领导有讲话、唱某某之歌。演员主要就是表演节目,也有唱某某之歌。然后就利用设计好的类创建对应对象,调用相应方法(行为)来逐步进行晚会。
面向过程的核心概念是函数,以功能为中心,实现了函数级别的代码重用。面向对象的核心概念是封装了属性和方法(行为)的类,以数据为中心,实现了类级别的代码重用。面向对象因为采用了类,具有继承和多态特性,可以进一步重用代码和简化编程,而面向过程中没有继承和多态特性。
———————————————————————————————————————
1.1.2使用面向对象进行设计
下面就开始电子宠物系统的设计和开发之路吧,这一章的任务是用类来描述宠物,然后实现领养宠物功能。首先需要根据需求进行面向对象的设计。
——问答———————————————————————————————————
我们要设计一个电子宠物系统,其中领养宠物功能的详细需求如下。
Ø 根据控制台提示,输入领养宠物的昵称。
Ø 根据控制台提示,选择领养宠物的类型,有两种选择:狗狗和企鹅。
Ø 如果类型选择狗狗,要选择狗狗的品种,有两种选择:”聪明的拉布拉多犬”或者”酷酷的雪娜瑞”。
Ø 如果类型选择企鹅,要选择企鹅的性别:”Q仔”或”Q妹”。
Ø 所领养宠物的健康值默认是100,表示非常健康。
Ø 所领养宠物和主任的亲密度默认是0,表示和主人还不熟悉。
Ø 在控制台打印出宠物信息,包括昵称、健康值、亲密度、品种或性别,表示领养成功。
如何依据需求,使用面向对象思想来设计我们的电子宠物系统呢?
———————————————————————————————————————
——分析———————————————————————————————————
面向对象设计的过程就是抽象的过程,我们分三步来完成。
第一步:发现类。
第二步:发现来的属性。
第三步:发现类的方法。
———————————————————————————————————————
面向对象设计的过程就是抽象的过程,根据业务需求,关注与业务相关的属性和行为,忽略不必要的属性和行为,由现实世界中”对象”抽象出软件开发中的”对象”,如图1.2所示。
抽象 |
图1.2 面向对象设计的过程就是抽象的过程
接下来我们就按照发现类、发现类的属性和发现类的方法的步骤完成设计。
我们可以通过在需求中找出名词的方式确定类和属性,找出动词的方式确定方法。并根据需要实现业务的相关程度进行筛选。
第一步:发现类。
需求中名词有控制台、宠物、昵称、狗狗、企鹅、类型、品种、聪明的拉布拉多犬、酷酷的雪娜瑞、性别、Q仔、Q妹、健康值、亲密度和主人等。
根据仔细筛选,发现可以作为类的名词有宠物、狗狗、企鹅和主人。本章要实现领养宠物功能,主要用到两个类:狗狗(Dog)和企鹅(Penguin)。宠物和主人在完善设计和增加功能时再使用。
第二步:发现类的属性
需求中动词主要有输入、选择、领养、打印等。某些明显与设计无关、不重要的词语可以直接忽略。
通过仔细筛选,发现可作为属性的名词有昵称、健康值、亲密度、品种和性别,还有一些名词是作为属性值存在的,例如聪明的拉布拉多犬、酷酷的雪娜瑞是品种的属性值,Q仔和Q妹是性别的属性值。
根据需求,定义狗狗类的属性有昵称(name)、健康值(health)、亲密度(love)和品种(strain)。企鹅类的属性有昵称(name)、健康值(health)、亲密度(love)和性别(sex)。狗狗和企鹅的某些属性,例如年龄、体重、颜色等与领养业务需求无关,不予设置。
第三步:发现类的方法。
通过仔细筛选,发现类的方法主要是打印宠物信息。狗狗和企鹅的方法主要就是打印出自己的信息,取名为print()。至于狗狗睡觉、洗澡等行为,企鹅吃饭、游泳等行为,与领养业务需求无关,现在先不为其设定方法,在后续业务中若有需求再添加。
设计是一个逐步调整、完善的过程,类图是面向对象设计的”图纸”、使用”图纸”进行设计方便沟通和修改。将设计的结果通过类图来表示,如图1.3和图1.4所示。
图1.3 Dog类图 图1.4 Penguin类图
——小结———————————————————————————————————
抽象时遵循的原则。
Ø 属性、方法的设置是为了解决业务问题的。
Ø 关注主要属性、方法。
Ø 如果有必要,勿增加额外的类、属性与方法。
———————————————————————————————————————
1.2通过创建对象实现领养宠物功能
1.2.1创建类的对象
已经设计出了类及其属性和方法,下面需要把类图表示的内容转变为Java的类代码。
狗狗类的代码如示例1所示。
示例1
/**
* 宠物狗狗类。
* @author 南京
*/
public class Dog {
String name = "无名氏"; // 昵称,默认值是"无名氏"
int health = 100; // 健康值,,默认值是100
int love = 0; // 亲密度
String strain = "聪明的拉布拉多犬"; // 品种
/**
* 输出狗狗的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name +
",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",我是一只 " + this.strain + "。");
}
}
企鹅类的代码如示例2所示。
示例2
/**
* 宠物企鹅。
* @author 南京
*/
public class Penguin {
String name = "无名氏"; // 昵称
int health = 100; // 健康值
int love = 0; // 亲密度
String sex = "Q仔"; // 性别
/**
* 输出企鹅的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name +
",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",性别是 " + this.sex + "。");
}
}
从示例1和示例2中我们学习了类的基本结构,其主要由属性和行为组成,称为类的成员变量(或者成员属性)和成员方法,统称为类的成员(除此之外,类的成员还包括构造方法、代码块等)。
——问题———————————————————————————————————
已经有了狗狗和企鹅的类,如何领养宠物呢?
———————————————————————————————————————
——分析———————————————————————————————————
领养宠物的步骤如下。
Ø 根据控制台提示输入宠物的类型、昵称等内容。
Ø 根据输入内容创建相应的宠物对象。
Ø 打印出宠物信息表,示领养成功。
———————————————————————————————————————
通过测试类来创建具体的宠物对象并输出信息,如示例3所示。
示例3
import java.util.Scanner;
/**
* 领养宠物。
* @author 南京
*/
public class Test {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("欢迎您来到宠物店!");
// 1、 输入宠物名称
System.out.print("请输入要领养宠物的名字:");
String name = input.next();
// 2、 选择宠物类型
System.out.print("请选择要领养的宠物类型:(1、狗狗 2、企鹅)");
switch (input.nextInt()) {
case 1:
// 2.1、如果是狗狗
// 2.1.1、选择狗狗品种
System.out.print("请选择狗狗的品种:(1、聪明的拉布拉多犬" +
" 2、酷酷的雪娜瑞)");
String strain = null;
if (input.nextInt() == 1) {
strain = "聪明的拉布拉多犬";
} else {
strain = "酷酷的雪娜瑞";
}
// 2.1.2、创建狗狗对象并赋值
Dog dog = new Dog();
dog.name = name;
dog.strain = strain;
// 2.1.3、输出狗狗信息
dog.print();
break;
case 2:
// 2.2、如果是企鹅
// 2.2.1、选择企鹅性别
System.out.print("请选择企鹅的性别:(1、Q仔 2、Q妹)");
String sex = null;
if (input.nextInt() == 1)
sex = "Q仔";
else
sex = "Q妹";
// 2.2.2、创建企鹅对象并赋值
Penguin pgn = new Penguin();
pgn.name = name;
pgn.sex = sex;
// 2.2.3、输出企鹅信息
pgn.print();
}
}
}
运行结果如图1.5和图1.6所示。
图1.5 领养狗狗运行结果
图1.6 领养企鹅运行结果
从示例3中我们学习了Java中对象的创建和成员的调用方法,语法和C#中是相同的。
Ø 通过构造方法来创建对象,例如”Penguin p=new Penguin();”。
Ø 通过对象名、属性名的方式调用属性,例如”p.name=“qq”;”。
Ø 通过对象名、方法名的方式调用方法,例如”p.print();”。
类(Class)和对象(Object)是面向对象中的两个核心概念。类是对某一类事物的描述,是抽象的、概念上的定义。对象是实际存在的该事物的个体,是具体的、现实的。类和对象就好比模具和铸件的关系,建筑物图纸和建筑物实物的关系。我们可以由一个类创建多个对象。
示例1是一个Dog类的代码,示例2是一个Penguin类的代码。但是如果要实现我们的需求,只有类是不行的,还需要创建对应类的示例,也就是对象。在示例3中我们根据输入的数据创建了宠物对象并输出宠物信息。
——规范———————————————————————————————————
类名、属性名、方法名以及常量名的命名规范如下。
Ø 类名由一个或几个单词组成,每个单词的第一个字母大写,如Dog、StringBuffer。
Ø 属性名和方法名由一个或几个单词组成,第一个单词首字母小写,其他单词首字母大写,例如health、stuName、println()、getMessage()。
Ø 常量名由一个或几个单词组成,所有字母大写,如PI、SEX_MALE。
———————————————————————————————————————
——问题———————————————————————————————————
如果我们创建了很多企鹅对象,它们的性别分别取值为”Q仔”或”Q妹”,但是后来要求变化,规定企鹅的性别只能取值”雄”或”雌”,此时已创建的每个企鹅对象的性别都要做相应修改,修改量很大,且代码可能分散在多个文件,不易查找,有没有更好地解决办法呢?
———————————————————————————————————————
——分析———————————————————————————————————
可以定义两个常量SEX_MALE和SEX_FEMALE,分别取值为”Q仔”和”Q妹”,在给企鹅赋值时直接将常量名SEX_MALE或SEX_FEMALE赋给sex属性。
如果以后要修改sex为”雄”或”雌”时,不管已创建了多少个对象,只需要修改两个常量的值就可以了,这样就方便了很多。
Ø final String SEX_MALE=“Q仔”: SEX_MALE是常量,值只能是”Q仔”,但是必须在创建对象后,通过对象名,SEX_MALE方式使用,很不方便。
Ø static final String SEX_MALE=“Q仔”: SEX_MALE是常量,值只能是”Q仔”,可以再创建对象后,通过对象名,SEX_MALE方式,也可以直接通过类名. SEX_MALE方式使用,建议采用此种方式。
———————————————————————————————————————
给企鹅添加两个静态常量SEX_MALE和SEX_FEMALE,如示例4所示。
示例4
/**
* 宠物企鹅类,使用静态常量。
*/
public class Penguin {
String name = "无名氏"; // 昵称
int health = 100; // 健康值
int love = 0; // 亲密度
static final String SEX_MALE ="Q仔";
static final String SEX_FEMALE="Q妹";
//static final String SEX_MALE = "雄";
//static final String SEX_FEMALE = "雌";
String sex = SEX_MALE; // 性别
/**
* 输出企鹅的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name
+ ",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",性别是 " + this.sex + "。");
}
}
编写测试类,创建三个企鹅对象并对其性别赋值,如示例5所示。
示例5
/**
* 测试静态常量的使用。
* @author 南京
*/
public class Test {
public static void main(String[] args) {
Penguin pgn = null;
pgn = new Penguin();
System.out.println("第一个企鹅的性别是" + pgn.sex + "。");
pgn = new Penguin();
pgn.sex = Penguin.SEX_FEMALE;
System.out.println("第二个企鹅的性别是" + pgn.sex + "。");
pgn = new Penguin();
pgn.sex = Penguin.SEX_MALE;
System.out.println("第三个企鹅的性别是" + pgn.sex + "。");
}
}
运行结果如图1.7所示。
图1.7 输出企鹅的性别
如图要改变企鹅的性别取值为”雄”和”雌”,只需要修改Penguin类中两个常量的值即可,如示例6所示,而创建对象的类如示例5所示,不用做任何修改。
示例6
/**
* 宠物企鹅类,使用静态常量。
*/
public class Penguin {
String name = "无名氏"; // 昵称
int health = 100; // 健康值
int love = 0; // 亲密度
// static final String SEX_MALE ="Q仔";
// static final String SEX_FEMALE="Q妹";
static final String SEX_MALE = "雄";
static final String SEX_FEMALE = "雌";
String sex = SEX_MALE; // 性别
/**
* 输出企鹅的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name
+ ",健康值是" + this.health+ ",和主人的亲密度是"
+ this.love + ",性别是 " + this.sex + "。");
}
}
再次运行示例5,运行结果如图1.8所示。
图1.8 修改常量值后运行结果
static可以用来修饰属性、方法和代码块。static修饰的变量属于这个类所有,即由这个类创建的所有对象公用同一个static变量。通常把static修饰的属性和方法称为类属性(类变量)、类方法。不使用static修饰的属性和方法,属于单个对象,通常称为示例属性(示例变量)、实例方法。
类属性、类方法可以通过类名和对象名访问,实例属性、实例方法只能通过对象名访问。
final可以用来修饰属性、方法和类。用final修饰的变量成为常量,其值固定不变。关于final的具体内容会在第二章详细讲解。
1.2.2构造方法及其重载
——问题———————————————————————————————————
在示例3中是先创建对象,再给属性赋值,通过多个语句实现。例如:
Penguin pgn = new Penguin();
pgn.name = name;
pgn.sex = sex;
能不能在创建对象的时候就完成赋值操作呢?
———————————————————————————————————————
——分析———————————————————————————————————
能!就是通过带参数的构造方法。
下面就让我们先认真理解一条熟悉的陌生语句吧。
Penguin pgn = new Penguin();
———————————————————————————————————————
在Penguin类中增加一个无参的Penguin(),如示例7所示,看看会出现什么情况。
示例7
/**
* 宠物企鹅类,测试无参构造方法。
* @author 南京
*/
public class Penguin {
String name = "无名氏"; // 昵称
int health = 100; // 健康值
int love = 0; // 亲密度
String sex = "Q仔"; // 性别
/**
* 无参构造方法。
*/
public Penguin() {
name = "楠楠";
love = 20;
sex = "Q妹";
System.out.println("执行构造方法");
}
/**
* 输出企鹅的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name
+ ",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",性别是 " + this.sex + "。");
}
/**
* 测试无参构造方法的使用。
*/
public static void main(String[] args) {
Penguin pgn = null;
pgn = new Penguin();
pgn.print();
}
}
运行结果如图1.9所示。
图1.9 显示构造方法被执行
其中Penguin()就是Penguin类的构造方法,从执行结果可以看到当执行语句pgn=new Penguin()时就会执行Penguin()中的代码。没有Penguin()时,系统会提供一个空的Penguin()。
构造方法(Constructor)是一个特殊的方法,它用于创建类的对象,因此一个类必须包含至少一个构造方法,否则就无法创建对象。
构造方法的名字和类名相同,没有返回值类型。构造方法的作用主要就是在创建对象时执行一些初始化操作,如给成员属性赋初值。
让我们通过MyEclipse的断点追踪法来追踪构造方法的执行过程,从而更清楚、更直观的理解该过程。首先在示例7的main方法的”pgn = new Penguin();”语句处设置断点,然后以调试方式运行该程序,进入调试透视图并在断点处暂停,如图1.10所示。
图1.10 构造方法执行过程(一)
按调试窗口中的单步跳入按钮(或按F5键),进入Penguin类,连续按单步跳过按钮(或按F6键),首先执行Penguin类的属性定义语句依次给各属性赋初值,如图1.11所示。
图1.11 构造方法执行过程(二)
继续按单步跳过按钮(或按F6键),会依次执行构造方法中的语句,用构造方法中的值替代属性初始值,如图1.12所示。
图1.12 构造方法执行过程(三)
执行完构造方法内语句后,会跳回到如图1.10所示界面,表示创建对象成功,并把对象引用赋给变量pgn,至此构造方法执行完毕。
——问题———————————————————————————————————
示例7中通过构造方法完成了对象成员属性的赋值,但属性值已经在构造方法中写死了,能不能在创建对象的时候完成不同属性的动态赋值呢?
———————————————————————————————————————
——分析———————————————————————————————————
能!就是通过带参数的构造方法,这就涉及到了构造方法的重载。
———————————————————————————————————————
为Penguin类增加两个有参的构造方法,如示例8所示。
示例8
/**
* 宠物企鹅类,指定多个构造方法。
* @author 南京
*/
public class Penguin {
String name = "无名氏"; // 昵称
int health = 100; // 健康值
int love = 0; // 亲密度
String sex = "Q仔"; // 性别
/**
* 无参构造方法。
*/
public Penguin() {
name = "楠楠";
love = 20;
sex = "Q妹";
System.out.println("执行构造方法");
}
/**
* 两个参数构造方法。
*/
public Penguin(String name, String sex) {
this.name = name;
this.sex = sex;
}
/**
* 四个参数构造方法。
*/
public Penguin(String name, int health, int love, String sex) {
this.name = name;
this.health = health;
this.love = love;
this.sex = sex;
}
/**
* 输出企鹅的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name
+ ",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",性别是 " + this.sex + "。");
}
/**
* 测试构造方法的使用。
*/
public static void main(String[] args) {
Penguin pgn=null;
pgn = new Penguin();
pgn.print();
pgn = new Penguin("亚亚", "企鹅");
pgn.print();
pgn = new Penguin("美美", 80, 20, "Q仔");
pgn.print();
}
}
运行结果如图1.13所示。
图1.13 构造方法的重载
示例8中共有三个构造方法,方法名相同,参数列表不同,这称为构造方法的重载。可以通过构造方法重载来实现多种初始化行为,我们在创建对象时可以根据需要选择合适的构造方法。
下面我们把示例8中无参的构造方法注释掉,看看会出现什么情况。
运行结果如图1.14所示。
图1.14 取消无参构造方法后出错
为什么会出现这个错误呢?同C#一样,在没有给类提供任何构造方法时,系统会提供一个无参的方法体为空的默认构造方法。一旦提供了自定义构造方法,系统将不会再提供这个默认构造方法。如果要使用它,程序员必须手动添加。强烈建议此时为Java类手动提供默认构造方法。
学习了创建对象,如何销毁对象呢?在Java中,对象的销毁不需要程序员来做,而是通过Java系统中的垃圾回收器在后台自动实现。
如果同一个类中包含了两个或两个以上方法,它们的方法名相同,方法参数个数或参数类型不同,则称该方法被重载了,这个过程称为方法重载。成员方法和构造方法都可以进行重载。
其实之前我们已经无形之中在使用方法重载了。
例如:
System.out.println(45);
System.out.println(true);
System.out.println(“狗狗在玩耍!”);
例如:java.lang.Math类中的max方法就实现了重载,如图1.15所示。
图1.15 max方法的重载
——注意———————————————————————————————————
方法重载的判断依据如下。
Ø 必须是在同一个类里。
Ø 方法名相同。
Ø 方法参数个数或参数类型不同。
Ø 与方法返回值和方法修饰符没有任何关系。
———————————————————————————————————————
1.2.3常见错误
- 在类中可以定义static变量,在方法里是否可以定义static变量
常见错误1
/**
* 宠物狗狗类,测试方法中是否可以定义static变量。
* @author 南京
*/
class Dog {
private String name; // 昵称
private int health; // 健康值
private int love; // 亲密度
public void play(int n) {
static int staticVar = 5; //定义static变量
health = health - n;
System.out.println(name + " " + staticVar + " " + health);
}
public static void main(String[] args) {
Dog d = new Dog();
d.play(5);
}
}
运行结果如图1.16所示。
图1.16 运行结果显示static修饰符不合法
把static int localv=5;语句改为int localv=5;,则问题解决。
结论:在方法里不可以定义static变量,也就是说类变量不能是局部变量。
- 给构造方法加上返回值类型会出现什么情况
常见错误2
/**
* 宠物企鹅类,给构造方法加上返回值类型会出现什么情况呢?
* @author 南京
*/
class Penguin {
String name = "无名氏"; // 昵称
int health = 100; // 健康值
String sex = "Q仔"; // 性别
/**
* 给无参构造方法加上返回值类型为void。
*/
public void Penguin() {
name = "欧欧";
sex = "Q妹";
System.out.println("执行构造方法");
}
/**
* 输出企鹅的信息。
*/
public void print() {
System.out.println("企鹅的名字是" + name
+ ",性别是" + sex + "。");
}
public static void main(String[] args) {
Penguin pgn3 = new Penguin();
pgn3.print();
}
}
运行结果如图1.17所示。
图1.17 运行结果显示构造方法没有执行
从运行结果,我们可以看到,Penguin()方法并没有执行,这是为什么呢?不符合构造方法的定义,自然就不是构造方法了,不会再创建对象时执行。
结论2:构造方法没有返回值类型。如果有,就不是构造方法,而是和构造方法同名的成员方法。
1.3使用封装优化电子宠物系统的类
——问题———————————————————————————————————
设计的类有没有缺陷呢?比如执行语句
d=new Dog();
d.health=1000;
再比如示例8中的语句
Penguin pgn = new Penguin(“亚亚”,”企鹅”);
pgn.print();
这些语句在语法上是完全正确的,但是却不符合实际规定,因为我们规定最大health值是100,企鹅的性别只能是Q仔或Q妹。再比如如果一个类有年龄、成绩属性,实际中是有取值范围的,随意赋值也会出现同样的问题。
———————————————————————————————————————
——分析———————————————————————————————————
在Java中已经考虑到了这种情况,解决途径就是对类进行封装,通过private、protected、public和默认权限控制符来实现权限控制。在此例中,我们将属性均设为private权限,将只在类内可见。然后再提供public权限的setter方法和getter方法实现对属性的存取,在setter方法中对输入的属性值的范围进行判断。
———————————————————————————————————————
采用类图来表示封装后的Dog类和Penguin类,运行结果如图1.18和图1.19所示,请大家把它们和图1.3以及图1.4进行比较,看有什么不同。
图1.18 Dog类图 图1.19 Penguin类图
对Dog类进行封装处理,如示例9所示。
示例9
/**
* 宠物狗狗类,使用权限修饰符private和public进行封装。
* @author 南京
*/
class Dog {
private String name = "无名氏"; // 昵称
private int health = 100; // 健康值
private int love = 0; // 亲密度
private String strain = "聪明的拉布拉多犬"; // 品种
/**
* 读取狗狗昵称。
* @return 昵称
*/
public String getName() {
return name;
}
/**
* 指定狗狗昵称。
* @param name 昵称
*/
public void setName(String name) {
this.name = name;
}
/**
* 读取狗狗健康值。
* @return 健康值
*/
public int getHealth() {
return health;
}
/**
* 指定狗狗健康值,对健康值范围进行判断。
* @param health 健康值
*/
public void setHealth(int health) {
if (health > 100 || health < 0) {
this.health = 40;
System.out.println("健康值应该在0和100之间,默认值是40");
} else {
this.health = health;
}
}
/**
* 读取狗狗亲密度。
* @return 亲密度
*/
public int getLove() {
return love;
}
/**
* 指定狗狗亲密度。
* @param love 亲密度
*/
public void setLove(int love) {
this.love = love;
}
/**
* 读取狗狗品种。
* @return 品种
*/
public String getStrain() {
return strain;
}
/**
* 指定狗狗品种。
* @param strain 品种
*/
public void setStrain(String strain) {
this.strain = strain;
}
/**
* 输出狗狗的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name
+ ",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",我是一只 " + this.strain + "。");
}
}
编写测试类,如示例10所示。
示例10
/**
* 测试类的封装。
* @author 南京
*/
class Test {
public static void main(String[] args) {
Dog dog = new Dog();
//dog.health=300;
dog.setName("欧欧");
dog.setHealth(300);
System.out.println("昵称是" + dog.getName());
System.out.println("健康值是" + dog.getHealth());
dog.print();
}
}
运行结果如图1.20所示。
图1.20 测试类的封装
去掉示例10中”d.health=300;”一行的注释符后并执行,会出现什么结果呢?
运行结果如图1.21所示。
图1.21 调用private属性出错
从示例10的两次运行结果图我们可以看到封装之后的两个变化:采用了private修饰符的变量不能再类外部访问,而是通过public修饰的setter方法实现;通过在setter方法中编写相应存取控制语句可以避免出现不符合实际需求的赋值。
封装(Encapsulation)是类的三大特性之一,就是将类的状态信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
封装的具体步骤:修改属性的可见性来限制对属性的访问;为每个属性创建一对赋值(setter)方法和取值(getter)方法,用于对这些属性的存取;在赋值方法中,加入对属性的存取控制语句。
封装的好处主要有:隐藏类的实现细节;让使用者只能通过程序员规定的方法来访问数据;可以方便地加入存取控制语句,限制不合理操作。
封装时会用到多个权限控制符来修饰成员变量和方法,区别如下。
Ø private:成员变量和方法只能在类内被访问,具有类可见性。
Ø 默认:成员变量和方法只能被同一个包里的类访问,具有包可见性。
Ø protected:可以被同一个包中类访问,被同一个项目中不同包中的子类访问(父类、子类的概念将在第二章讲解)。
Ø public:可以被同一个项目中所有类访问,具有项目可见性,这是最大的访问权限。
——问题———————————————————————————————————
电子宠物系统有如下要求。
Ø 领养宠物对象时可以指定昵称、品种,以后不允许改变。
Ø 领养宠物对象时健康值和亲密度采用默认值,只有通过玩耍、吃饭、睡觉等行为来改变。
———————————————————————————————————————
——分析———————————————————————————————————
实际开发中封装哪些属性、如何封装取决于业务需求。根据需求,应对示例9做如下修改。
- 去掉所有setter方法,保留所有的getter方法。
- 提供有name和strain两个参数的构造方法实现对昵称和品种的赋值。
- 提供eat()、play()、sleep()等方法实现健康值和亲密度的变化。
———————————————————————————————————————
采用类图来表示改变封装后的Dog类和Penguin类,结果如图1.22和图1.23所示,请大家把它们和图1.18以及图1.19进行比较,看有什么不同。
图1.22 Dog类图 图1.23 Penguin类图
改变封装后的Dog类如示例11所示。
示例11
/**
* 宠物狗狗类,使用权限修饰符private和public进行封装。
* @author 南京
*/
class Dog {
private String name = "无名氏"; // 昵称
private int health = 100; // 健康值
private int love = 0; // 亲密度
private String strain = "聪明的拉布拉多犬"; // 品种
/**
* 通过构造方法指定狗狗的昵称、品种
* @param name 昵称
* @param strain 品种
*/
public Dog(String name, String strain) {
this.name = name;
this.strain = strain;
}
/**
* 通过吃饭增加健康值。
*/
public void eat() {
if (health >= 100) {
System.out.println("狗狗需要多运动呀!");
} else {
health = health + 3;
System.out.println("狗狗吃饱饭了!");
}
}
/**
* 通过玩游戏增加与主人亲密度,减少健康值。
*/
public void play() {
if (health < 60) {
System.out.println("狗狗生病了!");
} else {
System.out.println("狗狗正在和主人玩耍。");
health = health - 10;
love = love + 5;
}
}
/**
* 读取狗狗昵称。
* @return 昵称
*/
public String getName() {
return name;
}
/**
* 读取狗狗健康值。
* @return 健康值
*/
public int getHealth() {
return health;
}
/**
* 读取狗狗亲密度。
* @return 亲密度
*/
public int getLove() {
return love;
}
/**
* 读取狗狗品种。
* @return 品种
*/
public String getStrain() {
return strain;
}
/**
* 输出狗狗的信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name
+ ",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + ",我是一只 " + this.strain + "。");
}
}
编写测试类,如示例12所示。
示例12
/**
* 测试类的封装。
* @author 南京
*/
class Test{
public static void main(String[] args) {
Dog dog = new Dog("欧欧", "酷酷的雪娜瑞");
dog.play();
System.out.println("健康值是" + dog.getHealth());
dog.eat();
dog.print();
}
}
运行结果如图1.24所示。
图1.24 示例12运行结果
接下来介绍this关键字。
在示例9中的一系列setter方法中我们都用到了this这个关键字,this是什么含义呢?
它还有什么其他的用法?
this关键字是对一个对象的默认引用。在每个实例方法内部,都有一个this引用变量,指向调用这个方法的对象。
在示例10中,我们创建了一个Dog对象dog,dog对象的昵称是欧欧,健康值是300,但是在示例9中Dog类代码的编写是早于创建Dog对象的,当时并不知道以后创建的对象的名字呢,this关键字就用来表示以后调用当前方法的那个对象的引用,当调用dog.setName(“欧欧”)、dog.setHealth(300)时,this就代表dog,而当创建另外Dog对象xxx,然后调用xxx.setName(“yyy”):时,this就表示xxx,this和xxx指向同一个对象。
this使用举例
Ø 使用this调用成员变量,解决成员变量和局部变量同名冲突。
public void setName(String name) {
this.name = name; //成员变量和局部变量同名,必须使用this
}
public void setName(String xm) {
name = xm; //成员变量和局部变量不同名,this可以省略
}
Ø 使用this调用成员方法。
public void play(int n) {
health = health – n;
this.print(); //this可以省略,直接调用print();
}
Ø 使用this调用重载的构造方法,只能在构造方法中使用,必须是构造方法的第一条语句。
public Penguin(String name, String sex) {
this.name = name;
this.sex = sex;
}
public Penguin(String name, int health, int love, String sex) {
this(name,sex); //调用重载的构造方法
this.health = health;
this.love = love;
}
——注意———————————————————————————————————
因为this是在对象内部指代自身的引用,所以this只能调用实例变量、实例方法和构造方法。
Ø this不能调用类变量和类方法。
Ø this也不能调用局部变量。
———————————————————————————————————————
1.4上机练习
上机练习1
练习——用类图设计Dog和Penguin类
训练要点
Ø 面向对象设计的过程。
Ø 用类图描述设计。
需求说明
根据本章电子宠物系统中领养宠物功能的需求,运用面向对象思想抽象出Dog类和Penguin类,并使用类图表示。
——提示———————————————————————————————————
面向对象设计的过程就是抽象的过程,分三步来完成:
发现类、发现类的属性和发现类的方法。
———————————————————————————————————————
上机练习2
指导——领养宠物并打印宠物信息
训练要点
Ø 类的结构。
Ø 对象的创建,类的属性和方法的调用。
需求说明
根据控制台信息选择领养宠物为狗狗,输入昵称、品种等信息,然后打印宠物信息表示领养成功。
实现思路及关键代码
- 创建Dog类,定义属性和方法,定义print()方法,定义默认构造方法。
- 编写Test类,根据控制台信息选择领养宠物为狗狗,输入昵称、品种等信息,创建Dog对象并打印对象信息。
上机练习3
练习——给Dog类增加Dog(name)构造方法
训练要点
Ø 构造方法的定义和使用。
构造方法的重载,是否提供带参构造方法对默认构造方法的影响。
需求说明
给Dog增加Dog(name)构造方法,使用该构造方法创建对象;去掉默认构造方法,分析出现问题的原因。
上机练习4
练习——对企鹅对象的性别属性值进行设定和修改
训练要点
Ø static变量和实例变量的区别。
Ø 使用final修饰变量。
需求说明
给Penguin类提供SEX_MALE和SEX_FEMALE两个静态常量,分别取值”Q仔”或”Q妹”,后来要求变化,规定企鹅的性别只能取值”雄”或”雌”,通过修改静态常量值实现该需求。
——提示———————————————————————————————————
创建多个企鹅对象,通过对静态常量值的修改体会通过这种方式改变企鹅性别取值的高效性。
———————————————————————————————————————
本章总结
Ø 现实世界是”面向对象”的,面向对象就是采用”现实模拟”的方法设计和开发程序。
Ø 面向对象技术是目前计算机软件开发中最流行的技术。面向对象设计的过程就是抽象的过程。
Ø 类是对某一类事物的描述,是抽象的、概念上的定义。对象是实际存在的该事物的个体,是具体的、现实的。
Ø 如果同一个类中包含了两个或两个以上方法,它们的方法名相同,方法参数个数或参数类型不同,则称该方法被重载了,这个过程称为方法重载。
Ø 构造方法用于创建类的对象。构造方法的作用主要就是在创建对象时执行一些初始化操作。可以通过构造方法重载来实现多种初始化行为。
Ø 封装就是将类的成员属性声明为私有的,同时提供公有的方法实现对该成员属性的存取操作。
Ø 封装的好处主要有:隐藏类的实现细节;让使用者只能通过程序员规定的方法来访问数据;可以方便地加入存取控制语句,限制不合理操作。
本章作业
一、 选择题
1.给定如下Java代码,下列( )方法可以加入到Sample类中,并且能够编译正确。
public class Sample {
public int getSomething(int d) {
return d;
}
}
A.private int getSomething(int i, String s) {}
B.public void getSomething(int i) {}
C.private int getSomething(int i, String s) {return 20;}
C.public double getSomething() {return “abc”;}
2.给定如下Java代码,编译运行,结果将是( )。
public class Sample {
private int x;
public Sample() {
x = 1;
}
public void Sample (double f) {
this.x = (int) f;
}
public int getX() {
return x;
}
public static void main(String[] args) {
Sample s = new Sample(5.2);
System.out.pringln(s.getX());
}
}
A.发生编译期错误,编译器提示:出现重复地方法Sample
B.发生编译期错误,编译器提示:未定义构造方法Sample(double)
C.正常运行,输出结果:5.2
D.正常运行,输出结果:5
3.给定如下Java代码,编译运行,结果将是( )。
public class Sample {
public double result(double d1, double d2) {
return d1 < d2 ? d1:d2;
}
public double result(int d1, double d2) {
return d1 > d2 ? d1:d2;
}
public int result(int d1, int d2) {
return d1 - d2;
}
private int resule(int i) {
return i;
}
public static void main(String[] args)
Sample s = new Sample();
System.out.print(s.result(2 , 3.0) + “ , “);
System.out.print(s.result(4.0 , 4.5) + “ , “);
System.out.print(s.result(10 , 9));
}
}
A.3.0 , 4.0 , 1
B.2 , 4.0 , 1
C.3.0 , 4.5 , 1
D.-1 , 4.0 ,1
4.构成方法重载的要素不包括( )。
A.方法名与类名相同
B.返回类型不同
C.参数列表不同
D.在同一个类中
5.在如下所示的Sample类中,共有( )个构造方法。
public class Sample {
private int x;
private Sample() {
x = 1;
}
public void Sample(double f) {
this.x = (int)f;
}
public Sample(String s){
}
}
A.4
B.3
C.2
D.1
二 、简答题
- 请指出下面代码中存在的错误,并什么错误原因。
class Teacher1 {
public Teacher1() {
}
}
class Teacher2 {
public void Teacher2(String name) {
}
}
public class TeacherTest {
public static void main(String[] args) {
Teacher1 t1 = new Teacher1();
Teacher2 t2 = new Teacher2(“Mr lee”);
}
}
- 编写一个类Student1,代表学员,要求如下。
Ø 具有属性:姓名、年龄,其中年龄不能小于16岁,否则输出错误信息。
Ø 具有方法:自我介绍,负责输出该学员的姓名、年龄。
编写测试类Student1Test进行测试,看是否符合需求。
——提示———————————————————————————————————
Ø 在学员类的SetAge()方法中验证年龄大小。
Ø 在测试类中分别测试学员年龄小于16岁、大于16岁时的输出结果。
———————————————————————————————————————
- 请指出下面代码中存在的错误,并说明错误原因。
public class Sample {
public void amethod(int i, String s) { }
public void amethod(String s, int i) { }
public int amethod(String s1, String s2) { }
private void amethod(int i, String mystring) { }
public void Amethod(int i, String s) { }
private void amethod(int i);
}
- 编写一个类Student2,代表学员,要求如下。
Ø 具有属性:姓名、年龄、性别和专业。
Ø 具有方法:自我介绍,负责输出该学员的姓名、年龄、性别以及专业。
Ø 具有两个带参构造方法:第一个构造方法中,设置学员的性别为男,专业为LE,其余属性的值由参数给定;第二个构造方法中,所有属性的值都由参数给定。
编写测试类Student2Test进行测试,分别以两种方式完成对两个Student2对象的测试化工作,并分别调用它们的自我介绍方法,看看输出结果是否正确。
——提示———————————————————————————————————
在学员类中定义两个构造方法完成初始化工作。
public Student2(String name, int age) {}
public Student2(String name,int age, String sex, String subject) {}
———————————————————————————————————————
- 简述类的封装的定义、具体步骤和好处。
第2章
继 承
◇本章工作任务
Ø 优化电子宠物系统
Ø 实现汽车租赁系统的计价功能
◇本章技能目标
Ø 掌握继承的优点和实现
Ø 掌握子类重写父类的方法
Ø 掌握继承下构造方法的执行过程
Ø 掌握抽象类和抽象方法的使用
Ø 使用final关键字修饰属性、方法和类
本章单词
请在预习时学会下列单词的含义和发音,并填写在横线处。
1.inheritance:
2.extend:
3.super:
4.override:
5.constructor:
6.public:
7.abstract:
8.final:
本章简介
在本章中我们将对上一章领养宠物功能进行优化。首先引入继承功能抽象出Dog类和Penguin类的父类Pet类,实现代码重用;然后讲解子类重写父类的方法,继承下构造方法的执行过程,这些都是继承中非常重要的技能;再结合业务讲解abstract和final的使用,这是两个功能正好相反的关键字;最后是综合练习,要求大家利用本章所学内容完成汽车租赁系统计价功能的设计和代码实现。
2.1 继承基础
——问题———————————————————————————————————
在上一章中根据需求抽象出了Dog类和Penguin类,在这两个类中有许多相同的属性和方法,例如name、health和love属性以及相应的getter方法,还有print()方法。这样设计的不足之处主要表现在两方面:一是代码重复,二是如果要修改的话,两个类都要修改,如果涉及的类较多,那修改量就更大了。如何有效地解决这个问题呢?
———————————————————————————————————————
——分析———————————————————————————————————
可以将Dog类和Penguin类中相同的属性和方法提取出来放到一个单独的Pet类中,然后让Dog类和Penguin类继承Pet类,同时保留自己特有的属性和方法,这需要通过Java的集成功能来实现。
———————————————————————————————————————
如图2.1和图2.2所示是采用继承之前的类图,如图2.3所示是采用继承优化后的类图。通过对比发现相同的属性和方法都被移到了Pet类中,重新定义的Dog类和Penguin类只包括特有的属性和方法。相同属性和方法从父类继承,避免了代码重复,也方便了日后的代码修改。
图2.1 Dog类 图2.2 Penguin类图
图2.3 采用集成优化后的类图
抽象出的Pet类的代码如示例1所示。
示例1
/**
* 宠物类,狗狗和企鹅的父类。
* @author 南京
*/
public class Pet {
private String name = "无名氏";// 昵称
private int health = 100;// 健康值
private int love = 0;// 亲密度
/**
* 无参构造方法。
*/
public Pet() {
this.health = 95;
System.out.println("执行宠物的无参构造方法。");
}
/**
* 有参构造方法。
* @param name 昵称
*/
public Pet(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
public int getLove() {
return love;
}
/**
* 输出宠物信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" +
this.name + ",我的健康值是" + this.health
+ ",我和主人的亲密程度是" + this.love + "。");
}
}
Dog类继承Pet类,代码如示例2所示。
示例2
/**
* 狗狗类,宠物的子类。
* @author 南京
*/
public class Dog extends Pet {
private String strain;// 品种
/**
* 有参构造方法。
* @param name 昵称
* @param strain 品种
*/
public Dog(String name, String strain) {
super(name); //此处不能使用this.name=name;
this.strain = strain;
}
public String getStrain() {
return strain;
}
}
Penguin类继承Pet类,代码如示例3所示。
示例3
/**
* 企鹅类,宠物的子类。
* @author 南京
*/
public class Penguin extends Pet {
private String sex;// 性别
/**
* 有参构造方法。
* @param name 昵称
* @param sex 性别
*/
public Penguin(String name, String sex) {
super(name);
this.sex = sex;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
编写测试类,创建三个类的对象并输出对象信息,如示例4所示。
示例4
/**
* 测试类,测试类的继承。
* @author 南京
*/
public class Test {
public static void main(String[] args) {
// 1、创建宠物对象pet并输出信息
Pet pet = new Pet("贝贝");
pet.print();
// 2、创建狗狗对象dog并输出信息
Dog dog = new Dog("欧欧", "雪娜瑞");
dog.print();
// 3、创建企鹅对象pgn并输出信息
Penguin pgn = new Penguin("楠楠", "Q妹");
pgn.print();
}
}
运行结果如图2.4所示。
图2.4 示例4的运行结果
语法
修饰符 SubClass extends SuperClass {
//类定义部分
}
在Java中,继承(Inheritance)通过extends关键字来实现,其中SubClass称为子类,SuperClass称为父类、基类或超类。修饰符如果是public,该类在整个项目中可见;不写public修饰符则该类只在当前包可见;不可以使用private和protected修饰类。
继承是类的三大特征之一,是Java中实现代码重用的重要手段之一。Java中只支持单继承,即每个类只能有一个直接父类。继承表达的是is a的关系,或者说是一种特殊和一般的关系,例如Dog is a Pet。同样我们可以让学生继承人,让苹果继承水果,让三角形继承几何图形。
在Java中,所有的Java类都直接或间接地继承了java.lang.Object类。Object类是所有Java类的祖先。在定义一个类时,没有使用extends关键字,那么这个类直接继承Object类。例如:public class MyObject{ }这段代码表明:MyObject类的直接父类为Object类。
——资料———————————————————————————————————
企业面试题:请写出java.lang.Object的六个方法。
这个问题把许多Java大拿都难住了,并不是题目难,而是平时不太注意细节。留心这个题目,其中很多方法会在以后用到时详细讲解,主要方法如图2.5所示。
图2.5 Object类的方法列表
———————————————————————————————————————
在Java中,子类可以从父类中继承到哪些“财产”呢?
Ø 继承public和protected修饰的属性和方法,不管子类和父类是否在同一个包里。
Ø 继承默认权限修饰符修饰的属性和方法,但子类和父类必须在同一个包里。
Ø 无法继承private修饰的属性和方法。
Ø 无法继承父类的构造方法。
下面采用断点追踪法观察采用继承后创建子类对象的执行过程,从而深化对继承的理解。首先在示例4中main方法的“Penguin pgn=new Penguin(“楠楠”,“Q妹”);”语
句处设置断点,然后以调试方式运行该程序,会进入调试透视图并在断点处暂停,如图2.6所示。
图2.6 继承条件下构造方法执行过程图1
通过调试窗口中单步跳入按钮(F5键)和单步跳过按钮(F6键),执行控制程序,期间主要执行步骤如下。
- 进入Penguin构造方法,如图2.7所示。
图2.7 继承条件下构造方法执行过程图2
- 进入父类Pet构造方法,如图2.8所示。
图2.8 继承条件下构造方法执行过程图3
- 返回到Penguin构造方法继续执行,如图2.9所示。
图2.9 继承条件下构造方法执行过程图4
- 执行完Penguin构造方法内的语句后,会跳回到图2.6页面,表示创建对象成功,并把对象引用赋给变量pgn,至此构造方法执行完毕。
2.2 重写和继承关系中的构造方法
2.2.1 子类重写父类方法
——问题———————————————————————————————————
在示例4中,Dog对象和Penguin对象的输出内容是父类Pet的print()方法的内容,所以不能显示Dog的strain信息和Penguin的sex信息,这显然是不符合需求的。该怎样解决呢?
———————————————————————————————————————
——分析———————————————————————————————————
如果从父类继承的方法不能满足子类的需求,在子类中可以对父类的同名方法进行重写(覆盖),以符合需求。
———————————————————————————————————————
在Dog类中重写父类的print()方法,如示例5所示。
示例5
/**
* 狗狗类,宠物的子类。
* @author 南京
*/
public class Dog extends Pet {
private String strain;// 品种
/**
* 有参构造方法。
* @param name 昵称
* @param strain 品种
*/
public Dog(String name, String strain) {
super(name); //此处不能使用this.name=name;
this.strain = strain;
}
public String getStrain() {
return strain;
}
/**
* 重写父类的print方法。
*/
public void print(){
super.print(); //调用父类的print方法
System.out.println("我是一只 " + this.strain + "。");
}
}
在Penguin类中重写父类的print()方法,如示例6所示。
示例6
/**
* 企鹅类,宠物的子类。
* @author 南京
*/
public class Penguin extends Pet {
private String sex;// 性别
/**
* 有参构造方法。
* @param name 昵称
* @param sex 性别
*/
public Penguin(String name, String sex) {
super(name);
this.sex = sex;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
/**
* 重写父类的print方法
*/
public void print() {
super.print();
System.out.println("性别是 " + this.sex + "。");
}
}
再次运行示例4,运行结果如图2.10所示。
图2.10 重写父类方法后运行结果
从运行结果可以看出,dog.print()和pgn.print()调用的相应子类的print()方法而不是Pet类的print()方法,符合需求。
在子类中可以根据对父类继承的方法进行重新编写,称为方法的方法重写或方法的覆盖(overriding)必须满足如下要求。
Ø 重写方法和被重写方法必须具有相同的方法名。
Ø 重写方法和被重写方法必须具有相同的参数列表。
Ø 重写方法的返回值类型必须和被重写方法的返回值类型相同或者是其子类。
Ø 重写方法的不能缩小被重写方法的访问权限。
——问答———————————————————————————————————
问题:重载(overloading)和重写(overriding)有什么区别和联系?
解答:重载涉及同一个类中的同名方法,要求方法名相同,参数列表不同,与返回值类型无关。
重写涉及的是子类和父类之间的同名方法,要求方法名相同、参数列表相同、返回值类型相同(或者是其子类)。
———————————————————————————————————————
如果在子类中想调用父类的被重写方法,如何实现呢?如示例5和示例6所示,可以在子类方法中通过“super.方法名”来实现。
super代表对当前对象的直接父类对象的默认引用。在子类中可以通过super关键字来访问父类的成员。
Ø super必须是出现在子类中(子类的方法和构造方法中),而不是其他位置。
Ø 可以访问父类的成员,例如父类的属性、方法、构造方法。
Ø 注意访问权限的限制,例如无法通过super访问private成员。
例如,在Dog类中可以通过如下语句来访问父类成员。
Ø super.name: //访问直接父类的name属性(如果name是private权限,则无法访问)
Ø super.print(): //访问直接父类的print()方法
Ø super(name): //访问直接父类的对应构造方法,只能出现在构造方法中
2.2.2 继承关系中的构造方法
——问题———————————————————————————————————
示例5中Dog类的构造方法:
public Dog(String name,String strain) {
super(name);
this.strain=strain;
}
示例6中Penguin类的构造方法:
public Penguin(String name,String sex) {
super(name);
this.sex=sex;
}
如果把其中“super(name);”一行注释掉会出现什么情况?
———————————————————————————————————————
——分析———————————————————————————————————
很多学员可能会想当然,无非就是不调用父类对应的构造方法,而仅仅给strain或sex属性赋值。大错特错了!
我们可以做如下实验:把Dog类的构造方法中的“super(name);”注释掉,而保留Penguin类的构造方法中“super(name);”,对比一下看有什么不同。
———————————————————————————————————————
测试类代码如示例7所示。
示例7
/**
* 测试类,测试继承条件下的构造方法。
* @author 南京
*/
public class Test {
public static void main(String[] args) {
// 1、创建狗狗对象dog并输出信息
Dog dog = new Dog("欧欧", "雪娜瑞");
dog.print();
// 2、创建企鹅对象pgn并输出信息
Penguin pgn = new Penguin("楠楠", "Q妹");
pgn.print();
}
}
运行结果如图2.11所示
图2.11 执行了Pet类的无参构造方法后的运行结果
结果出乎我们的意料吧!Dog类的构造方法居然调用了Pet类的无参方法,可我们并没有在Dog的构造方法中添加“super();”语句啊?这究竟是怎么回事呢?这就涉及了Java中一个非常重要的知识点:继承条件下构造方法的调用。
Ø 如果子类的构造方法中没有通过super显式调用父类的有参构造方法,也没有通过this显式调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法。在这种情况下,写不写“super();”语句,效果都是一样的。
Ø 如果子类的构造方法中通过super显式调用父类的有参构造方法,那将执行父类相应构造方法,而不执行父类无参构造方法。
Ø 如果子类的构造方法中通过this显式调用自身的其他构造方法,在相应构造方法中应用以上两条规则。
Ø 特别注意的是,如果存在多级继承关系,在创建一个子类对象中,以上规则会多次向更高一级父类应用,一直到执行顶级父类Object类的无参构造方法为止。
——资料———————————————————————————————————
- 在构造方法中如果有this语句或super语句出现,只能是第一条语句。
- 在一个造方法中不允许同时出现this和super语句(否则就有两条第一条语句)。
- 在类方法中不允许出现this或super关键字。
- 在实例方法中this和super语句不要求是第一条语句,可以共存。
———————————————————————————————————————
下面我们通过一个存在多级继承关系的示例更深入地理解继承条件下构造方法的调用规则,即继承条件下创建子类对象时的执行过程。代码如示例8所示。
示例8
class Person {
String name;// 姓名
public Person() {
// super();//写不写该语句,效果一样
System.out.println("execute Person()");
}
public Person(String name) {
this.name = name;
System.out.println("execute Person(name)");
}
}
class Student extends Person {
String school;// 学校
public Student() {
// super();//写不写该语句,效果一样
System.out.println("execute Student() ");
}
public Student(String name, String school) {
super(name); // 显示调用了父类有参构造方法,将不执行无参构造方法
this.school = school;
System.out.println("execute Student(name,school)");
}
}
class PostGraduate extends Student {
String guide;// 导师
public PostGraduate() {
// super();//写不写该语句,效果一样
System.out.println("execute PostGraduate()");
}
public PostGraduate(String name, String school, String guide) {
super(name, school);
this.guide = guide;
System.out.println("execute PostGraduate(name, school, guide)");
}
}
class TestInherit {
public static void main(String[] args) {
PostGraduate pgdt=null;
pgdt = new PostGraduate();
System.out.println();
pgdt=new PostGraduate("刘致同","北京大学","王老师");
}
}运行结果如图2.12所示。
图2.12 创建PostGraduate对象运行结果
执行“pgdt=new PostGraduate();”后,共计创建了四个对象。按照创建顺序,依次是Object、Person、Student和PostGraduate对象,不要忘记除了PostGraduate对象外还有另外三个对象,尤其是别忘了还会创建Object对象。在执行Person()时会调用它的直接父类Object的无参构造方法,该方法内容为空。
执行“pdgt=new PostGraduate(“刘致同”,“北京大学”,“王老师”);”后,共计也创建了四个对象,只是此次调用的构造方法不同,依次是Object()、public Person(String name)、public Student(String name,String school)和public PostGraduate (String name,String school,String guide)。
2.2.3 上机练习
上机练习1
指导——创建宠物对象并输出信息
训练要点
Ø 继承语法、子类可以从父类继承的内容。
Ø 子类重写父类方法。
Ø 继承条件下构造方法的执行过程
需求说明
从Dog类和Penguin类中抽象出Pet父类,让Dog类和Penguin类继承Pet类,属性及方法如图2.13所示,然后创建狗狗和企鹅对象并输出它们自己的信息。
图2.13 采用继承优化后的类图
实现思路及关键代码
- 创建Pet类,定义属性和方法,定义print()方法,定义无参和有参构造方法。
- 创建Dog类,继承Pet类,增加strain属性及相应的getter方法。
- 创建Penguin类,继承Pet类,增加sex属性及相应的getter方法。
- 创建测试类Test,在测试类中创建Dog、Penguin对象,打印出相应宠物信息。
- 在Dog类和Penguin类中增加print()方法,实现子类对父类方法的覆盖。
- 运行测试类Test打印宠物信息,观察不同之处。
- 在测试类中设置断点,观察创建子类对象时的执行过程。
注意编写注释。
2.3 抽象类和final
2.3.1 抽象类和抽象方法
——问题———————————————————————————————————
在示例4中,有如下语句
Pet pet=new Pet(“贝贝”);
pet.print();
但是创建Pet对象是没有意义的,因为实际生活中有狗狗、有企鹅,而没有一种叫宠物的动物,宠物只是我们抽象出来的一个概念。如何把Pet限制为不能实例化呢?
———————————————————————————————————————
——分析———————————————————————————————————
可以使用Java中的抽象类来实现,用abstract来修饰Pet类。抽象类不能通过new实例化。
———————————————————————————————————————
示例9
/**
* 宠物抽象类,狗狗和企鹅的父类。
* @author 南京
*/
public abstract class Pet {
private String name = "无名氏";// 昵称
private int health = 100;// 健康值
private int love = 0;// 亲密度
/**
* 无参构造方法。
*/
public Pet() {
this.health = 95;
System.out.println("执行宠物的无参构造方法。");
}
/**
* 有参构造方法。
* @param name 昵称
*/
public Pet(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
public int getLove() {
return love;
}
/**
* 输出宠物信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name +
",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + "。");
}
}
测试类如示例10所示
示例10
/**
* 测试抽象类是否能实例化。
* @author 南京
*/
class Test {
public static void main(String[] args) {
Pet pet = new Pet("贝贝");
pet.print();
}
}
运行结果如图2.14所示,提示抽象类Pet不能实例化。
图2.14 显示抽象类不能实例化
——问题———————————————————————————————————
Pet类提供了print()方法,如果子类重写该方法,将正确打印字类信息,如图2.10所示。可是如果子类中没有重写该方法,子类将继承Pet类的该方法,从而无法正确打印字类信息,如图2.4所示。能否强迫子类必须重写该方法,否则就提示出错呢?
——分析———————————————————————————————————
可以使用Java中的抽象方法来实现,用abstract来修饰print方法,则子类必须重写该方法。
———————————————————————————————————————
修饰示例9,将print()方法修改为抽象方法,代码如示例11所示。
示例11
/**
* 宠物抽象类,狗狗和企鹅的父类。
* @author 南京
*/
public abstract class Pet {
private String name=“无名氏”;//昵称
private int health=100;//健康值
private int love=0;//亲密度
/**
* 有参构造方法。
* @param name 昵称
* /
public Pet(String name) {
this.name =name;
}
//省略其他代码
/**
* 抽象方法,输出宠物信息
* /
public abstract void print();
}
在Dog类中去掉print()方法的定义,即不重写print()方法,然后编写测试类创建狗狗对象并输出信息,代码如示例12所示。
示例12
/**
*测试类, 测试抽象方法必须重写。
* @author 南京
*/
public class Test {
public static void main(String[] args) {
Dog dog=new Dog(“欧欧”,“雪娜瑞”);
dog.print();
}
}
运行结果如图2.15所示,提示Dog类print()方法出错,重写后问题解决。
图2.15 子类不重写抽象方法会报错
抽象类和抽象方法都通过abstract关键字来修饰。
抽象类不能实例化。抽象类中可以没有,可以有一个或多个抽象方法,甚至可以全部方法都是抽象方法。
抽象方法只有方法声明,没有方法实现。有抽象方法的类必须声明为抽象类。子类必须重写所有的抽象方法才能实例化,否则子类还是一个抽象类。
——注意———————————————————————————————————
- “public void print() { }”不是抽象方法,而是有实现但实现为空的普通方法。
“public abstract void print() { }”才是抽象方法,别忘记了最后的分号。
Ø abstract可以用来修饰类和方法,但不能用来修饰属性和构造方法。
———————————————————————————————————————
2.3.2 上机练习
上机练习2
指导——修改Pet类为抽象类,强迫子类实现print()方法
训练要点
Ø 抽象类的定义和继承。
Ø 抽象方法定义和重写。
需求说明
在上机练习1的基础上,修改Pet类为抽象类,把该类中的print()方法定义为抽象方法,创建Dog对象并输出信息。
实现思路及关键代码
- 修改Pet类为抽象类,修改print()为抽象方法。
- 修改Dog类继承Pet类,重写print()方法。
- 修改测试类Test,创建Dog对象并输出对象信息。
- 注释Dog类中print()方法,运行测试类查看错误信息。
注意编写注释。
2.3.3 final修饰符
——问题———————————————————————————————————
问题1:如果我们让企鹅类不被其他类继承,不允许再有子类,应该如何实现呢?
问题2:如果企鹅类可以有子类,但是它的print()方法不能再被子类重写,应该如何实现呢?
问题3:如果企鹅类可以有子类,但是增加一个居住地属性home,规定只能取值“南极“,应该如何实现呢?
———————————————————————————————————————
——分析———————————————————————————————————
对于问题1可以通过给Penguin类添加final修饰符实现。
对于问题2可以通过给print()方法添加final修饰符实现。
对于问题3可以通过给home属性添加final修饰符实现。
———————————————————————————————————————
Ø 用final修饰的类,不能再被继承。
final class Penguin {
}
class SubPenguin extends Penguin{ //错误,Penguin类不能被继承
}
Ø 用final修饰的类,不能被子类重写。
class Penguin {
public final void print() { }
}
class SubPenguin extends Penguin{
public void print() { } //错误,Penguin类不能被继承
}
Ø 用final修饰的变量(包括成员变量和局部变量)将变成常量,只能赋值一次。
class Penguin {
final String home=“南极”;//居住地
public void setHome(String name) {
this.home=home; //错误,home不可以再次赋值
}
}
——注意———————————————————————————————————
- final和abstract是功能相反的两个关键字,可以对比记忆。
- abstract可以用来修饰类和方法,不能用来修饰属性和构造方法。
final可以用来修饰类、方法和属性,不能修饰构造方法。
- Java提供有很多类就是final类,比如String类、Math类,它们不能再有子类。Object类中一些方法,如getClass()、notify()、wait()都是final方法,只能被子类继承而不能被重写,但是hashCode()、toString()、equals(Object obj)不是final方法,可以被重写。
———————————————————————————————————————
2.3.4 常见错误
1.final修饰引用型变量,变量所指对象的属性值是否能改变
常见错误1
请找出下面程序中存在错误的位置。
class Dog {
String name;
public Dog(String name) {
this.name=name;
}
}
class Test {
public static void main(String[] args) {
final Dog dog=new Dog(“欧欧”);
dog.name=“美美”;
dog=new Dog(“亚亚”);
}
}
可能的出错位置锁定在“dog.name=“美美”;和dog=new Dog(“亚亚”);”两条语句,很多学员认为这两行都是错误的,因为dog已经定义为final修饰的常量,其值不可改变,但是其实“dog.name=“美美;”一行却是正确的。
对于引用型变量,一定要区分对象的引用值和对象的属性值两个概念。使用final修饰引用型变量,变量不可以再指向另外的对象,所以“dog=new Dog(“亚亚”);”是错误的。但是所指对象的内容却是可以改变的,所以“dog.name=“美美;”是正确的。
——结论———————————————————————————————————
使用final修饰引用型变量,变量的值是固定不变的,而变量所指向的对象的属性值是可变的。
———————————————————————————————————————
2.abstract是否可以和private、static、final共用
常见错误2
下面选项中关于abstract的使用正确的是( )。
- private abstract void sleep();
- static abstract void sleep();
- final abstract void sleep();
- public abstract void sleep();
A选项是错误的。抽象方法是让子类来重写的,而子类无法继承到private方法,自然就无法重写。
B选项是错误的。抽象方法只有声明没有实现,而static方法可以通过类名直接访问,难道要访问一个没有实现的方法吗?
C选项是错误的。抽象方法是让子类在重写的,而final修饰的方法不能被重写。同理抽象类只有让子类继承才能实例化,而final修饰的类不允许被子类继承;
D选项是正确的,两个关键字不冲突。
——结论———————————————————————————————————
abstract不能和private同时修饰一个方法;
abstract不能和static同时修饰一个方法;
abstract不能和final同时修饰一个方法或类;
———————————————————————————————————————
2.4 综合练习:实现汽车租赁系统计价功能
——问题———————————————————————————————————
某汽车租赁公司出租多种轿车和客车,出租费用以日为单位计算。出租车型及信息如表2-1所示。
表2-1 租赁业务表
如果采用面向对象思想进行设计,该如何编写程序计算汽车租赁价呢?
———————————————————————————————————————
——分析———————————————————————————————————
面向对象涉及的过程就是抽象的过程。如1.1.2节的设计过程一样,还是通过在需求中找出名词的方式确定类和属性,找出动词的方式确定方法。然后对找到的词语进行筛选,剔除无关、不重要的词语,还要对词语之间的关系进行梳理,从而确定类、属性、属性值和方法。
设计分以下五步完成。
第一步:发现类。
第二步:发现类的属性。
第三步:发现类的方法。
第四步:优化设计。
第五步:梳理运行过程。
———————————————————————————————————————
需求中和业务相关的名词主要有:汽车租赁公司、汽车、轿车、客车、别克、宝马、金杯、金龙、商务舱GL8、550i、林荫大道、座位数、日租金、租赁价等。动词主要是计算租赁价。
第一步:发现类。
轿车和客车是两个常用类,汽车可以作为两者的父类设计。
因为只有一家汽车租赁公司,在计算租赁价时不需要该属性来标记某汽车,剔除该名词。
别克、宝马、金杯、金龙是汽车的品牌,没有必要设计为汽车的子类,作为汽车的一个属性品牌(brand)的值存在更简单更合理。
商务舱GL8、550i、林荫大道都是轿车的型号,也没有必要设计为轿车的子类,可以作为轿车的一个属性型号(type)的值存在。
基于分析,我们从需求中抽象出如下类:汽车、轿车和客车。把汽车设计为父类,轿车和客车作为汽车的子类存在,结果如图2.16所示。
图2.16 发现类
第二步:发现类的属性。
基于分析,汽车的属性有车牌号(no)、品牌(brand)等属性,品牌的属性值可以是别
克、宝马、金杯和金龙。
轿车除了具有汽车类的属性外,还有型号(type)属性,例GL8、550i、林荫大道等,型号和租金有直接关系,不可忽略。
客车除了具有汽车类的属性外,还有座位数(seatCount)属性,同样不能忽略。
结果如图2.17所示。
图2.17 发现类的属性
第三步:发现类的方法。
在本需求中,类的方法只有一个,就是计算租金。取名为calRent(int days),设计为父类方法,让子类重写。结果如图2.18所示。
图2.18 发现类的方法
第四步:优化设计。
把汽车设计为抽象类,不允许实例化。把轿车和客车设计为final类,不允许再有子类。把父类中的calRent(int days)设计为抽象方法,强迫子类重写。
第五步:梳理运行过程。
首先编写汽车、轿车和客车的类代码,然后根据用户输入数据创建对象并调用calRent(int days)方法计算租金。
程序运行结果如图2.19和图2.20所示。
图2.19 租赁汽车界面(一)
图2.20 租赁汽车界面(二)
请大家根据设计结果及参考界面,编写程序实现计算汽车租赁费功能。
本章总结
Ø 继承是Java中实现代码重用的重要手段之一。Java中只支持单继承,即一个类只能有一个直接父类。Java.lang.Object类是所有Java类的祖先。
Ø 在子类中可以根据实际需求对从父类继承的方法进行重新编写,称为方法的重写或覆盖。
Ø 子类中重写的方法和父类中被重写方法具有相同的方法名、参数列表,返回值类型必须和被重写方法的返回值类型相同或者是其子类。
Ø 如果子类的构造方法中没有通过super显式调用父类的有参构造方法,也没有通过this显式调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法。
Ø 抽象类不能实例化。抽象类中可以没有,可以有一个或多个抽象方法。子类必须重写所有的抽象方法才能实例化,否则子类还是一个抽象类。
Ø 用final修饰的类,不能再被继承。用final修饰的方法,不能被子类重写。用final修饰的变量将变成常量,只能赋值一次。
本章作业
一、选择题
1.给定如下Java代码,下列( )选项可以加入到Sub类中,并能保证编译正确。
class Super {
public float getNum() {
return 3.0f;
}
}
Public class Sub extends Super {
}
- public float getNum(){return 4.Of;}
- public void getNum() { }
- public getNum(double d) { }
- public double getNum(float d){return 4.Od;}
2.编译运行如下Java代码,以下说法正确的是( )。
class Base {
private String name;
public Base() {
name=“Base”;
}
Public void method() {
System.out.println(name);
}
}
class Child extends Base {
public Child() {
name=“cc”;
}
}
public class Sample
public static void main(String[ ] agrs) {
Child c=new Child();
c.method();
}
}
- 发生编译期错误
- 正常运行,输出结果:Base
- 正常运行,输出结果:Child
- 正常运行,输出结果:cc
3.在子类的构造方法中,使用( )关键字调用父类的构造方法。
A. base
B. super
C. this
D. extends
4.编译运行如下Java代码,输出结果是( )。
class Base {
private String name;
public Base() {
name=“Base constructor”;
}
public Base(String pName) {
name=pName;
}
public void method() {
System.out.println(name);
}
}
class Child extends Base {
public Child() {
super(“Child constructor”);
}
public void method() {
System.out.println(“Child method”);
}
}
public class Sample {
public static void main(String[ ] args) {
Child c=new Child();
c.method();
}
}
- Base constructor
- Child constructor
- Child method
- 以上均不正确
5.下列选项中关于Java中抽象类和抽象方法说法正确的是( )。
A. 抽象类中不可以有非抽象方法
B. 某个非抽象类的父类是抽象类,则这个类必须重载父类的所有抽象方法
C. 抽象类无法实例化
D. 抽象方法的方法体部分必须用一对大括号{ }括住
二、简答题
1、给定如下Java代码,编译运行后,输出结果是什么?并解释原因。
class Base {
public Base() {
System.out.println(“Base”);
}
}
class Child extends Base {
public Child() {
System.out.println(“Child”);
}
}
public class Sample {
public static void main(String[ ] args) {
Child c=new Child();
}
}
2.请指出如下Java代码中存在的错误,并解释原因。
class Base {
public void method() {
}
}
class Child extends Base{
public int method() {
}
private void method() {
}
public void method(String s) {
}
}
3.请指出如下Java代码中存在的错误,并改正。
class Base extends Object {
private String name;
public Base() {
name=“Base”;
}
}
class Child extends Base {
public Child() {
super(“Child”);
}
}
public class Sample {
public static void main(String[ ] args) {
Child c=new Child();
}
}
4.请指出如下Java代码中存在的错误,并解释原因。
class Other {
public int i;
}
class Something {
public static void main(String[ ] args) {
Other o=new Other();
new Something().addOne(o);
}
public void addOne(final Other 0) {
o.i++;
o=new Other();
}
}
5.设计Bird、Fish类,都继承自抽象类Animal,实现其抽象方法info(),并打印它们的信息,参考运行结果如图2.21所示。要求画出类图。
图2.21 参考运行结果
——提示———————————————————————————————————
定义抽象类Animal,具有age属性、info()方法。
定义Bird类、具有本身的特有属性、color。
定义Fish类,具有本身的特有属性weight。
———————————————————————————————————————
第3章
多 态
◇本章工作任务
Ø 通过多态实现主人给宠物喂食的功能
Ø 通过多态实现主人与宠物玩耍的功能
Ø 通过多态计算汽车租赁的总租金
◇本章技能目标
Ø 掌握多态的优势和应用场合
Ø 掌握父类和子类之间的类型转换
Ø 掌握instanceof运算符的使用
Ø 使用父类作为方法形参实现多态
本章单词
请在预习时学会下列单词的含义和发音,并填写在横线处。
- polymorphism:_______________________
- instance:____________________________
- override:____________________________
- constructor:_________________________
- ClassCastException:___________________
- upcasting:___________________________
- downcasting:________________________
- abstract:____________________________
本章简介
本章我们将学习Java中非常重要的内容——多态,多态不仅可以减少代码量,还可以提高代码的可拓展性和可维护性。使用多态实现主人给宠物喂食功能和主人与宠物玩耍功能,期间穿插多态理论的讲解。在练习阶段将使用多态完善与增加汽车租赁系统的功能,强化对该技能点的理解和运用。学习过程中要深刻体会多态的优势和应用场合。
3.1 为什么使用多态
——问题———————————————————————————————————
下面我们实现主人给宠物喂食功能,具体需求如下。
Ø 给Dog喂食,其健康值增加3,输出吃饱信息。
Ø 给Penguin喂食,其健康值增加5,输出吃饱信息。
———————————————————————————————————————
——分析———————————————————————————————————
首先采用如下步骤实现。
- 给抽象类Pet增加抽象方法eat()方法。
- 让Dog类重写Pet类的eat()方法,实现狗狗吃饭功能。
- 让Penguin类重写Pet类的eat()方法,实现企鹅吃饭功能。
- 创建主人类Master,添加feed(Dog dog)方法,调用Dog类的eat()方法,实现狗狗的喂养。添加feed(Penguin pgn)方法,调用Penguin类的eat()方法,实现企鹅的喂养。
- 创建测试类,在类中创建主人、狗狗和企鹅对象,调用相应方法实现主人喂养宠物功能。
———————————————————————————————————————
下面我们就按照分析得步骤逐步来完成该任务吧。首先给抽象类Pet增加抽象方法eat()方法,代码如示例1所示。
示例1
/**
* 宠物类,狗狗和企鹅的父类。
* @author 南京
*/
public abstract class Pet {
protected String name = "无名氏";// 昵称
protected int health = 100;// 健康值
protected int love = 0;// 亲密度
/**
* 有参构造方法。
* @param name 昵称
*/
public Pet(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
public int getLove() {
return love;
}
/**
* 输出宠物信息。
*/
public void print() {
System.out.println("宠物的自白:\n我的名字叫" + this.name +
",健康值是" + this.health + ",和主人的亲密度是"
+ this.love + "。");
}
/**
* 抽象方法eat(),负责宠物吃饭功能。
*/
public abstract void eat();
}
让Dog类重写Pet类的eat()方法,实现狗狗吃饭功能,代码如示例2所示。
示例2
/**
* 狗狗类,宠物的子类。
* @author 南京
*/
public class Dog extends Pet {
private String strain;// 品种
/**
* 有参构造方法。
* @param name 昵称
* @param strain 品种
*/
public Dog(String name, String strain) {
super(name);
this.strain = strain;
}
public String getStrain() {
return strain;
}
/**
* 重写父类的print方法。
*/
public void print(){
super.print(); //调用父类的print方法
System.out.println("我是一只 " + this.strain + "。");
}
/**
* 实现吃饭方法。
*/
public void eat() {
super.health = super.health + 3;
System.out.println("狗狗"+super.name + "吃饱啦!健康值增加3。");
}
}
让Penguin类重写Pet类的eat()方法,实现企鹅吃饭功能,代码如示例3所示。
示例3
/**
* 企鹅类,宠物的子类。
* @author 南京
*/
public class Penguin extends Pet {
private String sex;// 性别
/**
* 有参构造方法。
* @param name 昵称
* @param sex 性别
*/
public Penguin(String name, String sex) {
super(name);
this.sex = sex;
}
public String getSex() {
return sex;
}
/**
* 重写父类的print方法。
*/
public void print() {
super.print();
System.out.println("性别是 " + this.sex + "。");
}
/**
* 实现吃饭方法。
*/
public void eat() {
super.health = super.health + 5;
System.out.println("企鹅" + super.name
+ "吃饱啦!健康值增加5。");
}
}
创建主人类Master,在类中添加feed(Dog dog)方法,调用Dog类的eat()方法,实现狗狗的喂养。添加feed(Penguin pgn)方法,调用Penguin类的eat()方法,实现企鹅的喂养。代码如示例4所示。
示例4
/**
* 主人类。
* @author 南京
*/
public class Master {
private String name = "";// 主人名字
private int money = 0; // 元宝数
/**
* 有参构造方法。
* @param name 主人名字
* @param money 元宝数
*/
public Master(String name, int money) {
this.name = name;
this.money = money;
}
public int getMoney() {
return money;
}
public String getName() {
return name;
}
/**
* 主人给Dog喂食。
*/
public void feed(Dog dog) {
dog.eat();
}
/**
* 主人给Penguin喂食。
*/
public void feed(Penguin pgn) {
pgn.eat();
}
}
创建测试类,创建主人、狗狗和企鹅对象,调用相应的方法实现主人喂养宠物功能,代码如示例5所示。
示例5
/**
* 测试类,领养宠物并喂食。
* @author 南京
*/
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("欧欧", "雪娜瑞");
Penguin pgn = new Penguin("楠楠", "Q妹");
Master master=new Master("王先生",100);
master.feed(dog);//主人给狗狗喂食
master.feed(pgn);//主人给企鹅喂食
}
}
运行结果如图3.1所示。
图3.1 领养宠物并喂食
从示例5的运行结果看,已经顺利实现了主人给宠物的喂食功能。但是,如果主人又领养一只猫或者更多宠物,该如何实现给宠物喂食呢?
当然,我们可以再Master中重载feed()方法,添加一个feed(Cat cat)方法,但这样做存在以下缺点:每次领养宠物都需要修改Master类源代码,增加feed()的重载方法;如果领养宠物过多,Master类中就会有很多重载的feed()方法。
如果能实现如下效果就好了:Master类中只有一个feed()方法,可以实现对所有宠物的喂食;不管领养多少宠物,均无需修改Master类源代码。能够实现吗?答案是肯定的。
3.2 什么是多态
简单来说,多态(Polymorphism)是具有表现多种形态的能力的特征。更专业化的说法是:同一个实现接口,使用不同的示例而执行不同的操作。
图3.2将有助于讲解多态的概念。
图3.2 多态的示例图
打印机可以看作是父类,黑白打印机、彩色打印机是它的两个子类。父类打印机中的方法“打印”在每个子类中有各自不同的实现方式,比如:对黑白打印机执行打印操作后,打印效果是黑白的;而对彩色打印机执行打印操作后,打印效果时彩色的。很明显,子类分别对父类的“打印”方法进行了重写。从这里也可以看出,多态性与继承、方法重写密切相关。
如果要采用多态完善主人给宠物喂食的功能,我们必须掌握多态的如下技能。
3.2.1 子类到父类的转换(向上转型)
在《使用Java语言理解程序逻辑》中我们学习了基本数据类型之间的类型转换,例如:
//把int型常量或变量的值赋给double型变量,可以自动进行类型转换
int i = 5;
double d1 = 5;
//把double型常量或变量的值赋给int型变量,须进行强制类型转换
double d2 = 3.14;
int a = (int)d2;
实际上在引用数据类型的子类和父类之间也存在着类型转换问题。如以下代码。
Dog dog = new Dog(“欧欧“,”雪娜瑞”); //不涉及类型转换
dog.eat();
Pet pet = new Dog(“欧欧”,”雪娜瑞”); //子类到父类的转换
pet.eat(); //会调用Dog类的eat()方法,而不是Pet类的eat()方法
pet.catchingFlyDisc(); //无法调用子类特有的方法
我们可以通过进一步说明来加深对上面代码的理解。
Ø Pet pet = new Dog(“欧欧”,”雪娜瑞”);
主人需要一个宠物,一条狗狗肯定符合要求,不用特别声明,所以可以直接将子类对象赋给父类引用变量。
Ø pet.eat();
主人给宠物喂食时看到的肯定是狗狗在吃饭而不是企鹅在吃饭,也不是那个抽象的Pet在吃饭。
Ø pet.catchingFlyDisc();
假定主人可以同时为狗狗和企鹅喂食,但只能和狗狗玩接飞盘游戏,只能和企鹅玩游泳。在没有断定宠物的确是狗狗时,主人不能与宠物玩接飞盘游戏,因为他需要的是一个宠物,但是没有明确要求是一条狗狗,所以很有可能过来的是一只企鹅,因此就不能够确定是玩接飞盘还是游泳。
从上面语句中可以总结出子类转换成父类时的规则。
Ø 将一个父类的引用指向一个子类对象,称为向上转型(upcasting),自动进行类型转换。
Ø 此时通过父类引用变量调用的方法是子类覆盖或继承父类的方法,不是父类的方法。
Ø 此时通过父类引用变量无法调用子类特有的方法。
3.2.2 使用父类作为方法形参实现多态
使用父类作为方法的形参,是Java中实现和使用多态的主要方式。下面我们就通过示例6进行演示。该示例演示了不同国家人吃饭的不同形态。
示例6
class Person { //Person类
String name; //姓名
int age; //年龄
public void eat() { //吃饭
System.out.println(“person eating with mouth”);
}
public void sleep() { //睡觉
System.out.println(“sleeping in night”);
}
}
class Chinese extends Person { // 中国人类
public void eat() { // 覆盖父类的eat()方法
System.out.println(“Chinese eating rice with mouth by chopsticks”);
}
public void shadowBoxing() { //练习太极拳,子类特有的方法
System.out.println(“practice dashadowBoxing every morning”);
}
}
class English extends Person { //英国人类
public void eat() { //覆盖父类的eat()方法
System.out.println(“English eating meat with mouth by knife”);
}
}
class TestEat { //测试类
public static void main(String[] args) {//测试不同国家人吃饭
showEat(new Person());
showEat(new Chinese());
showEat(new English());
}
public static void showEat(Person person) { //显示不同国家人吃饭
person.eat();
}
// public static void showEat(Chinese Chinese) {
// Chinese.est();
// }
// public static void showEat(English english) {
// English.eat();
// }
}
运行结果如果3.3所示。
图3.3 使用父类作为方法形参实现多态
从该示例及运行结果可以看到,本示例中只使用了一个showEat()方法,使用父类作为方法形参,就可以正确显示多个国家的人的吃饭形态,无需再编写示例6中注释掉的代码,从而大大减少了代码量。
把实参赋给形参的过程中涉及了父类和子类之间的类型转换。例如调用showEat(new Chinese())会执行Person person = new Chinese()。
在showEat()方法中执行person.eat()会调用person对象真实引用的对象的eat()方法。
例如执行showEat(new English())时,person.eat()会调用English类的eat()方法。
更奇妙的是,当我们再增加法国人、埃及人时也无需添加或修改showEat()方法。
从示例6中可以看出,使用分类作为方法形参优势明显,或者说使用多态的优势明显:
可以减少代码量,可以提高代码的可拓展性和可维护性。
——总结———————————————————————————————————
通过本节对多态功能的详解,让我们总结出实现多态的三个条件。
Ø 继承的存在(继承是多态的基础,没有继承就没有多态)。
Ø 子类重写父类的方法(多态下调用子类重写后的方法)。
Ø 父类引用变量指向子类对象(子类到父类的类型转换)。
———————————————————————————————————————
学习了多态的部分功能后,下面就使用多态对主人给宠物喂食的代码进行重构,看看会有什么不同之处。按照以下步骤依次进行重构。
修改Master类,删除feed(Dog dog)和feed(Penguin pgn)方法,增加唯一的feed(Pet pet)方法,以父类Pet作为形参。如示例7所示。
示例7
/**
* 主人类。
* @author 南京
*/
public class Master {
private String name = "";// 主人名字
private int money = 0; // 元宝数
/**
* 有参构造方法。
* @param name 主人名字
* @param money 元宝数
*/
public Master(String name, int money) {
this.name = name;
this.money = money;
}
/**
* 主人给宠物喂食。
*/
public void feed(Pet pet) {
pet.eat();
}
}
修改后再次运行示例5,会得到和图3.1完全相同的结果。这是怎么回事呢?学习了多态的内容后,你一定不会再为这样的结果而感到疑惑了吧。
继续增加宠物Cat类,继承Pet类并重写eat()方法,代码如示例8所示。
示例8
/**
*猫类,宠物的子类。
*@author 南京
*/
public class Cat extends Pet {
private String color; //颜色
public Cat(String name, String color) {
super(name);
this.color = color;
}
/**
*实现吃饭方法。
*/
public void eat() {
super.health = super.health + 4;
System.out.println(“猫咪”+ super.name +“吃饱啦!体力增加4。 ”);
}
}
在Test类中添加领养猫和给猫喂食的语句,代码如示例9所示。
示例9
/**
*测试类,领养宠物并喂食。
*@author 南京
*/
public class Test {
public static void main(String[] args) {
Dog dog = new Dog(“欧欧”,”雪娜瑞”);
Penguin pgn = new Penguin(“楠楠”,”Q妹”);
Master master = new Master(“王先生”,100);
master.feed(dog); //主人给狗狗喂食
master.feed(pgn); //主人给企鹅喂食
master.feed(new Cat(“Tomcat”,”黄色”)); //主人给猫喂食
}
}
运行结果如图3.4所示。
图3.4 增加领养宠物猫并喂食
通过示例9的运行结果,我们对多态可以提高代码的可扩展性和可维护性这一特点,是不是有了更直观和更深入地理解。
多态的内容并不止以上这些,让我们借助下面的问题继续学习多态的其他内容吧。
——问题———————————————————————————————————
下面我们实现主人与宠物玩耍功能,具体需求如下。
Ø 和狗狗玩接飞盘游戏,狗狗的健康值减少10,与主人亲密度增加5。
Ø 和企鹅玩游泳游戏,企鹅的健康值减少10,与主人亲密度增加5。
———————————————————————————————————————
——分析———————————————————————————————————
采用如下思路实现。
- 给Dog添加catchingFlyDisc()方法,实现接飞盘功能。
- 给Penguin添加swimming()方法,实现游泳功能。
- 给主人添加play(Pet pet)方法,如果pet代表Dog就玩接飞盘游戏,如果pet代表 Penguin就玩游泳游戏。
- 创建测试类,其中创建主人、狗狗和企鹅对象,调用相应的方法实现主人和宠物玩耍功能。
———————————————————————————————————————
下面就按照分析得步骤逐步来完成该任务。首先给Dog添加catchingFlyDisc()方法,实现接飞盘功能,代码如示例10所示。
示例10
/**
*狗狗类,宠物的子类。
*@author 南京
*/
public class Dog extends Pet {
private String strain; //品种
public Dog(String name, String strain) {
super(name);
this.strain = strain;
}
//其他方法略
/**
*实现接飞盘方法。
*/
public void catchingFlyDisc() {
System.out.println(“狗狗” + super.name + “正在接飞盘。 ”);
super.health = super.health - 10;
super.love = super.love + 5;
}
}
给Penguin添加swimming()方法,实现游泳功能,代码如示例11所示。
示例11
/**
*企鹅类,宠物的子类。
*@author 南京
*/
public class Penguin extends Pet {
private String sex; //性别
public Penguin(String name, String sex) {
super(name);
this.sex = sex;
}
//其他方法略
/**
*实现游泳方法。
*/
public void swimming() {
System.out.println(“企鹅” + super.name + “正在游泳。 ”);
super.health = super.health - 10;
super.love = super.love + 5;
}
}
然后给主人添加play(Pet pet)方法,如果pet代表Dog,就玩接飞盘游戏;如果pet代表Penguin,就玩游泳游戏。
但是此时就出现问题了。在给宠物喂食案例中,Pet类提供eat()方法,Dog和Penguin类分别重写eat()方法,即三个类都包含同名方法eat()。但是在于宠物玩耍功能中,Dog类提供方法catchingFlyDisc(),而Penguin类提供的方法是swimming(),父类Pet没有相应的抽象方法定义。
如果要解决该问题,需要使用多态的另外一个技能:父类到子类的转换,同时会使用instanceof运算符来判断对象的类型。
3.2.3 父类到子类的转换(向下整型)
前面已经提到,当向上转型发生后,将无法调用子类特有的方法。但是如果需要调用子类特有的方法,可以通过把父类再转换为子类来实现。
将一个指向子类对象的父类引用赋给一个子类的引用,称为向下转型,此时必须进行强制类型转换。
如果把Dog对象赋给Pet类型引用变量后,又希望和Dog玩接飞盘游戏,该怎么办呢?
示例12
/**
* 测试类,测试父类到子类的转换。
* @author 南京
*/
public class TestPoly {
public static void main{String [] args){
Pet pet = new Dog( “欧欧” , “雪娜瑞”);
pet .eat();
//pet.catchingFlyDisc(); //编译错误,无法调用子类特有的方法
Dog dog =(Dog)pet; //必须进行强制类型转换
Dog .catchingFlyDisc(); //Ok!NO PROBLEM
Penguin pgn =(Penguin) pet; // 出现ClassCastException异常
Pgn . swimming(); // 上一句已经异常了,执行不到此句
}
}
示例12的运行结果如图3.5所示
图3.5 测试父类到子类的转换
从示例12及运行结果可以看出,把pet强制转换为dog后,可以访Dog类特有的玩飞盘方法。但是必须转换为父类指向的真实子类类型Dog,不是任意强制转换,比如转换为Penguin类时将出现类型转换异常ClassCastExcept。
——对比———————————————————————————————————
基本数据类型之间进行强制类型转换是在对被强转换类型“做手术”,例如:
double di=5;//对5做手术,变为5.0
int a=(int)3.14 //对3.14做手术,变为3
引用数据类型之间强制转换时是还原子类的真实面目,而不是给子类“做手术”,
例如:
Pet pet=new Dog(“欧欧” , “雪娜瑞”);
Dog dog =(Dog)pet;//正确!还原子类的真实面目
Penguin pgn =(Penguin) pet;//出现异常!给子类”做手术”了
———————————————————————————————————————
3.2.4 instanceof运算符
在示例12中进行向下转型时,如果没有转换为真实子类类型,就会出现类型转换异常。如何有效避免出现这种异常呢?Java提供了instanceof运行符类进行类型的判断。
语法
对象instanceof类或接口
该运算符用来判断一个对象是否属于一个类或者实现了一个接口,结果为true或false。在强制类型转换之前通过instanceof运算符检查对象的真实类型,然后再进行相应的强制类型转换,这样就可以避免类型转换异常,从而提高代码健壮性。
示例13
/**
* 测试instanceof运算符的使用。
* @author 南京
*/
public class TestPoly2 {
public static void main{String [] args){
pet pet = new Penguin( “楠楠” , “Q妹”);
//pet pet = new Dog( “欧欧” , “雪娜瑞”);
pet .eat();
if (pet instanceof Dog) {
Dog dog =(Dog)pet;
dog .catchingFlyDisc();
} else if (pet instanceof Penguin) {
Penguin pgn =( Penguin) pet;
Pgn . swimming()
}
}
}
运行结果如图3.6所示。
图3.6 测试instanceof运算符的使用(一)
注释示例13中创建Penguin对象语句,取消创建Dog对象语句的注释,再次运行该示例,结果如图3.7所示。
图3.7 测试instanceof运算符的使用(二)
通过该示例我们可以发现,在进行引用类型转换时,首先通过instanceof运算符进行类型判断,然后进行相应的强制类型转换,这样可以有效地避免出现类型转换异常。
——资料———————————————————————————————————
使用instanceof时,对象的类型必须和instanceof的第二个参数所指定的类或接口在继承树上有上下级关系,否则会出现编译错误。例如:pet instanceof String,会出现编译错误。
instanceof通常和强制类型转换结合使用。
———————————————————————————————————————
下面就采用多态的相关技能实现主人与宠物玩耍的功能,代码如示例14所示。给主人类添加play(Pet pet)方法,如果pet代表Dog,就玩飞盘游戏;如果pet代表Penguin,就玩游泳游戏。
示例14
/**
* 主人类
* @author 南京
*/
public class Master {
private String name =“”;//主人名字
private int money=0;//元宝数
public Master(String name, int money) {
this.name = name;
this.money = money;
}
/**
*主人与宠物玩耍
*/
public void play(Pet pet) {
If (pet instanceof Dog) {//如果传入的是狗狗
Dog dog = (Dog) pet
Dog.catchingFlyDisc()
}
else if (pet instanceof penguin ) {//如果传入的是企鹅
Penguin pgn=(Penguin)pet;
Pgn . swimming
}
}
}
创建测试类,实现主人和宠物玩耍功能,代码如示例15所示。
示例16
/**
* 测试类,领养宠物并玩耍。
* @author 南京
*/
public class Test {
public static void main{String [] args){
Dog dog = new Dog( “欧欧” , “雪娜瑞”);
Penguin pgn = new Penguin( “楠楠” , “Q妹”);
Master master=new master(“王先生”,100);
master.play(dog);//狗狗接飞盘
master.play(pgn);//企鹅游泳
}
}
运行结果如图3.8所示。
图3.8领养宠物并玩耍
3.3 上机练习
上机练习1
练习——使用多态实现主人给宠物喂食功能
训练要点
Ø 子类到父类的自动类型转换。
Ø 使用父类作为方法形参实现多态。
Ø 多态可以减少代码量,可以提高代码的可扩展性和可维护性。
需求说明
给狗狗喂食,其健康值增加3,输出吃饱信息;给企鹅喂食,其健康值增加5,输出吃饱信息。增加宠物猫并喂食,其健康值增加4,输出吃饱信息,实现以上功能。
实现思路及关键代码
(1)给抽象类Pet增加抽象方法eat()方法。
(2)让Dog类重写Pet类的eat()方法,实现狗狗吃饭功能。
(3)让Penguin类重写Pet类的eat()方法,实现企鹅吃饭功能。
(4)创建主人类Master,添加feed(Pet pet)方法,在该方法中调用相应宠物eat()方法,实现宠物的喂养。
(5)创建测试类Test,在类中创建主人,狗狗和企鹅对象,调用feed(Pet pet)实现主人喂养宠物功能。
(