Java实现面向对象编程

目录

第1章... 10

抽象和封装... 10

1.1用面向对象设计电子宠物系统... 14

1.1.1为什么使用面向对象... 14

1.1.2使用面向对象进行设计... 15

1.2通过创建对象实现领养宠物功能... 17

1.2.1创建类的对象... 17

1.2.2构造方法及其重载... 23

1.2.3常见错误... 28

1.3使用封装优化电子宠物系统的类... 30

1.4上机练习... 38

上机练习1. 38

上机练习2. 39

上机练习3. 39

上机练习4. 39

本章总结... 40

本章作业... 41

一、  选择题... 41

二 、简答题... 43

第2章... 45

继  承... 45

本章单词... 46

2.1  继承基础... 47

2.2  重写和继承关系中的构造方法... 53

2.2.1  子类重写父类方法... 53

2.2.2  继承关系中的构造方法... 56

2.2.3  上机练习... 59

上机练习1. 59

指导——创建宠物对象并输出信息... 59

2.3  抽象类和final 60

2.3.1  抽象类和抽象方法... 60

2.3.2  上机练习... 64

上机练习2. 64

指导——修改Pet类为抽象类,强迫子类实现print()方法... 64

2.3.3  final修饰符... 64

2.3.4  常见错误... 65

1.final修饰引用型变量,变量所指对象的属性值是否能改变... 65

2.abstract是否可以和private、static、final共用... 66

2.4  综合练习:实现汽车租赁系统计价功能... 67

本章总结... 70

本章作业... 71

一、选择题... 71

二、简答题... 73

第3章... 76

多    态... 76

3.1  为什么使用多态... 78

3.2  什么是多态... 83

3.2.1  子类到父类的转换(向上转型)... 83

3.2.2  使用父类作为方法形参实现多态... 84

3.2.3  父类到子类的转换(向下整型)... 89

3.2.4   instanceof运算符... 91

3.3 上机练习... 93

上机练习1. 93

练习——使用多态实现主人给宠物喂食功能... 93

上机练习2. 94

练习——使用多态实现主人与宠物玩耍功能... 94

3.4  综合练习:使用多态完善汽车租赁系统计价功能... 95

上机练习3. 95

指导——计算一次租赁多辆汽车的总租金... 95

上机练习4. 96

指导——增加租赁卡车业务,计算汽车租赁的总租金... 96

本章总结... 98

本章作业... 99

一、选择题... 99

二、简答题... 101

第4章... 105

接 口... 105

4.1  接口基础知识... 107

4.2  接口表示一种约定... 110

上机练习1. 114

指导——采用面向接口编程思想书写一封家书... 114

4.3  接口表示一种能力... 116

上机练习2. 120

练习——软件工程师编写代码、讲解业务... 120

4.4  在C#中使用接口... 120

上机练习3. 124

指导——打印机支持不同墨盒和纸张类型... 124

本章总结... 126

本章作业... 127

一、选择题... 127

二、简答题... 128

第5章... 131

项目案例:QuickHit 131

5.1  案例分析... 133

5.1.1  需求概述... 133

5.1.2  开发环境... 133

5.1.3  案例覆盖的技能点... 133

5.1.4  问题分析... 134

5.2  项目需求... 137

5.2.1  用例1:游戏输出字符串... 137

5.2.2  用例2:确认输入并输出结果... 138

5.2.3  用例3:玩家玩游戏... 139

5.2.4  用例4:初始化各个级别的具体参数... 140

5.3  进度记录... 142

第6章... 143

指导学习:动物乐园... 143

6.1  复习串讲... 144

6.1.1  难点突破... 144

6.1.2 知识梳理... 144

6.2  综合练习... 146

6.2.1  任务描述... 146

6.2.2  练习... 146

阶段1:指导——设计猫和鸭的类结构,画出类图并写出代码... 146

阶段2:指导——增加新成员海豚,重新设计类结构... 147

阶段3:练习——输出各种动物如何叫... 149

第7章... 151

在线培训:面向对象设计... 151

