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

扫描二维码关注公众号,回复: 2219943 查看本文章

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)实现主人喂养宠物功能。

(6)增加宠物Cat类,继承Pet类,重写eat()方法。

(7)在测试类Test类中添加领养猫和给猫喂食语句,执行Test类,观察运行结果。

 

上机练习2

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

 

训练要点

Ø    父类到子类的强制类型转换。

Ø    instanceof运算符的使用

 

需求说明

主人和狗狗玩接飞盘游戏,狗狗健康值减少10,与主人亲密度增加5;主人与企鹅玩游泳游戏,企鹅健康值减少10,与主人亲密度增加5。实现主人和狗狗,企鹅玩耍功能。

 

实现思路及关键代码

(1)   给Dog类添加catchingFlyDisc()方法,实现接飞盘功能。

(2)   给Penguin类添加swimming()方法,实现游泳功能。

(3)   给主人添加play(Pet pet)方法,如果pet代表Dog就玩接飞盘游戏;如果pet代表Penguin就玩游泳游戏。

(4)   创建测试类Test,在类中创建主人,狗狗和企鹅对象,调用相应方法实现玩耍功能。

注意编写注释。

 

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

上机练习3

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

 

训练要点

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

Ø    使用多态减少代码量。

 

需求说明

在上一章中我们已经实现了汽车租赁系统的简单计价功能,客户可以租赁一辆某种型号的汽车若干天。现在增加业务需求;客户可以一次租赁多辆不同品牌的不同型号的汽车若干天(一个客户一次租赁的各汽车的租赁天数均相同),要求计算出总租赁价。程序运行结果如图3.9所示。

 

 

图3.9 计算汽车租赁的总租金

 

实现思路及关键代码

(1)在第二章“汽车租赁系统计价功能”的基础上进行完善,可以复用该练习的代码。

(2)   创建顾客类Customer,提供calcTotalRent(MotoVehicle motos[],int days)方法,传入的参数是客户租赁的汽车列表信息和租赁天数,在方法中调用相应汽车的calRent(int days)方法得到相应租赁价,求和计算出多辆汽车总租赁价格。

(3)编写测试类TestRent,指定要租赁的多辆汽车信息,并存储在一个MotoVehicle类型数组motos中,指定租赁天数days,调用Customer类的calcTotalRent(MotoVehicle motos[],int days)方法计算出总租赁价格并输出。

 

参考解决方案

Custemer类的关键代码如下。

//计算多辆汽车总租赁价格

public int calcTotalRent(MotoVehicle motos[],int days){

       int sum=0;

       for(int i=0;i<motos.length;i++)

           sum+=motos[i].calRent(days);

       return sum;

}

TestRent类的关键代码如下。

 

int days;//租赁天数

int totalRent;总租赁费用

//1、客户租赁的多辆汽车信息及租赁天数

MotoVehicle motos[]=new MotoVehicle[4]

motos[0]=new Car(“京NY28588”,“宝马”,“550i”);

motos[1]=new Car(“京NNN3284”,“宝马”,“550i”);

motos[2]=new Car(“京NT43765”,“别克”,“林荫大道”);

motos[3]=new Bus(“京5643765”,“金龙”,34);

days = 5;

 

上机练习4

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

 

训练要点

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

Ø    使用多态增强系统的扩展性和可维护性。

 

需求说明

汽车租赁公司业务扩展,增加租赁卡车业务,租赁费用以吨为计算,每吨每天计价50元,要求对系统进行扩展,计算汽车租赁的总租金。程序运行结果如图3.10所示。

 

 

图3.10 增加租赁卡车业务后计算汽车租赁的总租金

 

实现思路及关键代码

(1)   在本章上机练习3的基础上进行完善,重用其代码。

(2)   增加卡车类Truck,继承类MotoVehicle,重写其calrent(int days)方法。

(3)   修改测试类Testrent,增加租赁卡车的信息,指定租赁天数,调用Customer类的calcTotalRent(MotoVehicle motos[],int days)方法计算出总租赁价格并输出。

 

参考解决方案

Truck 类的关键代码如下。

/**

 * 计算卡车的租赁价。

 */

public int calRent(int days){

return tonnage * 50 * days;//tonnage代表吨位

}

 

 

本章总结

 

Ø    通过多态可以减少类中代码量,可以提高代码的可扩展性和可维护性。继承是多态的基础,没有继承就没有多态

Ø    把子类转换为父类,称为向上转型,自动进行类型转换。把父类转换为子类,称为向下转型,必须进行强制类型转换。

Ø    向上转型后通过父类引用变量调用的方法是子类覆盖或继承父类的方法,通过父类引用变量无法调用子类特有的方法。

Ø    向下转型后可以访问子类特有的方法。必须转换为父类指向的真实子类类型,否则将出现类型转换异常ClassCastException。

Ø    instanceof运算符用于判断一个对象是否属于一个类或实现了一个接口。

Ø    instanceof运算符通常和强制类型转换结合使用,首先通过instanceof进行类型判断,然后进行相应的强制类型转换。

Ø    使用父类作为方法形参是使用多态的常用方式。

 

 

 

本章作业

 

一、选择题

1.编译运行如下Java代码,输出结果是(  )

class Base{

public void method(){

      System.out.print (“Base method”);

}

}

class Child extends Base{

public void method(){

      System.out.print (“Child method”);

}

}

class sample{

public static void main(String[] args) {

       Base base=new child();

       base.method()

}

}

A.  Base method

B.  Child method

C.  Base method Child method

D.  编译错误

 

2.编译运行如下Java代码,输出结果是(  )

class Base{

public void method(){

        System.out.print (“Base method”);

}

}

class Child extends Base{

public void methodB(){

    System.out.print (“Child methodB”);

}

}

class sample{

public static void main(String[] args) {

       Base base=new Child();

       base.method();

}

}

A. Base method

B. Child methodB

C. Base method Child MethodB

D. 编译错误

 

3.编译运行如下Java代码,输出结果是(  )

class Base{

public void method(){

        System.out.print (“Base method”);

}

}

class Child extends Base{

public void methodB(){

        System.out.print (“Child methodB”);

}

}

class sample{

public static void main(String[] args) {

        Base base=new Child();

        base.methodB()

}

}

A.Base method

B.Child methodB

C.Base method Child MethodB

D.编译错误

 

4.编译运行如下Java代码,输出结果是(  )

class Person{

String name=“Person”;

public void shout(){

System.out.print (name)

}

}

class Student extends Person{

String name=“student”;

String school=“school”;

}

class Test{

public static void main(String[] args) {

         Person p=new Student();

         p.shout();

}

}

A.person

B.student

C.person student

D.编译错误

 

5.下列Java代码中Test类中的四个输出语句的输出结果依次是(  )

class Person{

String name=“Person”;

public void shout(){

System.out.println (name)

}

}

class Student extends Person{

String name=“student”;

String school=“school”;

}

class Test{

public static void main(String[] args) {

        Person p=new Student();

       System.out.print (p instanceof student)

       System.out.print (p instanceof Person)

       System.out.print (p instanceof Object)

       System.out.print (p instanceof System)

}

}

A.true、false、true、false

B.false、true、false、false

C.true、true、true、编译错误

D.true、true、false、编译错误

 

二、简答题

1.简述多态的概念,子类和父类之间转换是遵循的规则。

2.请指出如下Java代码中存在的错误,并改正。

class LeTeacher{

   public void giveLesson(){

        System.out.println(“知识点讲解”);

        System.out.println(“总结提问”);

}

}

class LeDBTeacher extends LeTeacher {

   public void giveLesson(){

        System.out.println(“启动Sqlserver”);

        Super.giveLesson();

}

public void sayHi(){

System.out.println(“Hi”);

}

}

class Test{

public static void main(String[] args) {

LeTeacher t = new LeDBTeacher ();

t.sayHi();

t.giveLesson();

}

}

3.请指出如下Java代码中存在的错误,并解释原因。注释错误语句后的输出结果是什么,并解释原因。

class 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 (){

System.out.println (“Chinese eating rice with mouth by chopsticks”);

}

public void shadowBoxing(){

System.out.println (“practice shadowBoxing every morning”);

}

}

class Test {

   public static void main(String[] args) {

       Chinese ch=new Chinese();

       ch.eat();

       ch.sleep();

       ch.shandowBoxing();

       Person p=nex Chinese();

       p.eat();

       p.sleep();

       p.shandowBoxing();

}

}

 

4.请指出如下Java代码中存在错误,并解释原因,注释错误语句后的输出结果是什么,并解释原因。

abstract class Shape { // 几何图形

   public abstract double getArea();

}

Class Square extends Shape{

private double height = 0;//正方形的边长

public Square (double height){

      this.height=height;

}

public double getArea(){

      return (this.height * this.height);

}

}

Class Circle extends Shape{

private double r =0;//圆的半径

private final static double PI=3.14;//圆周率

public Circle (double r){

      this.r=r;

}

public double getArea(){

      return (PI * r * r);

}

}

Class TestShape{

public static void main (String[] args) {

      Shape square=new Square(3);

      Shape circle=new Circle(2);

      System.out.println(square.getArea());

      System.out.println(circle.getArea());

      Square  sq =(Square) circle();

      System.out.println(sq.getArea());

}

}

5.编码创建一个打印机类Printer,定义抽象方法print();创建两个子类——针式打印机类DotMatrixPrinter和喷墨打印机类InkpetPrinter,并在各自类中重写方法print(),编写测试类实现两种打印机打印。再添加一个激光打印机类LaserPrinter,重写方法print()。修改测试类实现该打印机打印。

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

利用向上转型,将子类对象赋给父类Printer的引用变量。

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

 

            4章

                

 

 

本章工作任务

Ø 使用接口思想设计USB设备、打印机

Ø 使用接口思想设计家书、软件工程师

本章技能目标

Ø 掌握接口的基础知识

Ø 掌握接口表示“一种约定”的含义

Ø 掌握接口表示“一种能力”的含义

 

 

 

 

 

 

本章单词

 

 

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

                                      1.interface:                                           

                                      2.public:                                      

                                      3.static:                                              

                                      4.final:                                       

                                      5.abstract:                                        

                                      6.implement:                                      

                                   

                                    

 

本章简介

本章讲解Java中非常重要的内容——接口,接口和多态及抽象类有非常密切的关系。首先讲解接口的基础知识,然后对接口进行深入讲解,并结合案例从“接口表示一种约定”和“接口表示一种能力”的角度理解接口的应用场合,最后简单讲解C#中接口的使用,通过对比学习牢固掌握接口在两种语言中的使用。讲解过程会涉及多个案例,通过多练习来加深对接口的理解和掌握。

4.1  接口基础知识

在第二章中我们学习了抽象类的知识,如果抽象类中所有的方法都是抽象方法,就可以使用Java提供的接口来表示。从这个角度来讲,接口可以看作是一种特殊的“抽象类”,但是采用与抽象类完全不同的语法来表示,两者的设计理念也是不同的。

下面通过生活中USB接口及其实现的例子开始Java接口的学习。

USB接口实际上是某些企业和组织等制定的一种约定或标准,规定了接口的大小、形状、各引脚信号电平的范围和含义、通信速度、通信流程等,按照该约定设计的各种设备,例如U盘、USB风扇、USB键盘都可以插到USB口上正常工作,如图4.1所示。

 

 

图4.1  生活中的USB接口

在现实生活中,相关工作是按照如下步骤进行的。

  1. 约定USB接口标准。
  2. 制作符合USB接口约定的各种具体设备。
  3. 把USB设备插到USB口上进行工作。

下面通过编写Java代码来模拟以上过程。首先定义USB接口,通过service()方法提供服务,这时会用到Java中接口的定义语法,代码如示例1所示。

 

 

示例1

 

/**

 * USB接口。

 * @author 南京

 */

public interface UsbInterface {

    /**

     * USB接口提供服务。

     */

    void service();

}

定义U盘类,实现USB接口,进行数据传输,代码如示例2所示。

示例2

/**

 * U盘。

 * @author 南京

 */

public class UDisk implements UsbInterface {

    /*

     * (non-Javadoc)

     * @see cn.le.usb.UsbInterface#service()

     */

    public void service() {

        System.out.println("连接USB口,开始传输数据。");

    }

}

定义USB风扇类,实现USB接口,获得电流让风扇转动,代码如示例3所示。

示例3

/**

 * USB风扇。

 * @author 南京

 */

public class UsbFan implements UsbInterface {

    /*

     * (non-Javadoc)

     * @see cn.le.usb.UsbInterface#service()

     */

    public void service() {

        System.out.println("连接USB口,获得电流,风扇开始转动。");

    }

}

编写测试类,实现U盘传输数据,实现USB风扇转动,代码如示例4所示。

示例4

/**

 * 测试类。

 * @param args

 */

public class Test {

    public static void main(String[] args) {

       

        //1、U盘

        UsbInterface uDisk = new UDisk();

       uDisk.service();

       

        //2、USB风扇

        UsbInterface usbFan= new UsbFan();

       usbFan.service();

    }

}运行结果如图4.2所示。

图4.2  示例4的运行结果

通过该案例我们学习了Java中接口的定义语法和类实现接口的语法,这些技能在后面章节中将被反复用到。

语法

[修饰符]  interface  接口名  extends  父接口1,父接口2,……{

常量定义

方法定义

}

语法

class  类名  extends  父类名  implements  接口1,接口2,……{

类的内容

}

说明如下。

Ø 接口和类、抽象类是一个层次的概念,命名规则相同。如果修饰符是public,则该接口在整个项目中可见。如果省略修饰符,则该接口只在当前包可见。

Ø 接口中可以定义常量,不能定义变量。接口中属性都会自动用public  static  final修饰,即接口中属性都是全局静态常量。接口中的常量必须在定义时指定初始值。

public  static  final  int  PI=3.14;

int  PI=3.14;//在接口中,这两个定义语句效果完全相同

int  PI;//错误!在接口中必须指定初始值,在类中会有默认值

Ø 接口中所有方法都是抽象方法。接口中方法都会自动用public  abstract修饰,即接口中只有全局抽象方法。

Ø 和抽象类一样,接口同样不能实例化,接口中不能有构造方法。

Ø 接口之间可以通过extends实现继承关系,一个接口可以继承多个接口,但接口不能继承类。

Ø 一个类只能有一个直接父类,但可以通过implements实现多个接口。类必须实现接口的全部方法,否则必须定义为抽象类。类在继承父类的同时又实现了多个接口时,extends必须位于implements之前。

4.2  接口表示一种约定

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

要求实现打印机打印功能。打印机的墨盒可能是彩色的,也可能是黑白的,所用的纸张可以是多种类型,例如A4、B5等,并且墨盒和纸张都不是打印机厂商提供的。打印机厂商如何避免自己的打印机与市场上的墨盒、纸张不符呢?

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

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

有效解决问题的途径是制定墨盒、纸张的约定或标准,然后打印机厂商按照约定对墨盒、纸张提供支持,不管最后使用的哪个厂商的墨盒或纸张,只要符合统一的约定,打印机都可以使用。Java中的接口就表示这样一种约定。

通过Java实现打印机打印的具体步骤如下。

  1. 定义墨盒接口InkBox,约定墨盒的标准。
  2. 定义纸张接口Paper,约定纸张的标准。
  3. 定义打印机类,引用墨盒接口、纸张接口实现打印功能。
  4. 墨盒厂商按照InkBox接口实现ColorInkBox类和GrayInkBox类。
  5. 纸张厂商按照Paper接口实现A4Paper类和B5Paper类。
  6. “组装”打印机,让打印机通过不同墨盒和纸张实现打印。

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

定义墨盒接口InkBox,约定墨盒的标准,代码如示例5所示。

示例5

/**

 * 墨盒接口。

 * @author 南京

 */

public interface InkBox {

    /**

     * 得到墨盒颜色

     * @return 墨盒颜色

     */

    public String getColor();

}

定义纸张接口Paper,约定纸张的标准,代码如示例6所示。

示例6

/**

 * 纸张接口。

 * @author 南京

 */

public interface Paper {

   

    /**

     * 得到纸张大小

     * @return 纸张大小

     */

    public String getSize();

}

定义打印机类,引用墨盒接口,纸张接口实现打印功能,代码如示例7所示。

示例7

/**

 * 打印机类。

 * @author 南京

 */

public class Printer  {

 

    /**

     * 使用墨盒在纸张上打印

     * @param inkBox 打印使用的墨盒

     * @param paper 打印使用的纸张

     */

    public void print(InkBox inkBox,Paper paper){

        System.out.println("使用"+inkBox.getColor()+

                "墨盒在"+paper.getSize()+"纸张上打印。");

    }

}

墨盒厂商按照InkBox接口实现ColorInkBox类和GrayInkBox类,代码如示例8所示。

示例8

/**

 * 彩色墨盒。

 * @author 南京

 */

public class ColorInkBox implements InkBox {

    public String getColor() {

        return "彩色";

    }

}

 

/**

 * 黑白墨盒。

 * @author 南京

 */

public class GrayInkBox implements InkBox {

    public String getColor() {

        return "黑白";

    }

}

纸张厂商按照Paper接口实现A4Paper类和B5Paper类,代码如示例9所示。

示例9

/**

 * A4纸类。

 * @author 南京

 */

public class A4Paper implements Paper {

    public String getSize() {

        return "A4";

    }

 

}

 

/**

 * B5纸类。

 * @author 南京

 */

public class B5Paper implements Paper {

    public String getSize() {

        return "B5";

    }

 

}

“组装”打印机,让打印机通过不同墨盒和纸张实现打印,代码如示例10所示。

示例10

/**

 * 测试类

 * @author 南京

 */

public class Test {

   

    public static void main(String[] args) {

        //1、定义打印机

        InkBox inkBox = null;

       Paper paper = null;

       Printer printer=new Printer();

       

        //2.1、使用黑白墨盒在A4纸上打印

        inkBox=new GrayInkBox();

       paper=new A4Paper();    

       printer.print(inkBox, paper);

 

        //2.2、使用彩色墨盒在B5纸上打印

        inkBox=new ColorInkBox();

       paper=new B5Paper();

       printer.print(inkBox, paper);

    }

 

}

运行结果如图4.3所示。

 

图4.3  示例10的运行结果

通过以上案例深刻理解接口表示一种约定,其实生活中这样的例子还有很多。例如两相电源插座中接头的形状,两个接头间的距离和两个接头间的电压都遵循统一的约定。主板上的PCI插槽也遵循了PCI接口约定,遵循同样约定制作的显卡、声卡、网卡可以插到任何一个PCI插槽上。

在面向对象编程中提倡面向接口编程,而不是面向实现编程。

如果打印机厂商只是面向某一家或几家厂商的墨盒产品规格生产打印机,而没有一个统一的约定,就无法使用更多厂商的墨盒,如果这些墨盒厂商倒闭了,那这些打印机就无用武之地了。为什么会出现这种情况?就是因为彼此依赖性太强了,或者说耦合性太强了。而如果按照统一的约定生产打印机和墨盒,就不会存在这个问题。

针对本案例,在示例7中就体现了面向接口编程的思想,Printer类的print()方法的两个参数使用了接口InkBox和Paper接口,就可以接受所有实现了这两个接口的类的对象,

即使是新推出的墨盒类型,只要遵守该接口,就能够接受。如果采用面向实现编程,两个参数类型使用GrayInkBox和B5Paper,大大限制了打印机的适用范围,无法对新推出的ColorInkBox提供支持。

接口实现了约定和实现相分离的原则,通过面向接口编程,可以降低代码间的耦合性,提高代码的可扩展性和可维护性。面向接口编程就意味着:开发系统时,主体构架使用接口,接口构成系统的骨架,这样就可以通过更换实现接口的类来实现更换系统。

——经验———————————————————————————————————

面向接口编程可以实现接口和实现的分离,这样做的最大好处就是能够在客户端未知的情况下修改实现代码。那么什么时候应该抽象出接口呢?一种是用在层和层之间的调用。层和层之间最忌讳耦合度过高或是修改过于频繁。设计优秀的接口能够解决这个问题。另一种是用在那些不稳定的部分上。如果某些需求的变化性很大,那么定义接口也是一种解决之道。设计良好的接口就像是我们日常使用的万用插座一样,不论插头如何变化,都可以使用。

最后强调一点,良好的接口定义一定是来自于需求的。它绝对不是程序员绞尽脑汁想出来的。

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

上机练习1

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

训练要点

Ø 接口的基础知识。

Ø 接口表示一种约定

需求说明

采用面向接口编程思想书写一封家书。家书的组成部分依次是称谓、问候、内容、祝福和落款,这是固定不变的,但每部分的具体内容却不尽相同。要求采用接口定义家书的组成,采用实现类书写出具体的家书。运行结果如图4.4所示。

 

 

图4.4  书写家书的运行结果

实现思路及关键代码

  1. 定义家书接口HomeLetter。
  2. 编写家书类HomeLetterImpl,实现家书接口。
  3. 编写测试类Test,写出家书。

注意编写注释。

参考解决方案

家书接口HomeLetter.java的代码如下。

/**

 * 家书接口。

 * @author 南京

 */

public interface HomeLetter {

   

    /**

     * 书写称谓。

     */

    public void writeTitle();

    /**

     * 书写问候。

     */

    public void writeHello();

    /**

     * 书写内容。

     */

    public void writeBody();

    /**

     * 书写祝福。

     */

    public void writeGreeting();

    /**

     * 书写落款。

     */

    public void writeSelf();

}

家书写手类HomeLetterWriter.java的代码如下。

 

/**

 * 家书写手。

 * @author 南京

 */

public class HomeLetterWriter {

    /**

     * 按照约定格式书写家书。

     * @param letter

     */

    public static void write(HomeLetter letter){

        letter.writeTitle();

        letter.writeHello();

        letter.writeBody();

        letter.writeGreeting();

        letter.writeSelf();

    }

}

 

测试类Test.java的代码如下。

/**

 * 测试类。

 * @author 南京

 */

public class Test {

    public static void main(String[] args) {

        //1、创建家书对象

        HomeLetter letter = new HomeLetterImpl();

        //2、书写家书

        HomeLetterWriter.write(letter);

    }

}

4.3  接口表示一种能力

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

作为一名合格的软件工程师,不仅要具备熟练的编码能力,还要懂业务,具备和客户、同事良好的交流业务的能力。在单位招聘中,招聘软件工程师,就是要招聘具备这些能力的人。只要符合招聘要求,胜任工作,就有机会被录用,而不是具体针对某些人而招聘。在Java编程中,如何描述和实现这样一个问题呢?

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

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

项目经理和部门经理同样要精通业务,初级程序员、高级程序员等也具备编写代码的能力,两种能力并非软件工程师独有,为了降低代码间的耦合性,提高代码的可扩展性和可维护性,可以考虑把这两种能力提取出来作为接口存在,让具备这些能力的类来实现这些接口,具体步骤如下。

  1. 定义Programmer接口,具备编码能力。
  2. 定义BizAgent接口,具备讲解业务能力。
  3. 定义SoftEngineer类,同时实现Programmer和BizAgent接口。
  4. 编写测试类,让软件工程师写代码,讲业务。

还可以进行优化,把Programmer接口和BizAgent接口中的重复方法定义提取出来,放到Person接口中,成为这两个接口的父接口。

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

 

定义Person接口,可以返回自己的姓名,代码如示例11所示。

示例11

/**

 * 人 接口。

 * @author 南京

 */

public interface Person {

    /**

     * 返回人的姓名。

     * @return 姓名

     */

    public String getName();

}

定义Programmer接口,继承Person接口,具备编码的能力,代码如示例12所示。

示例12

/**

 * 编码人员 接口。

 * @author 南京

 */

public interface Programmer extends Person {

   

    /**

     * 写程序代码。

     */

    public  void writeProgram();

}

定义BizAgent接口,继承Person接口,具备讲解业务的能力,代码如示例13所示。

示例13

/**

 * 业务人员 接口。

 * @author 南京

 */

public interface BizAgent extends Person {

    /**

     * 讲解业务。

     */

    public void giveBizSpeech();

}

 

定义SoftEngineer类,同时实现Programmer和BizAgent接口,代码如示例14所示。

示例14

/**

 * 软件工程师。

 * @author 南京

 */

public class SoftEngineer implements Programmer, BizAgent {

    private String name;// 软件工程师姓名

    public SoftEngineer(String name) {

        this.name = name;

    }

    public String getName() {

        return name;

    }

    public void giveBizSpeech() {

        System.out.println("我会讲业务。");

    }

    public void writeProgram() {

        System.out.println("我会写代码。");

    }

}编写测试类,让软件工程师编写代码,讲解业务,代码如示例15所示。

示例15

 

/**

 * 测试类。

 * @author 南京

 */

public class Test {

    public static void main(String[] args) {

       

        //1、创建软件工程师对象

        SoftEngineer xiaoMing = new SoftEngineer("小明");

        System.out.println("我是一名软件工程师,我的名字叫"

                +xiaoMing.getName()+"。");

        //2、软件工程师进行代码编写

        xiaoMing.writeProgram();

       

        //3、软件工程师进行业务讲解

        xiaoMing.giveBizSpeech();

    }

}

运行结果如图4.5所示。

 

图4.5  示例15的运行结果

通过以上案例深刻理解接口表示一种能力。一个类实现了某个接口,就表示这个类具备了某种能力。其实生活中这样的例子还有很多,例如钳工、木匠并不是指某个人,而是代表一种能力,招聘钳工、木匠就是招聘具备该能力的人。类似生活中一个人可以具有多项能力,一个类可以实现多个接口。

在Java  API中,可以发现很多接口名都是以“able”为后缀的,就是表示“可以做……”,例如Serializable、Comparable、Iterable等。在微软的.NET中也有很多的接口名以“able”为后缀的,例如IComparable、INullable、IClonable等,也表示同样的意思。

下面对示例15进行修改,其中涉及多态技能,代码如示例16所示,运行结果与示例15相同,结合多态仔细体会和理解。

示例16

/**

  * 测试类。

   * @author 南京

  * /

public  class  Test  {

     public  static  void  main(String[ ]  args)  {

         Programmer programmer = new SoftEngineer("小明");

        System.out.println("我是一名软件工程师,我的名字叫"

                +programmer.getName()+"。");       

        programmer.writeProgram();

        //coder.giveBizSpeech();

       BizAgent bizAgent=(BizAgent)programmer;      

       bizAgent.giveBizSpeech();    

 }

}

上机练习2

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

训练要点

Ø 接口的基础知识。

Ø 接口表示一种能力。

 

需求说明

软件工程师不仅要具备编写能力,还要能讲解业务,采用接口技术正确表示该业务关系,实现代码之间的松耦合,提高代码的可扩展性和可维护性。

实现思路及关键代码

  1. 定义Person接口,可以返回自己的姓名。
  2. 定义Programmer接口,继承Person接口,具备编码能力。
  3. 定义BizAgent接口,继承Person接口,具备讲解业务能力。
  4. 定义SoftEnginner类,同时实现Programmer和BizAgent接口。
  5. 编写测试类,让软件工程师编写代码,讲解业务。

注意编写注释。

4.4  在C#中使用接口

C#中同样可以定义接口,和Java中接口设计理念相同,但在语法上存在一些差异。

语法

[修饰符]  interface  接口名:父接口1,父接口2,……{

     属性定义

     方法定义

}

语法

class  类名:父类名,接口1,接口2,……{   }

说明如下。

Ø 接口之间可以通过冒号“:”实现继承关系,一个接口可以继承多个接口,但接口不能继承类。类只能继承一个父类,但可以实现多个接口,使用冒号“:”来继承类和实现接口。

Ø 接口定义零个或多个成员,成员主要是方法、属性和索引器。接口中不能包含常量、变量或构造方法,也不能包含任何静态成员。

Ø 接口中成员访问权限是public,定义接口时显示指定任何修饰符都是非法的。

Ø 按照惯例,C#中接口的名字以大写字母“I”开头。

下面用C#完成4.2节中打印机打印的案例,通过对比熟悉两者的异同,更好地掌握接口在两种语言中的使用。

定义墨盒接口InkBox和纸张接口Paper,约定墨盒和纸张的标准,代码如示例17所示。

示例17

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// 墨盒接口。

     /// </summary>

     public  interface  InkBox

     {

          /// <summary>

          /// 只读属性颜色。

          /// </summary>

          string  Color  { get;}

      }

}

 

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// 纸张接口。

     /// </summary>

     public  interface  Paper

     {

          /// <summary>

          /// 只读属性纸张大小。

          /// </summary>

          string  Size  { get;}

      }

}

定义打印机类,引用墨盒接口,纸张接口实现打印功能,代码如示例18所示。

示例18

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// 打印机类。

     /// </summary>

     class  Printer

     {

          /// <summary>

/// 打印。

          /// </summary>

          /// <param  name=“inkBox”>打印使用的墨盒。</param>

          /// <param  name=“paper”>打印使用的纸张。</param>

          public  void  print(InkBox  inkBox,Paper  paper)

          {

              Console.WriteLine(“使用”+inkBox.Color

                 +“墨盒在”+paper.Size+“纸上打印”);

           }

}

}

墨盒厂商按照InkBox接口实现ColorInkBox类和GrayInkBox类,代码如示例19所示。

示例19

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// 彩色墨盒。

     /// </summary>

     public  class  ColorInkBox : InkBox

     {

         public  string  Color { get { return “彩色”; } }

      }

}

 

using  System;

namespace  cn.le.printer

{

     /// <summary>

     ///黑白墨盒。

     /// </summary>

     public  class  GrayInkBox : InkBox

         {

             public  string  Color { get { return “黑白”; } }

      }

}

按照Paper接口约定来定义A4Paper类符合B5Paper类,如代码示例20所示。

示例20

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// A4纸张。

     /// </summary>

     public  class  A4Paper : Paper

     {

         public  string  Size { get { return “A4”; } }

      }

}

 

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// B5纸张。

     /// </summary>

     public  class  B5Paper : Paper    

     {     

         public  string  Size { get { return “B5”; } }

      }

}

“组装”打印机,让打印机通过不同墨盒和纸张实现打印,代码如示例21所示。

示例21

using  System;

namespace  cn.le.printer

{

     /// <summary>

     /// 测试类。

     /// </summary>

     public  class  Test 

{

         public  static  void  Main() 

{

            //1、定义墨盒、纸张和打印机。

            InkBox  inkBox=null;

            Paper  paper=null;

            Printer  printer=new  Printer();

 

            //2、使用黑白墨盒在A4纸上打印。

            inkBox=new  GrayInkBox();

            paper=new  A4Paper();

            printer.print(inkBox,paper);

 

           //3、使用彩色墨盒在B5纸上打印。

           inkBox=new  ColorInkBox();

           paper=new  B5Paper();

           printer.print(inkBox,paper);

      }

   }

}

运行结果如图4.6所示。

 

 

图4.6  示例21的运行结果

上机练习3

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

训练要点

C#接口的基础知识。

需求说明

打印机支持不同的墨盒和纸张类型,避免打印机与市场上的墨盒。纸张标准不符。

实现思路及关键代码

  1. 定义墨盒接口InkBox和纸张接口Paper,约定墨盒和纸张的标准。
  2. 定义打印机类,引用墨盒接口、纸张接口实现打印功能。
  3. 墨盒厂商按照InkBox接口实现ColorInkBox类和GrayInkBox类。
  4. 按照Paper接口约定定义A4Paper类和B5Paper类。
  5. “组装”打印机,让打印机通过不同墨盒和纸张实现打印。

注意编写注释。

——对比———————————————————————————————————

Java与C#中接口具有以下区别。

Ø Java中接口通过extends继承父接口,类通过implements实现接口,C#中通过冒号“:”来实现这两个功能。

Ø Java接口中的成员变量(属性)一律是常量,自动用public  static  final修饰,C#接口中不允许存在成员变量,但可以有属性。

Ø Java接口中属性和方法都可以使用public修饰,C#中默认public,但不允许显式使用public修饰。

Ø Java接口中可以定义静态常量和方法,但是C#接口中不允许包含任何静态成员。

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

 

 

本章总结

Ø 接口中属性都是全局静态常量,接口中方法都是全局抽象方法,接口中没有构造方法。

Ø 类只能继承一个父类,但可以实现多个接口。一个类要实现接口的全部方法,否则必须定义为抽象类。Java通过实现接口达到了多重继承的效果。

Ø 接口表示一种约定,接口表示一种能力,接口体现了约定和实现相分离的原则。

Ø 通过面向接口编程,可以降低代码间的耦合性,提高代码的可扩展性和可维护性。

Ø 面向接口编程意味着:开发系统时,主体构架使用接口,接口构成系统的骨架,这样就可以通过更换接口的类来更换系统的实现。

Ø C#中接口中成员主要是方法、属性和索引器。接口中不能包含常量、变量、构造方法和任何静态成员。定义接口时显式指定任何修饰符是非法的。

 

本章作业

一、选择题

1.下面的程序中定义了一个Java接口,其中包含(  )处错误。

public  interface  Utility  {

private  int  MAX_SIZE=20;

int  MIN_SIZE=10;

void  use() {

   System.out.println(“using  it”);

}

private  int  getSize();

void  setSize(int  i);

}

  1. 1
  2. 2
  3. 3
  4. 4

 

2.给定如下Java代码,可以填入横线处的语句是(  )。

public  interface  Utility  { }

 

class  FourWheeler   implements  Utility{ }

class  Car  extends  FourWheeler{ }

class  Bus  extends  FourWheeler{ }

 

public  class  Test  {

public  static  void  main(String[]  args)  {

_________________

}

}

  1. Utility  car=new  Car();
  2. FourWheeler  bus=new  Bus();
  3. Utility  ut=new  Utility();
  4. Bus  bus=new  FourWheeler();

 

3.以下关于接口的说法中,正确的是(  )。

A. 接口中全部方法都是抽象方法,所有方法必须是public访问权限

B.  接口中属性都使用pubic  static  final修饰,没有显式赋值将使用默认值

C. 接口可以有构造方法

D. 一个类只能有一个父类,但可以同时实现多个接口

 

4.给定下面的Java代码,可以填入下划线处的语句是(  )。

public  interface  Constants {

int  MAX=50;

int  MIN=1;

}

public  class  Test  {

public  static  voi  main(String[]  args) {

___________

}

}

  1. Constants  con=new  Constants();
  2. Constants.MAX=100;
  3. int  i=Constants.MAX-Constants.MIN;
  4. Constants.MIN>0;

5.给定下面的C#代码,可以填入下划线处的语句是(  )。

public  interface  IUtility  {

_____________

}

  1. int  PI=3.14;
  2. public  void  show();
  3. static  string  comp();
  4. string  concat();

二、简答题

1.请说明下面的代码中存在什么问题,该如何解决?

public  interface  Utility  { }

class  Phone  implements  Utility  {

   void  use()  {

       System.out.println(“using  phone”);

   }

}

public  class  Test  {

   public  static  void  main(String[ ]  args)  {

      Utility  util=new  Phone();

      util.use();

   }

}

2.阅读如下Java代码,指出其中存在的错误,并说明错误原因。

public  interface  Constants  {

   int  MAX=10000;

   int  MIN=1;

}

public  class  Test  {

   public  static  void  main(String[ ]  args)  {

       Constants  con=new  Constants();

       System.out.println(con.MAX);

       int  i=50;

       if(i>Constants.MAX)  {

           Constants.MAX=i;

       }

   }

}
3.阅读如下Java代码,给出运行结果。

public  interface  Animal {

void  shout();

}

class  Dog  implements  Animal  {

public  void  shout()  {

    System.out.println(“W W!”);

}

}

class  Cat  implements  Animal  {

public  void  shout()  {

     System.out.println(“M M!”);

}

}

class  Store  {

public  static  Animal  get(String  choice)  {

     if(choice.equalsIgnoreCase(“dog”))  {

          return  new  Dog();

     }  else  {

          return  new  Cat();

     }

}   

}

public  class  AnimalTest  {

public  static  void  main(String[ ]  args)  {

        Animal  al=Store.get(“dog”);

        al.shout();

    }

}

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

结合面向接口编程以及多态性来理解这段代码。

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

4.在第3题基础上进行功能扩展,要求如下。

Ø 增加一种新的动物类型:Pig(猪),实现shout()方法。

Ø 修改Store类的get方法:如果传入的参数是字符串dog,则返回一个Dog对象,如果传入的参数是字符串pig,则返回一个Pig对象;否则,返回一个Cat对象。

Ø 在测试类Test中加以测试:向Store的get方法中传入参数“pig”,并在返回的对象上调用shout方法,看看与预期的结果是否一致。

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

按照题目要求增加pig类、修改Store类的get方法。

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

5.对贯穿案例电子宠物系统的类结构进行重构,要求如下。

Ø 定义Eatable接口,在接口中定义eat()方法,表示吃饭功能。

Ø 定义FlyingDiscCatchable接口,在接口中定义catchingFlyDisc()方法,表示玩飞盘

功能。

Ø 定义Swimmable接口,在接口中定义swim()方法,表示游泳功能。

Ø 定义抽象类Pet,包括宠物名称name、健康值health和主人亲密度love属性,并提供抽象方法print(),用来输出宠物信息。

Ø 定义狗类Dog,继承Pet类,实现Eatable、FlyingDiscCatchable接口,并重写或实现各个方法。

Ø 定义企鹅类Penguin和继承类Pet,实现Eatable和Swimmable接口,并重写或实现各个方法。

Ø 编写测试类,实现狗吃饭、企鹅游泳和狗玩接飞盘游戏的功能,并输出企鹅信息。

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

按照题目要求依次创建相应接口、抽象类、类并进行测试,参考本章贯穿案例。

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

 

 

 

5章

项目案例:QuickHit

 

 

 

 

◇本章工作任务

 

Ø    开发QuickHit游戏

 

 

◇本章技能目标

 

Ø    使用面向对象思想进行程序设计

 

 

 

本章单词

 

 

 

 

 

 

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

 

  1. time:______________________________
  2. limit:______________________________
  3. elapse:____________________________
  4. level:______________________________
  5. parameter:_________________________
  6. static:_____________________________
  7. code:______________________________
  8. block:______________________________

 

 

本章简介

 

前面我们学习了面向对象的设计过程、抽象、封装、继承和多态,学习了接口的应用。

本章我们将利用所学技能完成一个QuickHit游戏,重点训练根据需求进行面向对象设计的能力,并在成熟设计的基础上完成该案例的开发。

 

5.1  案例分析

5.1.1  需求概述

QuickHit游戏考验你键盘输入的速度和准确性。

根据输入速率和正确率将玩家分为不同级别、级别越高,一次显示的字符数越多,玩家正确输入一次的得分也越高。如果玩家在规定时间内完成规定次数的输入,正确率达到规定要求,则玩家升级(为了简单起见,规定用户错误输入一次,游戏结束)。玩家最高级别为六级,初始级别一律为一级。

 

5.1.2  开发环境

开发工具:JDK6.0、MyEclipse7.5。

 

5.1.3  案例覆盖的技能点

Ø    面向对象设计的思想。

Ø    使用类图理解类的关系。

Ø    类的封装。

Ø    构造方法的使用。

Ø    this和static关键字的使用。

 

5.1.4  问题分析

1.需要使用到的类

本案例功能简单,代码量较小,采用面向过程的思想可能更容易实现,此处的解关键是锻炼大家的面向对象的设计能力,分析各段功能代码放到什么位置更合理一些,为大型项目的设计能力打好基础。

面向对象设计的过程就是抽象的过程,与1.1.2节中设计过程一样,还是通过在需求中找出名词的方式确定类和属性,找出动词的方式确定方法。然后对找到的词语进行筛选,剔除无关、不重要的词语,最后对词语之间的关系进行梳理,从而确定类、属性、属性值和方法。

需求中和业务相关的名词主要有:游戏、输入速率、玩家、级别、一次显示的字符数、正确输入一次的得分、规定时间、规定次数、超时、玩家的积分和玩家用时等。动词主要是输出、输入、确认和显示。

第一步:发现类。

玩游戏肯定离不开玩家与游戏,可以首先抽象出玩家、游戏两个类。同时一次显示的字符数、正确输入一次的得分、规定时间和规定次数都与玩家的当前级别有关、可以再抽象出一个级别类。而积分和用时可以考虑设置为玩家属性,在此忽略。

经过分析,我们暂时从需求中抽象出如下类:游戏(Game)、玩家(Player)和级别(Level)。相应类图如图5.1所示。

 

 

图5.1  发现类

第二步:发现类的属性。

经过分析,玩家的属性有当前级别、玩家的积分和玩家用时等,而玩家用时其实是当前时间与开始时间之差。级别的属性有级别号、一次显示的字符数、正确输入一次的得分、规定时间和规定次数。

进行整理并命名,得到如下结果。

玩家类(Player)的属性:玩家当前级别号(levelNo)、玩家当前级别积分(currScore)、当前级别开始时间(startTime)和当前级别已用时间(elapsedTime)。

级别类(Level)的属性:各级别号(levelNo)、各级别一次输出字符串的长度(strLength)、各级别输出字符串的次数(strTime)、各级别闯关的时间限制(timeLimit)和各级别正确输入一次的得分(perScore)。

相应类图如图5.2所示。

 

 

图5.2  发现类的属性

第三步:发现类的方法。

经过分析,游戏类的主要方法有三个:输出字符串、确认玩家输入是否正确和输出相应结果信息,其中第二个方法很简单,可以和第三个合并。而玩家类的方法只有一个就是玩游戏(根据游戏的输出来输入相同字符串)。级别类主要是存放信息,没有具体操作。

进行整理并命名,得到如下结果。

玩家类的方法:玩游戏play()。

游戏类的方法有两个:String  printStr(),输出字符串,返回字符串用于和玩家输入比较。

void  printResult(String  out, String  in),比较游戏输出out和玩家输入in,根据比较结果输出相应信息。

相应类图如图5.3所示。

 

 

图5.3  发现类的方法

第四步:优化设计。

在Game类的printResult(String  out, String  in)方法中,输出相应结果时肯定会涉及Player类的信息,如当前级别号、当前级别积分、当前级别已用时间等,可以通过把Player对象作为Game类的属性,轻松解决该问题。
Level类不包含各个级别的具体参数信息,可以增加LevelParam类,在该类中创建一个长度为六的Level数组,存放各个级别的具体参数信息。并把该数组使用public  final  static修饰,可以直接通过类名来访问,却无法修改其值。

优化后类图如图5.4所示。

 

 

图5.4  优化设计

2.主要功能分析

1)游戏输出字符串

主要步骤包括生成字符串、输出字符串和返回字符串(必须返回,用于和玩家输入比较),这里关键是如何生成长度固定但内容随机的字符串。

2)确定输入并输出结果

根据玩家输入是否正确输出不同结果。如果输入正确并且未超时,要输出玩家的当前级别、当前积分和已用时间。如何计算玩家的当前级别、当前积分和已用时间是关键。

3)玩家玩游戏

游戏六个级别可以通过大循环实现。而每个级别中多次字符串输出通过内部子循环实现,该控制功能放在Player类的play()方法中。每次玩家晋级后积分清零、计时清零。

3.界面分析

如果玩家输入正确且未超时,输出玩家输入正确信息,并输出玩家当前积分、当前级别和已用时间。如果玩家输入正确但超时,输出玩家超时信息,并退出系统。参考运行界面如图5.5所示。

 

 

图5.5  玩家输入正确与输入超时界面截图

如果玩家输入错误,输出玩家输入的错误信息,并退出系统。参考运行界面如图5.6所示。

 

 

图5.6  玩家输入错误界面截图

 

5.2  项目需求

QuickHit项目的执行步骤描述如下。

  1. 游戏根据玩家的级别在控制台输出指定数量字符。
  2. 玩家根据控制台输出来输入相同字符,按Enter键确认。
  3. 游戏确认玩家输入是否正确。
  4. 如果输入错误,输出玩家输入错误提示,游戏非正常结束。
  5. 如果输入正确但超时,输出玩家速度太慢提示,游戏非正常结束。
  6. 如果输入正确且没有超时,输出玩家的积分、级别和用时信息。然后重复以上步骤,继续输出、输入和确认。
  7. 玩家在规定时间内连续正确输入规定次数后,将显示玩家升级提示,游戏将重新计时计分,将一次输出更多字符。六级玩家闯关成功,输出恭喜信息,游戏正常结束。

 

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

实现步骤可以设计为以下几步。

  1. 生成字符串。
  2. 输出字符串。
  3. 返回字符串(必须返回,用于和玩家输入比较)。

这其中的关键是第一步的实现,字符串的长度是多少?如何生成长度固定但内容随机的字符串?

 

 

 

 

 

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

Game类中的player属性,代表玩家,查询player的级别号,然后根据级别号到LevelParam类中获取该级别的字符串长度。

字符串长度固定可以通过for循环来实现,而随机内容可以通过获取随机数,而不同随机数对应不同字符来实现。

StringBuffer  buffer = new  StringBuffer();

Random  random = new  Random();

// 1、通过循环生成要输出的字符串

for  (int  i = 0; i < strLength; i++) {

        //1.1、产生随机数

        int  rand = random.nextInt(strLength);

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

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

        //1.2、根据随机数拼接字符串

        switch (rand) {

        case  0:

               buffer.append(“>”);

               break;

         case  1:

                buffer.append(“<”);

                break;

        case  2:

               buffer.append(“*”);

               break;

         case  3:

                buffer.append(“&”);

                break;

         case  4:

               buffer.append(“%”);

               break;

         case  5:

                buffer.append(“#”);

                break;

         }

}

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

 

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

实现步骤可以设计为以下几步。

  1. 确认玩家输入是否正确。
  2. 如果输入不正确,则直接输出错误信息并退出程序。
  3. 如果输入正确,则会有以下两种情况。

Ø    如果超时,则直接输出错误信息并退出程序。

Ø    如果没有超时,则执行以下操作。

  • 计算玩家当前积分。
  • 计算玩家已用时间。
  • 输出玩家当前级别、当前积分和已用时间。
  • 判断用户是否已经闯过最后一关并处理。

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

这其中关于已用时间、当前积分和当前级别的操作都会涉及到Game类的player属性,有些操作还会用到LeveParam类中levels数组的数据。

long  currentTime = System.currentTimeMillis();

//如果超时

if  ((currentTime – player.getStartTime()) / 1000

        >LevelParam.levels[player.getLevelNo() – 1].getTimeLimit()) {

        System.out.println(“你输入太慢了,已经超时,退出!”);

        System.exit(1);

}

 

//计算玩家当前积分

player.setCurScore(player.getCurScore()

                   +LevelParam.levels[player.getLevelNo() – 1].getPerScore());

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

 

5.2.3  用例3:玩家玩游戏

实现步骤可以设计为以下几步。

  1. 创建Game对象并传入player属性。
  2. 外层循环(循环次数是6,每循环一次玩家级别升一级)。
  1. 晋级
  2. 积分清零,计时清零
  3. 内层循环(循环次数是该级别的strTime,每循环一次完成一次人机交互)。

Ø    游戏输出字符串。

Ø    玩家输入字符串。

Ø    游戏判断玩家输入并输出相应结果。

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

创建Game对象并传入player属性可以通过“Game  game = new  Game(this);”语句实现,这涉及到this关键字的用法。Game的构造方法Game(Player  player)需要传入一个Player对象,此时还没有创建Player对象,当然就谈不上对象的名字,而当前方法正是Player类的方法,使用this代表以后创建的Player对象的引用。当以后执行语句

player  player = new  Player();

player.play();

时,this就代表已经创建对象的引用player。

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

 

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

各个级别的具体参数信息,如各级别号、各级别一次输出字符串的长度、各级别输出字符串的次数、各级别闯关的时间限制和各级别正确输入一次的得分,应该在游戏开始之前进行初始化。

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

/**

*级别参数类,配置各个级别参数。

*@author 南京

*/

public  class  LevelParam {

        public  final  static  Level  levels[] = new  Level[6]; //对应六个级别

        static {

                levels[0] = new  Level(1, 2, 10, 30, 1);

                levels[1] = new  Level(2, 3, 9, 26, 2);

                levels[2] = new  Level(3, 4, 8, 22, 5);

                levels[3] = new  Level(4, 5, 7, 18, 8);

                levels[4] = new  Level(5, 6, 6, 15, 10);

                levels[5] = new  Level(6, 7, 5, 12, 15);

        }

}

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

该类中涉及到static关键字的用法。在1.2.1节,我们提到了可以用static来修饰属性,方法和代码块。在该类中就用到了static属性和static代码块。

Ø    static属性属于这个类所有,即由这个类创建的所有对象共用同一个static属性。既可以创建对象后通过“对象名.static属性名”的方式来访问,也可以再创建对象之前直接通过“类名.static属性名”的方式来访问。

Ø    static方法也可通过“类名.static方法名”和“对象名.static方法名”两种方式来访问。

Ø    static代码块的用法比较特殊:当类被载入的时候(类第一次被使用到的时候载入,例如创建对象或直接访问static属性与方法)执行静态代码块,且只被执行一次,主要作用是实现static属性的初始化。根据如下示例1所示深入理解static代码块的执行时机。

 

示例1

class  StaticCodeBlock {

       static  String  name = “defname”;

       static {

              name = “staticname”;

              System.out.println(“execute  static  code  block”);

       }

       public  StaticCodeBlock() {

              System.out.println(“execute  constructor”);

      }

}

public  class  TestStaticCodeBlock {

      static {

                System.out.println(“execute static code block in Test”);

      }

      public  static  void  main(String[] args) {

                System.out.println(“execute  main()”);

                new  StaticCodeBlock();

                new  StaticCodeBlock();

                new  StaticCodeBlock();

      }

}

运行结果如图5.7所示。

 

 

图5.7  static代码块的执行时机

——注意———————————————————————————————————

static属性的生命周期是从类被载入到类被销毁,与类同生死。

示例属性的生命周期从某一个对象被创建开始,到系统销毁这个对象为止,与相应对象共存亡。

不创建对象就无法访问实例属性和方法,却可以访问static属性和方法。

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

 

 

5.3  进度记录

根据开发进度及时填写表5-1

表5-1  开发进度记录表

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

用例                              开发完成时间     测试通过时间     备注

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

用例1:游戏输出字符串

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

用例3:玩家玩游戏

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

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

 

 

6章

                   指导学习:动物乐园

 

 

本章工作任务

Ø 设计动物乐园系统

本章技能目标

Ø 掌握类和对象的关系

Ø 掌握面向对象的特征:封装、继承、多态

Ø 掌握抽象类和接口的概念、语法、用处

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

 

 

6.1  复习串讲

6.1.1  难点突破

表6-1中列出了本学习阶段的难点,这些技能你都掌握了吗?

如果还存在疑惑,请写下你对这个技能感到疑惑的地方。我们可以通过教材复习,或者从网上查找资料学习,或者和同学探讨,或者向老师请教等方法突破这些难点。直到这个技能已经掌握后在“是否掌握”一栏中画上“√”。这些技能是学习后面技能的基础,一定要在继续学习前全部突破。如果在学习中遇到其他难点,也请填写在表中。

表6-1  本学习阶段难点记录表

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

难点                          感到疑惑的地方         突破方法        是否掌握

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

面向对象设计的过程

使用权限修饰符进行类的封装

继承关系下构造方法的执行

利用多态减少代码量,提高代

码的可扩展性和可维护性

this、super关键字的使用

static、final关键字的使用

接口与抽象类的异同

 

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

6.1.2 知识梳理

在前五章中我们学习了Java面向对象的核心内容:封装、继承、多态、抽象类和接口等,知识体系如图6.1所示。我们可以借助这个图梳理这些知识的体系结构。

在知识体系图中,图标表示我们学过该知识;图标表示我们部分学习过该知识或者学习过相关知识,但需要巩固和补齐;图标表示我们需要通过自学学习这个知识。

Java面向对象的核心内容涉及概念多、理解难度大,不仅要掌握其技能,更要领悟其思想,学好这部分内容会为Java后续课程学习打下坚实基础。我们已经学习了其中最常用的技能,这些技能已经能够满足日常开发的需要,建议大家以后进一步学习相关知识,把这部分内容学习得更深更广是很有必要的。

 

图6.1  Java  OOP知识体系图

6.2  综合练习

6.2.1  任务描述

本次综合练习的任务是:以面向对象思想设计动物乐园系统。动物乐园中有猫、鸭等成员,还可能增加新成员。猫和鸭都有自己的名字,都有腿,但腿的条数不同,猫和鸭子会发出叫声,猫的叫声是“喵喵喵……”,鸭的叫声是“嘎嘎嘎……”。要求进行面向对象设计,画出类图并写出代码。本次练习难度并不高,关键还是明确面向对象设计是一个不断优化的过程,是为了有效解决业务问题。

6.2.2  练习

分阶段完成练习。

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

需求说明

以面向对象思想设计猫和鸭的类结构,画出类图并写出代码。

实现思路及关键代码

根据任务描述,可以设计出两个类,猫类Cat和鸭类Duck,均具有名字name、腿的条数legNum属性,均具有shout()方法。类图如图6.2所示,在其中增加两个属性的getter方法。

 

 

图6.2  Cat和Duck类图(一)

观察如图6.2所示的类图,发现Cat、Duck类均具有相同的属性名和方法名,可以提取出父类——Animal类,进行代码重用,让Cat、Duck均继承Animal类,类图如图6.3所示。

 

 

图6.3  Cat和Duck类图(二)

在图6.3中,通过继承实现了代码重用,但是Animal类不是一种具体的动物,创建Animal对象是没有实际意义的,另外Animal对象也无法真正的发出叫声,且子类必须重写shout()方法,所以可以把Animal类设计成抽象类,把shout()方法设计成抽象方法,类图如图6.4所示(图中Animal和shout()以斜体出现,表示Animal类是抽象类,shout()方法是抽象方法)。

 

 

图6.4  Cat和Duck类图(三)

请自己动手画出类图并根据图6.4的设计结果写出Java代码。

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

需求说明

在动物乐园中增加一个新成员海豚,海豚的叫声是“海豚音……”。

实现思路及关键代码

让海豚直接继承Animal类合适吗?别忘了,海豚是没有腿的,所以不能继承Animal类,但是海豚又确实是一种动物,不继承Animal也不合适。怎么办呢?只有对Animal类进行重新设计,去掉其中的legNum属性和getLegNum()方法,新的类图如图6.5所示。

如何再约束Cat类和Duck类,而且必须有getLegNum()方法,应如何定义呢?

可以采用如下思路来实现:创建Terrestrial(陆生的)接口,声明getLegNum()方法,然后让Cat和Duck在继承Animal类的同时实现Terrestrial接口,类图如图6.6所示。

 

 

图6.5  增加Dolphin类需要重新设计Animal类

 

 

图6.6  抽象出Terrestrial接口

请根据图6.6的设计结果写出Java代码。

 

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

需求说明

在阶段2编写的Java代码的基础上,分别创建Cat、Duck和Dolphin对象并放到一个数组中,对数组进行遍历输出各种动物如何叫,运行结果如图6.7所示。

 

 

图6.7  阶段3的参考运行结果

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

创建Animal类型数组存放各种动物对象,利用多态实现功能。

Animal[]  animals=new  Animal[3];

animals[0]=new  Cat(“加菲猫”,4);

animals[1]=new  Duck(“唐小鸭”,2);

animals[2]=new  Dolphin(“海豚奇奇”);

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

阶段4:指导——输出各种动物腿的条数

需求说明

在阶段3编写的Java代码的基础上,分别创建Cat、Duck和Dolphin对象并放到一个数组中,对数组进行遍历输出各种动物腿的条数,运行结果如图6.8所示。

 

 

图6.8  阶段4的参考运行结果

实现思路及关键代码

数组中有Dolphin元素,但是海豚没有腿,输出时应判断各个对象是否实现了Terrestrial接口,可以使用instanceof运算符判断。

 

语法

对象instanceof  类或接口

说明如下。

Ø 作用:判断一个对象是否属于一个类或者实现了一个接口,结果为true或false。

Ø 对象的类型必须和instanceof的第二个参数所指定的类或接口在继承树上有上下级关系,否则会出现编译错误。例如:

Cat  cat=new  Cat(“加菲猫”,4);

Dolphin  dolphin=new  Dolphin(“海豚奇奇”);

System.out.println(cat  instanceof  Cat);     //true

System.out.println(cat  instanceof  Animal);     //true

System.out.println(cat  instanceof  Terrestrial);     //true

System.out.println(dolphin  instanceof  Terrestrial);     //false

System.out.println(cat  instanceof  Dolphin);     //编译错误!

System.out.println(dolphin  instanceof  Cat);     //编译错误!

知道对象实现了Terrestrial接口,我们就可以知道它肯定实现了getLegNum()方法,就可以把它当作Terrestrial的实例来引用。代码如下所示。

if  (animal[i]  instanceof  Terrestrial)  {

     Terrestrial  trtl=( Terrestrial)  animal[i];

     int  legNum=trtl.getLegNum();

     System.out.println(animal[i].getName()+“\t”+legNum);

}

 

 

 

 

7章

在线培训:面向对象设计

 

 

本次学习完成的任务

Ø 制作PPT,讲解类与类之间的关系。

Ø 制作PPT,讲解面向对象设计的基本原则。

Ø 使用类图设计佩恩沃星球。

学习的技能点

Ø 类和类之间的依赖、关联、聚合以及组合关系。

Ø 面向对象设计原则:“开—闭”原则、里氏代换原则和单一职能原则。

Ø 面向对象设计过程。

7.1  学习任务

根据提供的电子资料,完成如下任务。

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

需求说明

我们已经学习了类和类之间的继承关系,类和接口的实现关系。除此之外,类和类之间还存在依赖、关联、聚合和组合关系。

请自学相关内容并制作PPT,然后在课上讲解给其他同学。

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

  • 主要讲解自己的理解和体会。
  • 结合具体实例讲解。

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

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

需求说明

能真正理解抽象、封装、继承和多态并在程序设计中得体地使用,只是刚迈进“面向对象设计殿堂”的门槛。接下来,掌握面向对象设计的基本原则,是往上更进一步的台阶。这些基本原则包括:“开—闭”原则,里氏代换原则和单一职能原则等。

请自学相关内容并制作PPT,然后在课上给其他同学讲解。

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

  • 从“开—闭”原则,里氏代换原则和单一职能原则中选择一个讲解即可。
  • 也可以讲解这三个基本原则之外的其他基本原则。
  • 主要讲解自己的理解和体会。
  • 结合具体实例讲解。

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

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

需求说明

请根据下面的需求描述,进行面向对象设计,画出类图,并在课上讲解你的设计。

在遥远的佩恩沃(Painwoor)星球上,所有的部族在战斗中成长进化,所有生灵在痛苦和折磨中变得强大。

打倒敌对部族的生灵,可以获得经验,打倒级别越高的敌人,获得经验越多,如表7-1所示。

表7-1  打败不同级别敌人获得的经验

敌人级别

1      2      3      4      5      6      7      8      9 

获得经验

1      2      3      5      6      9      12     13     18

每个生灵都看重自己在速度、攻击和防御方面的表现,每个生灵只有100点生命,被攻击会减少相应点数,但自身的防御值会抵消相应的点数。速度值高的生灵可以更快地到达战场,每个生灵攻击的频率都是一样的。初生婴儿的速度、攻击和防御都只有两点。

在战斗中累计经验,达到升级的标准时,获得一定的点数,获得升级的生灵可以自由分配点数到自己期望成长的属性(速度、攻击或防御)上。每次升级后,经验清零。

升级所需的经验和获得的点数如表7-2所示。

7-2  不同升级级别所需的经验和获得点数

升级级别

所需经验

1      2      3      4      5      6      7      8      9      +

5      10     15     40     48     72     120    260    900    1000

获得点数

3      3      3      4      4      4      6      6      6      7

达到九级以后,级别不再增长,但每累计1000点经验后,仍可获得七个点数。多人同时攻击,点数均分,经验由最后三个人平分。

每个生灵都有自己的装备,不同的装备可以安装到身上不同的位置(比如头部、身体等),装备可以提升自身在速度、攻击和防御方面的表现。具体增效如表7-3所示。

 

表7-3  各种装备的增效

装备

速度增效

攻击增效

战神头盔    连环锁子甲    波斯追风靴     蓝魔指环     欺霜弑神铲

-2           -1            8              2            0

0            0             2              12           21

防御增效

6            15            3              2            5

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

设计师不断修改和完善的过程。

面向对象设计的步骤如下所述。

  1. 发现类。
  2. 发现类的属性,注意封装性。
  3. 发现类的方法,注意构造方法的设置要符合业务需求。
  4. 优化设计,使用继承、多态、抽象类和接口进行优化。
  5. 理顺程序运行过程。

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

7.2  参考资料

可根据如下网站,关键字查找网上的相关学习资料。

1,推荐网站

2,搜索关键字

  • 依赖、关联、组合、聚合。
  • 面向对象,设计原则。
  • “开—闭”原则、里氏代换原则、单一职能原则。

 

 

 

 

 

8章

    常

◇本章工作任务

 

Ø    解决计算商时出现的各种问题

Ø    使用log4j记录日志

 

 

◇本章技能目标

 

Ø    使用try-catch-finally处理异常

Ø    使用throw、throws抛出异常

Ø    掌握异常及其分类

Ø    使用log4j记录日志

 

 

 

 

 

本章单词

 

 

 

 

 

 

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

 

  1. exception:____________________________
  2. try:__________________________________
  3. catch:_______________________________
  4. finally:________________________________
  5. throw:________________________________
  6. arithmetic:_____________________________
  7. log:__________________________________
  8. property:_____________________________

 

 

本章简介

 

本章讲解Java中的异常及异常处理机制。通过该机制使程序中的业务代码与异常处理代码分离,从而使代码更加优雅,使程序员更专心于业务代码的编写。本章首先学习什么是异常,然后学习使用try-catch-finally捕获异常,使用throw,throws抛出和声明异常,以及异常的分类,最后介绍目前流行的用于记录日志的开源框架——log4j,并使用log4j记录异常日志。

 

8.1异常概述

8.1.1  生活中的异常

在生活中,异常(exception)情况随时都有可能发生。

拿上下班举个例子吧:在正常情况下,小王每日开车去上班,耗时大约30分钟。但是,由于车多、人多、路窄,异常情况很有可能发生。有时会遇上比较严重的堵车,偶尔还会很倒霉地与其他汽车进行了“亲密接触”。这种情况下,小王往往很晚才能到达单位。这种异常虽然偶尔才会发生,但是真若来临也是件极其麻烦的事情。

这就是生活中的异常,接下来,我们看看程序运行过程中会不会发生异常。

 

8.1.2程序中的异常

示例1中给出了一段代码,这段代码要完成的任务是:根据提示输入被除数和除数,计算并输出商,最后输出“感谢使用本程序!”的信息。

 

示例1

/**

 *演示程序中的异常

 *@author 南京

 */

public  class  Test1 {

      public  static  void  main(String[]  args) {

            Scanner  in = new  Scanner(System.in);

            System.out.print(“请输入被除数:”);

            int  num1 = in.nextInt();

            System.out.print(“请输入除数:”);

            int  num2 = in.nextInt();

            System.out.println(String.format(“%d / %d =%d”, numl,

                        num2, num1 / num2));

            System.out.println(“感谢使用本程序!”);

      }

}

在正常情况下,用户会按照系统的提示输入整数,除数不输入0,运行效果如图8.1所示。

 

 

图8.1  正常情况下的运行效果图

但是,如果用户没有按要求进行输入,例如被除数输入了“B”,则程序运行时将会发生异常,运行效果如图8.2所示。

 

 

图8.2  被除数非整数情况下的运行效果图

或者除数输入了“0”,则程序运行时也将发生异常,运行效果如图8.3所示。

 

 

图8.3  除数为0情况下的运行效果图

从结果中可以看出,一旦出现异常,程序将会立刻结束,不仅计算和输出商的语句不被执行,而且输出“感谢使用本程序!”的语句也不执行。应该如何解决这些异常呢?我们可以尝试通过增加if-else语句来对各种异常情况进行判断处理,这是在《使用Java语言理解程序逻辑》中学习的方法。代码如示例2所示。

 

示例2

import  java.util.Scanner;

/**

 *尝试通过if-else来解决异常问题。

 *@author 南京

 */

public  class  Test2 {

      public  static  void  main(String[]  args) {

            Scanner  in = new  Scanner(System.in);

            System.out.print(“请输入被除数:”);

            int  num1 = 0;

            if (in.hasNextInt()) { //如果输入的被除数是整数

                   num1 = in.nextInt();

            } else { //如果输入的被除数不是整数

                   System.err.println(“输入的被除数不是整数,程序退出。”);

                   System.exit(1); //结束程序执行

            }

            System.out.print(“请输入除数:”);

            int  num2 = 0;

            if (in.hasNextInt()) { //如果输入的除数是整数

                    num2 = in.nextInt();

                    if (0 == num2) { //如果输入的除数是0

                          System.err.println(“输入的除数是0,程序退出。“);

                          System.exit(1);

                    }

            } else { //如果输入的除数不是整数

                    System.err.println(“输入的除数不是整数,程序退出。“);

                    System.exit(1);

            }

            System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

            System.out.println(“感谢使用本程序!”);

      }

}

通过if-else语句进行异常处理的机制,主要有以下缺点。

Ø    代码臃肿,加入了大量的异常情况判断和处理代码。

Ø    程序员把相当精力放在了异常处理代码上,放在了“堵漏洞“上,减少了编写业务代码的时间,必然影响开发效率。

Ø    很难穷举所有的异常情况,程序仍旧不健壮。

Ø    异常处理代码和业务代码交织在一起,影响代码的可读性,加大日后程序的维护难度。

如果“堵漏洞“的工作能由Java系统来提供,用户只关注于业务代码的编写,对于异常只需调用Java提供的相应的异常处理程序就好了。Java就是这么做的。

 

8.1.3什么是异常

示例1展示了程序中的异常,那么究竟什么是异常?面对异常时,该如何有效处理呢?

异常就是在程序的运行过程中所发生的不正常的事件,比如所需文件找不到、网络连接不通或中断、算术运算出错(如被零除)、数组下标越界、装载了一个不存在的类、对null对象操作、类型转换异常等,异常会中断正在运行的程序。

在生活中,小王会这样处理上下班过程中遇到的异常:如果发生堵车,小王会根据情况绕行或者等待;如果发生撞车事故,小王会及时打电话通知交警,请求交警协助解决,然后继续赶路。也就是说,小王会根据不同的异常进行相应的处理,而不会因为发生了异常,就手足无措,中断了正常的上下班。

在生活中,发生异常后,我们懂得如何去处理异常。那么在Java程序中,又是如何进行异常处理的呢?在排除了通过if-else语句进行异常处理的机制后,下面就来学习Java中的异常处理。

 

8.2异常处理

8.2.1  什么是异常处理

异常处理机制就像我们对平时可能会遇到的意外情况,预先想好了一些处理的办法。也就是说,在程序执行代码的时候,万一发生了异常,程序会按照预定的处理办法对异常进行处理,异常处理完毕之后,程序继续运行。

Java的异常处理是通过五个关键字来实现的:try、catch、finally、throw和throws,下面将依次学习。

 

8.2.2  try-catch

对于示例1采用Java的异常处理机制进行处理,把可能出现异常的代码放入try语句块中,并使用catch语句块捕获异常,代码如示例3所示。

 

示例3

import  java.util.Scanner;

/**

 *使用try-catch进行异常处理。

 *@author 南京

 */

public  class  Test3 {

      public  static  void  main(String[]  args) {

            try {

                   Scanner  in = new  Scanner(System.in);

                   System.out.print(“请输入被除数:”);

                   int  num1 = in.nextInt();

                   System.out.print(“请输入除数:”);

                   int  num2 = in.nextInt();

                   System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

                   System.out.println(“感谢使用本程序!”);

             } catch (Exception  e) {

                   System.err.println(“出现错误:被除数和除数必须是整数,” +

                               “除数不能为零。”);

                   e.printStackTrace();

             }

      }

}

 

try-catch程序块的执行流程比较简单,首先执行的是try语句块中的语句,这时可能会有以下三种情况。

Ø    如果try块中所有语句正常执行完毕,不会发生异常,那么catch块中的所有语句都将会被忽略。当我们在控制台输入两个整数时,示例3中的try语句块中的代码将正常执行,不会执行catch语句块中的代码,运行效果如图8.4所示。

 

 

图8.4  正常情况下的运行效果图

Ø    如果try语句块在执行过程中碰到异常,并且这个异常与catch中声明的异常类型相匹配,那么在try块中其余剩下的代码都将被忽略,而相应的catch块将会被执行。匹配是指catch所处理的异常类型与所生成的异常类型完全一致或是它的父类。当在控制台提示输入被除数时输入了“B“,示例3中try语句块中的代码:”int  num1 = in.nextInt();”将抛出InputMismatchException异常。由于InputMismatchException是Exception的子类,程序将忽略try块中其余剩下的代码而去执行catch语句块。运行效果如图8.5所示。

 

 

图8.5  抛出异常情况下的输出结果(一)

如果输入的除数为0,运行效果如图8.6所示。

 

 

图8.6  抛出异常情况下的输出结果(二)

Ø    如果try语句块在执行过程中碰到异常,而抛出的异常在catch块里面没有被声明,那么程序立刻退出。

如示例3所示,在catch块中可以加入用户自定义处理信息,也可以调用异常对象的方法输出异常信息,常用的方法主要有以下两种。

  • void  printStackTrace():输出异常的堆栈信息。堆栈信息包括程序运行到当前类的执行流程,它将打印从方法调用处到异常抛出处的方法调用序列,如图8.5所示。该例中java.util.Scanner类中的throwFor()方法是异常抛出处,而Test3类中的main方法在最外层的方法调用处。
  • String  getMessage():返回异常信息描述字符串,该字符串描述异常产生的原因,是printStackTrace()输出信息的一部分。

——资料———————————————————————————————————

如果try语句块在执行过程中碰到异常,那么在try块中其余剩下的代码都将被忽略,系统会自动生成相应的异常对象,包括异常的类型、异常出现时程序的运行状态以及对该异常的详细描述。如果这个异常对象与catch中声明的异常类型相匹配,会把该异常对象赋给catch后面的异常参数,相应的catch块将会被执行。

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

表8-1列出了一些常见的异常及它们的用途。现在只需初步了解这些异常即可。在以后的编程中,要多注意系统报告的异常信息,根据异常类型来判断程序到底出了什么问题。

 

 

 

 

 

 

表8-1  常见的异常类型

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

异常                                   说明

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

Exception                              异常层次结构的根类

ArithmeticException                    算术错误情形,如以零作除数

ArrayIndexOutOfBoundsException         数组下标越界

NullPointerException                   尝试访问null对象成员

ClassNotFoundException                 不能加载所需的类

InputMismatchException                 欲得到的数据类型与实际输入的类型不匹配

IllegalArgumentException               方法接收到非法参数

ClassCastException                     对象强制类型转换出错

NumberFormatException                  数字格式转换异常,如把”abc”转换成数字

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

 

8.2.3  try-catch-finally

如果希望示例3中不管是否发生异常,都执行输出“感谢使用本程序!”的语句,该如何实现呢?

在try-catch语句块后加入finally块,把该语句放入finally块。无论是否发生异常,finally块中的代码总能被执行,如示例4所示。

 

示例4

import  java.util.Scanner;

/**

 *使用try-catch-finally进行异常处理。

 *@author 南京

 */

public  class  Test4 {

      public  static  void  main(String[]  args) {

            try {

                   Scanner  in = new  Scanner(System.in);

                   System.out.print(“请输入被除数:”);

                   int  num1 = in.nextInt();

                   System.out.print(“请输入除数:”);

                   int  num2 = in.nextInt();

                   System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

             } catch (Exception  e) {

                   System.err.println(“出现错误:被除数和除数必须是整数,” +

                               “除数不能为零。”);

                   System.out.println(e.getMessage());

             } finally {

                   System.out.println(“感谢使用本程序!”);

             }

      }

}

try-catch-finally程序块的执行流程大致分为两种情况。

Ø    如果try块中所有语句正常执行完毕,那么finally块就会被执行。比如:当我们在控制台输入两个数字时,示例4中的try语句块中的代码将正常执行,不会执行catch语句块中的代码,但是finally块中的代码将被执行。运行效果如图8.7所示。

 

 

图8.7  正常情况下的运行效果图

Ø    如果try语句块在执行过程中碰到异常,无论这种异常能否被catch块捕获到,都将执行finally块中的代码。比如:当我们在控制台输入除数为0时,示例4中的try语句块中将抛出异常,进入catch语句块,最后finally块中的代码也将被执行。运行效果如图8.8所示。

 

 

图8.8  异常情况下的运行效果图

try-catch-finally结构中try块是必须的,catch和finally块为可选,但两者至少出现其中之一。

需要特别注意的是:即使在try块和catch块中存在return语句,finally块中语句也会执行。发生异常时的执行顺序是:执行try块或catch中return之前的语句,执行finally中语句,执行try块或catch中的return语句退出。

finally块中语句不执行的唯一情况是:在异常处理代码中执行System.exit(1),将退出Java虚拟机。

 

示例5

import  java.util.Scanner;

/**

 *测试finally的执行。

 *@author 南京

 */

public  class  Test5 {

      public  static  void  main(String[]  args) {

            try {

                   Scanner  in = new  Scanner(System.in);

                   System.out.print(“请输入被除数:”);

                   int  num1 = in.nextInt();

                   System.out.print(“请输入除数:”);

                   int  num2 = in.nextInt();

                   System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

                   // return;  // finally语句块仍旧会执行

             } catch (Exception  e) {

                   System.err.println(“出现错误:被除数和除数必须是整数,” +

                               “除数不能为零。”);

                   System.exit(1); //finally语句块不执行的唯一情况

                   //return; //finally语句块仍旧会执行

             } finally {

                   System.out.println(“感谢使用本程序!”);

             }

      }

}

运行效果如图8.9所示。

 

 

图8.9  finally中语句不执行的唯一情况

 

8.2.4  多重catch块

在上面计算并输出商的示例中,其实至少存在两种异常情况,输入非整数内容和除数为0,在示例3中我们统一按照Exception类型捕获,其实完全可以分别捕获,就是使用多重catch块。

一段代码可能会引发多种类型的异常,这时,我们可以再一个try语句块后面跟多个catch语句块,分别处理不同的异常。但排列顺序必须是从子类到父类,最后一个一般都是Exception类。因为按照匹配原则,如果把父类异常放到前面,后面的catch块将得不到执行的机会。

运行时,系统从上到下分别对每个catch语句块处理的异常类型进行检测,并执行第一个与异常类型匹配的catch语句,执行其中的一条catch语句之后,其后的catch语句都将被忽略。

对示例3进行修改,代码如示例6所示。

 

示例6

import  java.util.Scanner;

import  java.util.InputMismatchException;

/**

 *多重catch块。

 *@author 南京

 */

public  class  Test6 {

      public  static  void  main(String[]  args) {

            try {

                   Scanner  in = new  Scanner(System.in);

                   System.out.print(“请输入被除数:”);

                   int  num1 = in.nextInt();

                   System.out.print(“请输入除数:”);

                   int  num2 = in.nextInt();

                   System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

             } catch (InputMismatchException  e) {

                   System.err.println(“被除数和除数必须是整数。”);

             } catch (ArithmeticException  e) {

                   System.err.println(“除数不能为零。”);

             } catch (Exception  e) {

                   System.err.println(“其他未知异常。”);

             } finally {

                   System.out.println(“感谢使用本程序!”);

 

             }

      }

}

 

程序运行后,如果输入的不是整数,系统会抛出InputMismatchException异常对象,因此进入第一个catch语句块,并执行其中的代码,而其他的catch块将被忽略。运行效果如图8.10所示。

 

 

图8.10  进入第一个catch语句块

如果系统提示输入被除数时,输入200,系统会接着提示输入除数;输入0,系统会抛出ArithmeticException异常对象,因此进入第二个catch语句块,并执行其中的代码,其他的catch块将被忽略。运行效果如图8.11所示。

 

 

图8.11  进入第二个catch语句块

——注意———————————————————————————————————

在使用多重catch块时,catch块的排列顺序必须是从子类到父类,最后一个一般都是Exception类。下面的代码片断是错误的。

try{

       Scanner  in = new  Scanner(System.in);

       int  totalTime = in.nextInt();

} catch (Exception  e1) {

       System.out.println(“发生错误!”);

} catch (InputMismatchException  e2) {

       System.out.println(“必须输入数字!”);

}

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

 

8.2.5  上机练习

上机练习1

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

训练要点

Ø    理解异常及异常处理机制。

Ø    使用try-catch-finally捕获和处理异常。

 

需求说明

安装控制台提示信息输入1~3之间的任一个数字,程序将根据输入的数字输出相应的课程名称,如图8.12所示。课程代码和课程名称的对应关系如下所示。

1:C#编程;2:Java编程;3:SQL基础。

 

 

图8.12  根据输入的数字输出课程名称

实现思路及关键代码

  1. 控制台输出提示内容:“请输入课程代码(1~3之间的一个数字)“。
  2. 接收键盘输入。
  3. 根据键盘输入进行判断。如果输入正确,输出对应课程名称;如果输入错误,给出错误提示。
  4. 不管输入是否正确,均输出“谢谢查询“语句。

注意编写注释

8.2.6  声明异常——throws

如果在一个方法体中抛出了异常,我们就希望调用者能够及时地捕获异常,那么如何通知调用者呢?Java语言中通过关键字throws声明某个方法可能抛出的各种异常。throws可以同时声明多个异常,之间由逗号隔开。

在示例7中,把计算并输出商的任务封装在divide()方法中,并在方法的参数列表后通过throws声明了异常,然后再main方法中调用该方法,此时main方法就知道divide()方法中抛出了异常,可以采用两种方式进行处理。

Ø    通过try-catch捕获并处理异常。

Ø    通过throws继续声明异常。如果调用者不知道如何处理该异常,可以继续通过throws声明异常,让上一级调用者处理异常。main方法声明的异常将由Java虚拟机来处理。

 

示例7

import  java.util.Scanner;

/**

 *使用throws抛出异常。

 *@author 南京

 */

public  class  Test7 {

     /**

 *通过try-catch捕获并处理异常。

 *@param  args

 */

public  static  void  main(String[]  args) {

            try {

                   divide();

            } catch  (Exception  e) {

                   System.err.println(“出现错误:被除数和除数必须是整数,” +

                               “除数不能为零。”);

                   e.printStackTrace();

            }

      }

//    /**

//     *通过throws继续声明异常。

//     */

//    public  static  void  main(String[] args) throws  Exception {

//          divide();

//    }

       /**

    *输入被除数和除数,计算商并输出。

    *@throws  Exception

    */

       public  static  void  divide() throws  Exception {

              Scanner  in = new  Scanner(System.in);

              System.out.print(“请输入被除数:”);

              int  num1 = in.nextInt();

              System.out.print(“请输入除数:”);

              int  num2 = in.nextInt();

              System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

       }

}

 

8.3  抛出异常

8.3.1  抛出异常——throw

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

前面介绍了很多关于捕获异常的知识,大家一定会问:既然可以捕获到各种类型的异常,那么这些异常是在什么地方抛出的呢?

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

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

除了系统自动抛出异常外,在编程过程中,我们往往遇到这样的情形:有些问题是系统无法自动发现并解决的,比如年龄不在正常范围内,性别输入不是“男“或”女“等,此时需要程序员而不是系统来自行抛出异常,把问题提交给调用者去解决。

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

在Java语言中,可以使用throw关键字来自行抛出异常。在示例8的代码中抛出一个异常,抛出异常的原因在于“在当前环境无法解决参数问题,因此在方法内容通过throw抛出异常,把问题交给调用者去解决。在调用该方法的示例9中捕获并处理异常。

 

示例8

/**

 *使用throws在方法内抛出异常。

 *@author 南京

 */

public  class  Person {

      private  String  name = “”; //姓名

      private  int  age = 0; //年龄

      private  String  sex = “男”; //性别

    /**

 *设置性别。

 *@param  sex  性别

 *@throws  Exception

 */

public  void  setSex(String  sex) throws  Exception{

       if (“男”.equals(sex) ∣∣ “女”.equals(sex)){

             this.sex = sex;

       } else {

             throw  new  Exception(“性别必须是“男”或者“女”!”);

       }

}

/**

 *打印基本信息。

 */

public  void  print() {

       System.out.println(this.name + “(“ + this.sex

                   + ”, “+ this.age + ”岁)”);

}

}

 

示例9

/**

 *捕获throw抛出的异常。

 *@author 南京

 */

public  class  Test8 {

public  static  void  main(String[]  args) {

       Person  person = new  Person();

            try {

                   person.setSex(“Male”);

                   person.print();

            } catch  (Exception  e) {

                   e.printStackTrace();

            }

      }

}

运行效果如图8.13所示。

图8.13  测试throw抛出异常

——对比———————————————————————————————————

throw和throws的区别表现在以下三个方面。

Ø    作用不同:throw用于程序员自行产生并抛出异常,throws用于声明在该方法内抛出了异常。

Ø    使用的位置不同:throw位于方法体内部,可以作为单独语句使用。throws必须跟在方法参数列表的后面,不能单独使用。

Ø    内容不同:throw抛出一个异常对象,而且只能是一个。throws后面跟异常类,而且可以跟多个异常类。

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

8.3.2  异常的分类

Java的异常体系包括许多的异常类,它们之间存在继承关系。Java的异常体系结构图如图8.14所示。

 

 

图8.14  Java的异常体系结构图

Ø    Throwable类:所有异常类型都是Throwable类的子类,它派生两个子类:Error和Exception。

Ø    Error类:表示仅靠程序本身无法恢复的严重错误,比如说内存溢出动态链接失败,虚拟机错误。应用程序不应该抛出这种类型的对象(一般是由虚拟机抛出)。假如出现这种错误,除了尽力使程序安全退出外,在其他方面是无能为力的。所以在进行程序设计时,应该更关注Exception类。

Ø    Exception类:由Java应用程序抛出和处理的非严重错误,比如所需文件找不到,网络连接不通或中断、算术运算出错(如被零除)、数组下标越界、装载了一个不存在的类,对null对象操作,类型转换异常等。它的各种不同的子类分别对应不同类型的异常。

Ø    运行时异常:包括RuntimeException及其所有子类。不要求程序必须对它们做出处理。例如在示例6中ArithmeticException、InputMismatchException异常,在程序中并没有使用try-catch或throws进行处理,仍旧可以进行编译和运行,如果运行时发生异常,会输出异常的堆栈信息并中止程序运行。

Ø    Checked异常(非运行时异常):除了运行时异常外的其他由Exception继承来的异常类。程序必须捕获或者声明抛出这种异常,否则会出现编译错误,无法通过编译。处理方式包括两种:通过try-catch在当前位置捕获并处理异常,通过throws声明抛出异常交给上一级调用方法处理。

 

示例10

/**

 *不处理Checked异常。

 *@author 南京

 */

public  class  Test9 {

public  static  void  main(String[]  args) {

      FileInputStream  fis = null;

      // 创建指定文件的流

     fis = new  FileInputStream(new  File(“le.txt”));

     // 关闭指定文件的流

     fis . close();

     }

}

运行效果如图8.15所示,由于没有对Checked异常进行处理,结果显示无法通过编译。

 

 

图8.15  没有处理Checked异常的运行效果图

对示例10中的Checked异常进行处理,可以正常通过编译,代码如示例11所示。示例中的FileNotFoundExeption、IOException都是Checked异常。

 

示例11

import  java.io.*;

/**

 *处理Checked异常。

 *@author 南京

 */

public  class  Test10 {

public  static  void  main(String[]  args) {

       FileInputStream  fis  = null;

            try {

                   // 创建指定文件的流

                   fis = new  FileInputStream(new  File(“le.txt”));

            } catch  (FileNotFoundException  e) {

                   System.err.println(“无法找到指定文件!”);

                   e.printStackTrace();

            }

            try {

                   // 创建指定文件的流

                   fis . close();

            } catch  (IOException  e) {

                   System.err.println(“关闭指定文件输入流时出现异常!”);

                   e.printStackTrace();

            }

      }

}

 

8.3.3上机练习

上机练习2

练习——使用throw抛出异常

训练要点

Ø    使用throw抛出异常。

Ø    使用throws声明抛出异常。

需求说明

在示例8的基础上,在Persom类的setAge(int  age)方法中对年龄进行判断,如果年龄介于1~100之间直接赋值,否则抛出异常。然后再TestException2类中创建Person对象并调用setAge(int  age)方法,使用try-catch捕获并处理异常,如图8.16所示。

 

 

图8.16  上机练习2的结果图

实现思路及关键代码

参考示例8和示例9。

 

8.4开源日志记录工具log4j

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

在示例6中,根据控制台提示输入被除数和除数,然后计算并输出商,不同的异常被正确地捕获,并在控制台上输出相应信息。有时,我们还希望以文件的形式记录这些异常信息,甚至记录程序正常运行的关键步骤信息,以便日后查看,这种情况该如何处理呢?

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

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

虽然,我们可以自行编程实现这一效果。但是从更注重效率和性能方面考虑,还有一个更好的选择,那就是使用流行的开源项目:log4j。

在MyEclipse中使用log4j的步骤比较简单,主要分为四个步骤。

  1. 在项目中加入log4j所使用的JAR文件。
  2. 创建log4j.properties文件。
  3. 编写log4j.properties文件,配置日志信息。
  4. 在程序中使用log4j记录日志信息。

在学习log4j的具体用法之前,我们先来了解一下什么是日志及其日志的分类、了解一下log4j。

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

8.4.1日志及分类

软件的运行过程离不开日志,日志主要用来记录系统运行过程中的一些重要的操作信息,便于监视系统运行情况,帮助用户提前发现和避开可能出现的问题,或者出现问题后根据日志找到发生的原因。

日志根据记录内容的不同,主要分成以下三类。

Ø    SQL日志:记录系统执行的SQL语句。

Ø    异常日志:记录系统运行中发生的异常事件。

Ø    业务日志:用于记录系统运行过程,例如用户登录、操作记录。

log4j是一个非常优秀的日志(log)记录工具。通过使用log4j,我们可以控制日志的输出级别,可以控制日志信息输送的目的地是控制台、文件等,还可以控制每一条日志的输出格式。

要使用log4j,首先需要下载log4j的JAR包。log4j是Apache的一个开源项目,官方网站是:http://logging.apache.org/log4j。这里使用log4j 1.2.15版本,下载地址是http://logging.apache.org/log4j/1.2/dowload.html,下载其中的zip文件并解压,里面包含的主要内容及在zip包内的路径如下。

Ø    log4j的JAR包:apache-log4j-1.2.15\log4j-1.2.15.jar。

Ø    使用手册(manual):apache-log4j-1.2.15\site\manual.html。

Ø    JavaDoc(APIDocs):apache-log4j-1.2.15\site\apidocs\index.html。

 

8.4.2  如何使用log4j记录日志

下面就开始具体学习log4j,使用log4j来记录日志。

  1. 在项目中加入log4j所使用的JAR文件。

在MyEclipse中选中要使用log4j的项目,然后选择“项目“→”属性“→”Java构建路径“→”库“→”添加外部JAR“选项,弹出选择JAR的窗口,找到自己电脑上存放的文件:log4j-1.2.15.jar,如图8.17所示。确认后回到项目的属性窗口,单击”确定“按钮即可。

 

 

图8.17  添加外部JAR

  1. 创建log4j.properties文件。

使用log4j需要创建log4j.properties文件,该文件专门用来配置日志信息,例如输出级别、输出目的地、输出格式等。

选择要使用log4j的项目,右击src,选择“新建’→‘文件“命令,弹出”新建文件“对话框,输入文件名”log4j.properties“,单击”完成“按钮,结束创建。创建后结果如图8.18所示。

                               

图8.18  创建log4j.properties文件

  1. 编写log4j.properties文件,配置日志信息。

现在,我们就一起来编写这个文件,内容如示例12所示。各配置项的具体含义我们会在后面详细讲解。根据配置,将在控制台和文件中同时记录日志信息,日志文件的名字是le.log。

 

示例12

### 设置Logger输出级别和输出目的地 ###

log4j.rootLogger = debug, stdout, logfile

 

### 把日志信息输出到控制台 ###

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.Target=System.err

log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout

 

### 把日志信息输出到文件: le.log ###

log4j.appender.logfile=org.apache.log4j.FileAppender

log4j.appender.logfile.File =le.log

log4j.appender.logfile.layout =org.apache.log4j.PatternLayout

log4j.appender.logfile.layout.ConversionPattern=%d{yyyy-MM-dd  HH:mm:ss}

  %l  %F  %P  %m%n

  1. 在程序中使用log4j记录日志信息。

现在可以再程序中使用log4j了。对示例6进行修改,代码如示例13所示。

 

示例13

import  java.util.InputMismatchException;

import  java.util.Scanner;

import  org.apache.log4j.Logger;

/**

 *使用log4j记录日志。

 *@author 南京

 */

public  class  Test11 {

      private static Logger logger = Logger.getLogger(Test11.class.getName());

      public  static  void  main(String[]  args) {

            try {

                Scanner  in = new  Scanner(System.in);

                System.out.print(“请输入被除数:”);

                int  num1 = in.nextInt();

                logger.debug(“请输入被除数:” + num1);

                System.out.print(“请输入除数:”);

                int  num2 = in.nextInt();

                logger.debug(“请输入被除数:” + num2);

                System.out.println(String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

                logger.debug(“输出运算结果:” + String.format(“%d / %d =%d”,

numl, num2, num1 / num2));

             } catch (InputMismatchException  e) {

                logger.error(“被除数和除数必须是整数”, e);

             } catch (ArithmeticException  e) {

                logger.error(e.getMessage());

             } catch (Exception  e) {

                logger.error(e.getMessage());

             } finally {

                System.out.println(“感谢使用本程序!”);

 

             }

      }

}

怎么样?简单吧!首先创建一个私有静态的Logger对象,然后就可以通过它的debug或者error等方法输出日志信息了。控制台运行效果如图8.19和图8.20所示。

 

 

图8.19  异常情况下输出到控制台的日志信息

 

 

图8.20  正常情况下输出到控制台的日志信息

打开日志文件le.log文件,内容如图8.21所示。

 

 

图8.21  输出到文件le.log中的日志信息

Logger是用来替代System.out或者System.err的日志记录器,用来供程序员出错日志信息。它提供了一系列方法来输出不同级别的日志信息。
Ø    public  void  debug(Object  msg)

Ø    public  void  debug(Object  msg, Throwable  t)

Ø    public  void  info(Object  msg)

Ø    public  void  info(Object  msg, Throwable  t)

Ø    public  void  warn(Object  msg)

Ø    public  void  warn(Object  msg, Throwable  t)

Ø    public  void  error(Object  msg)

Ø    public  void  error(Object  msg, Throwable  t)

Ø    public  void  fatal(Object  msg)

Ø    public  void  fatal(Object  msg, Throwable  t)

 

8.4.3  log4j配置文件

示例12是log4j的配置文件log4j.properties,下面对其中的配置信息进行详细解释。

  1. 输出级别

log4j.rootLogger = debug,stdout,logfile

其中debug指的是日志记录器(Logger)的输出级别,主要输出级别及含义如下。

Ø    fatal:指出每个严重的错误事件将会导致应用程序的退出。

Ø    error:指出虽然发生错误事件,但仍然不影响系统的继续运行。

Ø    warn:表明会出现潜在错误的情形。

Ø    info:在粗粒度级别上指明消息,强调应用程序的运行过程。

Ø    debug:指出细粒度信息事件,对调试应用程序是非常有帮助的。

各个输出级别优先级为:

 

fatal > error > warn > info > debug

 

日志记录器(Logger)将只输出那些级别高于或等于它的级别的信息。例如级别为debug,将输出fatal、error、warn、info、debug级别的日志信息。而级别为error,将只输出fatal、error级别的日志信息。

 

  1. 日志输出目的地Appender

log4j.rootLogger = debug, stdout, logfile

 

其中stdout、logfile指的是日志输出目的地的名字。

log4j允许记录日志到多个输出目的地,一个输出目的地被称作一个Appender。log4j中最常用的Appender有以下两种。

Ø    ConsoleAppender:输出日志事件到控制台。通过Target属性配置输出到System.out或者System.err,默认的目标是System.out。

Ø    FileAppender:输出日志事件到一个文件。通过File属性配置文件的路径及名称。

 

示例12中共有两个Appender,第一个命名为stdout,使用了ConsoleApeender,通过配置Target属性,把日志信息写到控制台System.err。第二个Appender命名为logfile,使用了FileAppender,通过配置File属性,把日志信息写到指定的文件le.log中。

 

  1. 日志布局类型Layout

Appender必须使用一个与之相关联的布局类型Layout,用来指定它的输出样式。log4j中最常用的Layout有以下三种。

Ø    HTMLLayout:格式化日志输出为HTML表格。

Ø    SimpleLayout:以一种非常简单的方式格式化日志输出,它打印基本Level,然后跟着一个破折号“——”,最后是日志消息。

Ø    PatternLayout:根据指定的转换模式格式化日志输出,从而支持丰富多样的输出格式。需要配置layout.ConversionPattern属性,如果没有配置该属性,则使用默转换模式。

 

示例12中的第一个Appender是stdout,使用了SimpleLayout。第二个Appender是logfile,使用了PatternLayout,需要配置layout.ConversionPattern属性来自定义输出格式。

 

  1. 转换模式ConversionPattern

对于PatterLayout,需要配置layout.ConversionPattern属性,常用的配置参数及含义如下。

Ø    %d:用来设置输出日志的日期和时间,默认格式ISO8601.也可以再其后指定格式,比如%d{yyyy-MM-dd  HH:mm:ss},输出格式类似于2010-03-09  17:51:08.

Ø    %m:用来输出代码中指定的消息。

Ø    %n:用来输出一个回车换行符。

Ø    %l:用来输出日志事件的发生位置,包括类名、发生的线程,以及在代码中的行数。例如:如果输出为cn.le.log.Test11.main(Test11.java:21)说明日志事件发生在cn.le.log包下的Test11类的main线程中,在代码中的行数为第21行。

Ø    %p:用来输出优先级,即debug、info、warn、error、fatal等。

Ø    %F:用来输出文件名。

Ø    %M:用来输出方法名。

 

8.4.4  上机练习

上机练习3

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

训练要点

Ø    在程序中使用log4j记录日志。

Ø    编写log4j.properties文件。

需求说明

按照控制台提示输入被除数和除数。如果除数为0,在控制台输出日志信息,包括完整的异常堆栈信息。

 

实现思路及关键代码

  1. 编写TestLog1类,主要步骤如下。

按照控制台提示输入被除数,必须是整数;按照控制台提示输入除数,输入0;在控制台输出异常日志信息,要求包括完整的异常堆栈信息;不管是否出现异常,均输出“感谢使用本程序!”语句。

  1. 编写log4j.properties文件,实现如下配置。

日志级别为debug;输出目的地名字为stdout;布局类型为PatternLayout,使用ConversionPattern配置输出格式,至少输出异常日期、完整的异常堆栈信息。

注意编写注释。

参考解决方案

log4j.properties

 

log4j.rootLogger = debug,stdout

log4j.appender.stdout = org.apache.log4j.ConsoleAppender

log4j.appender.stdout.Target = System.err

log4j.appender.stdout.layout = org.apache.log4j.PatternLayout

log4j.appender.stdout.layout.ConversionPattern = %d %l %m %n

 

上机练习4

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

训练要点

Ø    在程序中使用log4j记录日志。

Ø    编写log4j.properties文件。

需求说明

按照控制台提示输入被除数和除数。如果输入不为整数,记录error日志;如果除数为0,记录warn日志;如果正常输入记录info日志。

 

实现思路及关键代码

  1. 编写TestLog2类,主要步骤如下。

按照控制台提示输入被除数和除数;如果输入不为整数,记录error日志;如果除数为0,记录warn日志:如果正常输入记录info日志。不管是否出现异常,均输出“感谢使用本程序!”语句。

  1. 编写log4j.properties文件,实现如下配置。

日志级别为info;输出目的地名字为logfile,日志文件名为le.log;布局类型为PatternLayout,使用ConversionPattern配置输出格式。要求输出日志的日期和时间、日志优先级、源文件名和方法名。

注意编写注释。

 

 

 

 

本章总结

 

Ø    异常时由Java应用程序抛出和处理的非严重错误,它可以分为Checked异常和运行时异常两大类。

Ø    Checked异常必须捕获或者声明抛出,否则无法通过编译。运行时异常不要求必须捕获或者声明抛出。

Ø    Java的异常处理是通过五个关键字来实现的:try、catch、finally、throw和throws。

Ø    即使在try块,catch块中存在return语句,finally块中语句也会执行。finally块中语句不执行的唯一情况是:在异常处理代码中执行System.exit(1)。

Ø    可以再一个try语句块后面跟多个catch语句块,分别处理不同额异常。但排列顺序必须是从特殊到一般,最后一个一般都是Exeption类。

Ø    log4j是一个优秀的日志记录工具。常用使用方式是配置log4j.properties文件,从而控制日志的输出级别、控制日志的目的地和控制日志输出格式。

 

 

本章作业

 

一、选择题

  1. 下面选项中能单独和finally语句一起使用的是(  )。
  1. try
  2. catch
  3. throw
  4. throws

 

2.下面的程序的执行结果是(    )。

public  class  Test {

       public  static  void  main(String[] args) {

              new  Test().test();

       }

       public  void  test() {

              try {

                     System.out.print(“try”);

              } catch (ArrayIndexOutOfBoundsException  e) {

                     System.out.print(“catch1”);

              } catch (Exception  e) {

                     System.out.print(“catch2”);

              } finally {

                     System.out.print(“finally”);

              }

       }

}

  1. try  finally
  2. try  catch1  finally
  3. try  catch2  finally
  4. finally

 

3.以下代码段中正确的是(    )。

A.try{

            System.out.print(“try”);

      } catch(Exception  e)  {

             System.out.print(“catch”);

      }

B.try{

            System.out.print(“try”);

      }

C.try{

            System.out.print(“try”);

      } finally(Exception  e)  {

             System.out.print(“finally”);

      }

D.try{

            System.out.print(“try”);

      } finally  {

             System.out.print(“finally”);

      }

 

4.下面程序的执行结果是(    )。

public  class  Test {

       public  static  void  foo() {

              try {

                     String  s = null;

                     s = s.toLowerCase();

              } catch(NullPointerException  e) {

                     System.out.print(“2”);

              } finally {

                     System.out.print(“3”);

              }

              System.out.print(“4”);

        }

        public  static  void  main(String  args[]) {

               foo();

        }

}

A.2

B.34

C.23

D.234

 

5.下列异常类中在多重catch中同时使用时,(    )应该最后列出。

A.ArithmeticException

B.NumberFormatException

C.Exception

D.ArrayIndexOutOfBoundsException

 

二、简答题

  1. 请指出下列Java代码中的错误。

public  class  Test {

       public  void  test() {

              try {

                    System.out.println(“try”);

               } catch (ArrayIndexOutOfBoundsException  e) {

                     System.out.print(“catch1”);

              } catch (Exception  e) {

                     System.out.print(“catch2”);

              } catch(NullPointerException  e) {

                     System.out.print(“catch3”);

              }

       }

}

 

  1. 请给出下面这段程序的执行结果,并说明原因。

public  class  Test {

       public  static  void  foo(int  i) {

              try {

                    if(i==1) {

                         throw  new  Exception();

                    }

                    System.out.println(“1”);

               } catch (Exception  e) {

                     System.out.print(“2”);

              } finally {

                     System.out.print(“3”);

              }

              System.out.print(“4”);

       }

       public  static  void  main(String  arg[]) {

              foo(1);

       }

}

 

  1. 编写能产生ArrayIndexOutOfBoundsException异常的代码,在控制台上输出异常信息。

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

数组下标异常,使用try-catch进行捕获并处理。

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

 

  1. 简述Java异常体系结构。
  2. 修改第三题,使用log4j记录日志,在le.log文件中记录产生的异常信息。

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

在catch块中使用log4j记录日志。

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

 

 

 

9 章

                      合 框 架

 

 

 

本章工作任务

Ø 使用List保存和输出宠物信息

Ø 使用Map保存和输出宠物信息

Ø 使用Iterator遍历各种集合

Ø 使用泛型集合保存和输出宠物信息

本章技能目标

Ø 掌握集合框架包含的内容

Ø 掌握ArrayList和LinkedList的使用

Ø 掌握HashMap的使用

Ø 掌握Iterator的使用

Ø 掌握泛型集合的使用

 

 

本章单词

 

 

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

                                      1.collection:                                           

                                      2.set:                                      

                                      3.list:                                              

                                      4.map:                                       

                                      5.iterator:                                         

                                      6.generic:                                      

                                      7.remove:                                              

                                      8.contain:                                              

                                      9.key:                                 _

                                      10.value:_______________________________

             

 

 

本章简介

本章讲解Java中使用非常频繁的内容——集合框架。首先由实际问题引出集合框架并介绍它所包含的内容,然后详细讲解ArrayList、LinkedList和HashMap三种具体的集合类,详细讲解集合的统一遍历工具——迭代器Iterator,最后讲解使用泛型集合改进集合的使用。在学习中应首先从整体上把握集合框架所包含的内容,而在具体学习时集合类注意区分彼此的不同之处,通过对比加深理解和记忆。

9.1  集合框架概述

9.1.1  引入集合框架

在我们的电子宠物系统中,如果想存储多个宠物信息,可以使用数组来实现。比如,可以定义一个长度为50的Dog类型的数组,存储多个Dog对象的信息。但是采用数组存在以下一些明显缺陷。

Ø 数组长度固定不变,不能很好适应元素数量动态变化的情况。如果要存储大于50个狗狗的信息,则数组长度不足;如果只存储20个狗狗的信息,则造成内存空间浪费。

Ø 可通过数组名.length获取数组的长度,却无法直接获取数组中真实存储的狗狗个数。

Ø 数组采用在内存中分配连续空间的存储方式,根据下标可以快速获取对应狗狗信息,但根据狗狗信息查找时效率低下,需要多次比较。在进行频繁插入、删除操作时同样效率低下。

另外举个例子:在存储狗狗信息时,希望分别存储狗狗昵称与狗狗信息,两者具有一一对应的关系,狗狗昵称作为狗狗信息的键存在,可以根据昵称获得狗狗信息。这显然也无法通过数组来解决。

从以上分析可以看出数组在处理一些问题时存在明显的缺陷,而集合完全弥补了数组的缺陷,它比数组更灵活更实用,可大大提高软件的开发效率,并且不同的集合可适用于不同场合。如果写程序时并不知道程序运行时会需要多少对象,或者需要更复杂的方式存储对象,可以考虑使用Java集合来解决。

 

9.1.2  Java集合框架包含的内容

Java集合框架,为我们提供了一套性能优良、使用方便的接口和类,它们都位于java.util包中。Java集合框架包含的主要内容及彼此之间的关系如图9.1所示。

 

图9.1  Java集合框架简图

集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

Ø 接口:即表示集合的抽象数据类型,在图9.1中以虚线框表示,例如Collection、List、Set、Map等。

Ø 实现:即集合框中接口的具体实现,在图9.1中以实线框表示,粗实线框表示最常用的实现。例如ArrayList、LinkedList、HashMap、HashSet等。

Ø 算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。Java提供了进行集合操作的工具类Collections(注意不是Collection,类似于Arrays类),它提供了对集合进行排序等多种算法实现。大家在使用Collections的时候可以查阅JDK帮助文档,本章不做过多的讲解。

从图9.1中可以清楚的看出Java集合框架共有两大类接口,Collection和Map,其中Collection又有两个子接口——List和Set,所以通常说Java集合框架共有三大类接口,List、Set和Map。它们的共同点在于都是集合接口,都可以用来存储很多对象。它们的区别如下。

Ø Collection接口存储一组不唯一(允许重复)、无序的对象。

Ø Set接口继承Collection接口,存储一组唯一(不允许重复)、无序的对象。

Ø List接口继承Collection接口,存储一组不唯一(允许重复)、有序(以元素插入的次序来放置元素,不会重新排列)的对象。

Ø Map接口存储一组成对的键—值对象,提供key(键)到value(值)的映射。Map中的key不要求有序,不允许重复。value同样不要求有序,但允许重复。

 

在集合框架中,List可以理解为前面讲过的数组,元素的内容可以重复并且有序,如图9.2所示。Set可以理解为数学中的集合,或者理解成一个大麻袋,里面数据不重复并且无序,如图9.3所示。Map也可以理解为数学中的集合,或者理解成一个大麻袋,只是其中每个元素都由key和value两个对象组成,如图9.4所示。

 

 

0

1

2

3

4

5

 

aaaa

dddd

cccc

aaaa

eeee

dddd

 

图9.2  List集合示意图

                                                                                                                                          

           

            

aaaa

dddd     cccc

RU

Russia

       
 
   
 

France

FR

JP

Japan

CN

China

 

 

 

 

 

 

 

 

 

 

图9.3  Set集合示意图                    图9.4  Map集合示意图

9.2  List接口

实现List接口的常用类有ArrayList和LinkedList。它们都可以容纳所有类型的对象,包括null,允许重复,并且都保证元素的存储顺序。

ArrayList对数组进行了封装,实现了长度可变的数组,和数组采用相同存储方式,在内存中分配连续的空间,如图9.5所示。它的优点在于遍历元素和随机访问元素的效率比较高。

0

1

2

3

4

5

 

aaaa

dddd

cccc

aaaa

eeee

dddd

 

图9.5  ArrayList存储方式示意图

 

LinkedList采用链表存储方式,如图9.6所示,优点在于插入、删除元素时效率比较高。它提供了额外的addFirst()、addLast()、removeFirst()和removeLast()等方法,可以在LinkedList的首部或尾部进行插入或者删除操作。这些方法使得LinkedList可被用作堆栈(stack)或者队列(queue)。

 

aaaa        

dddd       

cccc       

aaaa       

 
 

 

图9.6  LinkedList存储方式示意图

9.2.1  ArrayList集合类

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

使用集合存储多个狗狗的信息,获取存储的狗狗的总数,如何按照存储顺序获取各个狗狗信息并逐条打印出相关内容?

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

 

 

 

 

 

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

元素个数不确定,要求获取存储的元素的实际个数,按照存储顺序获取并打印元素信息,可以通过List接口的实现类ArrayList实现该需求。

  1. 创建多个狗狗对象。
  2. 创建ArrayList集合对象并把多个狗狗对象放入其中。
  3. 输出集合中狗狗的数量。
  4. 通过遍历集合显示各条狗狗的信息。

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

实现代码如示例1所示。

示例1

/**

  * 测试ArrayList的add()、size()、get()方法。

   * @author 南京

  * /

public  class  Test1  {

     public  static  void  main(String[ ]  args)  {

       //1、创建四个狗狗对象

       Dog  ououDog=new  Dog(“欧欧”,“雪娜瑞”);

       Dog  yayaDog=new  Dog(“亚亚”,“拉布拉多”);

       Dog  meimeiDog=new  Dog(“美美”,“雪娜瑞”);

       Dog  feifeiDog=new  Dog(“菲菲”,“拉布拉多”);

       //2、创建ArrayList集合对象并把四个狗狗对象放入其中

       List  dogs=new  ArrayList();

       dogs.add(ououDog);

       dogs.add(yayaDog);

dogs.add(meimeiDog);

dogs.add(2, feifeiDog);   //添加feifeiDog到指定位置

       //3、输出集合中狗狗的数量

       System.out.println(“共计有”+dogs.size()+“条狗狗。”);

       //4、通过遍历集合显示各条狗狗信息

       System.out.println(“分别是:”);

       for  (int  i=0;i<dogs.size();i++﹚  {

           Dog  dog=(Dog)  dogs.get(i);

           System.out.println(dog.getName()+“\t”

                     +dog.getStrain());

    }

    }

}

运行结果如图9.7所示。

 

 

图9.7  使用ArrayList存储和输出狗狗信息

——注意———————————————————————————————————

List接口的add(Object  o)方法的参数类型是Object,即使在调用时实参是Dog类型,但系统认为里面只是Object,所以在通过get(int  i)方法获取元素时必须进行强制类型转换,如Dog  dog=(Dog)dogs.get(i),否则会出现编译错误。

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

示例1中只使用到了ArrayList的部分方法,接下来,我们在这个示例的基础上,扩充以下几部分功能。

Ø 删除指定位置的狗狗,如第一个狗狗。

Ø 删除指定的狗狗,如删除feifeiDog对象。

Ø 判断集合中是否包含指定狗狗。

List接口提供了相应方法,直接使用即可,实现代码如示例2所示。

示例2

/**

  * 测试ArrayList的remove()、contains()方法。

   * @author 南京

  * /

public  class  Test2  {

     public  static  void  main(String[ ]  args)  {

       //1、创建多个狗狗对象

       Dog  ououDog=new  Dog(“欧欧”,“雪娜瑞”);

       Dog  yayaDog=new  Dog(“亚亚”,“拉布拉多”);

       Dog  meimeiDog=new  Dog(“美美”,“雪娜瑞”);

       Dog  feifeiDog=new  Dog(“菲菲”,“拉布拉多”);

       //2、创建ArrayList集合对象并把多个狗狗对象放入其中

       List  dogs=new  ArrayList();

       dogs.add(ououDog);

       dogs.add(yayaDog);

dogs.add(meimeiDog);

dogs.add(2, feifeiDog);

       //3、输出删除前集合中狗狗的数量

       System.out.println(“删除之前共计有”+dogs.size()+“条狗狗。”);

       //4、删除集合中第一个狗狗和feifeiDog狗狗

       dogs.remove(0)

       dogs.remove(feifeiDog);

       //5、显示删除后集合中各条狗狗信息

       System.out.println(“\n删除之后还有”+dogs.size()+“条狗狗。”);

       System.out.println(“分别是:”);

       for  (int  i=0;i<dogs.size();i++﹚  {

           Dog  dog=(Dog)  dogs.get(i);

           System.out.println(dog.getName()+“\t”+dog.getStrain());

       //6、判断集合中是否包含指定狗狗信息

       if(dogs.contains(meimeiDog))

           System.out.println﹙“\n集合中包含美美的信息”﹚;

       else

           System.out.println﹙“\n集合中不包含美美的信息”﹚;

    }

    }

}

运行结果如图9.8所示。

 

 

图9.8  使用ArrayList删除狗狗信息

 

下面,我们总结一下在示例1和示例2中使用到的List接口中定义的各种常用方法(也是ArrayList的各种常用方法),见表9-1。

 

 

 

 

 

 

 

 

 

 

表9-1  List接口中定义的各种常用方法

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

返回类型    方法                     说明

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

boolean    add(Object o)              在列表的末尾顺序添加元素,起始索引位置从0开始

void       add(int index,Object o)  在指定的索引位置添加元素

                                      注意:索引位置必须介于0和列表中元素个数之间

int        size()                   返回列表中的元素个数

Object     get(int index)           返回指定索引位置处的元素

                                      注意:取出的元素是Object类型,使用前需要进行强制类型转换

boolean    contains(Object o)       判断列表中是否存在指定元素

boolean    remove(Object o)         从列表中删除元素

Object     remove(int index)        从列表中删除指定位置元素,起始索引位置从0开始

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

 

——对比———————————————————————————————————

Vector和ArrayList的异同。

在ArrayList类出现之前,JDK中存在一个和它同样分配连续存储空间、实现了长度可变数组的集合类Vector。两者实现原理相同,功能相同,在很多情况下可以互用。

两者的主要区别如下。

Ø Vector是线程安全的,ArrayList重速度轻安全,是线程非安全的,所以当运行到多线程环境中时,需要程序员自己管理线程的同步问题。

Ø 当长度需要增长时,Vectro默认增长为原来的一倍,而ArrayList只增长50%,有利于节约内存空间。

开发过程中,最好使用新版本的ArrayList。

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

上机练习1

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

训练要点

Ø List接口的特点。

Ø List接口的add()、size()、get()、remove()和contains()方法的使用。

 

 

需求说明

把多个企鹅的信息添加到集合中,查看企鹅的数量,遍历所有企鹅的信息,删除集合中部分企鹅的元素,判断集合中是否包含指定企鹅。

实现思路及关键代码

  1. 创建多个企鹅对象。
  2. 创建ArrayList集合对象并把多个企鹅对象放入其中。
  3. 输出集合中企鹅的数量。
  4. 遍历集合显示所有企鹅信息。
  5. 删除指定位置企鹅(根据下标)和指定企鹅(根据对象名)。
  6. 显示删除后集合所有企鹅信息。
  7. 判断集合中是否包含指定企鹅。

注意编写注释。

9.2.2  LinkedList集合类

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

如何在集合的头部或尾部添加、获取和删除狗狗对象呢?如何在集合的其他任何位置添加、获取和删除狗狗对象呢?

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

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

在示例2中讲解ArrayList时涉及了集合中元素的添加、删除操作,可以通过add(Object  o)、remove(Object  o)在集合尾部添加和删除元素,还可以通过add(int  index,Object  o)、remove(int  index)实现任意位置元素的添加和删除,当然也包括头部和尾部。

但是由于ArrayList采用了和数组相同的存储方式,在内存中分配连续的空间,在添加和删除非尾部元素时会导致后面所有元素的移动,性能低下。所以在插入、删除操作较频繁时,可以考虑使用LinkedList来提高效率。

在使用LinkedList进行头部和尾部元素的添加和删除操作时,除了使用List的add()和remove()方法外,还可以使用LinkedList额外提供的方法来实现操作。

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

在集合的头部或尾部添加、获取和删除狗狗对象的实现代码如示例3所示。

示例3

/**

  * 测试LinkedList的多个特殊方法。

   * @author 南京

  * /

public  class  Test3  {

     public  static  void  main(String[ ]  args)  {

       //1、创建多个狗狗对象

       Dog  ououDog=new  Dog(“欧欧”,“雪娜瑞”);

       Dog  yayaDog=new  Dog(“亚亚”,“拉布拉多”);

       Dog  meimeiDog=new  Dog(“美美”,“雪娜瑞”);

       Dog  feifeiDog=new  Dog(“菲菲”,“拉布拉多”);

       //2、创建LinkedList集合对象并把多个狗狗对象放入其中

       LinkedList  dogs=new  LinkedList();

       dogs.add(ououDog);

       dogs.add(yayaDog);

dogs.addLast(meimeiDog);

dogs.addFirst(feifeiDog);

       //3、查看集合中第一条狗狗的昵称  

       Dog  dogFirst=(Dog)dogs.getFirst();

       System.out.println(“第一条狗狗的昵称是”+dogFirst.getName()+“。”);

       //4、查看集合中最后一条狗狗的昵称

       Dog  dogLast=(Dog)dogs.getLast ();

       System.out.println(“最后一条狗狗的昵称是”+dogLast.getName()+“。”);

 

 

       //5、删除集合中第一个狗狗和最后一个狗狗

       dogs.removeFirst();

       dogs.removeLast();

       //6、显示删除部分狗狗后集合中各条狗狗信息

       System.out.println(“\n删除部分狗狗之后还有”+dogs.size()+“条狗狗。”);

       System.out.println(“分别是:”);

       for  (int  i=0;i<dogs.size();i++﹚  {

           Dog  dog=(Dog)  dogs.get(i);

           System.out.println(dog.getName()+“\t”+dog.getStrain());

    }

    }

}

运行效果如图9.9所示。

 

 

图9.9  使用LinkedList存储和处理狗狗信息

下面我们总结一下LinkedList的各种常用方法。LinkedList除了表9-1中列出的各种方法之外,还包括一些特殊的方法,见表9-2。

表9-2  LinkedList的一些特殊方法

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

返回类型       方法                  说明

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

void          addFirst(Object o)     在列表的首部添加元素

void          addLast(Object o)      在列表的末尾添加元素

Object        getFirst()           返回列表中的第一个元素

Object        getLast()            返回列表中的最后一个元素

Object        removeFirst()        删除并返回列表中的第一个元素

Object        removeLast()         删除并返回列表中的最后一个元素

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

9.3  Map接口

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

建立国家英文简称和中文全名之间的键和值映射,例如CN→中华人民共和国,根据“CN”可以查找到“中华人民共和国”,通过删除键实现对应值的删除,应该如何实现数据的存储和操作呢?

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

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

Java集合框架中提供了Map接口,专门来处理键值映射数据的存储。Map中可以存储多个元素,每个元素都由两个对象组成,一个键对象和一个值对象,可以根据键实现对应值的映射。

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

实现代码如示例4所示。

示例4

/**

  * 测试HashMap的多个方法。

   * @author 南京

  * /

public  class  Test4  {

     public  static  void  main(String[ ]  args)  {

       //1、使用HashMap存储多组国家英文简称和中文全称的键值对

       Map  countries=new  HashMap();

       countries.put(“CN”,“中华人民共和国”);

       countries.put(“RU”,“俄罗斯联邦”);

countries.put(“FR”,“法兰西共和国”);

countries.put(“US”,“美利坚合众国”);

       //2、显示“CN”对应国家的中文全称

       String  country=(String)  countries.get(“CN”);

       system.out.println(“CN对应的国家是:”+country);

       //3、显示集合中元素个数  

       System.out.println(“Map中共有”+countries.size ()+“组数据”);

       //4、两次判断Map中是否存在“FR”键

       System.out.println(“Map中包含FR的key吗?”+

      countries.containsKey(“FR”));

       countries.remove(“FR”)

       System.out.println(“Map中包含FR的key吗?”+

      countries.containsKey(“FR”));

       //5、分别显示键集、值集和键值对集

       System.out.println(countries.keySet());

       System.out.println(countries.values());

       System.out.println(countries);

   }

}

运行效果如图9.10所示。

 

 

图9.10  使用HashMap存储和处理国家信息

Map接口存储一组成对的键—值对象,提供key(键)到value(值)的映射。Map中的key不要求有序,不允许重复。value同样不要求有序,但允许重复。最常见的Map实现类是HashMap,它的存储方式是哈希表,优点是查询指定元素效率高。

下面,我们总结一下在示例4中使用到的Map接口中定义的各种常用方法(也是HashMap的各种常用方法),见表9-3。

 

 

 

 

 

 

 

 

 

 

 

 

表9-3  Map的常用方法

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

序号  返回类型  方法                           说明

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

1     Object     put(Object key,Object value)  以“键—值对”的方式进行存储

                                               注意:键必须是唯一的,值可以重复。如果试图添加重复的键,那么最后加入的键—值对将替换掉原先的键—值对

2     Object      get(Object key)             根据键返回相关联的值,如果不存在指定的键,返回null

3     Object      remove(Object key)         删除由指定的键映射的“键—值对”

4     int         size()                     返回元素个数

5     Set         keySet()                   返回键的集合

6     Collection  values()                   返回值的集合

7     boolean     containsKey(Object key)    如果存在由指定的键映射的“键—值对”,返回true

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

 

上机练习2

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

训练要点

Ø Map接口的特点。

Ø Map接口的put()、get()和containsKey()方法的使用。

需求说明

根据宠物昵称查找对应宠物,如果找到,显示宠物信息;否则给出错误提示。

实现思路及关键代码

  1. 创建多个狗狗对象。
  2. 创建HashMap集合对象并把多个狗狗对象放入其中,以狗狗昵称为键。
  3. 输出集合中狗狗的数量。
  4. 判断集合中是否包含指定昵称的狗狗,如果包含,显示宠物信息;否则给出错误提示。

注意编写注释。

 

——对比———————————————————————————————————

Hashtable和HashMap的异同。

HashMap类出现之前,JDK中存在一个和它同样采用哈希表存储方式、同样实现键值映射的集合类Hashtable。两者实现原理相同、功能相同,很多情况下可以互用。

两者的主要区别如下。

Ø Hashtable继承自Dictionary类,而HashMap实现了Map接口。

Ø Hashtable是线程安全的,HashMap重速度轻安全,是线程非安全的,所以当运行到多线程环境中时,需要程序员自己管理线程的同步问题。

Ø Hashtable不允许null值(key和value都不允许),HashMap允许null值(key和value都允许)。

开发过程中,最好使用新版本的HashMap。

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

9.4  迭代器Iterator

所有集合接口和类都没有提供相应的遍历方法,而是把遍历交给迭代器Iterator完成。Iterator为集合而生,专门实现集合的遍历。它隐藏了各种集合实现类的内部细节,提供了遍历集合的统一编程接口。

Collection接口的iterator()方法返回一个Iterator,然后通过Iterator接口的两个方法即可方便的实现遍历。

Ø boolean  hasNext():判断是否存在另一个可访问的元素。

Ø Object  next():返回要访问的下一个元素。

在示例1中我们通过for循环和get()方法配合实现了List中元素的遍历,下面我们通过Iterator来实现遍历,代码如示例5所示。

示例5

/**

  * 测试通过Iterator遍历List。

   * @author 南京

  * /

public  class  Test5  {

     public  static  void  main(String[ ]  args)  {

       //1、创建多个狗狗对象

       Dog  ououDog=new  Dog(“欧欧”,“雪娜瑞”);

       Dog  yayaDog=new  Dog(“亚亚”,“拉布拉多”);

       Dog  meimeiDog=new  Dog(“美美”,“雪娜瑞”);

       Dog  feifeiDog=new  Dog(“菲菲”,“拉布拉多”);

       //2、创建ArrayList集合对象并把多个狗狗对象放入其中

       List  dogs=new  ArrayList();

       dogs.add(ououDog);

       dogs.add(yayaDog);

dogs.add(meimeiDog);

dogs.add(2, feifeiDog);

       //3、通过迭代器依次输出集合中所有狗狗的信息

       System.out.println(“使用Iterator遍历,所有狗狗的昵称和品种分别是:”);

       Iterator  it=dogs.iterator();

       while  (it.hasNext())  {

           Dog  dog=(Dog)  it.next();

           System.out.println(dog.getName()+“\t”+dog.getStrain());

       }

     }

}

运行结果如图9.11所示。

 

 

图9.11  使用Iterator遍历集合信息

上机练习3

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

训练要点

Ø Iterator接口的优点。

Ø Iterator接口的hasNext()、next()方法的使用。

 

需求说明

使用ArrayList和LinkedList存储多个企鹅信息,然后统一使用Iterator进行遍历。

实现思路及关键代码

  1. 创建多个企鹅对象。
  2. 创建ArrayList集合对象并把多个企鹅对象放入其中。
  3. 使用Iterator遍历该集合对象。
  4. 创建LinkedList集合对象并把多个企鹅对象放入其中。
  5. 使用Iterator遍历该集合对象。

注意编写注释。

9.5  泛型集合

前面已经提到,Collection的add(Object  obj)方法的参数是Object类型,不管把什么对象放入Collection及其子接口或实现类中,认为只是Object类型,在通过get(int  index)方法取出集合中元素时必须进行强制类型转换,不仅繁琐而且容易出现ClassCastException异常。Map中使用put(Object  key,Object  value)和get(Object  key)存取对象时,使用Iterator的next()方法获取元素时存在同样问题。

JDK1.5中通过引入泛型(Generic)有效解决了这个问题。在JDK1.5中已经改写了集合框架中的所有接口和类,增加了泛型的支持。

使用泛型集合在创建集合对象时指定集合中元素的类型,从集合中取出元素时无需进行类型强制转换,并且如果把非指定类型对象放入集合,会出现编译错误。

对List和ArrayList应用泛型,代码如示例6所示。

示例6

/**

  * 测试通过Iterator遍历List。

   * @author 南京

  * /

public  class  Test6  {

     public  static  void  main(String[]  args)  {

       //1、创建多个狗狗对象

       Dog  dog1=new  Dog(“欧欧”,“雪娜瑞”);

       Dog  dog2=new  Dog(“亚亚”,“拉布拉多”);

       Dog  dog3=new  Dog(“美美”,“雪娜瑞”);

       Dog  dog4 =new  Dog(“菲菲”,“拉布拉多”);

       //2、创建ArrayList集合对象并把多个狗狗对象放入其中

       List<Dog>  dogs=new  ArrayList<Dog>();//标记元素类型

       dogs.add(dog1);

       dogs.add(dog2);

dogs.add(dog3);

dogs.add(2,dog4);

//dogs.add(“hello”);//出现编译错误,元素类型不是Dog。

       //3、显示第三个元素的信息

       Dog  dog=dogs.get(2);//无需类型强制转换

       System.out.println(“第三个狗狗的信息如下:”);

       System.out.println(dog.getName()+“\t”+dog.getStrain());

       //4、使用Iterator遍历dogs对象

       System.out.println(“\n所有狗狗的信息如下:”);

       Iterator<Dog>  it=dogs.iterator();//标记元素类型

       while  (it.hasNext())  {

            dog=it.next();//无需类型强制转换

            System.out.println(dog.getName()+“\t”+dog.getStrain());

       }

    }

}

运行结果如图9.12所示。

 

 

图9.12  对List应用泛型

对Map和HashMap应用泛型,代码如示例7所示。

示例7

/**

  * 测试对Map应用泛型。

  * @author 南京

  * /

public  class  Test7  {

     public  static  void  main(String[ ]  args)  {

       //1、使用HashMap存储多组国家英文简称和中文全称的键值对

       Map <String,String> countries=new  HashMap<String,String>();

       countries.put(“CN”,“中华人民共和国”);

       countries.put(“RU”,“俄罗斯联邦”);

countries.put(“FR”,“法兰西共和国”);

       //2、显示“CN”对应国家的中文全称

       String  country=countries.get(“CN”);//无需类型强制转换

       system.out.println(“CN对应的国家是:”+country);

   }

}

 

查询JDK帮助文档中集合的接口和类,如图9.13和图9.14所示,从图中可以看到我们上面写法中的依据。图中的<E>、<K,V>就是List和Map的类型形参,而示例6和示例7中的相应位置的<Dog>、<String,String>就是类型实参。

 

 

 

            

 

图9.13  Java  API中List接口截图        图9.14  Java  API中Map接口截图

——对比———————————————————————————————————

数组和集合的主要区别包括以下几个方面。

  • 数组可以存储基本数据类型和对象,而集合中只能存储对象(可以以包装类形式存储基本数据类型)。
  • 数组长度固定,集合长度可以动态改变。
  • 定义数组时必须指定数组元素类型,集合默认其中所有元素都是Object。
  • 无法直接获取数组实际存储的元素个数,length用来获取数组的长度,但可以通过size()直接获取集合实际存储的元素个数。
  • 集合有多种实现方式和不同的适用场合,而不像数组仅采用分配连续空间方式。
  • 集合以接口和类的形式存在,具有封装、继承和多态等类的特性,通过简单的方法和属性调用即可实现各种复杂操作,大大提高软件的开发效率。

JDK中有一个Arrays类,专门用来操作数组,它提供一系列静态方法实现对数组搜索、排序、比较和填充等操作。JDK中有一个Collections类,专门用来操作集合,它提供一系列静态方法实现对各种集合的搜索、复制、排序和线程安全化等操作。

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

 

本章总结

  • 集合弥补了数组的缺陷,它比数组更灵活更实用,可大大提高软件的开发效率,而且不同的集合可适用于不同场合。
  • 集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。
  • 通常说Java的集合框架共有三大类接口,List、Set和Map。区别如下。
  1. Collection接口存储一组不唯一,无序的对象。
  2. Set接口继承Collection接口,存储一组唯一,无序的对象。
  3. List接口继承Collectio接口,存储一组不唯一,有序的对象。
  4. Map接口存储一组成对的键—值对象,提供key到value的映射。key不要求有序,不允许重复。value同样不要求有序,但允许重复。
  • ArrayList和数组采用相同的存储方式,它的优点在于遍历元素和随机访问元素的效率比较高。LinkedList采用链表存储方式,优点在于插入、删除元素时效率比较高。
  • HashMap是最常见的Map实现类,它的存储方式是哈希表,优点是查询指定元素效率高。
  • Iterator为集合而生,专门实现集合的遍历。它隐藏了各种集合实现类的内部细节,提供了遍历集合的统一编程接口。
  • 使用泛型集合在创建集合对象时指定集合中元素的类型,在从集合中取出元素时无需进行类型强制转换,避免了ClassCastException异常。

 

本章作业

一、选择题

1.以下(  )的对象可以使用键—值的形式保存数据。

A. ArrayList

B.  LinkedList

C. HashMap

D. Collection

 

2.给定如下Java代码,可以填入下划线处的代码是(  )。

import  java.util.*;

public  class  Test  {

public  static  void  main(String[ ]  args)  {

      ____________

      list.add(“A”);

      list.addFirst(“B”);

}

}

  1. List  list=new  ArrayList();
  2. List  list=new  LinkedList();
  3. ArrayList  list=new  ArrayList();
  4. LinkedList  list=new  LinkedList ();

 

3.下列关于java.util.ArrayList的说法正确的是(  )。(选两项)

A. 这个集合中的元素是有序的

B. 可以通过键来获取这个集合中的元素

C. 可以通过addFirst()方法,在集合的首部插入元素

D. 在对这个集合执行遍历操作时,效率比较高

 

4.给定如下Java代码,编译运行的结果是(  )。

import  java.util.*;

public  class  Test  {

public  static  void  main(String[ ]  args)  {

      List  list=new  ArrayList();

      list.add(“A”);

      list.addFirst(2,“B”);

          String  s=(String)list.get(1);

          System.out.println(s);

    }

}

  1. 编译时发生错误
  2. 运行时引发异常
  3. 正确运行,输出:A
  4. 正确运行,输出:B

 

5.给定如下Java代码,编译运行的结果是(  )。

import  java.util. *;

public  class  Test  {

public  static  void  main(String[ ]  args)  {

         Map  map=new  HashMap();

         String  s=“key”;

         map.put(s,“Hello”);

         map.put(s,“World”);

         System.out.println(map.size());

}

}

  1. 编译时发生错误
  1. 运行时引发异常
  2. 正确运行,输出:1
  3. 正确运行,输出:2

二、简答题

1.简述Java集合框架的三大类接口及其区别。

2.根据你的理解,请说明一下ArrayList和LinkedList的区别。

3.请说明以下这段Java代码是否存在错误,如果存在,请改正并给出运行结果;如果不存在,请给出运行结果。

import  java.util. *;

public  class  TestList  {

public  static  void  main(String[ ]  args)  {

      List  list=new  ArrayList();

      list.add(“A”);

      list.add(“B”);

      list.add(“C”);

          print(list);

}

   public  static  void  print(List  pList)  {

      for (int  i=0;i<pList.size();i++)  {

           String  str=pList.get(i);

           System.out.print(str);

      }

   }

}

4.创建一个类Queue,代表队列(其特点为:先进先出),添加方法add(Object  obj)以及get(),并添加main()方法进行效果验证。

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

使用LinkedList实现队列:在向LinkedList中添加时,使用addFirst()方法;在从LinkedList中取出时,使用removeLast()方法。

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

5.创建一个HashMap对象,并在其中添加一些学员的姓名和他们的分数,键为学员姓名(使用String类型),值为学员分数(使用Integer类型)。从HashMap对象中获取这些学员的成绩并打印出来。修改其中一名学员的成绩,然后再次打印所有学员的成绩。

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

使用put()方法进行添加和修改操作;使用values()方法打印学员成绩。

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

 

 

10章

JDBC

 

 

 

 

 

 

◇本章工作任务

 

Ø    使用JDBC实现宠物信息的增删改查

Ø    使用JDBC实现宠物主人信息的查询

 

 

◇本章技能目标

 

Ø    理解JDBC

Ø    掌握Connection接口的使用

Ø    掌握Statement接口的使用

Ø    掌握ResultSet接口的使用

Ø    掌握PreparedStatement接口的使用

 

 

本章单词

 

 

 

 

 

 

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

 

  1. JDBC:__________________________________
  2. driver  manager:_________________________
  3. connection:______________________________
  4. statement:_______________________________
  5. execute:_________________________________
  6. append:_________________________________
  7. query:_________________________________
  8. result  set:_______________________________

 

 

本章简介

 

本章讲解Java访问数据库的技术——JDBC,它由一组使用Java语言编写的类和接口组成,可以为多种关系数据库提供统一访问。本章首先会引入JDBC,讲解JDBC的工作原理和使用JDBC访问数据库的基本步骤,然后重点对Connection、Statement、ResulSet、PreparedStatement等各种JDBC接口进行详细讲解。学习中要明白使用JDBC的基本步骤,做到思路清晰,通过多加练习熟悉掌握各种接口的使用。

 

10.1  JDBC简介

10.1.1  为什么需要JDBC

在前面章节中,我们通过控制台输入宠物的信息,并创建宠物对象,然后可以在控制台输出宠物信息,但是却无法保存数据,每次运行程序都要重新输入,在Java中如何实现把各种数据存入数据库,从而长久保存呢?

Java是通过JDBC技术实现对各种数据库访问的,换句话说,JDBC充当了Java应用程序与各种不同数据库之间进行对话的媒介。

JDBC是Java数据库连接(Java DataBase Connectivity)技术的简称,由一组使用Java语言编写的类和接口组成,可以为多种关系数据库提供统一访问。Sun公司提供JDBC的接口规范——JDBC API,而数据库厂商或第三方中间件厂商根据该接口规范提供针对不同数据库的具体实现——JDBC驱动。

 

10.1.2   JDBC的工作原理

JDBC的工作原理如图10.1所示。从图10.1中可以看到JDBC的几个重要组成要素。最顶层是我们自己编写的Java应用程序,Java应用程序可以使用集成在JDK中的Java.sql和javax.sql包中的JDBC API来连接和操作数据库。下面我们就采用从上到下的顺序依次讲解JDBC的组成要素。

 

1.  JDBC API

JDBC API由SUN公司提供,提供了Java应用程序与各种不同数据库交互的标准接口,如:Connection(连接)接口、Statement接口、ResultSet(结果集)接口、PreparedStatement接口等。开发者使用这些JDBC接口进行各类数据库操作。

 

2.  JDBC Driver Manager

JDBC Driver Manager由Sun公司提供,它负责管理各种不同的JDBC驱动,位于JDK的java.sql包中。

3.JDBC驱动

JDBC驱动由各个数据库厂商或第三方中间件厂商提供,负责连接各种不同的数据库。比如图中10.1中,访问SQL Server和Oracle时需要不同的JDBC驱动,这些JDBC驱动都实现了JDBC API中定义的各种接口。

在开发Java应用程序时,我们只需正确加载JDBC驱动,正确调用JDBC API,就可以进行数据库访问了。

 

 

图10.1 JDBC的工作原理

 

10.1.3   JDBC API介绍

JDBC API主要做三件事:与数据库建立连接,发送SQL语句、处理结果,如图10.2所示,图10.2在为我们展示JDBC的工作过程时,同时也展示了JDBC的主要API及作用。

Ø    DriverManager类:依据数据库的不同,管理相应的JDBC驱动。

Ø    Connection接口:负责连接数据库并担任传送数据的任务。

Ø    Statement接口:由Connection产生,负责执行SQL语句。

Ø    ResultSet接口:负责保存和处理Statement执行后所产生的查询结果。

Ø    PreparedStatement接口:Statement的子接口,也由Connection产生,同样负责执行SQL语句。与Stateme接口相比,具有高安全性、高性能、高可读性和高可维护性的优点。

图10.2   JDBC工作过程及JDBC API

10.1.4 JDBC访问数据库的步骤

开发一个JDBC应用程序,基本需要以下步骤。

(1)加载JDBC驱动

使用Class.forName()方法将给定的JDBC驱动类加载到Java虚拟机中。如果系统中不存在给定的类,则会引发异常,异常类型为ClassNotFoundException。代码示例:

 

Class.forName(“JDBC驱动类的名称”);

 

(2)与数据库建立连接。

DriverManager类是JDBC的管理层,作用于用户和驱动程序之间。DriverManager类跟踪可用的驱动程序,并在数据库和相应的驱动程序之间建立连接。当调用getConnection()方法时,DriverManager类首先从已加载的驱动程序列表中找到一个可以接受该数据库URL的驱动程序,然后请求该驱动程序使用相关的URL,用户名和密码连接到数据库中,于是就建立了与数据库的连接,创建连接对象并返回引用。代码示例:
 

Connection con =DriverManager.getConnection(数据连接字符串,数据库用户名,密码);

 

(3)发送SQL语句,并得到返回结果。

一旦建立连接,就使用该连接创建Statement接口的对象,并将SQL语句传递给它所连接的数据库,如果是查询操作,并返回类型为ResultSet的结果集,它包含所执行SQL查询的结果。如果是其他操作,将根据调用方法的不同返回布尔值或操作影响的记录数目。代码示例:

 

Statement stmt =con.createStatement();

ResultSet rs =stmt.executeQuery(“SELECT id,name FROM master”);

 

(4)处理返回结果

主要是针对查询操作的结果集,通过循环取出结果集中每条记录并做相应的处理。处理结果的代码示例:

while (rs.next()){

        int id =rs.getInt(“id”) ;

String name =rs.getString(“name”);

        System.out.println(id+“  ”+name);

}

一定要明确使用JDBC的四个基本步骤,本章后续部分就是使用这四个步骤实现了对数据库的各种访问。而对于四个步骤的代码示例只需要有简单印象即可,后续部分会进行详细讲解。

 

10.2      Connection接口

10.2.1   两种常用的驱动方式

   JDBC驱动由数据库厂商或第三方中间件厂商提供。在我们实际编程过程中,有两种较为常用的驱动方式。第一种是JDBC-ODBC桥方式,适用于个人开发与测试,它通过ODBC与数据库进行连接。另一种是纯Java驱动方式,它直接同数据库进行连接,在生产型开发中,推荐使用该方式。这两种连接方式的示意图如图10.3所示。

 

 

图10.3  两种常用的驱动方式

 

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

JDBC-ODBC桥连就是将对JDBC API的调用转换为对另一组数据库连接(即ODBC)API的调用。如图10.4所示,描述了JDBC-ODBC桥的工作原理。

 

图 10.4 JDBC-ODBC桥连工作原理

JDK中已经包括了JDBC-ODBC桥连的驱动接口,所以进行JDBC-ODBC桥连时,不需要额外下载JDBC驱动程序,只需配置ODBC数据源即可,具体配置步骤请参考视频“配置ODBC数据源.swf”。

使用JDBC-ODBC桥连方式连接数据库,JDBC驱动类是“sun.jdbc.odbc.JdbcOdbcDriver”,数据库连接字符串将以“jabc:odbc”开始,后面跟随数据源名称。因此,假设我们已经配置了一个叫“conn_epet”的ODBC数据源,数据库连接字符串就是“jdbc:odbc:conn_epet”,假设登录数据库系统的用户名为“le”,口令为“le”,具体实现代码如示例1所示。

 

示例1

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.SQLException;

import org.apache.log4j.Logger;

/**

 *使用JDBC-ODBC桥连方式连接数据库连接并关闭。

 *@author 南京

 */

public class Test1{

   private static Logger logger= Logger .getLogger(Test1.class.getName())

   public static void main(String [ ] args){

       Connection conn = null;

       //1、加载驱动

       try{

          Class.forName(“sun.jdbc.odbc.JdbcOdbcDriver”);

       }catch (ClassNotFoundException e){

          logger.error(e);

       }

        //2、建立连接

       try{

           conn = DriverManager.getConnection(“jdbc:odbc:ConnSQLServer”,“le”,“le”);

           System.out.println(“建立连接成功!”);

}catch (SQLException e){

           logger.error(e);

       }finally{

           //3、关闭连接

           try{

              if(null != conn){

conn.close();

                  Symtem.out.printIn(“关闭连接成功!”);

               }

}catch (SQLException e){

               logger.error(e);

           }

       }

    }

}

需要注意的是,虽然通过了JDBC-ODBC桥连方式可以访问所有ODBC可以访问的数据库,但是JDBC-ODBC桥连方式不能提供非常好的性能,一般不适合在实际系统中使用。

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

纯Java驱动方式由JDBC驱动直接访问数据库,驱动程序完全用Java语言编写,运行速度快,而且具备了跨平台特点。但是,由于技术资料的限制,这类JDBC驱动一般只能由数据库厂商自己提供,即这类JDBC驱动只对应一种数据库,甚至只对应某个版本的数据库,如果数据库更换了或者版本升级了,一般需要更换JDBC驱动程序,纯Java驱动方式的工作原理如图10.5所示。

图10.5  纯Java驱动方式

如果我们使用纯Java驱动方式进行数据库连接,首先需要下载数据库厂商提供的驱动程序jar包,并将jar包引进工程中。本门课程我们使用的数据库是SQL Server 2008,因此可以从微软的官方网站下载驱动程序jar包,并查看相关帮助文档,获得驱动类的名称以及数据库连接字符串,接下来就可以进行编程,与数据库建立连接。此处假定在SQL Server 2008 中建立名称为“epet”的数据库,数据库用户名为“le”,密码为“le”,驱动程序包为sqljdbc.jar,具体实现代码如图示例2 所示。

 

示例2

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.SQLException;;

import org.apache.log4j.Logger;

/**

 *使用JDBC的纯Java方式建数据库连接并关闭。使用JDBC-ODBC桥连方式建立

 *数据库连接相比,需要修改JDBC驱动类字符串和URL字符串。

 *@author 南京

 */

public class Test2{

   private static Logger logger= Logger .getLogger(Test1.class.getName());

   public static void main(String [ ] args){

       Connection conn = null;

       //1、加载驱动

       try{

          Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       }catch (ClassNotFoundException e){

          logger.error(e);

       }

        //2、建立连接

       try{

           conn = DriverManager.getConnection(

“jdbc:sqlserver://localhost:1433;DatabaseName=epet”

“le”, le” );

           Symtem.out.printIn(“建立连接成功!”)

}catch (SQLException e){

           logger.error(e);

       }finally{

          //3、关闭连接

          try{

               if(null != conn){

conn.close();

                   Symtem.out.printIn(“关闭连接成功!”);

               }

}catch (SQLException e){

               logger.error(e);

          }

       }

    }

}

——注意——————————————————————————————————

常见的错误有以下几类。

Ø    JDBC驱动类的名称书写错误,出现ClassNotFoundException 异常。

Ø    数据连接字符串,数据库用户名,密码书写错误,出现SQLException异常。

Ø    数据库操作结束后,没有关闭数据库连接,导致仍旧占有系统资源。

Ø    关闭数据库连接语句没有放到finally语句块中,导致语句可能没有被执行。

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

 

10.2.4   上机练习

上机练习1

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

训练要点

Ø    纯Java方式连接数据库的步骤

Ø    纯Java方式连接数据库的参数

 

需求说明

数据库为SQL Server 2008 ,数据库名“epet”,用户名“le”,密码“le”,使用纯Java方式连接该数据库,如果连接成功,输出“建立连接成功!”,否则输出“建立连接失败!”,进行相关异常处理。

实现思路及关键代码

(1)   使用MyEclipse创建一个Java项目。

(2)   引用JDBC驱动文件sqljdbc.jar。

(3)   在cn.le.jdbctest包下建立类TestJDBC。

(4)   在main方法中使用纯Java方式连接epet数据库,如果连接成功,输出“建立连接成功!”,否则输出“建立连接失败!”。

注意进行相关异常处理,注意编写注释。

10.3       Statement接口和ResultSet接口

获取Connection对象后就可以进行各种数据库操作了,此时需要使用Connection对象创建Statement对象。Statement对象用于将SQL语句发送到数据库中,或者理解为执行SQL语句。Statement接口中包含很多基本数据库操作方法,下面列出了执行SQL命令的三个方法。

Ø   ResultSet executeQuery(String sql);可以执行SQL查询并获取ResultSet对象。

Ø   int executeUpdate(String sql);可以执行插入、删除、更新等操作,返回值是执行该操作所影响的行数。

Ø   boolean execute(String sql);这是一个最为一般的执行方法,可以执行任意SQL语句,然后获得一个布尔值,表示是否返回ResultSet。

首先,我们需要在SQL Sever 2008中建立数据库,此处命名为epet,接着,在epet数据库中建立表dog(狗狗)和master(宠物主人),并插入若干条记录。表结构如表10-1和表10-2所示。

 

 

 

 

 

 

表10-1  数据表dog

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

字段名称            字段说明           数据类型               其他

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

id                  序号               int                   主键、自增

name                昵称               varchar(12)                      

health              健康值             int

love                亲密度             int

strain              品种               varchar(20)

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

表10-2  数据表master

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

字段名称           字段说明           数据类型            其他

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

id                 序号               int                 主键、自增

name               昵称               varchar(12)                      

password           密码               varchar(20)

money              元宝数             int

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

10.3.1   使用Statement添加宠物

添加狗狗信息到数据库,操作很简单,只要创建Statement对象然后调用execute(String sql)方法或者executeUpdate(String sql)方法即可,代码如示例3所示。这里关键是SQL语句的拼接,可以直接利用“+”运算符进行拼接,也可以利用StringBuffer类的append()方法进行拼接。拼接时要非常小心,尤其是引号,逗号和括号的拼接避免出错。如果拼接错误,可通过在控制台输出SQL语句的方法查看错误。

 

示例3

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.SQLException;;

import org.apache.log4j.Logger;

/**

 *使用statement的execute()方法插入狗狗信息

*@author 南京

 */

public class Test3{

   private static Logger logger = Logger.getLogger(Test3.class.getName());

   public static void main(String[ ] args){

       Connection conn = null;

       Statement stmt = null;

String name =“欧欧”;//昵称

int health = 100;//健康值

int love = 0;//亲密度

String strain =“酷酷的雪娜瑞”;//品种

       //1、加载驱动

       try{

          Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       }catch (ClassNotFoundException e){

          logger.error(e);

       }

       try {

           //2、建立连接

           conn = DriverManager.getConnection(

“jdbc:sqlserver://localhost:1433;DatabaseName=epet”,

“le”,“le”);

           //3、插入狗狗信息到数据库

           stmt = conn.createStatement();

           StringBuffer sbSql = new StringBuffer(

             “insert into dog (name,health,love,strain)values(‘”);

sbSql.append(name+“‘,”);

sbSql.append(health+“,”);

sbSql.append(love+“,‘”);

sbSql.append(nstrain+“‘)”);

stmt.execute(sbSql.toString());

logger.infor(“插入狗狗信息成功!”);

}catch (SQLException e){

           logger.error(e);

        }finally{

           //4、关闭Statement和数据库连接

           try{

               if(null != stmt){

stmt.close();

               }

If(null != conn){

conn.close();

               }

}catch (SQLException e){

               logger.error(e);

           }

       }

    }

}

——资料———————————————————————————————————

StringBuffer的使用。

Java定义了String和StringBuffer两个类来封装对字符串的各种操作。String对象中的内容一旦被初始化就不能再改变,而StringBuffer用于存放内容可以改变的字符串。如果StringBuffer生成了最终想要的字符串,可以通过toString()方法转换为一个String对象。

Java为字符串提供了字符串连接运算符“+”可以通过toString()方法转换为一个String对象。

Java为字符串提供了字符串连接运算符“+”,可以把非字符串数据转换为字符串并连接新的字符串。“+”运算符的功能可以通过StringBuffer类的append方法实现。

例如:

int health=90,love=20;

String sql=“select * from epet where health > “+health+”and love > ”+love;

等效于

int health=90,love=20;

String sql=new Stringbuffer

         .append(“select * from epet where health > ”)

.append(health)

.append(“ and love > ”)

.append(love)

.toString();

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

 

10.3.2   使用Statement更新宠物

更新数据库中id=1的狗狗的健康值和亲密度信息,操作也很简单,只要创建Statement对象然后调用execute(String Sql)方法或者executeUpdate(String.sql)即可,这里关键还是SQL语句的拼接要细心。代码如图示例4所示。

 

示例4

import java.sql.*;

import org.apache.log4j.Logger;

/**

 *使用Statement的executeUpdate()方法更新狗狗信息。

 *@author 南京

 */

public class Test4{

   private static Logger logger= Logger .getLogger(Test4.class.getName()));

   public static void main(String [ ] args){

       Connection conn = null;

       Statement stmt = null;

       //1、加载驱动

       try{

           Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       }catch (ClassNotFoundException e){

           logger.error(e);

       }

try{

           //2、建立连接

           conn = DriverManager.getConnection(

“jdbc:sqlserver://localhost:1433;DatabaseName=epet”,

“le”,“le”);

           //3、更新狗狗信息到数据库

           stmt = conn.creatStatement();

           stmt.executeUpdate(“update dog set health=80,love=15 where id=1”);

logger.infor(“成功更新狗狗信息!”)

}catch (SQLException e){

           logger.error(e);

       }finally{

          //4、关闭Statement和数据库连接

          //省略关闭Statement和数据库连接

       }

    }

}

 

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

查询并输出dog表中所有狗狗的信息,首先还是创建Statement对象,然后调用executeQuery(String sql)方法执行查询操作,返回值是结果集ResultSet对象。

ResultSet可以理解为由所查询结果组成的一个二维表,每行代表一条记录,每列代表一个字段,并且存在一个光标,光标所指行为当前行,只能对结果集的当前行数据进行操作。光标初始位置是第一行之前(而不是指向第一行)。通过ResultSet的next()方法可以使光标向下移动一行,然后通过一系列getXxx方法实现对当前行各列数据的操作。

使用ResultSet对象的next()方法将光标指向下一行。最初光标位于第一行之前,因此第一次调用next()方法将把光标置于第一行上。如果执行next()后光标指向结果集的某一行,则返回true,否则返回false。如果光标已指向结果集最后一行,再次调用next()方法,会指向最后一行的后面,此时返回false。

方法getXxx提供了获取当前行中某列值得途径,列号或列名可用于标识要从中获取是咧(Xxx代表基本数据类型名,例如Int ,Float等,也可以是String)。例如,如果结果集中第一列的列名为id,存储类型为整型,则可以使用两种方法获取存储在该列中的值,如:int id=rs.getInt(1);或者int id=rs. getInt(“id”),采用列名来标识列可读性强,建议多采用这种方式。代码如示例5所示。

 

示例5

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Statement;

import java.sql.DriverManager;

import org.apache.log4j.Logger;

/**

 *使用Statement的executeQuery()方法查询并输出狗狗信息

*@author 南京

 */

public class Test5{

   private static Logger logger= Logger .getLogger(Test5.class.getName());

   public static void main(String [ ] args){

       Connection conn = null;

       Statement stmt = null;

       ResultSet rs = null;

       //1、加载驱动

       try{

          class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       }catch (ClassNotFoundException e){

          logger.error(e);

       }

       try{

         //2、建立连接

         conn = DriverManager.getConnection(

“jdbc:sqlserver://localhost:1433;DatabaseName=epet”,

“le”,“le”);

         //3、查询并输出狗狗信息

         stmt = conn.createStatement();

         rs = stmt.executeQuery(“select * from dog”);

         System.out.println(“\t\t狗狗信息列表”);

         System.out.println (“编号\t姓名\t健康值\t亲密度\t品种”);

         while (rs.next()){

             System.out.println (rs.getInt(1)+“\t”);

             System.out.println (rs.getString(2)+“\t”);

             System.out.println (rs.getInt(“health”)+“\t”);

             System.out.println (rs.getInt(“love”)+“\t”);

             System.out.println (rs.getString (“strain”));

        }

}catch (SQLException e){

        logger.error(e);

}finally{

        //4、关闭结果集、Statement和数据库连接

        //省略关闭Statement和数据库连接语句

     }

   }

}

运行效果如图10.6所示(具体输出结果因数据库表中数据不同而不同)。

 

图10.6  输出dog表中所有的狗狗信息

ResultSet接口常用方法及作用如表10-3所示

表10-3  ResultSet接口常用方法及作用

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

方法名                                 作用

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

boolean next()                      将光标从当前位置向下移动一行

boolean previous()                  游标从当前位置向上移动一行

void close()                        关闭ResultSet对象

int getInt(int columnIndex)         以int的形式获取结果集当前行指定列号的值

int getInt(String columnLabel)      以int的形式获取结果集当前行指定列名的值

float getFloat(int columnIndex)     以float的形式获取结果集当前行指定列号的值

float getFloat(String columnLabel)  以float的形式获取结果集当前行指定列名的值

Stringt getString(int columnIndex)  以String的形式获取结果集当前行指定列号的值

Stringt getString(String columnLabel)以String的形式获取结果集当前行指定列名的值

int getRow()                         得到光标当前所指行的行号

boolean absolute(int row)            光标移动到row指定的行

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

——注意———————————————————————————————————

Ø   作为一种好的编程风格,应该在不需要ResultSet对象、Statement对象和Connection对象时显式地关闭他们。语法形式为:

public void close()throws SQLException

Ø   要按先ResultSet结果集,后Statement,最后Connection的顺序关闭资源,因为ResultSet是通过Statement执行SQL命令得到,而Statement是需要在创建连接后才使用的,所以三者之间存在相互依存的关系,关闭时也需按照依存关系进行。

Ø   用户可以不关闭ResultSet。当Statement关闭,重新执行或用于从多结果序列中获取下一个结果时,该ResultSet将被自动关闭。

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

 

10.3.4上机练习

上机练习2

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

训练要点

Ø   使用Statement接口executeQuery(String sql)方法查询数据库。

Ø   使用ResultSet接next(),getXxx()方法遍历结果集。

需求说明

数据库为SQL Server 2008 ,数据库名“epet”,用户名“le”,密码“le”,使用纯JDBC查询其中数据表master中所有宠物主人编号、姓名、元宝数信息并在控制台输出,运算结果如图10.7所示。

 

 

图10.7  显示主人信息列表

实现思路及关键代码

(1)   在Java项目第8章的cn.le.jdbctest包下建立TestJDBC2,包含main方法。

(2)   加载JDBC驱动并创建数据库连接。

(3)   创建Statement对象

(4)  调用Statement的executeQuery(String sql)方法查询主人信息,得到结果集对象。

(5)  通过ResultSet的next()和getXxx()方法遍历结果集并输出编号、姓名、元宝数信息到控制台。

注意编写注释。

 

参考解决方案

//查询并输出宠物主人信息

stmt = conn.createStatement();

rs = stmt.executeQuery(“select * from master”);

System.out.println(“\t主人信息列表”);

System.out.println(“编号\t姓名\t元宝数”);

while (rs.next()){

        System.out.println(rs.getInt(id)+“\t”);

        System.out.println(rs.getString(“name”)+“\t”);

        Symtem.out.println(rs.getInt(“money”)+“\t”);

}

 

10.4   PreparedStatement接口

PreparedStatement接口继承自Statement接口,PreparedStatement比普通Statement对象使用起来更加灵活,更加效率。

 

10.4.1   为什么要使用PreparedStatement

我们首先通过示例来看一下使用Statement接口的一个缺点。要求宠物主人根据控制台提示输入用户名和密码,如果输入正确,输出“用户名登录成功!”,否则输出“用户登录失败!”,具体代码如示例6所示。

 

示例6

import java.sql.*;

import java.util.Scanner;

import org.apache.log4j.Logger;

/**

 *使用Statement安全性差,存在SQL注入隐患

*@author 南京

 */

public class Test6{

   private static Logger logger= Logger.getLogger(Test6.class.getName());

   public static void main(String [ ] args){

       Connection conn = null;

       Statement stmt = null;

       ResultSet rs = null;

       //0、根据控制台提示输入用户账号和密码

        Scanner input=new Scanner(System in);

        System.out.println(“\t宠物主人登录”);

        System.out.print(“请输入姓名”);

        String name =input .next();

        Symtem.out.print(“请输入密码”);

        String password =input .next();

       //1、加载驱动

       try{

          Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       }catch (ClassNotFoundException e){

          logger.error(e);

       }

       try{

          //2、建立连接

           conn = DriverManager.getConnection(

“jdbc:sqlserver://localhost:1433;DatabaseName=epet”,

“le”,“le”);

          //3、判断宠物主人登录是否成功

          stmt = conn.createStatement();

          String  sql = “select * from master where name=‘”+name+

“’ and password=’”+password+“‘”;

System.out.println(sql)

          rs = stmt.executeQuery(sql)

          if(rs.next())

              System.out.println(“登录成功,欢迎您!”);

          else

              System.out.println(rs.getInt(“登录成功,请重新输入”);

}catch (SQLException e){

          logger.error(e);

}finally{

            //4、关闭Statement和数据库连接

            //省略关闭Statement和数据库连接语句

        }

    }

}

如果正确输入用户名和密码,显示登录成功,如图10.8所示。

 

 

图10.8  正确输入姓名和密码显示登录成功

可是输入了精心设计的内容,即使用户名和密码是错误的,仍就可以显示登录成功。如图10.9所示。

 

图10.9  不知道姓名和密码也可以登录成功

这就是网上典型的SQL注入攻击。原因就是在于使用Statement接口方法时要进行SQL语句的拼接,不仅拼接繁琐麻烦,容易出错,还存在安全漏洞。而使用PreparedStatement接口就不存在这个问题,PreparedStatement接口的优点还不仅如此,我们会在讲解完PreparedStatement后讲解两种接口的区别。

10.4.2   使用PreparedStatement更新宠物信息

使用PreparedStatement更新宠物信息操作数据库的基本步骤有三步。

(1)   创建PreparedStatement对象

通过Connection接口的prepareStatement(String sql)方法来创建PreparedStatement对象,SQL语句可具有一个或多个输入参数。这些输入参数的值在SQL语句创建时未被指定,而是为每个输入参数保留一个问号(“?”)作为占位符。

以下的的代码段(其中con是Connection对象)将创建包含带有三个输入参数的SQl语句的PreparedStatement对象。

PreparedStatement pstmt =con. preparedStatement(“update dog set health=?,love=? where id=?”);

 

(2)设置每个输入参数的值

通过调用setXxx方法来完成,其中Xxx是与该参数相应的类型。例如,如果参数具有Java类型String,则使用的方法就是setString。SetXxx方法的第一个参数就是要设置参数的序数位置(从1开始计数),第二个参数就是设置给该参数的值。例如,以下代码将第一个参数设为整型值80,第二个参数设为整型值15.

pstmt.setInt(1,80);

pstmt.setInt(2,15);

pstmt.setInt(3,1);

 

(3)   执行SQL语句。

在设置了各个输入参数的值后,就可以调用PreparedStatement接口的三个执行方法来执行SQL语句。

Ø   ResultSet executeQuery();可以执行SQL查询并获取ResultSet对象。

Ø   int executeUpdate();可以执行插入、删除、更新等操作,返回值是执行该操作所影响的行数。

Ø   boolean execute();这是一个最为一般的执行方法,可以执行任意SQL语句,然后获得一个布尔值,表示是否返回ResultSet。

注意这三个执行方法和Statement接口中三个方法名字相同,作用相同,但还是不需要SQL语句做参数,SQL语句已经在创建对象PreparedStatement时指定了。例如;

 

pstmt.executeUpdate();

 

创建PreparedStatement对象时会对SQL语句进行预编译,所以执行速度要快于Statement对象。因此如果在程序中存在需要多次执行SQl语句时,应使用PrepareStatement对象来执行数据库操作,以提高效率。代码如示例7所示。

 

示例7

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.SQLException;;

import org.apache.log4j.Logger;

/**

 *使用PrepareStatement更新多条狗狗信息

*@author 南京

 */

public class Test7{

   private static Logger logger= Logger.getLogger(Test7.class.getName());

   public static void main(String [ ] args){

       Connection conn = null;

       PreparedStatement pstm = null;

       //1、加载驱动

       try{

           Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       } catch (ClassNotFoundException e){

          logger.error(e);

       }

       try{

           //2、建立连接

           conn = DriverManager.getConnection(

“jdbcL:sqlserver://localhost:1433;DatabaseName=epet”,

“le”,“le”);

           //3、更新狗狗信息到数据库

           String sql=“update dog set health=?,love=?where id=?”;

           pstmt = conn.prepareStatement(sql);

           pstmt.setInt(1,80);

           pstmt.setInt(2,15);

pstmt.setInt(3,1);

           pstmt.executeUpdate();

           pstmt.setInt(1,90);

pstmt.setInt(2,10);

           pstmt.setInt(3,2);

           pstmt.executeUpdate();

logger.infor(“更新狗狗信息成功!”)

}catch (SQLException e){

            logger.error(e);

        }finally{

            //4、关闭PreparedStatement和数据库连接

            //省略关闭PreparedStatement和数据库连接语句

        }

    }

}

——对比———————————————————————————————————

PreparedStatement比Statement 好在哪里?

Ø   提高了代码的可读性和可维护性。

虽然使用了PreparedStatement来代替Statement会多几行代码,但避免了繁琐麻烦有容易出错的SQL语句拼接,提高了代码的可读性和可维护性。

Ø   提高了SQL语句执行的性能。

创建Statement对象时不使用SQL语句做参数,不会解析和编译SQL语句,每次调用方法执行SQL语句时都要进行SQL语句解析和便已操作,即操作相同仅仅是数据不同。

创建PreparedStatement对象时使用占位符的SQL语句做参数,会解析和编译该SQL语句,在通过setXxx方法给占位符赋值后执行SQL语句时无需在解析和编译SQL语句,直接执行即可。多次执行相同操作可以大大提高性能。

Ø   提高了安全性。

PreparedStatement使用预编译语句,传入的任何数据都不会和已经预编译的SQL语句进行拼接,避免了SQL注入攻击。

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

 

10.4.3   上机练习

上机练习3

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

训练要点

PreparedStatement接口使用

 

需求说明

数据库为SQL Server 2008,数据库名为“epet”,用户名为“le”,密码为“le” ,使用PreparedStatement接口向数据表dog中插入两条狗狗信息。

实现思路及关键代码

(1) 在Java项目chapter8的cn.le.jdbctest包下建立TestJDBC3,包含main方法。

(2)   加载JDBC驱动并创建数据库连接。

(3)  调用Connection的prepareStatement(String sql)创建PreparedStatement对象。

(4)   PreparedStatement接口 的SetXxx方法给占位符赋值。

(5)   调用PreparedStatement接口的execute()执行SQL语句,插入两条狗狗信息。

参考解决方案

//更新狗狗信息到数据库

 pstmt=conn.prepareStatement(“insert into dog”+

“(name,health,love,strain) values(?,?,?,?)”);

pstmt.setString(1,“美美”);

pstmt.setInt(2,90)

pstmt.setInt(3,0)

pstmt.setString(4,“酷酷的雪娜瑞”);

pstmt.execute();

 

本章总结

 

Ø   JDBC由一组使用Java语言编写的类和接口组成,可以为多种关系数据库提供统一访问。

Ø   Sun公司提供了JDBC接口规范——JDBC API,而数据厂商或第三方中间件厂商提供针对不同数据库的具体实现——JDBC驱动

Ø   JDBC访问数据库的步骤:加载JDBC驱动;与数据库建立连接;发送SQL语句,并得到返回结果;处理返回结果。

Ø   纯Java驱动方式运行速度快,支持跨平台,是目前的常用方式。但是这类JDBC驱动只对应一种数据库,甚至只对应某个版本数据库。

Ø   数据库操作结束后,应该关闭数据库连接,释放系统资源,为了确保执行,关闭数据库连接语句要放到finally语句块中。

Ø   Statement接口负责执行SQL语句。ResultSet接口负责保存和处理Statement执行后所产生的查询结果。

Ø   PreparedStatement接口继承自Statement接口,提高代码的可读性和可维护性,提高了SQL语句执行的的性能,提高了安全性。

 

本章作业

一、选择题

1、假设已经获得ResultSet对象rs,那么获取第一步数据正确语句是(   )。

A.rs.hasNext();

B.rs.next();

C.rs.nextRow();

D.rs.hasNextRow();

 

2、给定如下Java代码片段,假定已经获得一个数据库连接,使用变量conn来表示。要从表FirstLeverTitle中删除所有creator列值为“张三”的记录(creator字段的数据类型为varchar),可以填入下划线处的代码是(   )。

string strSql =“delete from FirstLevelTitle while creator =?”;

PreparedStatement pstmt = con. preparedStatement(strSql);

_____________

pstmt .executeUpdate();

A.  pstmt.setString(0,“张三”);

B.  pstmt.setString(1,“张三”);

C.  pstmt.setInt(0,“张三”);

D.  pstmt.setInt(1,“张三”);

 

3.  假定已经获得了一个数据库连接,使用变量conn来表示。下列语句中能够正确获得结果集的有(    )。(选两项)

A.Statement stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery(“SELECT * FROM student”);

B.Statement stmt = conn.createStatement(“SELECT * FROM student”);

ResultSet rs = stmt.executeQuery();

C.PreparedStatement stmt = conn.prepareStatement()

ResultSet rs = stmt.executeQuery(“SELECT * FROM student”);

D.PreparedStatement stmt = conn.prepareStatement(“SELECT * FROM student”);

   ResultSet rs = stmt.executeQuery();

 

4.给定如下Java代码片段,假定查询语句是:select id,creator from FirstLevelTitle,并且已经或得了相应的结果集对象,使用变量rs表示,现在要在控制台上输出FirstLevelTitle表中各行creator列(存储类型为varchar)的值,可以填入下划线的代码是(    )。(选两项)

while (rs.next)){

   int id = rs .getInt(“id”);

   String creator =________

   System.out.printIn(creator);

}

A.  rs.getString(“creator”);

B.  rs.getString(1);

C.  rs.getString(2);

D.  rs.getString(creator);

 

5.  JDK中提供的(    )类的主要职责是:依据数据库的不同,管理不同的JDBC驱动程序。

A. DriverManager

B. Connection

C. Statement

D. Class

 

二、简答题

1. 根据你的理解,请说明一下Statement和PreparedStatement的区别。

2.请找出这个Java类中存在的错误,并说明错误原因。

package cn.le.jdbctst;

import java.sql.SQLException

public class TestMaxId{

   public static void main(String [ ] args){

       Connection conn = null;

       PrepareStatement pstm = null;

       ResultSet rs = null;

       try{

          Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);

       }catch (ClassNotFoundException e){

           e.printStackTrace();

       }

try{

           conn = DriverManager.getConnection(

“jdbc:sqlserver://localhost:1433;DatabaseName=epet”

“le”,“le”);

String sqlStr=“select max(id) from student”;

pstmt=conn.prepareStatement(sqlStr);

rs=pstmt.executeQuery()

System.out.printIn(rs.getInt(1));

       }catch (SQLException e)){

           e.printStackTrace();

       } finally{

           try{

if(null != conn){

conn.close();

              }

 If(null != rs){

rs.close();

              }

If(null != pstmt){

pstmt.close();

              }

}catch (SQLException e){

e.printStackTrace();

}

       }

}

}

3.在SQl Server 2008中,创建一个表student,表结构见表10-4,向其中添加一条或多条记录,然后在控制台输出目前表中记录的总数。

表10-4  数据表student

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

字段名称             字段说明           数据类型            大小

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

id                   序号               int                  4

name                 姓名               varcha               50

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

 

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

通过执行SQL语句“insert into student values(?,?)”,并为占位符赋值来添加记录。通过执行SQL语句“select count(*)from student”得到表中记录总数。

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

 

4.查询第3题中创建的student表中的所有记录,并在控制台中输出每行数据,包括id值,name值。

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

通过执行executeQuery()方法得到结果集,然后遍布结果集输出所有记录。

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

 

5.宠物主人根据控制台提示输入用户名和密码,如果输入正确,输出“用户登录成功!”,否则输出“用户登录失败!”,使用PrepareStatement接口实现该操作,避免SQL注入攻击。

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

参考本章示例6和示例7,使用PrepareStatement接口实现该操作。

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

 

11 章

Oracle基础

 

 

 

 

 

本章工作任务

Ø 使用Oracle创建数据库和用户

Ø 使用Oracle创建表并插入数据

本章技能目标

Ø 掌握Oracle的安装

Ø 掌握连接到Oracle的方法

Ø 掌握数据库的创建

Ø 掌握用户的创建和权限授予

Ø 掌握数据库表的创建

Ø 掌握序列的创建和使用

 

 

本章单词

 

 

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

                                      1.Oracle:                                           

                                      2.database:                                      

                                      3.grant:                                              

                                      4.revoke:                                       

                                      5.constraint:                                        

                                      6.column:                                      

                                      7.sequence:                                              

                                      8.increment:                                              

                                      9.cache:                                 

                                                   

本章简介

本章开始介绍目前市场上非常流行、功能强大的数据库管理系统——Oracle,按照使用Oracle的步骤组织本章内容。首先介绍Oracle基础知识,包括Oracle简介、基本概念和安装Oracle。然后介绍如何创建数据库、如何创建用户并授予权限。最后是使用Oracle创建数据库表,并使用序列实现主键的自动编号。连接Oracle服务器必须正确配置监听器listener和本地Net服务名,本章也会专门进行介绍。

11.1  Oracle基础知识

11.1.1  Oracle简介

1977年,Oracle公司创立,最早的名字叫“软件开发实验室”(Software  Development  Laboratories,SDL)。1979年,公司前往硅谷,更名为“关系软件公司”(Relational  Software  Inc.,RSI)。1983年,为了突出公司的核心产品,RSI再次更名为Oracle。2002年4月26日,正式启用“甲骨文”做为公司的中文注册商标。

Oracle是一个数据库管理系统,是Oracle公司的核心产品。Oracle在管理信息系统,企业数据处理,因特网及电子商务等领域使用非常广泛。因其在数据安全性与完整性控制方面的优越性能,以及跨操作系统、跨硬件平台的数据互操作能力,使得越来越多的用户将Oracle作为其应用数据的处理系统。与SQL  Server相同,两者均是关系型数据库,均支持SQL  92标准,但Oracle是目前最流行的数据库,它占有最大的市场份额,安全性更高,对大型数据库提供更好的支持。

Oracle数据库的主要版本有Oracle8i、Oracle9i、Oracle10g,最新版本是Oracle 11g。其中后缀i的含义Internet,表示Oracle公司进军互联网,后缀g的含义是grid,表示支持网格计算。本书中使用的版本是Oracle  10g。

Oracle数据库基于“客户/服务器”(Client/Server)系统结构。此概念将应用程序的处理分到两个系统中,即客户端系统和服务器系统。服务器系统执行数据库相关的所有活动,客户端系统执行与用户进行交互的活动。

Oracle数据库的主要特点如下。

  • 支持多用户、大事务量的事务处理。
  • 在保持数据安全性和完整性方面性能优越。
  • 支持分布式数据处理。将分布在不同物理位置的数据库用通信网络连接起来,在分布式数据库管理系统的控制下,组成一个逻辑上统一的数据库,完成数据处理任务。
  • 具有可移植性。Oracle可以在Windows、Linux等多个操作系统平台上使用,可以在不同操作系统间移植数据库。而SQL  Server只能在Windows平台上运行。

11.1.2  Oracle基本概念

对Oracle基本概念的理解和掌握将直接影响到以后学习,本节预先对几个常用概念进行简单介绍,为后续学习打好基础。

  1. 数据库

这里的数据库不是通常情况下我们所说的数据库,而是Oracle的一个专业名词。它是磁盘上存储数据的集合,在物理上表现为数据文件、日志文件和控制文件等,在逻辑上以表空间形式存在。

必须首先创建数据库,然后才能使用Oracle,可以在安装Oracle软件的同时创建数据库,也可以在安装后再单独创建数据库。

  1. 数据库实例

每个启动的数据库都对应一个数据库实例,然后由这个实例来访问和控制数据库。数据库实例就是为了运行数据库、Oracle系统所运行的所有进程和分配内存结构的组合体。

如果把数据库简单理解为硬盘上的文件,具有永久性,数据库实例就是在内存中处于运行状态的数据库,是临时的。

  1. 数据文件

数据文件的扩展名是.DBF,是用于存储数据库数据的文件,例如存储数据库表中的记录、索引、存储过程、视图、数据字典定义等。对于数据库操作中产生一些临时数据,以及为保证事务重做所必需的数据也有专门的数据文件负责存储。

一个数据文件中可能存储很多个数据库表的数据,而一个数据库表的数据也可能存放在多个数据文件中,即数据库表和数据文件不存在一对一关系。

  1. 控制文件

控制文件的扩展名是.CTL,是一个二进制文件。控制文件中存储信息很多,其中包括数据文件和日志文件的名字和位置。控制文件是数据库启动及运行所必需的文件。当Oracle读写数据时,要根据控制文件的信息查找数据文件。

由于控制文件的重要性,因此一个数据库至少要有一个以上的控制文件,Oracle  10g默认包含三个控制文件,各个控制文件内容相同,可以避免因为一个控制文件的损坏而导致无法启动数据库。

  1. 日志文件

日志文件的扩展名是.LOG,它记录了对数据的所有更改信息,并提供了一种数据恢复机制,确保在系统崩溃或其他意外出现后重新恢复数据库。

在Oracle数据库中,日志文件是成组使用的,每个日志文件组有一个或多个日志文件。在工作过程中,多个日志文件组之间循环使用,当一个日志文件组写满后,会转向下一个日志文件组。

  1. 表空间

每个Oracle数据库都是由若干个表空间构成的,用户在数据库中建立的所有内容都被存储到表空间中。一个表空间可以有多个数据文件,但一个数据文件只能属于一个表空间。与数据文件这种物理结构相比,表空间是属于数据库的逻辑结构。

在每一个数据库中,都有一个名为SYSTEM的表空间,即系统表空间,还会有SYSAUX、TEMP、UNDO、USERS等表空间,这些都是在创建数据库时自动创建的。管理员可以创建自定义的表空间并分配给指定用户,也可以为表空间增加和删除数据文件。

11.1.3  安装Oracle

本节介绍在Windows环境下安装Oracle数据库服务器软件,安装版本是Oracle  10g。安装过程很简单,按照安装向导的步骤依次进行操作,具体安装过程如下。

  1. 运行Oracle  10g关盘,或者直接双击安装包中的setup.exe文件,将开始Oracle  10g的安装,首先出现图11.1所示的“选择安装方法”窗口。如果选择“基本安装”,只需要输入基本信息即可。也可以选择“高级安装”,对安装过程进行更多控制。此处选择“高级安装”,并单击“下一步”按钮。

图11.1  选择安装方法

  1. 出现“选择安装类型”窗口,如图11.2所示,此处选择“企业版”,它提供更多的功能和更好的性能,并单击“下一步”按钮。

图11.2  选择安装类型

  1. 出现“指定主目录详细信息”窗口,如图11.3所示。此处指定所安装产品的名字以及安装产品的完整路径。建议采用默认值,尤其是保持默认的安装路径层次结构,但可以改变盘符。此处采用默认值,并单击“下一步”按钮。

图11.3  指定主目录详细信息

  1. 出现“产品特定的先决条件检查”窗口,如图11.4所示。系统会自动进行检查,显示检查通过后,单击“下一步”按钮即可。
  2. 出现“选择配置选项”窗口,如图11.5所示。默认选项是“创建数据库”,将在安装完毕后自动启动DBCA(数据库配置助手)进行数据库安装,建议选择此项。或选择“仅安装数据库软件”,安装完毕后需再手动创建数据库。“配置自动存储管理”是Oracle的高级内容,不予选择。此处选择“仅安装数据库软件”,并单击“下一步”按钮。

图11.4  产品特定的先决条件检查

图11.5  选择配置选项

  1. 出现“概要”窗口,如图11.6所示。此处是对前面选择内容的一个总结,可以查看内容,确定是否正确或满足要求,确认无误后,单击“安装”按钮。

图11.6  安装信息概要

  1. 出现“安装”窗口,如图11.7所示。此时用户的任务就是等待,需要几分钟的时间,视机器配置情况不同而不同。显示安装完毕后,单击“下一步”按钮。

图11.7  进行安装

  1. 出现“安装结束”窗口,如图11.8所示。该窗口中内容非常重要,它提供了多个Oracle工具的访问路径。单击“退出”按钮,在弹出的确认窗口中单击“是”按钮即可结束安装。

图11.8  安装结束

11.2  创建数据库和用户

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

已经安装了Oracle数据库软件,希望创建数据库LEDB,创建用户epet,该用户可以登录数据库并进行操作,应该如何实现呢?

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

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

  • 创建数据库可以在安装Oracle软件时进行(在图11.5中选择“创建数据库”),也可以在安装Oracle软件后进行。可以通过图形界面的DBCA(数据库配置助手)来实现,也可以通过SQL语句CREATE  DATABASE命令实现。后者操作比较复杂,建议初学者采用DBCA创建数据库,但应该对利用DBCA最后生成的创建数据库的SQL语句有简单的了解。
  • 统一数据库中可同时有多个用户,每个用户管理自己的数据库对象,如数据库表、索引、视图等。由谁来创建用户呢?每个数据库都有SYS和SYSTEM两个默认用户,两者都具有创建用户的权限,可以通过它们来登录,然后创建用户epet,创建用户的命令是CREATE  USER。
  • 最好为每个用户创建自己的表空间,实现每个用户数据的分开存放。创建表空间的命令是CREATE  TABLESPACE。
  • 通过CREATE  USER命令创建用户,但仅仅创建用户还不行,还需给它赋予相应权限,例如连接数据库、创建表、插入数据等,才可以进行相应操作,这需要用到GRANT命令。

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

11.2.1  创建数据库

此处通过DBCA来创建数据库。DBCA是Oracle提供的一款图形化界面工具,用来帮助数据库管理员快速、直观地创建数据库,避免了繁琐复杂的SQL命令操作。

使用DBCA创建数据库的过程如下。

  1. 依次单击“开始”→“程序”→“Oracle-OraDb10g_homel” →“配置和移植工具” →“DataBase  Configuration  Assistant”,打开DBCA欢迎界面,如图11.9所示。

图11.9  DBCA欢迎界面图

  1. 单击“下一步”按钮,出现“请选择希望执行的操作”窗口,如图11.10所示。此处选择“创建数据库”,单击“下一步”按钮。

图11.10  请选择希望执行的操作

  1. 出现“选择模板来创建数据库”窗口,如图11.11所示。此处选择默认选项“一般用途”,单击“下一步”按钮。

图11.11  选择模板来创建数据库

  1. 出现“数据库标识”窗口,如图11.12所示。填写全局数据库名为“LEDB”,数据库实例的SID为“ledb”,单击“下一步”按钮。在使用JDBC连接Oracle时的URL参数中会用到SID。

图11.12  数据库标识

  1. 出现“管理选项”窗口,如图11.13所示。采用默认项即可,单击“下一步”按钮。

图11.13  管理选项

  1. 出现“数据库身份证明”窗口,如图11.14所示。可以为Oracle  10g的四个解锁用户(其他用户处于锁定状态)SYS、SYSTEM、DBSNMP和SYSMAN指定相同的口令,或者分别指定不同的口令。此处指定的口令要牢记,首次连接服务器就会用到它。指定完毕后,单击“下一步”按钮。

图11.4  为解锁用户指定密码

  1. 出现“存储选项”窗口,选择数据库的存储机制,如图11.15所示。此处选择默认项“文件系统”,将使用操作系统的文件系统存储数据文件,单击“下一步”按钮。

图11.15  存储选项

  1. 出现“数据库文件所在位置”窗口,指定数据文件、控制文件和日志文件的存放位置,如图11.16所示。此处采用默认项“使用模板中的数据库文件位置”,单击“下一步”按钮。可以单击“文件位置变量”查看模板中规定的文件位置。

图11.16  指定数据库文件所在位置

  1. 出现“恢复配置”窗口,如图11.17所示。为了在系统出现故障时能够恢复数据库中存储的数据,需要指定快速恢复区。采用默认项即可,单击“下一步”按钮。

图11.17  指定快速恢复区

  1. 出现“数据库内容”窗口,如图11.18所示。直接单击“下一步”按钮即可。

图11.18  数据库内容

  1. 出现“初始化参数”窗口,如图11.19所示。此窗口有四个选项卡分别用于对数据库的四个方面进行设置。对于初学者来说,采用默认项即可,直接单击“下一步”按钮。

图11.19  初始化参数

  1. 出现“数据库存储”窗口,如图11.20所示。可以对控制文件、数据文件和重做日志组进行设置,此处采用默认设置,单击“下一步”按钮。

图11.20  设置数据库文件选项

(13)出现“创建选项”窗口,如图11.21所示。如果选择“创建数据库”复选框,将根据前面所做的选择创建数据库。如果选择“生成数据库创建脚本”复选框,会把前面所做的选择以创建数据库脚本的形式保存起来,当需要创建数据库时,直接运行该脚本即可。此处选中“创建数据库”和“生成数据库创建脚本”复选框,单击“下一步”按钮。

(14)出现“确认”窗口,确认创建信息无误后,单击“确定”按钮将开始数据库创建工作。创建完毕后,弹出创建完成窗口,该窗口提供了所创建数据库的数据库名、系统标识符(SID)等信息,还提供了Oracle企业管理器的访问路径,如图11.22所示。单击“退出”完成创建。重新启动机器,然后就可以使用Oracle了。

图11.21  选择数据库创建选项

图11.22  创建数据库结束

在Windows下,安装Oracle  10g后会生成多个服务。通过选择“控制面板”→“管理工具”→“服务”,打开“服务”窗口,可以看到如下Oracle服务,如图11.23所示。

 

图11.23  Oracle  10g安装后的服务

其中OracleService<SID>服务为数据库实例服务,是Oracle数据库的主要服务,必须启动。OracleOraDb10g_hom1TNSListener为Oracle数据库监听服务,通常情况下也要启动。如果要使用Oracle企业管理器,必须启用OracleDBConsoles<SID>服务。如果要使用Oracle的iSQL*Plus,必须启用OracleOraDb10g_hom1iSQL*Plus服务。

11.2.2  登录管理后台

创建数据库LEDB时,给四个解锁用户指定了密码。下面对SYS和SYSTEM用户进行介绍。

SYS用户是Oracle中的一个超级用户。数据库中所有数据字典和视图都存储在SYS模式中。数据字典存储了用来管理数据库对象的所有信息,是Oracle数据库中非常重要的系统信息。SYS用户主要用来维护系统信息和管理实例,在Oracle  10g版本中,SYS用户只能以SYSDBA或SYSOPER角色登录系统。

SYSTEM用户是Oracle中默认的系统管理员,它拥有DBA权限。该用户拥有Oracle管理工具使用的内部表和视图。通常通过SYSTEM用户管理Oracle数据库的用户、权限和存储等。不建议在SYSTEM模式中创建用户表。在Oracle  10g版本中,SYSTEM用户不能以SYSOPER或SYSDBA角色登陆系统,只能以Normal方式登录。

SYS和SYSTEM用户都是Oracle的系统用户,它们都使用SYSTEM表空间存储模式对象,SYS拥有更大的权限。

管理员和用户可以通过多种方式连接数据库。可以通过SQL*Plus方式连接,采用的是C/S方式。选择“开始”→“程序”→“Oracle-OraDb10g_homel” →“应用程序开发”→“SQL  Plus”,打开登录界面,如图11.24所示。以SYSTEM用户登录,“主机字符串”是创建数据库时指定的数据库实例名SID,连接成功后的界面如图11.25所示。

    

图11.24  SQL*Plus登录界面          图11.25  SQL*Plus登录界面成功的界面

Oracle  10g还提供了iSQL*Plus连接方式,这里的“i”表示“Internet”。通过浏览器以B/S方式与数据库建立连接。启动iSQL*Plus的URL参看图11.9。启动成功后出现登录页面,如图11.26所示,同样以SYSTEM用户登录,连接成功后如图11.27所示。

 

图11.26  iSQL*Plus登录界面         图11.27  iSQL*Plus登录界面成功界面

PL/SQL  Doveloper是一个非常便利的第三方开发工具,具有程序编辑、编译、调试、优化和查询功能,是一个专门用于Oracle数据库开发的集成开发环境。也可以通过它来连接Oracle数据库。登录窗口如图11.28所示,同样以SYSTEM用户登录,“Connect  as”栏中选择“Normal”,连接成功后如图11.29所示。在左侧下拉栏中选择“My  Objects”,即可查看本用户的各种数据库对象资源。

 

 图11.28  PL/SQL  Doveloper登录界面     图11.29  PL/SQL  Doveloper登录成功界面

另外,管理员可以通过Oracle  10g提供的Oracle企业管理器(Oracle  Enterprise  Manager  OEM)来实现对Oracle的全面管理。EM的登录界面如图11.30所示,同样以SYSTEM用户登录,“连接身份”栏中选择“Normal”,连接成功后如图11.31所示。可以通过“主目录”、“性能”、“管理”和“维护”四个选项卡实现对Oracle的各种管理。

  

图11.30  EM登录界面             图11.31  EM登录成功界面

通过以上方式连接Oracle服务器时,可能出现关于监听器listener或本地Net服务名的相关错误。只有正确配置这两项,才可以保证Oracle数据库的正常连接。

在Oracle产品安装完成后,客户端为了与数据库服务器连接进行数据访问,必须进行网络连接配置,网络配置包括服务器端配置和客户端配置。服务器端和客户端是个逻辑的概念。当一个数据库用户连接到一台数据库服务器时,就成为该数据库的一个客户端。客户端和服务器可以是同一台机器,也可以通过网络连接起来的不同操作系统,不同硬件平台的机器。

 

首先,要在Oracle服务器端配置监听器listener。然后客户端工作站或其他服务器需要配置一个本地Net服务名与网络监听器建立连接,通过该服务名登录到Oracle服务器。

Oracle中的Net  Configuration  Assistant和Net  Manager工具都能用来配置监听器和本地Net服务名信息,前者以向导的方式引导用户完成配置。选择“开始”→“程序”→“Oracle-OraDb10g_homel” →“配置和移植工具” →“Net   Configuration  Assistant”命令,出现“Oracle  Net  Configuration  Assistant:欢迎使用”对话框,如图11.32所示。分别选择“监听程序配置”和“本地Net服务名配置”,依据向导提示完成服务器端和客户端的配置。

图11.32  配置监听器和本地Net服务名

11.2.3  创建表空间

在数据库中创建用户时,基于应用性能和管理方面的考虑,最好为不同用户创建独立的表空间。

Oracle中的CREATE  TABLESPACE命令用于创建新表空间,示例1演示了如何创建名称为“epet_tablespace”的表空间。

示例1

CREATE  TABLESPACE  epet_tablespace

   DATAFILE  “E:\oracle\product\10.2.0\oradata\LEDB\EPET.DBF”

   SIZE  100MB

   AUTOEXTEND  ON  NEXT  32MB  MAXSIZE  UNLIMITED

   LOGGING

EXTENT  MANAGEMENT  LOCAL

SEGMENT  SPACE  MANAGEMENT  AUTO;//以分号结束

以上创建表空间语句具体解释如下。

  • 表空间名为“epet_tablespace”。
  • 与表空间关联的数据文件的位置及名称为“E:\oracle\product\10.2.0\oradata\

LEDB\EPET.DBF”。

  • 文件初始大小为100MB。
  • 文件大小可自动扩展,每次扩展32MB,允许文件扩展的最大限度为无限制。
  • Oracle生成表空间中数据库对象的任何创建或更改的日志记录项。
  • 表空间中的盘区管理采用本地化管理方式。
  • 表空间中段的管理方式为自动管理方式。

以上创建表空间语句涉及配置项比较多,其实让大多数配置项采用默认值即可,这样创建表空间的语句就可以简化,如示例2所示。

示例2

CREATE  TABLESPACE  epet_tablespace

   DATAFILE  “E:\oracle\product\10.2.0\oradata\LEDB\EPET.DBF”

   SIZE  100MB;//以分号结束

11.2.4  创建用户并授予权限

1.创建用户

同一数据库中可以同时有多个用户,每个用户管理自己的数据库对象,比如数据库表、索引、视图等。

Oracle中的CREATE  USER命令用于创建新用户。每个用户都有一个默认表空间和一个临时表空间。如果没有指定,Oracle就将SYSTEM设为默认表空间,将TEMP设为临时表空间。

语法

CREATE  USER  user

IDENTIFIED  BY  password

[DEFAULT  TABLESPACE  tablespace]

[TEMPORARY  TABLESPACE  tablespace]

说明如下:

  • user是用户名,用户名必须是一个标识符,不区分大小写。
  • password是用户口令,口令必须是一个标识符,不区分大小写。
  • DEFAULT或TEMPORARY   TABLESPACE为用户确定默认表空间或临时表空间。

示例3演示了如何创建名称为“epet”的用户。

示例3

CREATE  USER  epet

IDENTIFIED  BY  le

DEFAULT  TABLESPACE  epet_tablespace;

这段语句是用来创建一个名称为“epet”的用户,口令为“le”,默认表空间为“epet_tablespace”。

2.权限和角色

只创建用户还不行,必须赋予相应权限才可以连接和访问数据库。

权限指的是执行特定类型的SQL命令或访问其他对象的权利。例如:连接数据库、创建表、执行存储过程都需要一些权限。若没有任何权限,新创建的用户将无法登录Oracle数据库。

Oracle用户权限有两种类型:系统权限和对象权限。

  • 系统权限允许用户执行某些数据库操作。例如,创建表空间就是一个系统权限。
  • 对象权限允许用户对某一特定对象(如表、视图、序列等)执行特定的操作。

权限设置非常复杂,权限的类型很多,数据库中用户通常也很多,为DBA有效的管理权限带来了困难,为了简化权限管理,引入了角色的概念。

角色是具有名称的一组权限的组合,可以使用角色为用户授权,可以把角色理解为日常生活中的职务,一个人获取了某项职务,就自动具有该职位的所有权限,一个人被撤销了某职位,也就不再拥有相应的权限。

Oracle中的常用系统预定义角色如下。

  • CONNECT:临时用户,特别是那些不需要创建表的用户,通常赋予该角色。
  • RESOURCE:更为可靠和正式的数据库用户可以授予该角色,可以创建表、触发器、过程等。
  • DBA:数据库管理员角色,拥有管理数据库的最高权限。一个具有DBA角色的用户可以撤销任何别的用户甚至别的DBA权限,这是很危险的,所以不要把该角色轻易授予一些不是很重要的用户。

3.给用户分配权限或角色

GRANT命令用于为用户分配权限或角色,而REVOKE命令用于为用户撤销权限和角色。

语法

GRANT  privileges  or  role  TO  user;

REVOKE  privileges  or  role  FROM  user;

示例4演示了如何给用户epet分配角色和对象权限。

示例4

#把CONNECT、RESOURCE角色授予用户epet

GRANT  CONNECT,RESOURCE  TO  epet;

#撤销用户epet的RESOURCE角色

REVOKE  RESOURCE  FROM  epet;

#以下代码演示另一个用户SCOTT授予用户epet操作EMP表的对象权限

#允许用户查看EMP表中的记录

GRANT  SELECT  ON  EMP  TO  epet;

#允许用户更新EMP表中的记录

GRANT  UPDATE  ON  EMP  TO  epet;

经过授予权限或角色后,用户epet即可与数据库建立连接并进行相应操作。

11.2.5  上机练习

上机练习1

练习——创建数据库LEDB

训练要点

  • 掌握使用DBCA创建数据库。
  • 了解使用SQL语句创建数据库。

需求说明

在Oracle中使用DBCA创建数据库LEDB,数据库实例SID为sledb。

实现思路及关键步骤

  1. 启动DBCA创建数据库向导。
  2. 按照向导提示完成数据库创建。
  3. 按照向导提示生成数据库创建脚本并查看学习。

上机练习2

练习——创建用户epet

训练要点

  • 使用SQL*PLUS或iSQL*PLUS登录数据库。
  • 创建表空间。
  • 创建用户。

需求说明

创建用户epet,默认表空间EPET_TABLESPACE,授予CONNECT和RESOURCE角色。

实现思路及关键步骤

  1. 通过SYSTEM用户登录数据库。
  2. 创建表空间EPET_TABLESPACE,指定相应的属性。
  3. 创建用户epet,指定默认表空间为EPET_TABLESPACE。
  4. 将CONNECT和RESOURCE角色授予用户epet。
  5. 使用epet登录数据库,查看是否可以登陆,是否可以进行建表、插入数据等操作。

11.3  创建数据库表

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

在创建用户并授予权限后,在Oracle中如何实现数据库表的创建呢?请完成如下建表要求。

  • 主人表master(id,loginid,password,status)
  • 宠物种类表pet_type(id,name,status)
  • 宠物表pet(id,master_id,name,type_id,health,love,prop1,prop2,prop3,adopt_time,status)

三个表的名字和字段名见名知义,其中各个表的id字段均定义为主键,pet表的字段master_id作为外键参考master表的字段id,pet表的字段type_id作为外键参考pet_type表的id字段。pet表中prop1,prop2,prop3为备用字段。

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

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

  1. 需要根据业务需求确定各个字段的数据类型,这需要学习和掌握Oracle的数据类型。
  2. 创建数据库可以用CREATE  TABLE命令来实现,也可以通过图形界面例如PL/SQL      Developer来完成,但一定要熟悉SQL语句。

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

11.3.1  Oracle数据类型

当创建一个表时,必须为各个列指定数据类型。下面介绍Oracle中常用的几种数据类型。

  1. 字符数据类型

下面是Oracle支持的字符数据类型

  1. CHAR数据类型

CHAR数据类型用于存储固定长度的字符串。如果在定义时未指明大小,则默认占用一个字节。如果用户输入的值小于指定长度,则数据库用空格填充至固定长度。如果用户输入的值大于指定长度,则数据库返回错误报告。

  1. VARCHAR2数据类型

VARCHAR2数据类型用于存储可变长度的字符串。在定义该数据类型时,应指定其大小。与CHAR数据类型相比,使用VARCHAR2数据类型可节省磁盘空间,但存储效率没有CHAR高。例如有一个列被定义为VARCHAR2数据类型,且大小为30个字节。如果用户输入10个字节的字符,则该行中的列长度将只是10个字节,而不是30个字节。如果是CHAR型,它将占用30字节,因为剩余部分将由Oracle以空格填充。

3)NCHAR和NVARCHAR2

NCHAR和CHAR的区别在于NCHAR用来存储Unicode字符集类型,即双字节字符数据。比如我们定义CHAR(1)和NCHAR(1)类型的两个字段,字段长度为1个字节和1个字符(2个字节),分别插入“a”,“a”是没有问题的,但是占用的字节数分别是1和2。如果分别插入“的”和“的”,则前者无法实现正常插入,而后者可以。

NVARCHAR2和VARCHAR2的区别与其相同。

  1. 数值数据类型

NUMBER数据类型可以存储整数和浮点数。该数据类型的格式为NUMBER(p,s)。其中p为精度,表示数字的总位数。s为范围,表示小数点右边数字的位数。

下面描述了该数据类型的用法。

column_name  NUMBER             {p=38,s=0}

column_name  NUMBER (p)         {整数}

column_name  NUMBER (p,s)       {浮点数}

  1. 日期时间数据类型

日期时间数据类型用于存储日期值和时间值。

  1. DATE数据类型

DATE数据类型用于存储表中的日期和时间数据。Oracle数据库使用自己的格式存储日期,使用七个字节固定长度,每个字节分别存储世纪、年、月、日、小时、分和秒。

Oracle中的SYSDATE函数功能是返回当前日期和时间。

  1. TIMESTAMP数据类型

TIMESTAMP数据类型与DATE数据类型相似,但其中秒值精确到小数点后六位数,而Date数据类型没有秒的小数部分。

  1. LOB数据类型

LOB数据类型用于存储大型、没有被结构化的数据,例如二进制文件、图片文件等。LOB数据类型主要分为BLOB和CLOB数据类型。BLOB数据类型用于存储二进制对象,例如图像、音频和视频文件等;CLOB数据类型用于存储字符格式的大型对象。

——注意———————————————————————————————————

Oracle也支持INTEGER、FLOAT、DOUBLE等数值型数据类型,但建议采用Oracle自身的NUMBER;Oracle也支持VARCHAR数据类型,但建议采用Oracle自身的VARCHAR2。

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

11.3.2  创建数据库表的方法

Oracle中的CREATE  TABLE命令用于创建数据库表。

创建主人表master的SQL语句如示例5所示。

示例5

CREATE  TABLE  master (

   id  NUMBER(11,0)  PRIMARY  KEY,

   loginid  NVARCHAR2(50)  NOT  NULL,

   password  NVARCHAR2(20)  NOT  NULL,

   status  CHAR(1)  DEFAULT  1  NOT  NULL);

其中id字段被定义为主键(PRIMARY  KEY),另外三个字段均定义为非空字段(NOT  NULL),status字段的默认值是1(DEFAULT  1)。

创建宠物类型表pet_type的SQL语句如示例6所示。

示例6

CREATE  TABLE  pet_type(

id  NUMBER(11)  NOT  NULL,

   name  NVARCHAR2(50)  NOT  NULL,

   status  CHAR(1)  DEFAULT  1  NOT  NULL);

ALTER  TABLE  pet_type  ADD  CONSTRAINT  pet_type_pk  PRIMARY  KEY(id);

其中并没有在定义表时指定主键,而是在定义表后通过ALTER  TABLE命令来定义主键,并给主键名为pet_type_pk。

创建宠物表pet的SQL语句如示例7所示。

示例7

CREATE  TABLE  pet(

id  NUMBER(11),

   master_id  NUMBER(11)  NOT  NULL,

   name  NVARCHAR2(50),

   type_id  NUMBER(11)  NOT  NULL,

   health  NUMBER(11)  DEFAULT  100  NOT  NULL,

   love  NUMBER(11)  DEFAULT  100  NOT  NULL,

   prop1  NVARCHAR2(100),

   prop2  NVARCHAR2(100),

   prop3  NVARCHAR2(100),

   adopt_time  DATE  NOT  NULL,

   status  CHAR(1)  DEFAULT  1  NOT  NULL,

   CONSTRAINT  pet_pk  PRIMARY  KEY(id),

   CONSTRAINT  master_fk  FOREING  KEY(master_id)  REFERENCES  master(id),

   CONSTRAINT  type_fk  FOREING  KEY(type_id)  REFERENCES  pet_type (id) );

其中prop1,prop2,prop3三个字段为备用字段,便于以后扩充使用。在定义字段后通过专门语句定义表的主键和外键字段,主键字段为id字段,共有两个外键字段,其中外键字段master_id参考master表的id字段,外键字段type_id参考pet_type表的id字段。

创建数据库表过程中指定表和字段的注释非常重要,便于理解数据库表和字段的含义和作用。可以通过COMMENT语句为数据库表和字段增加注释,如示例8所示。

示例8

COMMENT  ON  TABLE  pet  IS ‘宠物’;

COMMENT  ON  COLUMN  pet.name  IS ‘宠物昵称’;

COMMENT  ON  COLUMN  pet.health  IS ‘宠物健康值’;

还可以通过图形界面工具创建数据库表。比如使用PL/SQL  Developer登录数据库后,在左侧“Tables”项上单击右键,在弹出的快捷菜单中选择New…命令,如图11.33所示。

图11.33  通过图形界面创建数据库表

通过General选项卡指定数据库表的名字、注释等,通过Columns选项卡指定数据库表的各个字段的名字,数据类型及是否为空、默认值、注释等内容,通过Keys选项卡指定数据库表的主键和外键,然后单击Close按钮,在弹出的确认窗口中选择保存修改即可。

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

已经创建了数据库表,下一步就要录入数据库记录。需要解决的一个问题是:三个表的ID字段如果能够实现自动编号功能就方便多了,Oracle中是如何实现逐渐自增的呢?

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

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

Oracle并没有类似SQL  Server中的identity来定义主键自增属性。如果要实现主键自增,常用的方法是定义一个序列Sequence,然后在插入数据时取序列中的下一个值nextval即可。

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

11.3.3  创建和使用序列

序列是Oracle提供的用于产生一组等间隔整型数值的数据库对象,可以通过在插入语句中引用序列值来实现主键自增。

Oracle中的CREATE  SEQUENCE命令用于创建序列。

语法

CREATE  SEQUENCE  seg_name

[START  WITH  start]

[INCREMENT  BY  increment]

[MINVALUE  mivalue∣NOMINVALUE]

[MAXVALUE  maxvalue∣NOMAXVALUE]

[CYCLE∣NOCYCLE]

[CACHE  cache∣NOCACHE]

[ORDER∣NOORDER]

说明如下。

  • seq_name:序列名。
  • START  WITH  start:用来指定序列的起始值。
  • INCREMENT  BY  increment:用来指定序列的增量。如果取负值,则为递减序列。
  • MINVALUE  mivalue∣NOMINVALUE:指定序列是否有最小值。
  • MAXVALUE  maxvalue∣NOMAXVALUE:用来指定序列是否有最大值。
  • CYCLE∣NOCYCLE:用来指定序列达到最大值或最小值后。
  • CACHE  cache∣NOCACHE:用来指定是否在缓存中保存预分配的序列值。如果选择保存,可以提高获取序列值的速度。
  • ORDER∣NOORDER:ORDER选项保证序列值的唯一性和顺序性,NOORDER选项只保证序列值的唯一性。

下面定义序列master_seq,用来实现master表的主键自动编号,如示例9所示。

示例9

CREATE  SEQUENCE  master_seq

   START  WITH  1

   INCREMENT  BY  1

   NOMAXVALUE

   CACHE  10;

使用序列时,需要用到序列的两个伪列NEXTVAL和CURRVAL。其中NEXTVAL将返回序列的下一个值,而CURRVAL将返回序列的当前值。

利用序列向master表插入记录,实现主键自动标号的语句如示例10所示。

示例10

INSERT  INTO  master  VALUES(master_seq.nextval,‘1k1’,‘1k1’,1);

INSERT  INTO  master  VALUES(master_seq.nextval,‘liyong’,‘801123’,1);

SELECT  master_seq. currval  FROM  dual;//查看序列的当前值

SELECT  master_seq. nextval  FROM  dual;//查看序列的下一个值

通过ALTER  SEQUENCE语句对序列进行修改,可以修改序列起始值之外的所有参数。如果要修改序列的起始值,则必须先删除序列,再重新创建。删除序列的语句是DROP  SEQUENCE。示例11演示了序列的修改和删除操作。

示例11

ALTER  AEQUENCE  master_seq

INCREMENT  BY  5

MAXVALUE  100000

NOCYCLE

NOCACHE;

INSERT  INTO  master  VALUES(master_seq.nextval,‘xhb’,‘xho’,2);

SELECT * FROM  master;

DROP  SEQUENCE  master_seq;

11.3.4  上机练习

上机练习3

练习——创建数据库表

训练要点

使用CREATE  TABLE命令,PL/SQL  Developer图形界面创建数据库表。

需求说明

创建数据库表master、pet_type、pet,各个表结构如11.3.2的示例所示。请创建数据库表,注意表的字段的数据类型、主键、外键的指定。

实现思路及关键步骤

  1. 在SQL*Plus下创建数据库表master。
  2. 在iSQL*Plus下创建数据库表pet_type。
  3. 在PL/SQL  Developer下创建数据库表pet。

上机练习4

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

训练要点

  • 序列的创建。
  • 使用序列实现主键自动编号。

 

需求说明

创建序列master_seq、pet_type_seq、pet_seq,使用序列向master、pet_type、pet表插入测试数据。

实现思路及关键步骤

  1. 创建序列master_seq、pet_type_seq、pet_seq。
  2. 使用序列向master、pet_type、pet表插入测试数据,实现主键自动编号。

本章总结

  • Oracle是一个对象关系数据库管理系统,是Oracle公司的核心产品。Oracle数据库基于“客户/服务器”系统结构。
  • 在Oracle中,数据库是指磁盘上存储据数据的集合,在物理上表现为数据文件、日志文件和控制文件等,在逻辑上以表空间形式存在。
  • 数据库实例就是为了运行数据库,Oracle系统所运行的所有进程和分配内存结构的组合体。
  • 创建数据库可以在安装Oracle软件时进行,也可以在安装Oracle软件后进行。可以通过图形界面的DBCA或CREATE  DATABASE命令实现。
  • 用户可以通过SQL*Plus、iSQL*Plus、PL/SQL  Developer,企业管理器EM等多种形式连接数据库。
  • 连接数据库前需在服务器端配置监听器,在客户端配置本地Net服务名。
  • 在数据库中创建用户时,基于应用性能和管理方面的考虑,最好为不同用户创建独立的表空间。
  • 创建用户后必须赋予相应权限。Oracle用户权限分为系统权限和对象权限。角色是具有名称的一组权限的组合,可以使用角色为用户授权。
  • 序列是Oracle提供的用于产生一组等间隔整型数值的数据库对象,可以通过在插入语句中引用序列值来实现主键自增。

本章作业

一、选择题

1. 以下选项中关于Oracle中数据库的说法正确的是( )。(选两项)

A.  在物理上以表空间形式存在

B. 在逻辑上表现为数据文件、日志文件和控制文件等。

C. 必须首先创建数据库,然后才能使用Oracle

D. 可以在安装Oracle软件时同时创建数据库,也可以在安装后再单独创建数据库

 

2. 以下选项中关于Oracle中表空间的说法正确的是(  )。(选两项)

A. 每个Oracle数据库都是由若干个表空间构成的,用户在数据库中建立的所有内容都被存储到表空间中

B. 一个表空间可以有多个数据文件,一个数据文件可以属于多个表空间

C. 在每一个数据库中,都有一个名为SYSTEM的表空间,即系统表空间

D. 管理员可以创建自定义的表空间并分配给指定用户,但不可以为已定义的表空间再增加和删除数据文件

 

3. 以下选项中关于Oracle监听器和本地Net服务名的说法错误的是(  )。(选两项)

A. 要实现与Oracle服务器以C/S方式正常建立连接,必须正确配置监听器listener或本地Net服务名

B. 要在Oracle客户端配置监听器listener,服务器端配置本地Net服务名

C. 监听器和本地Net服务名配置信息分别存储在listener.ora和tnsnames.ora文件中

D. 可以通过Oracle中的Net  Configuration  Assitant和DBCA工具来配置监听器和本地Net服务名信息

 

4. 以下选项中关于Oracle数据类型的说法错误的是(  )。(选两项)

A. 与CHAR相比,使用VARCHAR2数据类型可节省磁盘空间,提高存储效率

B. NCHAR数据类型用来存储Unicode字符集类型,一个英文字母要占用两个字节

C. NUMBER数据类型既可以存储整数,也可以存储浮点数

D. DATE数据类型只存储日期信息,而TIMESTAMP数据类型存储日期和时间信息

 

5. 以下选项中关于Oracle中序列的说法正确的是(  )。(选两项)

A. 序列用于产生一组等间隔整型数值,在插入语句中引用序列值可实现主键自增

B. 序列只能是递增序列,不能是递减序列

C. 序列的使用是通过序列的两个伪列NEXTVAL和CURRVAL实现的

D. 通过ALTER  SEQUENCE语句对序列进行修改,可以修改序列的所有参数

二、简答题

1. 介绍Oracle的两个系统用户SYS和SYSTEM。

2. 简述Oracle中数据库和数据库实例的区别和联系。

3. 简述安装Oracle后生成的Oracle服务名及作用。

4. 简述Oracle的各种数据类型。

5. 开发简单的图书订购系统,共涉及图书、订单、订单项三个数据库表,请按照如下步骤,使用Oracle完成相应的数据库操作。

(1)创建表空间order_tablespace,数据文件名是order.dbf,初始大小为200MB;文件大小可自动扩展,每次扩展20MB,最大文件长度为400MB;不进行日志记录。

(2)创建用户order,密码为le,默认表空间为order_tablespace,给用户order授予CONNECT、RESOURCE角色。

(3)使用order登录数据库,根据表11-1、表11-2和表11-3内容创建数据库表。

表11-1  图书表book

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

字段名称           字段说明         数据类型         大小         备注

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

id                 图书编号         NUMBER           11           主键

bookname           图书名称         VARCHAR2         50           非空

price              图书单价         NUMBER           11,2        非空

storage            库存数量         NUMBER           11           非空

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

表11-2  订单表orders

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

字段名称           字段说明         数据类型         大小         备注

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

id                 订单编号         NUMBER           11           主键

total              订单总额         NUMBER           11,2        默认值0

create_time        订单日期         DATE                          非空

status             订单状态         CHAR             1            默认值0

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

表11-3  订单项表orderitem

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

字段名称           字段说明         数据类型         大小         备注

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

id                 订单项编号       NUMBER           11           主键

book_id            图书编号         NUMBER           11           外键

price              图书单价         NUMBER           11,2         非空

num                购买数量         NUMBER           11           默认值1

order_id           所属订单编号     NUMBER           11           外键

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

 

  1. 针对以上三个表,分别创建序列book_seq、order_seq、item_seq。
  2. 为三个表分别插入测试记录,使用序列实现主键自动编号。

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

参考本章相应示例,按照题目要求依次完成各个操作。

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

12章

Oracle应用

 

◇本章工作任务

Ø    实现主人登录功能

Ø    实现主人所有宠物信息的显示

Ø    实现主人领养宠物

◇本章技能目标

Ø    掌握使用JDBC连接到Oracle

Ø    掌握Oracle常用函数的使用

Ø    掌握Oracle索引及其使用

Ø    掌握Oracle数据的导入导出

 

本章单词

 

 

 

 

 

 

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

 

  1. extract:_______________________________
  2. decode:_______________________________
  3. trim:__________________________________
  4. count:________________________________
  5. index:________________________________
  6. unique:_______________________________
  7. bitmap:_______________________________
  8. import:________________________________
  9. export:________________________________

 

本章简介

本章在上一章基础上介绍Oracle的基本应用。首先介绍使用JDBC访问Oracle,以使学员对JDBC为多种数据库提供统一访问有更直观的理解。然后介绍Oracle的常用函数,以案例驱动方式引出多个函数并讲解,并以表格的形式对常用函数进行分类总结。使用索引可以加快SQL查询速度。本章对索引的作用、原理、分类、实现进行介绍。最后介绍Oracle中数据的多种导入导出方式,以方便实现数据的备份、转移和恢复。

 

12.1使用JDBC访问Oracle

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

编写控制台程序,实现主人登录。

成功登录界面

----欢迎光临宠物乐园----

请输入登录名:le

请输入密码:*******

登录成功!

登录失败界面

----欢迎光临宠物乐园----

请输入登录名:le

请输入密码:*******

用户名或密码错误,登录失败!

要求:查询Oracle数据库;用户名、密码匹配,且用户没有被禁用则登录成功,否则,登录失败。

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

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

使用JDBC可以为多种数据库提供统一访问,都需要向项目中导入相应驱动程序jar包,基本访问步骤相同。

  1. 建立连接。
  2. 创建PreparedStatement对象。
  3. 设置参数的值。
  4. 执行查询。
  5. 处理查询结果。

不同之处在于数据库驱动类名称字符串、数据库连接字符串等参数不同。

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

使用JDBC访问Oracle数据库,判断主人登录是否成功的代码如示例1所示。

 

示例1

import  java.sql.Connection;

import  java.sql.DriverManager;

import  java.sql.PreparedStatement;

import  java.sql.ResultSet;

import  java.sql.SQLException;

import  java.util.Scanner;

/**

 *宠物主人业务类。

 *@author 南京

 */

public  class  MasterManager {

       /**

        *使用JDBC判断主人登录是否成功。

        *@param  loginId 登录名

        *@param  password 密码

        *@return 登录是否成功结果

        */

       private  boolean  login(String  loginId, String  password) {

              boolean  ret = false;

              //1、数据库连接信息

              String  driverClassName = “oracle.jdbc.driver.OracleDriver”;

              String  url = “jdbc:oracle:thin:@10.0.0.41.1521:sledb”;

              String  user = “epet”;

              String  dbPassword = “le”;

              //2、根据查询结果判断登录是否成功

              Connection  conn = null;

              PreparedStatement  pstmt = null;

              ResultSet  rs = null;

              try {

                  Class.forName(driverClassName);

                  conn = DriverManager.getConnection(url, user, dbPassword);

                  String  sql = “select 1 from  master  where  loginid = ?” + “ and  password = ? and  status=1”;

                   pstmt = conn.prepareStatement(sql);

                   pstmt.setString(1, loginId);

                   pstmt.setString(2, password);

                   rs = pstmt.executeQuery();

                   if (rs. next()) {

                         ret = true;

                   } else {

                         ret = false;

                   }

               } catch (ClassNotFoundException  e) {

                    e.printStackTrace();

               } catch (SQLException  e) {

                    e.printStackTrace();

               } finally {

                    try {

                          if (null ! = rs) {

                               rs.close();

                           }

                     } catch (SQLException  e) {

                           e.printStackTrace();

                     }

                     try {

                          if (null ! = pstmt) {

                               pstmt.close();

                           }

                     } catch (SQLException  e) {

                           e.printStackTrace();

                     }

                     try {

                          if (null ! = conn) {

                               conn.close();

                           }

                     } catch (SQLException  e) {

                           e.printStackTrace();

                     }

                 }

                 //3、返回登录结果

                 return  ret;

         }

         /**

          *主人登录界面。

          */

         pubic  void  login() {

                  //1、获得输入对象

                  Scanner  input = new  Scanner(System.in);

                  //2、打印欢迎信息

                  System.out.println(“----欢迎光临宠物乐园----“);

                  //3、获取用户输入的登录名、密码

                  System.out.print(“请输入登录名:”);

                  String  loginId = input.next();

                  System.out.print(“请输入密码:”);

                  String  password = input.next();

                  //4、检查登录名、密码是否合法,并输出提示信息

                  if (this.login(loginId, password)) {

                          System.out.println(“登录成功!”);

                  } else {

                          System.out.println(“用户名或密码错误,登录失败!”);

                  }

          }

}

编写测试类,使用MasterManager类的代码如示例2所示。

示例2

/**

 * 测试MasterManager。

 * @author 南京

 */

public  class  MasterManagerTest {

       /**

        *Main方法。

        *@param  args

        */

       public  static  void  main(String[] args) {

              MasterManager  instance = new  MasterManager();

              instance.login();

       }

}

运行测试类,主人登录成功和失败的运行结果如图12.1和图12.2所示。

图12.1  主人登录成功界面

图12.2  主人登录失败界面

从示例1和示例2可以看出,使用JDBC访问Oracle的步骤与访问SQL Server的步骤相同,只是具体连接参数不同。数据库驱动类名称字符串为”oracle.jdbc.driver.OracleDriver”,数据库连接字符串为”jdbc:oracle:thin: @10.0.0.41:1521:sledb”,其中”10.0.0.41:1521”为Oracle服务器IP地址和监听端口,”sledb”为数据库实例名(SID)。不要忘记向项目导入Oracle驱动程序jar包。

 

12.2 Oracle常用函数

上机练习1

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

训练要点

Ø    自定义日期的显示格式:EXTRACT()、TO_CHAR()。

Ø    字段的字符串显示:DECODE()。

Ø    给字段指定默认值: NVL()。

主人登录成功后,显示主人的所有宠物信息,要求如下。

Ø    如果宠物的name字段值为空,显示“无名”。

Ø    如果宠物的status字段值为1,显示“正常”;如果字段值为2,显示“禁用”。

Ø    自定义宠物adopt_time字段的显示格式。

图12.3是数据库表中数据的原样输出,要求运行结果如图12.4所示。

图12.3  数据库表中数据的原样输出

 

 

图12.4  参考运行结果

实现思路及关键代码

  1. 根据数据库表pet编写实体类Pet,并提供setter/getter方法。
  2. 编写类PerManager,使用JDBC查询(查询语句中使用相应函数对字段值进行处理),返回主人的所有宠物信息。
  3. 编写测试类PetManagerTest,在控制台显示主人的所有宠物信息。

注意编写注释。

参考解决方案

  1. 日期处理函数:EXTRACT()、TO_CHAR()

EXTRACT()函数可以从DATE型字段中抽取出某一部分内容,比如年、月、日等。

TO_CHAR()函数可以将日期转换成指定类型的字符串格式。

SELECT  SYSDATE  AS  now, EXTRACT(YEAR  FROM  SYSDATE) ∣∣ ‘年’ ∣∣ EXTRACT(MONTH  FROM  SYSDATE) ∣∣ ‘月’ ∣∣ EXTRACT (DAY  FROM  SYSDATE) ∣∣ ‘日’ FROM  dual;

其中,”∣∣”运算符可以进行字符串拼接,SQL语句执行结果如图12.5所示。

图12.5  EXTRACT()使用举例

SELECT  SYSDATE, TO_CHAR(SYSDATE) FROM dual;

日期的默认转换格式为”DD-MON-YY”,SQL语句执行结果如图12.6所示。

  图12.6  TO_CHAR()使用举例1

SELECT  SYSDATE, TO_CHAR (SYSDATE,’YYYY”年”MM”月”DD”日”HH”时”MI”分”SS”秒”’) FROM  dual;

SQL语句执行结果如图12.7所示。

图12.7  TO_CHAR()使用举例2

SELECT  SYSDATE, TO_CHAR (SYSDATE,’YY”年”MM”月”DD”日”HH24”时”MI”分”SS”秒”’) FROM  dual;

SQL语句执行结果如图12.8所示。

图12.8  TO_CHAR()使用举例3

  1. DECODE()函数

DECODE()函数相当于多重IF-ELSE-THEN逻辑,可以对某个表任何类型的任意列的值或一个通过计算所得的结果进行判断,根据其值返回指定结果。

语法格式:

DECODE(value,if1, then1, if2, then2, if3, then3, ······, else)

说明

如果value的值为if1,则返回then1的值:如果value的值为if2,则返回then2的值;如果value的值为if3,则返回then3的值,······,否则返回else的值。

SELECT  name, health, status, DECODE(status, 1,’正常’, 2, ‘禁用’) AS  status_str  FROM  pet

SQL语句执行结果如图12.9所示。

图12.9  DECODE()使用举例

  1. NVL()函数

NVL()函数用于将空值null替换为指定的缺省值,适用于字符、数字、日期等类型数据。

语法格式:

NVL(exp1,exp2)

说明

如果表达式exp1的值为null,则返回exp2的值,否则返回exp1的值。

SELECT  id, NVL(name,’无名’) AS 姓名,love,NVL(adopt_time,SYSDATE)AS领养日期 FROM  pet

SQL语句执行结果如图12.10所示。

图12.10  NVL()使用举例

 

上机练习2

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

训练要点

Ø    使用JDBC访问Oracle。

Ø    Oracle常用函数的使用。

主人登录成功后可以领养宠物,按照提示输入宠物的名字,选择宠物的类型即可。然后显示领养成功或失败提示,领养完毕后显示主人最新的领养宠物列表。运行结果如图12.11所示。

图12.11  主人领养宠物

实现思路及关键代码

  1. 在上机练习1基础上进行实现,会用到实体类Pet、业务类PetManager和测试类PetManagerTest。
  2. 编写类PetManager,完成主人领养宠物,并把领养信息存入数据库表。
  3. 编写测试类PetManagerTest,完成领主人养宠物并显示主人最新的领养宠物列表。

注意编写注释。

参考解决方案

可以重用上机练习1的代码,具体实现方案可参考本章示例1和示例2。

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

上机练习1中学习了如何显示主人的所有宠物信息,现在要显示主人在2010年以后领养宠物的信息,该如何实现呢?

——分析—————————————————————————————————-

Oracle中Date数据类型可以比较大小,很自然想到采用如下SQL语句查询:

SELECT * FROM  pet  WHERE  master_id = 1  AND  adopt_time > ‘2010-01-01’;

在PL/SQL  Developer中执行时弹出图12.12所示对话框,提示文字与格式字符串不匹配。

图12.12  日期比较出错信息

原因在于Oracle不会把“2010-01-01”当做日期识别,而认为是字符串,所以出现了该错误,可以采用如下几种方法来解决。

方法1:采用已经学习的TO_CHAR()函数。

select * from  pet  WHERE  TO_CHAR(adopt_time,’yyyy’)>’2009’

方法2:采用Oracle默认日期字符串格式DD-MON-YY。

SELECT * FROM  pet  WHERE  master_id=1  AND  adopt_time > ‘1-1月 -10’

方法3:采用Oracle的TO_DATE()函数把字符串按照指定格式转换成日期类型,下面将对这种方法进行介绍。

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

TO_DATE()函数可以将字符串转换为日期类型,该函数的两种使用方式为:

TO_DATE (char) //按缺省格式DD-MON-YYYY进行解析

TO_DATE (char,’format_model’) //按模式串指定的格式进行解析

使用举例如示例3所示,三个SQL语句都可以显示主人在2010年以后领养宠物的信息。

 

示例3

SELECT * FROM  pet  WHERE  master_id=1  AND  adopt_time > TO_DATE(’01-1月– 2010’);

SELECT * FROM  pet  WHERE  master_id=1  AND  adopt_time > TO_DATE(’2010-01-01’,’YYYY-MM-DD’);

SELECT * FROM  pet  WHERE  master_id=1  AND  adopt_time > TO_DATE(’2010年01月01日’,’YYYY”年”MM”月”DD”日”’);

Oracle提供了大量函数,使用这些函数可以大大提高SELECT语句操作数据库的能力。Oracle中函数划分为单行函数和多行函数。单行函数作用于数据库表的某一行并返回一个值。单行函数可以大致划分为:日期函数、字符函数、数字函数、转换函数和其他函数。多行函数也称为分组函数,该类函数基于数据库表的多行进行运算,返回一个结果,例如对多行记录的某个字段进行求和、求最大值运算等。

下面以表格形式对Oracle的主要函数进行较为全面介绍,其中大多数函数的使用都非常简单,部分比较复杂的转换函数和其他函数已在本章进行了介绍。

字符函数是Oracle中广泛使用的函数,对字符数据类型进行操作,操作结果可能是字符数据类型,也可能是数字类型。表12-1列出了Oracle中常用的字符函数。

表12-1  常用的字符函数

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

函数                            功能         实例                          结果

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

INITCAP(char)                                  首字母大写    INITCAP(‘hello’)                         Hello

LOWER(char)                                     转换为小写     LOWER(‘FUN’)                              fun

UPPER(char)                                     转换为大写     UPPER(‘sun’)                              SUN

LTRIM(char,set)                               左剪裁          LTRIM(‘xyzadams’,’xyz’)              adams

RTRIM(char,set)                               右剪裁          RTRIM(‘xyzadams’,’ams’)              xyzad

TRANSLATE(char,from,to)                    按字符翻译     TRANSLATE(‘jack’,’abcd’,’1234’)  Jl3k

REPLACE(char,search_str,replace_str) 替换字符串     REPLACE(‘jack and jue’,’j’,’bl’) black and blue

INSTR(char,substr[,pos])                   查找子串位置  INSTR(‘worldwide’,’d’)                5

SUBSTR(char,pos,len)                        取子字符串     SUBSTR(‘abcdefg’,3,2)                 cd

CONCAT(char1,char2)                          连接字符串    CONCAT(‘Hello’,’world’)               Helloworld

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

数字函数接受数字输入并返回数字作为输出结果,数字函数返回的值可以精确到小数点后38位。表12-2列出了Oracle中常用的数字函数。

表12-2常用的数字函数

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

函数          功能                 实例                       结果

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

ABS(n)       取绝对值             ABS(-15)                    15

CEIL(n)      向上取整             CEIL(44.778)                45

SIN(n)       正弦                 SIN(1.571)                 .999999979

COS(n)       余弦                 COS(0)                      1

SIGN(n)      取符号               SIGN(-32)                   -1

FLOOR(n)     向下取整             FLOOR(100.2)                100

POWER(m,n)   m的n次幂            POWER(4,2)                  16

MOD(m,n)      取余数               MOD(10,3)                  1

ROUND(m,n)    四舍五入             ROUND(100.256.2)           100.26

TRUNC(m,n)    截断                 TRUNK(100.256.2)           100.25

SQRT(n)       平方根               SQRT(4)                     2

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

日期函数对日期值进行运算,根据函数的用途产生日期数据类型或数值类型的结果。表12-3列出了Oracle中常用的日期函数。

 

 

 

表12-3  常用的日期函数

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

转换函数将值从一种数据类型转换为另一种数据类型。表12-4列出了Oracle中常用的转换函数。

表12-4  常用的转换函数

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

函数       功能            实例                                结果

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

TO_CHAR   转换成字符串类型  TO_CHAR(1234.5,’$9999.9’)            $1234.5

TO_DATE   转换成日期类型    TO_DATE(‘1980-01-01’,’yyyy-mm-dd’) 01-1月-80

TO_NUMBER 转化成数值类型    TO_NUMBER(‘1234.5’)                  1234.5

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

除去以上介绍的四种单行函数外,还有其他一些单行函数,在此统称为其他函数。表12-5列出了Oracle中常用的此类函数。

 

 

 

表12-5  常用的其他函数

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

函数                              功能

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

NVL(EXP1,EXP2)                                   如果exp1的值为null,则返回exp2的值,

                                                  否则返回exp1的值

NVL2(EXP1,EXP2,EXP3)                            如果exp1的值为null,则返回exp2的值,

                                                  否则返回exp3的值

DECODE(VALUE,IF1,THEN1,IF2,THEN2,……,ELSE)   如果value的值为if1,则返回then1的值;

                                                  如果value的值为if2,则返回then2的

                                                  值;······;否则返回else的值

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

多行函数也称为分组函数,它基于数据库表的多行进行运算,返回一个结果,例如对多行记录的某个字段进行求和、求最大值运算。表12-6列出了Oracle中常用的多行函数,其中SUM()、AVG()仅适用数值型,而COUNT()、MAX()、MIN()适用任何类型数据。

 

表12-6  常用的多行函数

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

函数   功能                               实例

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

SUM()  对符合条件的记录的指定列值求和    SELECT SUM(love)FROM pet;

AVG()  对符合条件的记录的指定列求平均值  SELECT AVG(health)FROM pet WHERE

master_id=1;

COUNT() 对符合条件的记录的指定列计数     SELECT COUNT(type_id)FROM pet;

MAX()   对符合条件的记录的指定列求最大值 SELECT MAX(adopt_time)FROM pet;

MIN()   对符合条件的记录的指定列求最小值 SELECT MIN(adopt_time)FROM pet

WHEREtype_id=1;

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

 

上机练习3

练习——Oracle函数练习

训练要点

Ø    Oracle字符函数、Oracle数字函数。

Ø    Oracle日期函数、Oracle转换函数、Oracle其他函数。

Ø    Oracle多行函数。

需求说明

根据表12-1~表12-6共计六个表格中函数名称、功能介绍、使用实例及运行结果,

学习Oracle函数,并进行上机练习,加深理解。

12.3  Oracle索引

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

当pet表中已存在很多条记录,如大于10万条时,数据的查询速度便成为了一个问题。

因为查询时要一次进行大量比较,从而消耗大量时间,导致查询速度骤减,超出了可

接受限度。Oracle提供了什么有效方法来解决这个问题呢?

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

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

当我们要在一本书中查询某一内容时,可以采用两种方法:一种是从第一页开始逐页翻阅查找,这显然不是一种有效的方法;二是首先在目录中查询所需要的知识点,然后根据目录中提供的页码找到要查询的内容,可以大大缩短查询的时间。

与此类似,在数据库中也可以建立类似目录的数据库对象,来实现数据的快速查询,这就是索引。合理设置并使用索引可以大大提高查询速度。

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

 

12.3.1  Oracle索引类型

Oracle中可以创建多种类型的索引,以适应各种表的特点。理解这些索引的原理和适用情况是正确创建和使用索引的前提。按照不同的分类标准会有不同类型分类结果。下面依次对各种常见索引进行介绍。

  1. B树索引、位图索引、反向键索引(分类标准是索引的存储结构)

B树索引是Oracle中最常用的索引,也是默认索引类型。B树是数据结构中的一个概念,它是一种特殊的树状结构,B为平衡之意(Balanced)。B树索引的结构就是一个树,主要数据都集中在最底层的叶子节点上,无论用户搜索哪个分枝上的叶子节点,所经过的索引层次是相同的,即花费的时间是一样的。

使用位图索引的优点在于,它最适用于低基数列,也就是不同值的数目比表的行数少的列。如果某个列的值重复了超过100次,则可以考虑在该列上创建位图索引。例如,对于“性别”列建立B树索引的意义不大,但可以再该列上建立位图索引。

反向键索引是一种特殊的B树索引,在索引基于含有序数的列时非常有用。如果一个索引基于一个含有这种数据的列,往往会因为数据过于密集而降低读取性能,反向键索引通过简单地反向被索引列中的数据来解决问题。首先反向每个列键值的字节,然后再反向后的新数据上进行索引,例如用户输入索引键3404,就反向转换为4043进行索引。反向键索引通常建立在一些值连续增长的列上,例如列中值是由序列产生的。

  1. 唯一索引、非唯一索引(分类标准是索引值是否唯一)

索引可以是唯一的,也可以是非唯一的。唯一索引可以确保在定义索引的列中,表的任意两行的值都不相同,非唯一索引没有在列值上规定此限制。Oracle自动为表的主键列创建唯一索引。

  1. 单列索引、组合索引、基于函数的索引

单列索引是在表中的单个列上创建的索引,组合索引是在表中的多个列上创建的索引。

组合索引中列的顺序时任意的,不必是表中相邻的列。如果SELECT语句中的WHERE子句引用了组合索引中的所有列或大多数列,则组合索引可以提高数据检索速度。创建索引时,应注意定义中使用的列的顺序,通常最频繁访问的列应放置在列表的最前面。

基于函数的索引是一种特殊的索引,它不是创建在表的单列或多列上,而是创建在函数或表达式上。例如搜索领养时间再2010年之前的宠物,使用WHERE adopt_time<’2010’,会出现类型匹配错误,但是使用WHERE TO_CHAR (adopt_time,’YYYY’) < ’2010’ 时,虽然可以进行比较,却不会使用在adopt_time列上建立的索引。为了方便此类操作,Oracle提供了一个选项,可以基于一个或多个列上的函数或表达式创建索引。当WHERE子句中包含函数或表达式以计算查询时,基于函数的索引十分有用。

12.3.2  创建和删除索引

Oracle中CREATE  INDEX命令用于创建索引,DROP  INDEX命令用于删除索引。

语法

CREATE [ UNIQUE ∣ BITMAP ] INDEX  index_name

ON  table_name

(column_name1 ∣ expression1 ASC∣SESC, column_name2 ∣ expression2 ASC∣DESC,······)

[REVERSE]

语法

DROP  INDEX  index_name;

说明如下

Ø    index_name:索引的名字。

Ø    [UNIQUE∣BITMAP]:如果指定UNIQUE,则创建唯一索引,需求索引列值唯一。如果指定BITMAP,则创建位图索引。如果省略这两个关键字,则创建B树索引。

Ø    ON table_name(column_name1∣expression1 ASC∣DESC,column_name2∣expression2 ASC∣DESC,…):在table_name表的指定列上建立索引。如果指定多个列,则为多列索引,如果指定expression,则为基于函数的索引。ASC表示升序排列,是默认顺序,DESC表示降序排列。

Ø    [REVERSE]:创建反向键索引。

通过以下练习加深对创建索引的理解和掌握。

Ø    在pet表的adopt_time字段上创建降序唯一索引adopt_time_index。

Ø    在pet表的type_id字段上创建位图索引type_id_bitmap_index。

Ø    在pet表的health、love字段上创建组合索引health_love_index。

Ø    在pet表上创建基于函数TO_CHAR(adopt_time,’YYYY’)的索引to_char_index。

Ø    在pet表的master_id字段上创建反向键索引master_id_reverse_index。

Ø    删除在pet表上创建的反向键索引master_id_reverse_index。

以上创建和删除索引操作的实现语句如示例4所示。

 

示例4

CREATE  UNIQUE  INDEX  adopt_time_index  ON  pet(adopt_time  DESC);

CREATE  BITMAP  INDEX  type_id_bitmap_index  ON pet(type_id);

CREATE  INDEX  health_love_index  ON  pet(health,love);

CREATE  INDEX  to_char_index  ON  pet(TO_CHAR(adopt_time,’YYYY’));

CREATE  INDEX  master_id_reverse_index  ON  pet(master_id) REVERSE;

DROP  INDEX  master_id_reverse_index;

通过一个循环向pet表中插入10万条数据,在PL/SQL中执行查询语句,查看执行时间。创建索引后,再次执行查询,看执行时间的变化量有多大。

 

12.4 Oracle中数据的导入导出

数据的备份与恢复是保证数据库安全运行的一项重要内容。当数据库因为意外情况而无法正常运行时,可以利用事先做好的的备份进行恢复,将损失减少到最小。备份和恢复的方法很多,本节讲解利用Oracle的导入导出功能实现数据的备份和恢复。导入导出可以通过Oracle的exp和imp命令实现,也可以通过PL/SQL提供的图形界面方式实现。

 

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

1.使用exp导出数据

exp是Oracle提供的一个导出工具,它是操作系统下的一个可执行文件,存放目录为\ORACLE_HOME\BIN,ORACLE_HOME指Oracle的主目录,此处为E:\oracle\product\10.2.0\db_1.在命令提示符窗口输入exp即可启动数据的导出,主要步骤如下。

  1. 按照提示输入用户名和密码进行登录。此处输入用户名“epet”,密码为“le”。
  2. 登录成功后,提示输入数组提取缓冲区大小,如果采用默认值,直接回车即可。
  3. 提示输入导出文件的路径和文件名,默认为export.dmp,如果采用默认值,直接回车即可。此处输入epet.dmp,然后回车。
  4. 提示选择导出方式。使用exp导出数据时,支持三种导出方式:表方式导出一个指定表,包括表的定义、数据和表上建立的索引和约束等;用户方式导出属于一个用户的所有对象,包括表、视图、序列、存储过程等;全数据库方式导出数据库中的所有对象,只有DBA可以选择这种导出方式。此处选择用户方式,它是默认选项。
  5. 提示是否导出权限、导出表数据,是否对导出数据进行压缩,采用默认项即可。
  6. 开始导出数据,导出完毕后提示成功,终止导出。执行过程如图12.13所示。

2.使用imp导入数据

imp是Oracle提供的一个导入工具,它也是操作系统下的一个可执行文件,存放目录与exp相同。使用exp导出数据后,可以使用imp将数据再导入数据库。可以导入自己导出的数据,也可以导入由其他用户导出的数据。

图12.13  使用exp导出数据

此处演示导入自己导出的数据,首先删除epet用户模式下的创建的数据库表和序列(便于演示导入效果),然后再命令提示符窗口中输入imp启动数据的导入,执行过程如图12.14所示。导入成功后可以看到epet用户模式下删除的数据库表和序列已经恢复。

图12.14  使用imp导入数据

主要步骤描述如下。

  1. 按照提示输入用户名和密码进行登录。此处输入用户名“epet“,密码为”le“。
  2. 登录成功后,提示输入导入文件的路径和文件名,默认为export.dmp,如果采用默认值,直接回车即可。此处输入aaa.dmp,然后回车。
  3. 提示输入插入缓冲区大小,如果采用默认值,直接回车即可。
  4. 提示是否只列出导入文件的内容,如果采用默认值,直接回车即可。
  5. 提示由于对象已存在,是否忽略创建错误,默认项为no。此处输入yes,回车继续。
  6. 提示是否导入权限,是否导入数据,采用默认值即可。
  7. 提示是否导入整个导出文件,默认项为no,此处输入yes,回车继续。
  8. 开始导入数据,导入完毕后给出成功终止导入提示。

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

PL/SQL  Developer中提供了更直观方便和更多方式的导入导出方法。

  1. 使用PL/SQL  Developer导出数据

登录PL/SQL  Developer成功后,选择Tools→Export  Tables…,出现如图12.15所示的窗口。

图12.15  使用PL/SQL  Developer导出数据

 

可以看到提供了三种导出方式,分别为Oracle  Export、SQL Inserts、PL/SQL  Developer,它们的区别如下。

Ø    Oracle  Export:使用的就是exp命令,导出为.dmp文件格式。.dmp文件是二进制的,无法查看,可以跨平台,效率高,使用最广。

Ø  SQL Insert:导出.sql文件格式,可以使用记事本等文件编辑器查看,效率不如第一种,适合小数据量导入导出。如果表中含有blob、clob等字段,不能采用此方式。

Ø  PL/SQL Developer:导出为.pde文件格式,它是PL/SQL Developer的自有文件格式,只能使用该软件来导入导出。

如图12.15所示,首先在窗口上侧选中要导出的数据库表(可以多选,默认导出所有数据库表),然后在窗口左下侧复选要导出的具体内容(例如表的权限、索引、触发器等,一般采用默认项即可),最后在下侧指定导出文件的路径和文件名,单击Export按钮即可完成导出。

2.使用PL/SQL Developer导入数据

登录PL/SQL Developer成功后,选择Tools→Import Tables…出现如图12.16所示窗口。

 

 

图12.16  使用PL/SQL Developer导入数据

同样可以看到三种导入方式,分别与三种导出方式对应,在窗口下侧指定导入文件的路径和文件名,单击Import按钮即可完成导入。

 

本章总结

 

Ø  使用JDBC访问Oracle的步骤与访问SQL Server的步骤相同,只是具体连接参数不同。

Ø  Oracle中函数分为单行函数和多行函数。前者作用于数据库表的某一个并返回一个值。后者基于数据库表的多行进行运算,返回一个结果。

Ø  Oracle中单行函数可以大致划分为:字符函数,数字函数,日期函数,转换函数和其它函数。

Ø  数据库中的索引类似于书本的目录,合理设置并使用索引可以大大提高查询速度。Oracle中可以创建多种类型的索引,以适应各种表的特点

Ø  Oracle中常见的索引类型有B数索引,位图索引,反向键索引,组合索引,基于函数的索引。

Ø  Oracle提供了多种导入导出方式,方便实现数据的备份,转移和恢复。可通过exp和imp命令实现,也可通过PL/SQL提供的图形界面方式实现。

 

本章作业

 

一、选择题

1.下列选项中关于使用JDBC连接Oracle数据库的说法错误的是(    )。(选两项)

A.必须向项目中导入Oracle驱动程序jar包

B.基本访问步骤与使用JDBC访问其他数据库差别很大

C.数据库连接字符串中必须指定全局数据库名

D.数据库连接字符串中必须指定数据库实例名

 

2.ROUND(123.456,2)、Trunc(123.456,2)ROUND(123.456,0)、Trunc(123.456,-1)四个函数的结果是(    )。

A. 123.45   123.45   123   120

B. 123.46   123.46   123   123

C. 123.46   123.45   123   120

D. 123.46   123.46   120   120

 

3.查询订单表orders中2009年10月及以后的订单列表,以下SQL语句错误的是(    )。

A .SELECT * FROM order WHERE create_time>=‘2009-10-1’;

B. SELECT * FROM order WHERE create_time>=‘1-10月2009’;

C. SELECT * FROM order WHERE create_time>=TO_DATE(‘2009-10-1’‘YYYY-MM-DD’);

D. SELECT * FROM order WHERE TO_CHAR(create_time,‘YYYY-MM-DD’)>=‘2009-10-1’;

 

4.以下选项中关于Oracle索引的说法错误的是(    )。(选两项)

A.使用索引的目的是要提高查询的速度,应该为尽可能多的列建立索引

B.B树索引是Oracle中最常用的索引,也是默认索引类型

C.即可以为某一列建立单列索引,也可以为多列建立组合索引

D.建立索引的列不允许有重复值存在

 

5.SQL语句“CREATE INDEX lower_index ON pet(LOWER (name));”创建的索引类型是(    )。

A.位图索引

B.唯一索引

C.基于函数的索引

D.组合索引

 

6.一个数据库表包含状态字段status,取值范围仅限于1-3,则在该字段上建立( )最合适。

A.位图索引

B.唯一索引

C.基于函数的索引

D.B树索引

 

7.exp支持三种导出方式,下列说法错误的是(    )。

A.表方式导出一个指定表,包括表的定义,数据和表上建立的索引和约束等

B.用户方式导出属于一个用户的所有对象,包括表、视图、序列、存储过程等

C.全数据库方式导出数据库中的所有对象,只有DBA可以选择这种导出方式

D.默认方式是表方式

 

8.PL/SQL Developer能够以多种文件格式导入导出数据,其中不包括(   )。

A. .dmp

B. .sql

C. .pde

D. .txt

 

二、简答题

1. 简述Oracle的常见索引类型及其特点

2. 简述PL/SQL Developer提供的三种导出方式。

在完成第11章作业题简答题3基础上完成如下题目。

3. 使用JDBC连接图书表book,显示所有图书信息。

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

参考本章示例1、示例2完成本题目。

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

4.按照以下要求给指定表建立索引。

Ø  在book表的price字段上创建降序索引price_index。

Ø  在orders表的status字段上创建位图索引status_bitmap_index。

Ø  在orders表的creat_time、total字段上创建组合索引creat_time_total_index。

Ø  在orderitem表上创建基于表达式price*num的索引itemsum_index。

Ø  删除orderitem表上创建基于表达式price*num的索引itemsum_index。

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

参考本章示例4完成本题目。

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

5.在命令提示符窗口使用imp/exp导入导出简单图书预购系统中的三个数据库表,在PL/SQL Developer中使用Oracle Export、SQL insert和PL/SQL Developer三种方式导入导出这三个数据库表。

 

13 章

数据访问层

 

 

 

 

本章工作任务

Ø 使用DAO模式实现主人登录

Ø 开发车辆购置税记录程序

本章技能目标

Ø 掌握DAO模式

Ø 掌握分层开发的优势和原则

Ø 使用实体类传递数据

Ø 掌握数据访问层的职责

本章单词

 

 

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

                                      1.data:                                           

                                      2.access:                                      

                                      3.object:                                              

                                      4.entity:                                       

                                      5.purchase:                                         

                                      6.tax:                                      

                                      7.vehicle:                                              

                                      8.guide:                                              

                                      9.invoice:                                 

                                                   

本章简介

本章内容相对独立,以DAO模式为例讲解软件开发中的分层开发思想技术。随着软件规模的扩大和业务的复杂,将一个软件分成多个层次进行开发,化大为小,分而治之,是缩短软件开发时间,提高软件开发效率的一种有效方法,也是目前软件开发一直使用的方法。本章首先讲解DAO模式,然后讲解分层开发的好处和原则,并辅以多个示例和上机练习来提高学员对分层开发思想和技术的理解和掌握。

13.1  数据持久化

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

在上一章的案例中以及实际项目中,很多程序都有保存数据、读取数据的需要。程序运行时,保存在内存中的数据是瞬时的,关机之后将丢失。为了持久保存数据,需要将数据保存到磁盘中,比如保存到数据库或文件中。这样,程序启动时可以从磁盘读取数据,数据发生改变或程序关闭时,保存数据到磁盘,实现了数据的持久保存。

如何才能更好地实现宠物信息的持久化,有没有前人总结出的固定模式或解决方案供遵循?

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

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

这里涉及到一个术语:持久化。持久化是将程序中的数据在瞬时状态和持久状态间转换的机制。JDBC就是一种持久化机制,将程序直接保存成文本文件也是持久化机制的一种实现,但我们常用的是将数据保存到数据库中。

持久化时有许多问题需要考虑和明白,比如不同程序持久化数据的格式和位置是不同的,可能是保存到数据库、普通文件,XML文件(下一章会讲解XML)中等:比如主要持久化操作包括保存、删除、修改、读取和查找等。

进行数据持久化时采用别人总结出的解决方案既可以保证代码的质量,又可以省去自己摸索的时间,何乐而不为呢?下面就结合宠物信息的持久化介绍一种常用的解决方案。

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

首先定义数据访问接口,隔离不同的存储操作,如示例1所示。采用面向接口编程,可以降低代码间的耦合性,提高代码的可扩展性和可维护性。

 

示例1

public  interface  PetDataAccessObject {

   void  save(Pet  pet);

   void  del(Pet  pet);

   void  update(Pet  pet);

   Pet  getByName(String  name);

   List<Pet>  findByName(String  name);

   List<Pet>  findByType(String  type);

}

 

 

——注意————————————————————————————————-———

尽量以对象为单位,而不是属性为单位来传递参数,给调用者提供面向对象的接口。例如以上类中的save(Pet  pet)、del(Pet  pet)、void  update(Pet  pet)方法,直接以对象pet为形参。可以想象,如果以Pet类的各个属性为形参进行传递,不仅会导致参数个数很多,还会增加接口和实现类中方法的数量等。

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

应该由哪个类来实现PetDataAccessObject接口呢?让实体类Pet、Master类实现不合适,因为这违反了“单一职能原则”,不利于程序的“低耦合,高内聚”。通常是重新创建类,比如PetDataAccessObjectJdbcOracleImpl、PetDataAccessObjectJdbcSQLServerImpl,分别给出该接口的不同实现。

定义的接口名和类名太长了,可以分别简化为PetDao、PetDDaoJdbcOracleImpl、PetDaoJdbcSQLServerImpl,这样既缩短了名字长度,也不影响可读性。

PetDao接口及两个实现类的关系可以用图13.1所示的类图表示。

 

 
 

图13.1  PetDao接口及两个实现类的关系

PetDao接口、PetDaoJdbcOracleImpl实现类的代码如示例2和示例3所示,其中示例3仅给出了部分代码。

示例2

import  java.util.List;

import  cn.le.epet.entity.Pet;

/**

  * 宠物Dao接口。

  * @author 南京

  * /

public  interface  PetDao  {

      /**

        * 保存宠物。

        * @param  pet 宠物

        * /

        void  save(Pet  pet);

      /**

        * 删除宠物。

        * @param  pet 宠物

        * /

       void  del(Pet  pet);

      /**

        * 更新宠物。

        * @param  pet 宠物

        * /

        void  update(Pet  pet);

      /**

        * 获取指定昵称的宠物,精确查询。

        * @param  name  昵称

        * @return  宠物

        * /

      Pet  getByName(String  name);

      /**

        * 获取指定昵称的宠物,精确查询。

        * @param  name  昵称

        * @return  宠物列表

        * /

      List<Pet>  findByName(String  name);

      /**

        * 获取指定类型的宠物列表。

        * @param  type  宠物类型

        * @return  宠物列表

        * /

      List<Pet>  findByType(String  name);

}

示例3

import  java.sql.Connection;

import  java.sql.DriverManager;

import  java.sql.PreparedStatement;

import  java.sql.SQLException;

import  java.util.List;

import  cn.le.epet.dao.PetDao;

import  cn.le.epet.entity.Pet;

/**

  * PetDao针对Oracle数据库的实现类。

  *@author 南京

  * /

public  class  PetDaoJdbcOracleImpl  implements  PetDao {

      public  void  del(Pet  pet)  {

        //1、数据库连接信息

        String  driver=“oracle.jdbc.driver.OracleDriver”;

        String  url=“jdbc:oracle:thin:@10.0.0.41:1521:sledb”;

        String  user=“epet”;

        String  password=“le”;

        //2、根据查询结果判断登录是否成功

        Connection  conn=null;

        PreparedStatament  pstmt=null;

        try  {

             Class.forName(driver);

             conn=DriverManager.getConnection(url,user,password);

             pstmt=conn.prepareStatement(“update pet set status=0 where id=?”);

             pstmt.setInt(1,pet.getId());

             pstmt.executeUpdate();

        }  catch  (ClassNotFoundException  e)  {

             e.printStackTrace();

        }  catch  (SQLException  e)  {

             e.printStackTrace();

        }  finally  {

             try  {

                  if  (null  !=pstmt)  {

                       pstmt.close();

                  }

             }  catch  (SQLException  e)  {

                  e.printStackTrace();

             }

             try  {

                   if  (null  !=conn)  {

                       conn.close();

                    }

                 }  catch  (SQLException  e)  {

                     e.printStackTrace();

                 }

            }

      }

      //省略实现PetDao的其他方法

}

示例2和示例3中都用到了实体Pet类,该类的属性与数据库表pet的字段对应,并提供相应的getter/setter方法,用来存放与传输宠物对象的信息。Pet类的代码如示例4所示。

示例4

import  java.util.Date;

/**

  * 宠物实体类。

   * @author 南京

  * /

public  class  Pet  {

     private  int  id;//宠物id

     private  int  masterId;//主人id

     private  String  name;//昵称

     private  int  typeId;//类型id

     private  int  health;//健康值

     private  int  love;//亲密度

     private  Date  adoptTime;//领养时间

     private  String  status;//状态

     public  int  getId()  {

        return  id;

     }

     public  void  setId(int  id)  {

        this.id;=id;

     }

     public  int  getMasterId()  {

        return  masterId;

     }

     public  void  setMasterId(int  masterId)  {

        this.masterId=masterId;

     }

     //省略其他属性的getter/setter方法

}

示例3中PetDaoJdbcOracleImpl类的各个方法中都会涉及数据库连接的建立和关闭操作,一方面导致代码重复,另外也不利于以后的修改。可以把数据库连接的建立和关闭操作提取出来,放到一个专门类BaseDao中。这样一来,在PetDaoJdbcOracleImpl中需要建立和关闭数据连接时只调用相应的方法即可。具体代码如示例5所示。

 

示例5

import  java.sql.Connection;

import  java.sql.DriverManager;

import  java.sql.ResultSet;

import  java.sql.Statement;

/**

  * 数据库连接与关闭工具类。

  * @author 南京

  * /

public  class  BaseDao  {

     private  static  String  driver=

        “oracle.jdbc.driver.OracleDriver”;//数据库驱动字符串

     private  static  String  url=

“jdbc:oracle:thin:@10.0.0.41:1521:sledb”;//连接URL字符串

     private  static  String  user=“epet”//数据库用户名

     private  static  String  password=“le”//用户密码

     /**

       * 获取数据库连接对象。

       * /

     public  static  Connection  getConnection()  {

         Connection  conn=null;//数据连接对象

         //获取连接并捕获异常

         try  {

               Class.forName(driver);

               conn=DriverManager.getConnection(url,user,password);

         }  catch  (Exception  e)  {

               e.printStackTrace();//异常处理

         }

         return  conn;//返回连接对象

}

/**

      * 关闭数据库连接。

      * @param  conn  数据库连接

      * @param  stmt  Statement  对象

      * @param  rs  结果集

      * /

public  static  void  closeAll(Connection  conn,Statement  stmt,

                ResultSet  rs)  {

      //若结果集对象不为空,则关闭

      if  (rs  !=null)  {

           try  {

                rs.close();

                }  catch  (Exception  e)  {

                    e.printStackTrace();

         }

}

    //若Statement对象不为空,则关闭

if  (stmt  !=null)  {

           try  {

                stmt.close();

                }  catch  (Exception  e)  {

                    e.printStackTrace();

            }

}

    //若数据库连接对象不为空,则关闭

     if  (conn  !=null)  {

           try  {

                conn.close();

                }  catch  (Exception  e)  {

                    e.printStackTrace();

         }

}

}

}

通过以上示例,我们应该对数据持久化有了一定的理解,其实我们已经在使用一种非常流行的数据访问模式——DAO模式,现在总结如下。

DAO,就是Data  Access  Object(数据存取对象),位于业务逻辑和持久化数据之间实现对持久化数据的访问。

在面向对象设计过程中,有一些“套路”用于解决特定问题,称为模式。DAO模式提供了访问关系型数据库系统所需操作的接口,将数据访问和业务逻辑分离,对上层提供面向对象的数据访问接口。

从以上DAO模式使用可以看出,DAO模式的好处就在于它实现了两次隔离。

  • 隔离了数据访问代码和业务逻辑代码。业务逻辑代码直接调用DAO方法即可,完全感觉不到数据库表的存在。分工明确,数据访问层代码变化不影响业务逻辑代码,这符合单一职能原则,降低了耦合性,提高了可复用性。
  • 隔离了不同数据库实现。采用面向接口编程,如果底层数据库变化,例如由Oracle变为SQL  Server,只要增加DAO接口的新实现类即可,原有Oracle实现不用修改。这符合“开—闭”原则,降低了代码的耦合性,提高了代码扩展性和系统的可移植性。

一个典型的DAO模式主要由以下几部分组成。

  • DAO接口:把对数据库的所有操作定义成一个个抽象方法,可以提供多种实现。
  • DAO实现类:针对不同数据库给出DAO接口定义方法的具体实现。
  • 实体类:用于存放与传输对象数据。
  • 数据库连接和关闭工具类:避免了数据库连接和关闭代码的重复使用,方便修改。

13.2  上机练习

上机练习1

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

训练要点

数据持久化。

需求说明

定义MasterDao接口,开发其实现类MasterDaoJabcOracleImpl,完成主人登录验证的JDBC操作代码,为宠物主人登录验证做好数据访问层代码准备。

数据库为Oracle  10g,数据库表使用第十一章中的master表。

实现思路及关键代码

  1. 定义实体类Master,与数据库表master对应,并提供getter/setter方法。
  2. 定义工具类BaseDao,实现数据库连接和关闭操作。
  3. 定义MasterDao接口,提供常用数据库操作的方法定义,包括登录验证的方法。

void  findByNamePwd (String  loginId,String  password);

  1.  开发MasterDaoJabcOracleImpl类,继承BaseDao类,实现MasterDao接口,给出

登录验证方法的具体实现代码即可。

注意编写注释。

参考解决方案

参考本章示例2~示例5的代码。

上机练习2

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

训练要点

数据持久化。

需求说明

在上机练习1的基础上,编写业务代码和测试类代码,完成主人登录验证功能。运行测试类,主人登录成功和失败的运行结果如图13.2和图13.3所示。

图13.2  主人登录成功页面

图13.3  主人登录失败页面

实现思路及关键代码

在上机练习1实现代码的基础上继续如下操作。

  1. 定义业务类MasterManager,编写login()方法,完成主人控制台登录过程并调用DAO代码验证主人登录是否成功。
  2. 定义测试类Test,测试登录验证功能。

参考解决方案

业务类MasterManager.java的代码如下。

import  java.util.Scanner;

import  cn.le.epet.dao.MasterDao;

import  cn.le.epet.dao.impl.MasterDaoJdbcOracleImpl;

import  cn.le.epet.entity.Master;

/**

  * 主人业务类。

  * @author 南京

  * /

public  class  MasterManager  {

      /**

        * 主人登录。

        * /

   public  void  login()  {

      //1、获得输入对象

      Scanner  input=new  Scanner(System.in);

      //2、打印欢迎信息

      System.out.println(“---- 欢迎光临宠物乐园 ----”);

      //3、获取用户输入的登录名、密码

      System.out.println(“请输入登录名:”);

      String  loginId=input.next();

      System.out.println(“请输入密码:”);

      String  password=input.next();

      //4、检查登录名、密码是否合法,并输出提示信息

      MasterDao  masterDao=new  MasterDaoJdbcOracleImpl();

      Master  master=masterDao.getByName(loginId);

      if  (null  !=password   &&  password.equals(master.getPassword()))  {

          System.out.println(“登录成功!”);

      }  else  {

          System.out.println(“用户名或密码错误,登录失败!”);

      }

}

}

测试类Test.java的代码如下。

import  cn.le.epet.manager.MasterManager;

/**

  * 测试类。

  * @author 南京

  * /

public  class  Test  {

      public  static  void  main(String[]  args)  {

           MasterManager  masterManager=new  MasterManager();

           masterManager.login();

      }

}

13.3  分层开发

随着软件规模的扩大、业务的复杂,将一个软件分成多个层次进行开发,化大为小,分而治之,是缩短软件开发时间,提高软件开发效率的一种有效方法,也是目前软件开发一直使用的方法。DAO模式就是分层开发思想的一种具体体现。下面结合DAO模式就分层开发进行专门介绍。

13.3.1  分层开发的优势

采用DAO模式后,数据访问代码被提取出来,由DAO接口和实现类来实现功能,形成了数据访问层,该层代码一般放在dao包下,其他层不用再考虑繁琐的数据访问操作,层层之间通过实体类来传输数据。

分层开发是确实可以带来好处,还是凭空增加负担呢?其实只要你仔细观察,就会发现在生活中分层处理的情况比比皆是。比如餐厅,就可以分为服务生、厨师、采购员三个层次,他们各司其职,依次配合,共同合作,保证餐厅正常运作。某一类人员的变动不对相邻层产生影响。再比如制造汽车,就可以分为汽车制造商、零件制造商、炼钢厂、采矿厂等层次,后一个层次为前一个层次提供直接服务。在现实生活中有没有一个厂商把所有这些工作都自己完成,恐怕很少,为什么呢?就是因为通过不同层次分工、不仅可以精研业务,提高服务质量,还可以减轻自身负担,集中精力发展自身优势服务。

在软件开发中,进行分层开发具有什么优势呢?结合前面的DAO模式和生活实例,我们很容易发现。

  • 每一层专注于自己功能的实现,便于提高质量。不同层次的关注点是不同的,数据访问层主要是数据库访问,业务逻辑层的重点是业务逻辑,前端表示层专注于页面的布局和美观。根据不同层次需要由最适合的技术人员来实现,从而提高开发质量。
  • 便于分工协作,从而提高效率。一旦定义好各个层次之间的接口,每个开发人员的任务得到了确认,不同层次的开发人员就可以各司其职,齐头并进,从而大大加快开发进度。
  • 便于代码复用。每个模块一旦定义好统一的对外接口,就可以被各个模块调用,而不用对相同的功能进行重复开发。比如不同的业务逻辑模块如果需要对相同数据库表进行操作,无需各自开发相应的DAO模块,复用即可。
  • 便于程序扩展。比如DAO模式中采用面向接口编程、底层数据库发生变化,可以通过增加接口的新实现来解决,实现无损替换,而不是修改原有代码,便于程序扩展。

13.3.2  分层的原则

分层开发的优势都是建立在合理分层基础上的,不合理分层可能适得其反,加大开发难度,延长开发时间。分层时应该坚持哪些原则呢?我们还是从日常生活案例谈起吧,比如电脑由硬件、操作系统、应用软件三个层次组成,这种分层有什么特点?

  • 每一层都要自己的职责。比如硬件负责存储、运算、通信等,而操作系统负责管理硬件,应用软件工作在操作系统上,实现业务功能,满足用户需要。
  • 上一层不用关心下一层的实现细节,上一层通过下一层提供的对外接口来使用其功能。应用软件不用知道操作系统是如何管理硬件的,而操作系统也无需关心硬件的具体生产流程。
  • 上一层调用下一层的功能,下一层不能调用上一层功能。下一层为上一层提供服务,而不使用上一层提供的服务。

与此类似,在分层开发中,分层也应检查类似原则。

  • 封装性原则:简单而言,就是每个层次向外提供公开的统一接口,而隐藏内部的功能实现细节,其他层次不能也没有必要了解其内部细节。
  • 顺序访问原则:下一层为上一层提供服务,而不使用上层提供的服务。业务逻辑层可以访问数据访问层的功能,而数据访问层不能访问业务逻辑层功能。不是相互访问,而是顺序访问。

每一层都自己的职责,对于数据访问层而言,它的职责就是执行数据访问操作。为了提高封装性,应注意提供给业务逻辑层的接口不应该包含数据访问细节,例如不能直接返回ResultSet对象,而要在DAO内部进行关系表到对象的转换,以集合对象返回。对于连接字符串、SQL语句,文件路径等直接在数据访问层提供,而不要通过业务逻辑层传入。

13.3.3  使用实体类传递数据

在分层结构中,不同层之间通过实体类来传输数据,例如本章前面的Pet类和Master类。把相关信息使用实体类封装后,在程序中把实体类作为方法的输入参数或返回结果,实现数据传递,非常方便。关于实体类,主要有以下特征。

  • 实体类的属性一般使用private修饰。
  • 根据业务需要和封装性要求对实体类的属性提供getter/setter方法,负责属性的读取和赋值,一般使用public修饰。
  • 对实体类提供无参构造方法,根据业务需要提供相应的有参构造方法。
  • 实体类最好实现Java.io.Serializable,支持序列化机制,可以将该对象转换成字节序列而保存在磁盘上或在网络上传输。
  • 如果实体类实现了java. io.Serializable,,就应该定义属性serialVersionUID,解决不同版本之间的序列化问题。例如

private  static  final  long  serialVersionUID=2070056025956126480L;

示例6提供了一个实体类的标准定义。

示例6

/**

  * 用户实体类。

  * @author 南京

  * /

public  class  User  implements  java.io.Serializable{

      private  static  final  long  serialVersionUID=2070056025956126480L

      private  int  id;//用户id

      private  String  name;//姓名

private  int  age;//年龄

      private  String  sex;//性别

      private  String  address;//家庭住址

      /**

        * 无参构造方法。

        * /

      public  User()  {

      }

  /**

        * 有参构造方法。

        * @param  name  姓名

        * @param  age  年龄

        * @param  sex  性别

        * @param  address  家庭住址

        * /

      public  User(String  name,int  age,String  sex,String  address)  {

             this.name=name;

             this.age=age;

             this.sex=sex;

             this.address=address;

      }

      public  int  getId()  {

             return  id;

      }

      public  void  setId(int  id)  {

             this.id=id;

      }

      public  String  getName()  {

             return  name;

      }

      public  void  setName(String  name)  {

             this.name=name;

      }   

      public  int  getAge()  {                        

             return  age;

      }

      public  void  getAge(int  age)  {

             this.age=age;

      }

      public  String  getSex ()  {

             return  sex;

      }

      public  void  getSex(String  sex)  {

             this.sex=sex;

      }

      public  String  getAddress ()  {

             return  address;

      }

      public  void  setAddress (String  address)  {

           this.address=address;

      }

}

13.4  上机练习

上机练习3

指导——记录车辆购置税

训练要点

  • 分层开发。
  • DAO模式。

需求说明

开发一个程序,用于记录车辆购置税。要求如下。

  • 由控制台录入数据,提交后保存到Oracle数据库,程序运行界面如图13.4所示。
  • 需要保存的信息包括:车主身份证号码(18位)、购车日期、车辆识别代码(17位)、车型、官方指导价、发票价格、缴纳车辆购置税金额。
  • 目前车辆购置税征收办法如下。

车辆购置税征收额根据计税价格计算,计税价格计算公式如下。

计税价格=购车发票价格/(1+17%)。

排量在1.6升及以下车型的计算公式如下。

车辆购置税=计税价格*7.5%。

排量在1.6升以上的车型计算公式如下。

车辆购置税=计税价格*10%。

 

图13.4  记录车辆购置税界面

实现思路及关键代码

  1. 创建数据库表vehicle_purchase_tax,根据需求定义字段名、字段类型、数据库采用Oracle  10g。
  2. 开发实体类VehiclePurchaseTax,属性与数据库表vehicle_purchase_tax的字段对应,并提供getter/setter方法和构造方法。
  3. 开发数据访问层。

定义VehiclePurchaseTaxDao接口,提供常用数据库操作的方法定义,包括保存车辆购置税的方法。

void  saveVehiclePurchaseTax  item);

开发VehiclePurchaseTaxDaoOracleImpl类,实现VehiclePurchaseTaxDao接口,只需给出保存车辆购置税方法的具体实现代码即可,其他未实现方法可以先使用如下语句抛出异常信息。

throws  new  Exception(“未实现”);

  1. 开发数据库连接关闭工具类(重用前面上机练习的相关代码即可)。
  2. 完成程序、实现车辆购置税记录功能。

注意编写注释。

参考解决方案

参考本章示例2~示例5的代码,参考上机练习1和上级练习2的代码。

本章总结

  • 持久化是将程序中数据在瞬时状态和持久状态间转换的机制。JDBC是一种持久化机制,将程序直接保存成文本文件也是持久化机制的一种实现。
  • DAO就是Data  Access  Object(数据存取对象),位于业务逻辑和持久化数据之间,实现对持久化数据的访问。
  • DAO模式提供了访问关系型数据库系统所需的操作接口,将数据访问和业务逻辑分离,对上层提供面向对象的数据访问接口。
  • 一个典型的DAO模式主要由DAO接口、DAO实现类、实体类、数据库连接和关闭工具类组成。
  • 使用分层开发便于提高开发质量、提高开发效率、便于代码复用、便于程序扩展、便于降低代码的耦合性。
  • 分层开发的种种优势都是建立在合理分层基础上的,分层时应坚持封装性原则和顺序访问原则。
  • 在分层结构中,不同层之间通过实体类传输数据。在程序中把实体类作为方法的输入参数或返回结果,实现数据的传递,非常方便。

本章作业

一、选择题

1. 以下选项中关于持久化的说法错误的是(  )。

A. 持久化是相对瞬时化、短暂化而言的

B. 把数据保存到文本文件中不属于持久化

C. 把数据保存到数据库中属于持久化

D. 主要持久化操作包括保存、删除、修改、读取和查找等

 

2. 以下选项中关于DAO模式的说法错误的是(  )。

A. DAO是数据存取对象的含义,实现对数据库资源的访问

B. DAO模式中要定义DAO接口和实现类,隔离了不同数据库的实现

C. DAO负责执行业务逻辑操作,将业务逻辑和数据访问隔离开来

D. DAO负责完成数据持久化操作

 

3. 一个典型的DAO模式的组成不包括(  )。

A. DAO接口

B. DAO实现类

C. 实体类

D. 业务逻辑类

 

4. 以下选项中关于分层开发的优势说法错误的是(  )。

A. 便于提高代码的耦合性

B. 便于提高开发质量

C. 便于代码复用和程序扩展

D. 便于提高开发效率

 

5.以下选项种关于实体类的说法错误的是(  )。

A. 在分层结构中,不同层之间通过实体类来传输数据

B. 实体类的属性一般使用public修饰

C. 实体类的方法一般使用public修饰

D. 实体类最好实现java.io.Serializable,支持序列化机制

二、简答题

1. 简述你对DAO模式的理解。

2. 简述软件开发中使用分层开发技术的优势。

3. 简述软件开发中使用分层开发技术时需坚持的原则。

4. 简述实体类的作用和主要特征。

5. 在完成第11章作业题简答题3基础上采用DAO模式进行开发,显示所有图书信息。并将结果与第12章作业题简答题1的结果进行比较,看看分层前后的不同之处。

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

定义实体类Book。

定义BaseDao类,专门负责数据库连接的建立和关闭。

定义BookDao接口,规定数据访问接口。

定义BookDaoOracleImpl,实现BookDao接口。

定义业务类BookManager,调用BookDao接口的方法进行数据库操作。

定义测试类Test,调用业务类相应方法,输出所有图书信息。

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

 

 

 

14章

XML和File I/O

 

 

 

◇本章工作任务

 

Ø    使用CSS修饰XML文档

Ø    使用DOM解析XML文档

Ø    按照规范格式保存宠物数据到文件

 

 

◇本章技能目标

 

Ø    使用XML概念及作用

Ø    使用CSS修饰XML文档

Ø    使用DOM解析XML文档

Ø    使用Reader读取文件内容

Ø    使用Writer输出内容到文件

 

本章单词

 

 

 

 

 

 

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

  1. XML:_____________________________
  2. XHTML:__________________________
  3. DTD:_____________________________
  4. XSD:_____________________________
  5. XSL:_____________________________
  6. CSS:_____________________________
  7. DOM:____________________________
  8. SAX:_____________________________

 

本章简介

 

本章讲解XML和FILE  I/O内容。XML是目前流行的数据存储和交换技术,主要讲解XML的定义和作用,结构定义标准,使用CSS修饰XML,使用DOM解析XML等内容。在FILE  l/O部分,将通过一个按照规范格式保存宠物数据到文本文件的例子讲解字符输入流类Reader和字符输出流类Writer的使用,实现对文件的输入输出操作。

 

14.1  XML简介

14.1.1  XML定义

XML(Extensible  Markup  Language)即可扩展标记语言,它是一种简单的数据存储语言,使用一系列简单的标签描述数据,而这些标签可以用方便的方式建立。

XML和XHTML都是标记语言,但它们之间又有很大的区别,就让我们从两者区别开始XML的学习吧。

 

示例1

example.xhtml

<?xml  version =”1.0”encoding=”UTF-8”?>

<!DOCTYPE  html  PUBLIC “-//W3C//DTD  XHTML  1.0  Transitional//EN”

http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>

<html  xmlns =”http://www.w3.org/1999/xhtml”  xml:lang =”en” lang =”en”>

        <head>

                 <title>XHTML网页示例</title>

        </head>

        <body>

                 这是一个XHTML网页!<br/>

                 <hr  width =”500” align =”left”/>

                 XHTML标签都是标准标签,不允许自定义标签。

         </body>

</html>

 

示例2

pet1.xml

<?xml  version =”1.0” encoding =”UTF-8”?>

<宠物们>

<狗狗  id =”1”>

        <姓名>YAYA</姓名>

        <健康值>100</健康值>

        <亲密度>0</亲密度>

        <品种>酷酷的雪娜瑞</品种>

</狗狗>

<狗狗  id =”2”>

        <姓名>OUOU</姓名>

        <健康值>90</健康值>

        <亲密度>15</亲密度>

        <品种>聪明的拉布拉多犬</品种>

</狗狗>

</宠物们>

通过示例1和示例2的比较,我们可以看到两种语言都是标记语言,都采用了大量有层次关系的标签。但是两者之间也有很多的区别,主要表现在以下两方面。

Ø   XHTML标签都有固定含义,不能去创造新的标签。而XML支持自定义标签,具有扩展性。

Ø   XHTML主要用来显示数据,可以通过标签和属性对页面显示进行排版,而XML用来存储和交换数据,无法描述页面的排版和显示形式。

XML文档总是以XML声明开始,它定义了XML的版本和所使用的编码等信息。该声明以”<?”开始,紧跟”xml”字符串,以”?>”结束。如示例2所示。

<?xml  version =”1.0” encoding =”UTF-8”?>

XML文档的主要部分是元素,元素由开始标签、元素内容和结束标签组成。元素内容可以包含子元素、字符数据等。

XML文档中的注释符号是<!--  -->。

 

14.1.2  XML结构定义

虽然可以发挥自己的想象力去建筑房屋,但实际中都要有相应建筑图纸,以此为依据来建筑,来验收。不管招标的是哪家建筑公司,依据同一份图纸建筑,都可以保证建筑的一致性。

同理,虽然在XML文件中可以任意自定义标签,但为了更好地编写XML文档,为了更好地验证XML文档,为了能通过XML有效地交换数据,为了让别人也看懂甚至修改XML文档,一般也要使用“图纸”来定义XML文档的结构。

目前定义XML文档结构的“图纸”有两种形式:DTD和XSD。

DTD(Document  Type  Definition文档类型定义)是一种保证XML文档格式正确的有效方法,虽然DTD不是必须的,但它为文档编制带来了极大方便。可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。

XML文件提供给应用程序一个数据交换的格式,DTD则让XML文件能够成为数据交换的标准,因为不同公司只需定义好标准的DTD,各公司都能够依照DTD建立XML文件,并且进行验证,如此就可以轻易地建立标准和交换数据,这样满足了网络共享和数据交互。

DTD可以定义在XML文档中,仅作用于当前文档,如示例3所示。也可作为一个外部文件存在,作用于所有导入它的XML文档。

 

示例3

pet2.xml

<?xml  version =”1.0”encoding =”UTF-8”?>

<!—使用内部DTD文件 -->

<!DOCTYPE  pets [

<!ELEMENT  pets (dogs, penguins) >

<!ELEMENT  dogs (dogs*) >

<!ELEMENT  penguins (penguin+) >

<!ELEMENT  dog (name, health, love, strain?) >

<!ATTLIST  dog  id  CDATA  #REQUIRED >

<!ELEMENT  penguin (name, health, love, sex) >

<!ATTLIST  penguin  id  CDATA  #REQUIRED >

<!ELEMENT  name  (#PCDATA) >

<!ELEMENT  health  (#PCDATA) >

<!ELEMENT  love  (#PCDATA) >

<!ELEMENT  strain  (#PCDATA) >

<!ELEMENT  sex  (#PCDATA) >

] >

<pets>

       <dogs>

              <dog  id =”1”>

                     <name>YAYA</name>

                     <health>100</health>

                     <love>0</love>

                     <strain>酷酷的雪娜瑞</strain>

             </dog>

             <dog  id =”2”>

                      <name>OUOU</name>

                      <health>90</health>

                      <love>15</love>

                      <strain>聪明的拉布拉多犬</strain>

                </dog>

         </dogs>

         <penguins>

                 <penguin  id =”3”>

                     <name>QQ</name>

                     <health>100</health>

                     <love>20</love>

                     <sex>Q仔</sex>

                </penguin>

         </penguins>

</pets>

在浏览器中查看该文件,效果如图14.1所示。

图14.1  使用浏览器查看XML文档源码

 

示例3中黑体部分即为该文档的内部DTD定义,其基本定义格式是

<!DOCTYPE  根元素  [元素声明] >

具体解释如下。

<!DOCTYPE  pets  [    //根元素是pets

<!ELEMENT pets (dogs, penguins)>//pets直接下级元素是dogs、penguins,顺序固

                                                          //定,只能出现一次

<!ELEMENT dogs(dog*)>//dogs直接下级元素是dog,*表示dog元素可以出现0到多次

<!ELEMENT penguins(penguin+)> //penguins直接下级元素是penguin,+表示penguin

                                                          //至少出现一次

<!ELEMENT  dog  (name, health, love, strain?)>
//dog的直接下级元素依次是name、health、love、strain,表示strain元素出现0次到1次

<!ATTLIST dog id CDATA  #REQUIRED > //dog元素有id属性,是CDATA类型,必须出现

<!ELEMENT  penguin (name, health, love, sex)>

<!ATTLIST  penguin  id  CDATA  #REQUIRED>

<!ELEMENT  name  (#PCDATA)>  //name元素为”#PCDATA”类型

<!ELEMENT  health (#PCDATA)>

<!ELEMENT  love  (#PCDATA)>

<!ELEMENT  strain  (#PCDATA) >

<!ELEMENT  sex  (#PCDATA) >

] >

如果多个XML文件使用同一个DTD文件,可以把DTD定义放到独立文件中,扩展名是.dtd,然后在XML文件中引入该文件,既避免了代码重复,也利于DTD的修改。如示例4和示例5所示。

 

示例4

pet3.xml

<?xml  version =”1.0”encoding =”UTF-8”?>

<!—使用外部DTD文件 -->

<!DOCTYPE  pets SYSTEM  “pet.dtd”>

<pets>

       <dogs>

              <dog  id =”1”>

                     <name>YAYA</name>

                     <health>100</health>

                     <love>0</love>

                     <strain>酷酷的雪娜瑞</strain>

             </dog>

             <dog  id =”2”>

                      <name>OUOU</name>

                      <health>90</health>

                      <love>15</love>

                      <strain>聪明的拉布拉多犬</strain>

                </dog>

         </dogs>

         <penguins>

                 <penguin  id =”3”>

                     <name>QQ</name>

                     <health>100</health>

                     <love>20</love>

                     <sex>Q仔</sex>

                </penguin>

         </penguins>

</pets>

在示例4总并没有DTD定义,而是引入了外部DTD文件pet.dtd,该文件内容如示例5所示。

 

 

 

示例5

pet.dtd

<!ELEMENT  pets (dogs, penguins) >

<!ELEMENT  dogs (dogs*) >

<!ELEMENT  penguins (penguin+) >

<!ELEMENT  dog (name, health, love, strain?) >

<!ATTLIST  dog  id  CDATA  #REQUIRED>

<!ELEMENT  penguin (name, health, love, sex) >

<!ATTLIST  penguin  id  CDATA  #REQUIRED>

<!ELEMENT  name  (#PCDATA) >

<!ELEMENT  health  (#PCDATA) >

<!ELEMENT  love  (#PCDATA) >

<!ELEMENT  strain  (#PCDATA) >

<!ELEMENT  sex  (#PCDATA) >

XSD(XML  SCHEMA)是继DTD之后,用来规范和描述XML文档结构的第二代标准,DTD相比,XSD本身就是XML文档结构,支持更多的数据类型,具有很强的扩展能力,提供对XML文档信息的更多描述。

——资料———————————————————————————————————

XHTML文档实际上就是指定了外部DTD文件的XML文档。因为外部DTD文件的存在,所有的XHTML文档都遵循统一标准,有着相同的结构。

所有的XHTML文档都以XML声明开始,并引入外部DTD文件。例如:

<?xml  version =”1.0” encoding =”UTF-8”?>

<!DOCTYPE  html  PUBLIC  “-//W3C//DTD  XHTML  1.0  Transitional//EN”

http://www.w3.org/TR /xhtml1/DTD/xhtml1-transitional.dtd”>

XHTML文档的外部DTD文件中关于table元素的部分定义如下

<!ELEMENT  table

        (caption?,(col*∣colgroup*),thead?,tfoot?,(tbody+∣tr+))>

<!ELEMENT  cation  %Inline;>

<!ELEMENT  thead      (tr)+>

<!ELEMENT  tfoot      (tr)+>

<!ELEMENT  tbody     (tr)+>

<!ELEMENT  colgroup  (col)*>

<!ELEMENT  col            EMPTY>

<!ELEMENT  tr             (th∣td)+>

<!ELEMENT  th            %Flow;>

<!ELEMENT  td            %Flow;>

<!ATTLIST  table

       %attrs;

       summary               %Text;               #IMPLIED

       width                 %Length;             #IMPLIED

       border                %Pixels;             #IMPLIED

       frame                 %TFrame;             #IMPLIED

       rules                 %TRules;             #IMPLIED

       cellspacing           %Length;             #IMPLIED

       cellpadding           %Length;             #IMPLIED

        align                %TAlign;             #IMPLIED

        bgcolor              %Color;             #IMPLIED

>

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

14.1.3  XML的作用

XML一经推出,就得到了IT业界巨头的响应,很快在各行各业展露了身影。XML独立于计算机平台、操作系统和编程语言来表示数据,凭借其简单性、可扩展性、交互性和灵活性在计算机行业中得到了世界范围的支持和采纳。XML主要作用如下。

Ø   数据存储:XML与Oracle和SQL  Server等数据库一样,都可以实现数据的持久化存储。两者相比,数据库提供了更强有力的数据存储和分析能力。例如:数据索引、排序、查找、相关一致性等。XML仅仅是存储数据。XML与其他数据表现形式最大的不同是它极其简单。

Ø   数据交换:在实际运用中,由于各个计算机所使用的操作系统、数据库不同,因此数据之间的交换向来是头痛的事。现在可以使用XML来交换数据。例如可以将数据库A中的数据转换成标准的XML文件,然后数据库B再将标准的XML文件转换成适合自己数据要求的数据,以达到交换数据的目的。

Ø   数据配置:许多应用都将配置数据存储在XML文件中,比如在Servlet中使用的web.xml,在Struts 2.0中使用struts.xml,在Hibernate中使用的hibernate.cfg.xml,在Spring中使用的applicationContext.xml等。使用XML配制文件可读性强,灵活性高,不用像其他应用那样要经过重新编译才能修改和维护应用系统。

 

14.1.4  XML和CSS共同使用

在图14.1中,显示了XML文件的源代码,不要指望XML文件会直接显示为HTML页面,因为XML文档只是用来存储数据,并没有携带任何如何显示数据的信息。由于XML标签由XML文档的作者“发明”的,浏览器无法确定像<table>这样一个标签究竟描述一个HTML表格还是一个餐桌。

如果希望XML文档出现HTML页面的显示效果,可以采用CSS为它定义如何显示的信息,实现XML文档内容的格式化输出。代码如示例6所示。

 

示例6

pet.css

pets {

       display: block;

       color: red;

}

dog, penguin {

       display: block;

       margin-left: 40pt;

}

把文件pet.css放到示例3和示例4相同目录下,然后再示例3和示例4的第二行添加引入CSS文件语句:

<?xml-stylesheet  type =”text/css”href =”pet.css”?>

显示效果如图14.2所示。

图14.2  使用CSS修饰的XML文档显示效果

 

使用CSS格式化XML不能代表XML文档格式化的未来。XML文档应当使用XSL标准进行格式化,XSL是首选的XML样式表语言,远比CSS更加完善。

——资料———————————————————————————————————

XML的其他概念。

Ø   XPath:一门在XML文档中查找信息的语言,用于在XML文档中通过元素和属性进行导航。

Ø   XSLT:XSL转换(XSL  Transformations),是一种用于将XML文档转换为XHTML文档或其他XML文档的语言。XSLT使用XPath在XML文档中进行导航。

Ø   XSL:可扩展样式表语言(Extensible  Stylesheet  Language),XSL之于XML,就像CSS之于HTML,是一种用于格式化XML数据输出的语言。XSL主要包含两个部分:XSLT和XPath,其中XSLT是XSL最重要的部分。

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

14.2  解析XML

在实际应用中,经常需要对XML文档进行各种操作,例如在应用程序启动时读取XML配置文件中的信息,或者把数据库中的内容读取出来转换为XML文档形式,这些时候都会用到XML文档的解析技术。

目前最常用的XML解析技术是DOM和SAX。Sun公司提供了JAXP(Java  API  for  XML  Procession)来使用DOM和SAX,JAXP包含三个包,这三个包都在JDK中。

Ø   org.w3c.dom:W3C推荐的用于使用DOM解析XML文档的接口。

Ø   org.xml.sax:用于使用SAX解析XML文档的接口。

Ø   javax.xml.parsers:解析器工厂工具,程序员获得并配置特殊的特殊语法分析器。

 

14.2.1  使用DOM解析XML

DOM是Document  Object  Model的缩写,即文档对象模型。DOM把XML文档映射成一个倒挂的树,以根元素为根节点,每个节点都以对象形式存在。通过存取这些对象就能够存取XML文档的内容。

下面使用DOM对示例3所示的XML文件进行解析,文件名是pet2.xml,要求查询并输出其中所有狗狗的信息。代码如示例7所示。

 

示例7

import  java.io.FileNotFoundException;

import  java.io.IOException;

import  javax.xml.parsers.Document  Builder;

import  javax.xml.parsers.Document  BuilderFactory;

import  javax.xml.parsers.ParserConfigurationException;

import  org.w3c.dom.Document;

import  org.w3c.dom.Element;

import  org.w3c.dom.Node;

import  org.w3c.dom.NodeList;

import  org.xml.sax.SAXException;

/**

 *使用DOM解析xml文档。

 *@author 南京

 */

public  class  TestDOM {

    public  static  void  main(String[] arg) {

        //1、得到DOM解析器的工厂示例

        DocumentBuilderFactory  dbf = DocumentBuilderFactory. newInstance();

        try {

             //2、从DOM工厂获得DOM解析器

             DocumentBuilder  db = dbf. newDocumentBuilder();

             //3、解析XML文档,得到一个Document,即DOM树

              Document  doc =db. parse(“pet2.xml”);

              //4、得到所有<DOG>节点列表信息

              NodeList  dogList = doc. getElementsByTagName(“dog”);

              System.out.println(“xml文档中共有”+

                        dogList.getLength()+”条狗狗信息”):

              //5、轮循狗狗信息

              for (int  i = 0; i<dogList. getLength(); i++) {

                   //5.1、获取第i个狗狗元素信息

                   Node  dog = dogList.item(i);

                   //5.2、获取第i个狗狗元素的id属性的值并输出

                   Element  element = (Element) dog;

                   String  attrValue = element. getAttribute(“id”);

                   System.out.println(“id:”+attrValue);

                   //5.3、获取第i个狗狗元素的所有子元素的名称和值并输出

                   for (Node  node = dog. getFirstChild(); node ! = null;

                                    node = node. getNextSibling()) {

                       if(node.getNodeType()s==Node.ELEMENT_NODE) {

                            String  name = node. getNodeName();

                            String  value = node.getFirstChild().getNodeValue();

                                             System.out.print(name+”:”+value+”\t”);

                       }

                   }

                   System.out.println();

              }

          } catch (ParserConfigurationException  e) {

              e.printStackTrace();

          } catch (FileNotFoundException  e) {

              e.printStackTrace();

         } catch (SAXException  e) {

              e.printStackTrace();

         } catch (IOException  e) {

              e.printStackTrace();

        }

    }

}

运行效果如图14.3所示。

图14.3  使用DOM解析XML文档

如示例7所示,使用DOM解析XML文档的步骤如下。

  1. 创建解析器工厂对象。
  2. 由解析器工厂对象创建解析器对象。
  3. 由解析器对象对指定的XML文件进行解析,构建相应DOM树,创建Document对象。
  4. 以Document对象为起点对DOM树的节点进行增删改查操作,这一步是重点,根据具体操作不同而不同。

示例7中涉及了DOM中的主要对象,如Document、NodeList、Node、Element等,它们的作用及主要方法如下。

Document对象代表了整个XML文档,所有其他的Node都以一定的顺序包含在Document对象之内,排列成一个树状结构,可以通过遍历这棵树来得到XML文档的所有内容。它也是对XML文档进行操作的起点,我们总是先通过解析XML源文件而得到一个Doucment对象,然后再来执行后续的操作。Document对象的主要方法如下。

Ø   getElementByTagName(String):返回一个NodeList对象,它包含了所有给定标签名字的标签。

Ø   getDocumentElement():返回一个代表这个DOM树的根节点的Element对象,也就是代表XML文档根元素的那个对象。

NodeList对象,顾名思义,就是指一个包含了一个或者多个节点(Node)的列表。可以简单地把它看成一个Node数组,可以通过方法来获得列表中的元素。

Ø   getLength():返回列表的长度。

Ø   item(int):返回指定位置的Node对象。

Node对象是DOM结构中最基本的对象,代表了文档树中的一个抽象节点。在实际使用时,很少会真正用到Node这个对象,而是用到诸如Element、Attr、Text等Node对象的子对象来操作文档。Node对象的主要方法如下。

Ø   getChildNodes():包含此节点的所有子节点的NodeList。

Ø   getFirstChild():如果节点存在子节点,则返回第一个子节点。

Ø   getLastChild():如果节点存在子节点,则返回最后一个子节点。

Ø   getNextSibling(0:返回在DOM树中这个节点的下一个兄弟节点。

Ø   getPreviousSibling():返回在DOM树中这个节点的上一个兄弟节点。

Ø   getNodeName():根据节点的类型返回节点的名称。

Ø   getNodeValue():返回节点的值。

Ø   getNodeType():返回节点的类型。

Element对象代表XML文档中的标签元素,继承自Node,也是Node最主要的子对象。在标签中可以包含属性,因而Element对象中也有存取其属性的方法。

Ø   getAttribute(String):返回标签中给定属性名称的属性的值。

Ø   getElementsByTagName(String):返回具有给定标记名称的所有后代Elements的NodeList。

——资料———————————————————————————————————

XML文档中的空白符也会被作为对象映射在DOM树中。因而,直接调用Node方法的getChildNodes方法有时会有些问题,有时不能够返回所期望的NodeList元素对象列表。

解决的办法如下。

Ø   使用Element的getElementByTagName(String),返回的NodeList就是所期待的对象了。然后,可以用item()方法提取想要的元素。

Ø   调用Node的getChildNodes方法得到NodeList对象,每次通过item()方法提取Node对象后判断node.getNodeType()==Node.ELEMENT_NODE,即判断是否是元素节点,如果未true,表示是想要的元素。

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

14.2.2  使用SAX解析XML

SAX(Simple  API  for  XML)是另一种常用的XML解析技术。SAX解析器不像DOM那样建立一个完整文档树,而是在读取文档时激活一系列事件,这些事件被推给事件处理器,然后由事件处理器提供对文档内容的访问。

与DOM相比,SAX解析器能提供更好的性能优势。SAX模型最大的优点是内存消耗小,因为整个文档无需一次加载到内存中,这使SAX解析器可以解析大于系统内存的文档。另外,无需像在DOM中那样为所有节点创建对象。

SAX的缺点是必须实现多个事件处理程序以便能够处理所有到来的事件,同时还必须在应用程序代码中维护这个事件状态,因为SAX解析器不能交流元信息,如DOM的父/子支持,所以你必须跟踪解析器处在文档层次的哪个位置。如此一来,文档越复杂,应用逻辑就越复杂。

 

上机练习1

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

训练要点

Ø   XML定义。

Ø   使用DTD定义XML文档结构。

Ø   根据DTD正确编写XML文档。

需求说明

在电子宠物系统中,宠物的健康值,亲密度都有初始值,各种宠物运动后健康值减少值,和主人亲密度增加值也是固定值,要求把这些属性和值都写到XML文档中,以后更改时无需重新编译源代码。相关的DTD文件内容如下所示,要求根据DTD文件定义编写出相应的XML文档。

<!ELEMENT  pet (dog, penguin) >   

<!ELEMENT  dog (health,love,decHealth,incLove) >

<!ELEMENT  penguin (health,love,decHealth,incLove) >

<!ELEMENT  health  (#PCDATA) >

<!ELEMENT  love  (#PCDATA) >

<!ELEMENT  decHealth  (#PCDATA) >

<!ELEMENT  incLove  (#PCDATA) >

实现思路及关键代码

  1. 创建Java项目TestXML。
  2. 根据需求提供的内容创建并编写外部DTD文件pet.dtd。
  3. 根据pet.dtd文件创建并编写XML文件pet.config。

参考解决方案

<?xml  version =”1.0”encoding =”UTF-8”?>

<!DOCTYPE  pets SYSTEM  “pet.dtd”>

<pet>

       <dog>

                     <health>100</health>

                     <love>0</love>

                     <decHealth>18</decHealth>

                     <incLove>10</incLove>

         </dog>

         <penguin>

                     <health>100</health>

                     <love>0</love>

                     <decHealth>15</decHealth>

                     <incLove>8</incLove>

         </penguin>

</pet>

 

上机练习2

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

训练要点

Ø   使用DOM解析XML文档的基本步骤。

Ø   JDK中DOM的主要接口及方法的使用。

需求说明

在上机练习1中,已把宠物初始信息放到XML文档中,本练习要求用DOM解析该XML文档,输出狗狗或企鹅的相关属性及初始值。参考运行效果如图14.4和图14.5所示。

图14.4  使用DOM解析宠物信息效果图1

图14.5  使用DOM解析宠物信息效果图2

实现思路及关键代码

  1. 在Java项目TestXML下创建类TestDOM,包含main方法。
  2. 在main方法中编写代码,使用DOM解析XML文档pet.config,实现需求。

注意编写注释和异常处理。

参考解决方案

System.out.pringtln(“XML文件中狗狗的初始化信息:”);

for (int  i = 0; i<petList.getLength(); i++) {

         Node  pet = petList.item(i);

         for (Node  node = pet.getFirstChild(); node ! = null;

                 node = node.getNextSibling()) {

                 if (node.getNodeType() == Node.ELEMENT_NODE) {

                       String  name = node.getNodeName();

                       String  value = node.getFirstChild().getNodeValue();

                       System.out.println(name +”:”+value+”\t”);

                 }

         }

}

 

14.3  读写文件

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

格式模板保存在文本文件pet.template中,内容如下:

您好!

我的名字是{name},我是一只{type}。

我的主人是{master}。

其中{name}、{type}、{master}是需要替换的内容,现在要求按照模板格式保存宠物数据到文本文件,即把{name}、{type}、{master}替换为具体的宠物信息,该如何实现呢?

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

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

可以把该问题分解如下。

Ø   如何从文件中读取模板?(使用Reader接口实现)

Ø   如何替换模板中的内容为当前宠物信息?(使用String的replace()方法实现)

Ø   如何将文本保存到文件?(使用Writer接口实现)

以上三个子问题中,都涉及到了文件的输入输出操作。

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

14.3.1  使用Reader读取文件内容

在Java中,文件的输入输出功能是通过流(Stream)来实现的。什么是流呢?可以理解为一组有顺序的、有起点和终点的动态数据集合。流可以看成是数据的导管,导管的一端是源端,一端是目的端,数据从源端输入,在目的端输出,我们不用考虑两端的数据是如何传输,只需要向源端输入数据和向目的端取出数据即可。注意流与文件的区别,文件是数据的静态存储方法,流是数据在传输时的一种形态。

文件输入输出的原理如图14.6所示。要复制文件source.txt到target.txt,首先要创建一个输入流类的对象,它负责对输入流的管理,将文件source.txt的内容读取到中转站中。还要创建一个输出流类的对象,它负责对输出流的管理,把中转站中的数据写入到文件target.txt中。输入流、输出流指的是传输中的连续不断的数据集合。中转站可以是一个数组,它是在内存中开辟的一块空间,是实现文件输入输出的桥梁。

图14.6  文件输入输出原理图

 

在本例中,文件source.txt称为I/O源,文件target.txt称为I/O目标,统称为流节点。需要特别注意的是输入流、输出流均是相对程序而言,而不是相对文件而言,例如输入流针对source.txt实际上是“输出流”。

流按照处理数据的单位可分为两种:字节流和字符流。字节流处理字节的输入和输出,例如使用字节流读取或书写二进制数据。字符流为字符输入和输出处理提供了方便,它采用Unicode编码标准,因而可以国际化。在某些方面如汉字的处理,它比字节流高效。

所有字符输入流类都是抽象类Reader的子类,Reader的主要方法如下。

Ø   int  read()从源中读取一个字符的数据,返回字符值。

Ø   int  read(char  b[])从源中试图读取b.length个字符到b中,返回实际读取的字符数目。返回-1表示读取完毕。

Ø   void  close()关闭输入流。

下面就使用Reader来实现文件的读取,文件hello.template位于C盘根目录下,内容是”hello  world!”,代码如示例8所示。

 

示例8

import  java.io.FileReader;

import  java.io.IOException;

import  java.io.Reader;

/**

 *使用字符输入流读取文件内容并输出,文件内容是”hello  world!"。

 *@author 南京

 */

public  class  TestIO1 {

      public  static  void  main(String[] arg) {

            Reader  fr = null;

            int  length = 0;

            char  ch[] = null;

            try {

                  //1、创建字符输入流对象,负责读取c:\hello.template文件

                  fr = new  FileReader(“c:/hello.template”);

          //2、创建中转站数组,存放读取的内容

                  ch = new  char[1024];

                  //3、读取文件内容到ch数组

                   length = fr.read(ch);

                   //4、输出保存在ch数组中的文件内容

                   System.out.println(new  String(ch, 0, length));

               } catch (IOException  e) {

                     e.printStackTrace();

               } finally {

                     //5、一定要关闭输入流

                     try {

                           if (null ! = fr)

                               fr.close();

                     } catch (IOException  e) {

                            e.printStackTrace();

                     }

               }

       }

}

运行效果如图14.7所示。

图14.7  使用Reader读取文件内容并输出

 

其中FileReader类是Reader的实现类,创建FileReader对象时需要传入文件的路径和文件名。需要注意的是路径中可以使用正斜杠”/”,如”c:/ pet.template”;如果使用反斜杠必须写”\\”如”c:\\pet.template”。创建Reader对象是要求指定文件必须存在,否则会出现异常。

如果文件的长度超过了中转站数组的长度,就应该采用循环读取的方式,实现代码可参考上机练习3参考解决方案。

 

上机练习3

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

训练要点

Ø   理解输入流和输入流类的概念。

Ø   使用Reader实现文件读取。

需求说明

模板文件pet.template位于C盘根目录下,要求读取模板文件的内容,在读取完成后一次性输出全部内容。参考运行效果如图14.8所示。

图14.8  读取模板文件内容并输出

 

实现思路及关键代码

  1. 在C盘根目录下创建模板文件pet.template,根据需求中参考用图输入文件内容。
  2. 创建Java项目TeslIO。
  3. 在cn.le.io包下创建TestReader类,包含main方法。
  4. 在main方法中编写代码实现需求,参考步骤如下。

1 创建字符输入流对象fr,负责对c:/pet.pemplate文件的读取。

2 创建中转站数组ch,存放每次读取的内容。

3 通过循环实现文件读取,把全部内容放入StringBuffer。

4 关闭输入流fr。

5 输出保存到StringBuffer中的文件内容。

注意编写注释和异常处理。

参考解决方案

//1、创建字符输入流对象,负责读取c:/pet.template文件

Reader  fr = new  FileReader(“c:/pet.template”);

//2、创建中转站数组,存放每次读取的内容

char  ch[] = new  char[1024];

//3、通过循环实现文件读取,把全部内容放入StringBuffer

StringBuffer  sb = new  StringBuffer();

int  length = fr.read(ch);

while (length ! = -1) {

         sb.append(ch);

          length = fr.read(ch);

}

 

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

替换模板文件中的占位符,使用String类replace(String  target, String  replacement)即可,返回值是替换后的字符串。

 

上机练习4

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

训练要点

String类replace(String  target, String  replacement)方法的使用。

需求说明

调用String类replace(String  target, String  replacement)方法替换上机练习3中从模板文件中读出内容中的占位符,输出替换后字符串。参考运行效果如图14.9所示。

图14.9  替换模板文件中的占位符

 

实现思路及关键代码

  1. 在项目TestIO的cn.le.io包下创建TestReplace类,包含main方法。
  2. 在main方法中编写代码实现需求。

参考解决方案

String  str = “您好!我的名字是{name},我是一直{type}。我的主人是{master}。“;

System.out.println(“替换前:”+str);

str = str.replace(“{name}”,”欧欧”);

str = str.replace(“{type}”,”狗狗”);

str = str.replace(“{master}”,”李伟”);

System.out.println(“替换后:”+str);

 

14.3.3  使用Writer输出内容到文件

所有字符输出流类都是抽象类Writer的子类,最常用的子类是FileWriter类。Writer的常用方法如下。

Ø   void  write(int  n)向输出流写入单个字符。

Ø   void  write(char  b[])向输出流写入一个字符数组。

Ø   void  write(String  str)向输出流写入一个字符串数组。

Ø   void  close()关闭输出流。

下面就使用Writer把字符串“Hello  World!欢迎您!”写到文件hello.txt中,代码如示例9所示。创建FileWriter对象时,如果指定路径下文件不存在,会自动创建该文件。

 

示例9

import  java.io.FileReader;

import  java.io.IOException;

/**

 *使用字符输出流输出字符串到指定文件中。

 *@author 南京

 */

public  class  TestIO2 {

      public  static  void  main(String[] arg) {

            String  str = “Hello  World!欢迎您!”;

            FileWriter  fw = null;

            try {

                  //1、创建字符输出流对象,负责向c:\hello.txt写入数据

                  fw = new  FileReader(“c:\\hello.tet”);

         //2、把str的内容写入到fw所指文件中

         fw.write(str);

              } catch (IOException  e) {

                     e.printStackTrace();

              } finally {

                     try {

                          //3、一定要关闭输出流

                           if (null ! = fw)

                                   fw.close();

                     } catch (IOException  e) {

                            e.printStackTrace();

                     }

               }

       }

}

 

上机练习5

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

训练要点

Ø   理解输出流和输出流类的概念。

Ø   使用Writer写内容到文件。

需求说明

把上机练习4中替换后的字符串写入到C盘根目录下的文件pet.txt中。

实现思路及关键代码

  1. 在Java项目TestXML下创建类TestWriter,包含main方法。
  2. 在main方法中创建输出流对象,实现内容写入并关闭输出流。

注意编写注释和异常处理。

参考解决方案

String  str = “您好!我的名字是欧欧,我是一只狗狗。我的主人是李伟。”;

//1、创建字符输出流对象,负责向c:\pet.txt写入数据

fw = new  FileWriter(“c:\\pet.txt”);

//2、把str的内容写入到fw所指文件中

fw.write(str);

//3、一定要关闭输出流

if (null ! = fw)

       fw.close();

 

14.3.4  综合练习

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

格式模板保存在文本文件pet.template中,内容如下:

<html>

         <head>

                 <title>{name}</title>

         </head>

         <body>

                  <h1>{name}</h1>

                   您好!

                   我的名字是{name},我是一只{type}。

                   我的主人是{master}。

          </body>

</html>

其中{name}、{type}、{master}是需要替换的内容,现在要求按照模板格式保存宠物数据到HTML文档,即把{name}、{type}、{master}替换为具体的宠物信息,该如何实现呢?

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

代码如示例10所示。

 

示例10

import  java.io.FileNotFoundExcepion;

import  java.io.FileReader;

import  java.io.FileWriter;

import  java.io.IOException;

import  java.io. Reader;

import  java.io.Writer;

/**

 *按照规范格式保存宠物信息到HTML文档。

 *@author 南京

 */

public  class  TestIO {

      public  static  void  main(String[] arg) {

            Reader  str = null;

            Writer  fw = null;

            StringBuffer  sb = new  StringBuffer();

            //1、读取模板文件内容到StringBuffer

            try {

                  fr = new  FileReader(“c:\\pet.template”);

                  char  ch[] = new  char[1024];

                  int  length = 0;

                  length = fr.read(ch);

                  while (length ! = -1) {

                         sb.append(ch);

                         length = fr.read(ch);

                  }

              } catch (FileNotFoundException  e) {

                     e.printStackTrace();

              } catch (IOException  e) {

                     e.printStackTrace();

              } finally {

                     try {

                           if (null ! = fr)

                                   fr.close();

                     } catch (IOException  e) {

                            e.printStackTrace();

                     }

               }

               //2、替换模板中占位符为具体内容

               String  str = sb.toString();

               str = str.replace(“{name}”,”欧欧”);

str = str.replace(“{type}”,”狗狗”);

str = str.replace(“{master}”,”李伟”);

               //3、输出替换后内容到HTML文档

               try {

                     fw = new  FileWriter(“c:\\pet.html”);

                     fw .write(str);

              } catch (IOException  e) {

                     e.printStackTrace();

              } finally {

                     try {

                           if (null ! = fw)

                                   fw.close();

                     } catch (IOException  e) {

                            e.printStackTrace();

                     }

               }

       }

}

 

 

本章总结

 

Ø   XHTML标签都有固定含义,不能去创造新的标签。而XML支持自定义标签,具有扩展性。

Ø   定义XML文档结构有两种方法:DTD和XSD。XSD本身就是XML文档结构,是继DTD之后,用来规范和描述XML文档结构的第二代标准。

Ø   可以使用CSS格式化XML。XSL是首选的XML样式表语言,远比CSS更加完善。

Ø   XML的主要作用有:数据存储、数据交换、数据配置。

Ø   目前最常用的XML解析技术是DOM和SAX。Sun公司提供了JAXP接口来使用DOM和SAX。

Ø   在Java中,文件的输入输出功能是通过流来实现的。流可以理解为一组有顺序的、有起点和终点的动态数据集合。

Ø   所有字符输入流类都是抽象类Reader的子类,所有字符输出流类都是抽象类Writer的子类。

 

 

本章作业

 

一.选择题

1.下面关于XML文档的说法正确的是(    )。(选两项)

A.XML文档允许用户自定义标签

B.XML文件不仅存储数据,还能显示数据

C.XML文档中所有标签不区分大小写

D.XML文档总是以一个XML声明开始

 

2.下面关于XML文档结构定义的说法错误的是(    )。

A.XML文档结构定义有两种方法,DTD和XSD

B.DTD是编写XML文档的依据,也是验证XML的依据

C.DTD根据位置的不同可以分为内部DTD和外部DTD

D.XSD与DTD相比,DTD包含的数据类型更多

 

3.DOM和SAX的主要区别有(    )。

A.SAX把XML文档映射成一个倒挂的树状结构

B.DOM模型内存消耗小,DOM解析器能提供更好的性能优势

C.SAX读取文档时会激活一系列事件,推给事件处理器,由事件处理器来访问文档

D.DOM用来解析XML,而SAX用来格式化XML

 

4.下面关于Java中文件输入输出的说法正确的是(    )。(选两项)

A.在Java中,文件的输入输出功能是通过流来实现的

B.如果要把数据写入到一个文件中,需要创建一个输入流对象

C.字符流在某些方面比如汉字的处理,比字节流更高效

D.可以通过Reader  r = new  Reader(“c:\\pet.txt”)创建一个输入流对象

5.在Java中,创建输入流对象,输出流对象时需要指定文件的路径和名称,下面说法正确的是(    )。(选两项)

A.创建输出流对象时,指定路径下文件不存在会自动创建该文件

B.创建输入流对象时,指定路径下文件必须存在,否则会出现异常

C.指定路径和文件名时只能使用反斜杠分隔符,不能使用正斜杠

D.创建输入流、输出流对象时指定路径下的文件都必须存在

 

二.简答题

  1. 编写如下结构的XML文档,具体值自行添加,至少添加两个学员信息。

<students>

          <student>

                 <name></name>

                 <age></age>

                 <school></school>

          </student>

</students>

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

参考本章示例3完成本题目。

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

2.根据简答题1中编写的XML文档编写相应的DTD文档。

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

参考本章示例5完成本题目。

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

3.编写CSS文档,使得简答题1中编写的XML文档的显示效果时:背景色为蓝色,文字颜色为白色;每个学员的信息分块显示,字体大小为12pt。

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

参考本章示例6完成本题目

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

4.使用DOM解析简答题1中编写的XML文档,输出所有学员信息。

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

参考本章示例7完成本题目。

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

5.使用Reader和Writer复制C盘根目录下文件source.txt到D盘根目录下,取名为target.txt。查看文件内容和文件大小是否一致。

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

创建Reader对象,负责读取c:\source.txt文件。

创建Writer对象,负责向d:\targer.txt文件写入数据。

创建中转站数组,存取读取的内容。

通过循环,利用中转站数组完成复制操作。

关闭Reader、Writer对象。

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

 

 

 

 

15 章

项目案例:宠物商店

 

 

 

 

本章工作任务

  • 开发项目案例——宠物商店

本章技能目标

  • 使用面向对象思想进行程序设计
  • 设计数据存储结构
  • 使用Oracle存储数据
  • 使用JDBC操作数据库数据
  • 使用DAO实现数据访问层

本章单词

 

 

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

                                      1.balance:                                           

                                      2.account:                                      

                                      3.deal:                                              

                                      4.breed:                                       

                                      5.entity:                                        

                                      6.factory:                                      

                                      7.charge:                                              

                                      8.stock:                                              

本章简介

本课程中我们首先学习了面向对象的核心内容,包括面向对象的设计过程、抽象、封装、继承、多态、接口、面向对象设计原则等。在此基础上,继续学习了Java集合、异常、JDBC、File  I/0、Oracle基础与应用、分层开发技术等内容。本章将利用前面所学技能特别是后一部分完成一个宠物医院的项目案例,训练学员根据需求进行面向对象设计的能力,使用JDBC操作Oracle数据库的能力,训练分层开发技术的应用。

15.1  案例分析

15.1.1  需求概述

在遥远的Wonderland,宠物可以自己繁殖,但速度很慢,品种也达不到广大爱犬人士的要求。“宠物商店”应运而生。

在商店里,宠物主人可以出卖、购买宠物、价格由店方确定。越普通,年龄越大的宠物越不受青睐,价格越低。价格也会受市场供需变化的影响。当然,每一笔买入、卖出的业务店家都会记录在账,商店还可以根据需求自己培育宠物品种。

随着业务激增,需要开办多家宠物商店,但每家商店必须按照“行业标准”来运营,提供的服务必须是一样的。这时,用户只是找一家“宠物商店”,而非特定哪一家,就像找一家“麦当劳”,而不是非特定某一家不可。

15.1.2  开发环境

开发工具:JDK  6.0、MyEclipse  7.5、Oracle 10g、PL/SQL  Developer。

15.1.3  案例覆盖的技能点

  • 面向对象程序设计的思想。
  • 使用类图设计系统。
  • 使用java集合存储和传输数据。
  • Java异常处理。
  • 使用JDBC操作数据库。
  • 使用Oracle存储数据。
  • DAO层的应用。

15.1.4  问题分析

1.设计数据库表结构

根据项目需求概述,分析需要保存到数据库中的数据,确定需要保存的内容包括宠物信息、宠物主人信息、宠物商店信息和账目信息。可以在数据库中创建这四个表,具体字段根据业务进行确定,可以参考之前章节中的设计。

四个表的名称可以定义为Pet、PetOwner、PetStore、Account,注意主键字段和外键字段的设计,通过外键建立表与表之间的关联关系,避免字段冗余,设计过程中要和其他学员进行讨论,避免闭门造车。

2.使用类图设计系统

采用DAO模式设计和开发本项目案例,确定需要用到的类和接口。以下分析仅提供一种思路和参考方案,并不代表最终解决方案和最佳解决方案,大家要在此基础上认真思考,讨论交流,设计出自己的方案,提高面向对象设计功能和对DAO模式的理解与掌握。

  1. 根据数据库表创建实体类

实体类一般和数据库表对应,实体类的属性对应于表的字段。可以分为四个数据库表分别创建实体类,实现数据库数据在各个层次的传输。

四个实体类的名称可以定义为Pet、PetOwner、PetStore、Account,如图15.1所示。根据实体类的特征进行定义,定义类名和属性名时主要Java和数据库名规则不同。

 

 

图15.1  实体类定义

  1. 创建DAO接口和实现类

采用面向接口编程的思想设计数据访问层的DAO,定义DAO接口和实现类。DAO接口中的方法定义可以先奉行“拿来主义”,参考第十三章中的定义、设计过程中根据业务需要再增加新的方法定义,或删除不用的方法定义。

四个DAO接口的名称可以定义为PetDao、PetOwnerDao、PetStoreDao、AccountDao,相应的实现类可命名为PetDaoOracleImpl、PetOwnerDaoOracleImpl、PetStoreDaoOracleImpl、AccountDaoOracleImpl,为了重用建立和关闭数据库的代码,可以创建BaseDao作为四个实现类的父类,如图15.2所示。

 

 

图15.2  DAO接口和实现类定义

  1. 创建业务接口和实现类

从业务角度考虑,该项目中主要是宠物主人和宠物商店的业务,例如主人可以购买宠物、卖出宠物、登录等,而商店则可以购买宠物、卖出宠物。培育宠物、查询待售宠物、查看商店结余、查看商店账目、新开宠物商店、登录等。可创建两个业务接口PetOwnerService和PetStoreService,其实现类分别命名为PetOwnerServiceImpl和PetStoreServiceImpl,如图15.3所示。在业务实现类中调用数据访问层的接口实现相应业务。

 

 

图15.3  业务接口和实现类定义

  1. 根据“单一职能原则”优化业务接口设计

图15.3所示的接口从业务上满足需要,但从面向对象设计的角度分析却是不好的,比如它明显违背了“单一职能原则”,各个接口包含功能过多,不利于重用和维护。对该接口定义进行优化,比如可以抽取出Buyable、Sellable、Breedable、Accountable等接口,而PetOwnerService、PetStoreService接口根据自身功能继承其中的一个或多个接口。优化设计后的结果如图15.4所示。

  1. 根据以上分析结果,给出伪代码,完成设计

经过以上步骤分析,已经设计出了各个层次的类和接口及关系。根据设计的类图,给出伪代码,完成设计,为下一步开发做好准备。

 

 

图15.4  优化设计结果

3.难点分析

本项目的设计难点主要是数据库表结构的设计,它是实体类设计和数据库操作的基础。上面设计中只给出了数据表的名称而没有给出具体字段。如何区分一个宠物是否被卖出,如何区分一个宠物是库存还是新培育的,如何定义一个宠物的所属商店等,都需要用相应字段来实现。需要大家认真思考,讨论交流,设计出符合需求的数据库表结构。具体实现时要按照功能把本项目案例分解为一个个用例,化大为小,逐个击破,最终完成整个项目案例的开发。还要注意代码在各个层次的分配,做到层次清晰,分配合理。

15.2  项目需求

本项目案例主要涉及宠物主人和宠物商店等,其中宠物主人操作要求必须在课上集中完成,对于技术水平较高的学员可以在课上继续宠物商店的操作,所有学员必须在课下完成宠物商店的操作并做为作业提交。

最终提交结果包括创建数据库表的脚本文件、系统类图、程序代码三部分内容。

宠物主人操作的主要用例及介绍如下。

15.2.1  用例1:系统启动

所有宠物、宠物主人、宠物商店身处Wonderland,Wonderland系统沉睡时,他们蛰伏到Oralce。Wonderland醒来,他们从Oracle中苏醒。在系统启动时,显示所有的宠物信息、宠物主人信息、宠物商店信息。参考运行图如图15.5所示。

访问数据库查询所有宠物信息,以List形式返回,然后进行遍历即可显示所有宠物信息。宠物主人、宠物商店信息的显示与此类似。

系统启动后,提示选择登录模式,输入1为宠物主人登录,输入2为宠物商店登录,此处要考虑如果输入了其他字符应该如何处理。

 

 

图15.5  系统启动参考运行图

15.2.2  用例2:宠物主人登录

系统启动后,可以选择登录模式,以宠物主人登录或以宠物商店登录,将分别显示相应的功能菜单,参考运行图如图15.6和图15.7所示。

选择以宠物主人身份登录,输入用户名和密码,访问数据库判断登录是否成功。如果成功,输出主人基本信息并提示选择相应操作。如果登录失败,提示确认用户名和密码后重新输入。

 

 

图15.6  宠物主人成功登录参考运行图

 

 

 

图15.7  成功登录宠物商店参考运行图

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

主人成功登录后,即可选择购买宠物或者卖出宠物操作。如果选择购买宠物,必须继续选择是购买库存宠物还是购买新培育宠物。选择购买库存宠物后,先显示所有库存宠物列表供主人选择,输入宠物编号即可完成购买,购买成功将显示提示信息。参考运行图如图15.8所示。

 

 

图15.8  购买库存宠物参考运行图

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

主人购买新培育宠物的步骤与购买库存宠物相同。两者的差别主要体现在数据库操作中,在数据库表pet中存放着所有的宠物,如何区分是库存宠物还是新培育宠物,可以增加一个字段来实现,该字段不同取值分别代表库存宠物和新培育宠物。

在这种情况下,要注意数据访问层代码的重用。如果把购买库存宠物和购买新培育宠物视为两种不同业务,在业务接口和实现类中就应该定义不同的方法。

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

宠物主人也可以卖出宠物给商店。首先显示主人的宠物列表,选择要出售的宠物序号,然后显示宠物商店列表,选择买家序号即可完成该项交易。参考运行图如图15.9所示。

 

 

图15.9  卖出宠物参考运行图

15.3  进度记录

根据开发进度及时填写表15-1.

表15-1  开发进度记录表

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

用例                          开发完成时间            测试通过时间        备注

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

用例1:系统启动

用例2:宠物主人登录

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

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

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

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

 

16章

指导学习:课程总复习

 

 

 

 

◇本章工作任务

 

Ø    开发简单的内容管理系统CMS

 

◇本章技能目标

Ø    掌握面向对象程序设计

Ø    掌握Java集合框架

Ø    掌握Java异常处理

Ø    掌握使用JDBC实现数据库访问

Ø    掌握Java  File  I/O

Ø    使用DAO模式开发数据访问层

 

 

16.1  复习串讲

16.1.1  核心技能目标

学完本门课程需要达到如下技能目标。

Ø    具备进行面向对象程序设计的能力,使用类图描述设计。

Ø    掌握面向对象的抽象性、封装性、继承性和多态性。

Ø    掌握抽象类和接口的使用,理解面向接口编程的优势。

Ø    理解面向对象设计的基本原则。

Ø    掌握Java集合框架的主要内容。

Ø    掌握Java异常处理的原理与应用,使用log4j记录日志。

Ø    理解JDBC的原理,能够使用JDBC进行各种数据库操作。

Ø    掌握Oracle的基础知识与简单应用,为深入学习Oracle打下坚实基础。

Ø    掌握DAO模式,理解分层开发技术的优势。

Ø    掌握使用Java进行文件操作,掌握XML技术的基础知识。

我们可以自我检查,看看是否达到了这些技能目标,如果还有疏漏和不足,就赶快查漏补缺吧。

 

16.1.2  知识梳理

在前五章我们学习了Java面向对象的核心内容:封装、继承、多态、抽象类、接口等,知识体系如图16.1所示。我们可以借助这个图梳理清这些知识体系结构,该图在第6章已经进行了学习。

在学习Java面向对象核心内容的基础上,我们又学习了Java集合框架、异常处理、JDBC、File  I/O等内容,这些都是频繁使用的技能,是Java程序员的必备技能。明白其知识体系,深入学习其内容,将为以后Java学习与应用打下坚实基础。该部分内容的知识体系如图16.2所示。

在知识体系图中,图标表示我们学过该知识;图标表示我们部分学习过该知识或者学习过相关知识,但需要巩固和补齐;图标表示我们要通过自学学习这个知识。

图16.1  Java  OOP知识体系图(一)

 

图16.2  Java  OOP知识体系图(二)

 

Java面向对象内容涉及概念多、理解难度大,不仅要学习掌握其技能,更要领悟其思想,学好这部分内容会为学习Java后续课程打下坚实基础。我们已经学习了其中最常用技能,这些技能已经能够满足日常开发的需要,建议大家以后进一步学习相关知识,把这部分内容学习得更深更广是很有必要的。

 

16.2  综合练习

CMS是“Content  Management  System”的缩写,意思为内容管理系统。CMS的最大特征是“内容和界面分离”,内容存储在数据库或独立文件中,而界面设计存储在模板里,以内容替换模板就可以快速生成和更新网页。它具有很多基于模板的优秀设计,可以加快网站开发速度的和减少开发成本。

CMS其实是一个很广泛的称呼,从一般的博客程序、新闻发布程序到综合性网站的管理程序都可以成为内容管理系统。而博客程序的博主、新闻发布程序和综合性网站的编辑人员就是CMS的最终使用者。

 

16.2.1  任务描述

本次综合练习的任务是:开发简单的CMS。在数据库中创建新闻数据库表news,包含标题、作者、日期、正文等字段;创建HTML模板文件;读取数据库中所有新闻信息,并使用新闻信息替换模板文件中的占位符,从而为每一条新闻生成一个HTML静态页面。

 

16.2.2  任务分析

数据库采用Oracle  10g,该任务涉及使用JDBC访问Oracle数据库、Java集合框架、文件读写等技能点。虽然任务代码量不大,但要使用DAO模式进行开发,通过练习更熟练地掌握分层开发技术。可以参考图16.3所示的项目包结构以及图16.4所示的最终运行效果图。

图16.3  参考包结构

 

图16.4  参考运行效果图

 

 

16.2.3  练习

分阶段完成练习。

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

需求说明

Ø    数据库采用Oracle 10g。

Ø    创建数据库表news、存储新闻信息,包括标题、作者、日期、正文等字段。

Ø    字段名定义要见名知义,字段的数据类型与实际相符。添加测试记录不少于三条。

 

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

需求说明

按照HTML文件的语法格式创建HTML模板文件news.template,在要显示具体新闻内容的位置使用占位符。注意该模板文件仅对应一条新闻信息。

实现思路及关键代码

可以使用表格或DIV等形式显示新闻,参考实现代码如示例1所示。

 

示例1

<head>

         <title>{title}</title>

</head>

<body>

          <table  align =”center” width =”95%” border =”1”>

                    <tr>

                           <td  width =”10%”><b>标题:</b></td>

                           <td>{title}</td>

                     </tr>

                     <tr>

                           <td  width =”10%”><b>作者:</b></td>

                           <td>{author}</td>

                    </tr>

                    <tr>

                          <td  width =”10%”><b>时间:</b></td>

                          <td>{createTime}</td>

                    </tr>

                    <tr>

                           <td  width =”10%”><b>内容:</b></td>

                           <td>{content}</td>

                    </tr>

          </table>

<body>

 

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

需求说明

从数据库读取所有新闻信息,保存在泛型集合中,为替换模板文件中的占位符做好准备。

实现思路及关键代码

采用DAO模式,创建NewsDao接口和NewsDaoOraclelmpl实现类,完成新闻信息的读取。还需要创建实体类News来存储和传输数据,创建数据连接和关闭工具类BaseDao来简化DAO的操作,避免代码重复。

 

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

需求说明

读取HTML模板文件news.template,为使用新闻信息替换其中的占位符做好准备。

实现思路及关键代码

使用Reader类或InputStream类读取模板文件。可以通过工具类FileIO的String  readFile(String  filePath)方法实现该功能,其中参数filePath为模板文件的路径,返回值为模板文件的内容,以String类型表示。

 

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

需求说明

利用替换模板文件后的数据生成HTML文件。

实现思路及关键代码

使用Writer类或OutputStream类完成该操作,可以通过工具类FileIO的void  writeFile(String  filePath,String  str)方法实现该功能,其中参数filePath为HTML文件的路径,str为利用一条新闻信息替换模板文件后的结果。

 

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

需求说明

遍历保存在泛型集合中的新闻信息,替换模板文件中的占位符,为每一条新闻生成一个HTML文件。

实现思路及关键代码

有了阶段3、阶段4和阶段5已实现的功能,本阶段的操作就比较简单了。可以通过NewsManager类的toHtml()方法完成该功能,其中替换占位符的功能可以通过String类的replace()方法实现。参考实现代码如示例2所示。

完成后可以创建测试类Test测试运行结果,若发现问题及时解决。

 

示例2

import  java.util.List;

import  cn.le.cms.dao.NewsDao;

import  cn.le.cms.dao.impl.NewsDaoOracleImpl;

import  cn.le.cms.entity.News;

import  cn.le.cms.util.FileIO;

/**

  *使用数据库内容替换模板文件,生成HTML文件

 *@author南京

 */

public  class  NewsManager {

     public  void  toHtml() {

          //1、读取模板文件内容,返回文件内容字符串

          FileIO  fileio = new  FileIO();

           String  templatestr = fileio.readFile(“c:\\news.template”);

          //2、读取数据库表,获取新闻列表

          NewsDao  newsDao = new  NewsDaoOracleImpl();

          List<News>newsList = newsDao.findAll()

          //3、替换模板文件,为每一条新闻创建一个HTML文件来显示其信息

          for(int  i=0; i<newList.size(); i++) {

               //3.1、获取一条新闻

               News  news = newsList.get(i);

               //3.2、使用该条新闻i型内息替换对应占位符

               String  replacestr = new  String();

               replacestr = templatestr;

               replacestr = replacestr.replace(“{title}”,news.getTitle());

               replacestr = replacestr.replace(“{author}”,news.getAuthor());

               replacestr = replacestr.replace(“{createTime}”,

news.getCreateTime().toString());

               replacestr = replacestr.replace(“{content}”,news.getContent());

                //3.3、为该条新闻生成HTML文件

                String  filePath = “c:\\news”+i+”.html”;

               fileio.writeFile(filePath,replacestr);

          }

     }

}

 

 

 

猜你喜欢

转载自blog.csdn.net/GHHCNGC/article/details/81083481
今日推荐