7.1  学习任务... 152

任务一:制作PPT,讲解类和类之间的关系... 152

任务二:制作PPT,讲解面向对象设计的基本原则... 152

任务三:使用类图设计佩恩沃星球... 153

7.2  参考资料... 154

1,推荐网站... 154

2,搜索关键字... 154

第8章... 155

异    常... 155

8.1异常概述... 157

8.1.1  生活中的异常... 157

8.1.2程序中的异常... 157

8.1.3什么是异常... 160

8.2异常处理... 160

8.2.1  什么是异常处理... 160

8.2.2  try-catch块... 160

8.2.3  try-catch-finally块... 163

8.2.4  多重catch块... 165

8.2.5  上机练习... 168

上机练习1. 168

练习——根据输入的课程编号输出相应的课程名称... 168

8.2.6  声明异常——throws. 168

8.3  抛出异常... 170

8.3.1  抛出异常——throw.. 170

8.3.2  异常的分类... 172

8.3.3上机练习... 174

上机练习2. 174

练习——使用throw抛出异常... 174

8.4开源日志记录工具log4j 175

8.4.1日志及分类... 175

8.4.2  如何使用log4j记录日志... 176

8.4.3  log4j配置文件... 179

8.4.4  上机练习... 181

上机练习3. 181

指导——使用log4j输出异常日志到控制台... 181

上机练习4. 181

练习——使用log4j记录日志到文件... 181

本章总结... 183

本章作业... 184

一、选择题... 184

二、简答题... 186

第 9 章... 188

集 合 框 架... 188

9.1  集合框架概述... 190

9.1.1  引入集合框架... 190

9.1.2  Java集合框架包含的内容... 190

9.2  List接口... 192

9.2.1  ArrayList集合类... 192

上机练习1. 196

练习——添加多个企鹅信息到List中... 196

9.2.2  LinkedList集合类... 197

9.3  Map接口... 199

上机练习2. 201

练习——根据宠物昵称查找宠物... 201

9.4  迭代器Iterator 202

上机练习3. 203

练习——使用Iterator迭代显示存储在List中的企鹅信息... 203

本章总结... 207

本章作业... 208

一、选择题... 208

二、简答题... 209

第10章... 211

JDBC. 211

10.1  JDBC简介... 213

10.1.1  为什么需要JDBC. 213

10.1.2     JDBC的工作原理... 213

10.1.3     JDBC API介绍... 214

10.1.4 JDBC访问数据库的步骤... 215

10.2       Connection接口... 216

10.2.1               两种常用的驱动方式... 216

10.2.2               使用JDBC-ODBC桥方式连接数据库... 216

10.2.3               使用纯Java方式连接数据库... 218

10.2.4               上机练习... 220

上机练习1. 220

练习——使用纯Java方式连接数据库,并进行异常处理... 220

10.3             Statement接口和ResultSet接口... 220

10.3.1               使用Statement添加宠物... 221

10.3.2               使用Statement更新宠物... 223

10.3.3               使用Statement和ResultSet查询所有宠物... 224

10.3.4上机练习... 227

上机练习2. 227

指导——查询所有宠物主人信息... 227

10.4         PreparedStatement接口... 228

10.4.1               为什么要使用PreparedStatement 228

10.4.2               使用PreparedStatement更新宠物信息... 230

10.4.3               上机练习... 232

上机练习3. 232

指导——使用PreparedStatement插入宠物信息... 232

本章总结... 234

本章作业... 235

一、选择题... 235

二、简答题... 236

第 11 章... 238

Oracle基础... 238

11.1  Oracle基础知识... 240

11.1.1  Oracle简介... 240

11.1.2  Oracle基本概念... 241

11.1.3  安装Oracle. 242

11.2  创建数据库和用户... 246

11.2.1  创建数据库... 246

11.2.2  登录管理后台... 254

11.2.3  创建表空间... 256

11.2.4  创建用户并授予权限... 257

11.2.5  上机练习... 259

上机练习1. 259

练习——创建数据库LEDB. 259

上机练习2. 259

练习——创建用户epet 259

11.3  创建数据库表... 260

11.3.1  Oracle数据类型... 260

11.3.2  创建数据库表的方法... 261

11.3.3  创建和使用序列... 263

11.3.4  上机练习... 265

上机练习3. 265

练习——创建数据库表... 265

上机练习4. 265

练习——创建序列,向数据库表录入测试数据... 265

本章总结... 266

本章作业... 267

一、选择题... 267

二、简答题... 267

第12章... 269

Oracle应用... 269

12.1使用JDBC访问Oracle. 271

12.2 Oracle常用函数... 275

上机练习1. 275

指导——主人登录成功后,显示主人的所有宠物信息... 275

上机练习2. 277

练习——主人登录成功后,领养宠物... 277

上机练习3. 281

练习——Oracle函数练习... 281

12.3  Oracle索引... 282

12.3.1  Oracle索引类型... 282

12.3.2  创建和删除索引... 283

12.4 Oracle中数据的导入导出... 284

12.4.1  使用imp和exp导入导出数据... 284

12.4.2  使用PL/SQL  Developer导入导出数据... 286

本章总结... 288

本章作业... 289

一、选择题... 289

二、简答题... 290

第 13 章... 291

数据访问层... 291

13.1  数据持久化... 293

13.2  上机练习... 299

上机练习1. 299

练习——定义MasterDao接口和MasterDaoJabcOracleImpl实现类... 299

上机练习2. 300

指导——调用DAO类实现主人登录... 300

13.3  分层开发... 302

13.3.1  分层开发的优势... 302

13.3.2  分层的原则... 303

13.3.3  使用实体类传递数据... 303

13.4  上机练习... 305

上机练习3. 305

指导——记录车辆购置税... 305

本章总结... 307

本章作业... 308

一、选择题... 308

二、简答题... 308

第14章... 310

XML和File I/O.. 310

14.1  XML简介... 312

14.1.1  XML定义... 312

14.1.2  XML结构定义... 313

14.1.3  XML的作用... 318

14.1.4  XML和CSS共同使用... 318

14.2  解析XML. 319

14.2.1  使用DOM解析XML. 320

14.2.2  使用SAX解析XML. 322

上机练习1. 323

指导——根据DTD定义编写XML文档,存放宠物初始信息... 323

上机练习2. 324

指导——使用DOM解析存储宠物初始信息的XML文档... 324

14.3  读写文件... 325

14.3.1  使用Reader读取文件内容... 325

上机练习3. 327

指导——读取模板文件内容并输出... 327

14.3.2  替换模板文件中的占位符... 329

上机练习4. 329

指导——替换模板文件中的占位符... 329

14.3.3  使用Writer输出内容到文件... 330

上机练习5. 331

指导——写宠物信息到文本文件... 331

14.3.4  综合练习... 331

本章总结... 334

本章作业... 335

一.选择题... 335

二.简答题... 336

第 15 章... 337

项目案例:宠物商店... 337

15.1  案例分析... 339

15.1.1  需求概述... 339

15.1.2  开发环境... 339

15.1.3  案例覆盖的技能点... 339

15.1.4  问题分析... 340

15.2  项目需求... 342

15.2.1  用例1:系统启动... 342

15.2.2  用例2:宠物主人登录... 343

15.2.3  用例3:宠物主人购买库存宠物... 345

15.2.4  用例4:宠物主人购买新培育宠物... 345

15.2.5  用例5:宠物主人卖出宠物给商店... 345

15.3  进度记录... 346

第16章... 347

指导学习:课程总复习... 347

16.1  复习串讲... 348

16.1.1  核心技能目标... 348

16.1.2  知识梳理... 348

16.2  综合练习... 351

16.2.1  任务描述... 351

16.2.2  任务分析... 351

16.2.3  练习... 352

阶段1:练习——创建数据库表news. 352

阶段2:练习——创建HTML模板文件... 352

阶段3:指导——从数据库读取新闻信息,保存在泛型集合中... 353

阶段4:指导——读取模板文件... 354

阶段5:指导——编写生成HTML文件的方法... 354

阶段6:指导——遍历集合,生成HTML文件... 354

 

 

 

 

1章

抽象和封装

 

 

 

◇本章工作任务

 

Ø    用类图描述电子宠物系统的设计

Ø    编写代码实现领养宠物功能

 

 

◇本章技能目标

 

Ø    使用类图描述设计

Ø    掌握面向对象设计的基本步骤

Ø    掌握类和对象的概念

Ø    掌握构造方法及其重载

Ø    掌握封装的概念及其使用

 

 

 

本章单词

 

 

 

 

 

 

请在预习时学会下列单词的含义和发音,并填写在横线处。

 

  1. class:_____________________________
  2. object:____________________________
  3. static:_____________________________
  4. final:______________________________
  5. private:____________________________
  6. public:_____________________________
  7. protect:____________________________
  8. overloading:________________________
  9. constructor:________________________
  10. 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常见错误

  1. 在类中可以定义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变量,也就是说类变量不能是局部变量。

  1. 给构造方法加上返回值类型会出现什么情况

常见错误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做如下修改。

  1. 去掉所有setter方法,保留所有的getter方法。
  2. 提供有name和strain两个参数的构造方法实现对昵称和品种的赋值。
  3. 提供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

指导——领养宠物并打印宠物信息

训练要点

Ø    类的结构。

Ø    对象的创建,类的属性和方法的调用。

需求说明

根据控制台信息选择领养宠物为狗狗,输入昵称、品种等信息,然后打印宠物信息表示领养成功。

实现思路及关键代码

  1. 创建Dog类,定义属性和方法,定义print()方法,定义默认构造方法。
  2. 编写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

 

、简答题

  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”);

      }

}

  1. 编写一个类Student1,代表学员,要求如下。

Ø    具有属性:姓名、年龄,其中年龄不能小于16岁,否则输出错误信息。

Ø    具有方法:自我介绍,负责输出该学员的姓名、年龄。

编写测试类Student1Test进行测试,看是否符合需求。

——提示———————————————————————————————————

Ø    在学员类的SetAge()方法中验证年龄大小。

Ø    在测试类中分别测试学员年龄小于16岁、大于16岁时的输出结果。

———————————————————————————————————————

  1. 请指出下面代码中存在的错误,并说明错误原因。

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);

}

  1. 编写一个类Student2,代表学员,要求如下。

Ø    具有属性:姓名、年龄、性别和专业。

Ø    具有方法:自我介绍,负责输出该学员的姓名、年龄、性别以及专业。

Ø    具有两个带参构造方法:第一个构造方法中,设置学员的性别为男,专业为LE,其余属性的值由参数给定;第二个构造方法中,所有属性的值都由参数给定。

编写测试类Student2Test进行测试,分别以两种方式完成对两个Student2对象的测试化工作,并分别调用它们的自我介绍方法,看看输出结果是否正确。

——提示———————————————————————————————————

在学员类中定义两个构造方法完成初始化工作。

public  Student2(String  name, int  age) {}

public  Student2(String  name,int  age, String  sex, String  subject) {}

———————————————————————————————————————

  1. 简述类的封装的定义、具体步骤和好处。

 

 

 

 

          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键),执行控制程序,期间主要执行步骤如下。

  1. 进入Penguin构造方法,如图2.7所示。

 

图2.7  继承条件下构造方法执行过程图2

  1. 进入父类Pet构造方法,如图2.8所示。

图2.8  继承条件下构造方法执行过程图3

  1. 返回到Penguin构造方法继续执行,如图2.9所示。

 

图2.9  继承条件下构造方法执行过程图4

 

  1. 执行完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  采用继承优化后的类图

实现思路及关键代码

  1. 创建Pet类,定义属性和方法,定义print()方法,定义无参和有参构造方法。
  2. 创建Dog类,继承Pet类,增加strain属性及相应的getter方法。
  3. 创建Penguin类,继承Pet类,增加sex属性及相应的getter方法。
  4. 创建测试类Test,在测试类中创建Dog、Penguin对象,打印出相应宠物信息。
  5. 在Dog类和Penguin类中增加print()方法,实现子类对父类方法的覆盖。
  6. 运行测试类Test打印宠物信息,观察不同之处。
  7. 在测试类中设置断点,观察创建子类对象时的执行过程。

注意编写注释。

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对象并输出信息。

实现思路及关键代码

  1. 修改Pet类为抽象类,修改print()为抽象方法。
  2. 修改Dog类继承Pet类,重写print()方法。
  3. 修改测试类Test,创建Dog对象并输出对象信息。
  4. 注释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的使用正确的是(  )。

  1. private  abstract  void  sleep();
  2. static  abstract  void  sleep();
  3. final   abstract  void  sleep();
  4. 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  {

 

}

  1. public  float  getNum(){return  4.Of;}
  2. public  void  getNum() { }
  3. public  getNum(double  d)  { }
  4. 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();

   }

}

  1. 发生编译期错误
  2. 正常运行,输出结果:Base
  3. 正常运行,输出结果:Child
  4. 正常运行,输出结果: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();

   }

}

  1. Base  constructor
  2. Child  constructor
  3. Child  method
  4. 以上均不正确

 

 

 

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运算符的使用

Ø    使用父类作为方法形参实现多态

 

 

 

本章单词

 

 

 

 

 

 

请在预习时学会下列单词的含义和发音,并填写在横线处。

 

  1. polymorphism:_______________________
  2. instance:____________________________
  3. override:____________________________
  4. constructor:_________________________
  5. ClassCastException:___________________
  6. upcasting:___________________________
  7. downcasting:________________________
  8. abstract:____________________________

 

 

 

本章简介

 

本章我们将学习Java中非常重要的内容——多态,多态不仅可以减少代码量,还可以提高代码的可拓展性和可维护性。使用多态实现主人给宠物喂食功能和主人与宠物玩耍功能,期间穿插多态理论的讲解。在练习阶段将使用多态完善与增加汽车租赁系统的功能,强化对该技能点的理解和运用。学习过程中要深刻体会多态的优势和应用场合。

 

3.1  为什么使用多态

——问题———————————————————————————————————

下面我们实现主人给宠物喂食功能,具体需求如下。

Ø    给Dog喂食,其健康值增加3,输出吃饱信息。

Ø    给Penguin喂食,其健康值增加5,输出吃饱信息。

———————————————————————————————————————

——分析———————————————————————————————————

首先采用如下步骤实现。

  1. 给抽象类Pet增加抽象方法eat()方法。
  2. 让Dog类重写Pet类的eat()方法,实现狗狗吃饭功能。
  3. 让Penguin类重写Pet类的eat()方法,实现企鹅吃饭功能。
  4. 创建主人类Master,添加feed(Dog dog)方法,调用Dog类的eat()方法,实现狗狗的喂养。添加feed(Penguin pgn)方法,调用Penguin类的eat()方法,实现企鹅的喂养。
  5. 创建测试类,在类中创建主人、狗狗和企鹅对象,调用相应方法实现主人喂养宠物功能。

———————————————————————————————————————

 

下面我们就按照分析得步骤逐步来完成该任务吧。首先给抽象类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。

———————————————————————————————————————

——分析———————————————————————————————————

采用如下思路实现。

  1. 给Dog添加catchingFlyDisc()方法,实现接飞盘功能。
  2. 给Penguin添加swimming()方法,实现游泳功能。
  3. 给主人添加play(Pet  pet)方法,如果pet代表Dog就玩接飞盘游戏,如果pet代表     Penguin就玩游泳游戏。
  4. 创建测试类,其中创建主人、狗狗和企鹅对象,调用相应的方法实现主人和宠物玩耍功能。

———————————————————————————————————————

下面就按照分析得步骤逐步来完成该任务。首先给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)实现主人喂养宠物功能。