目录

第一章:Java开始 1

学习目标 1

Java历史 2

Java技术概述 3

Java技术的优点 3

Java虚拟机 4

类加载器 6

Windows环境变量 8

内容总结 13

独立实践 14

第二章: 面向对象概述 15

学习目标 15

面向对象(Object Oriented) 16

面向对象的主要特性 18

抽象(Abstraction) 18

封装(Encapsulation): 19

多态(Polymorphism) 20

面向对象的优点 22

面向对象的分析、设计和编程 23

面向对象语言的发展简史 26

内容总结 29

独立实践 30

第三章:面向对象的程序设计 31

学习目标 31

类和对象的描述 32

声明类 32

声明属性 33

声明成员方法 34

源文件的布局 36

包的声明 36

包与目录的布局 38

内容总结 45

独立实践 46

第四章: Java语法基础 47

学习目标 47

基本语法元素 48

Java关键字 49

基本Java数据类型 50

变量声明和赋值 52

引用(Reference)类型 52

存储器分配和布局 53

this引用 55

Java编码约定 56

运算符的优先级 58

升级和表达式的类型转换 62

独立实践 70

第五章:数组 71

学习目标 71

数组的描述 72

创建数组 72

多维数组 78

拷贝数组 80

内容总结 83

独立实践 84

第六章:继承 86

学习目标: 86

单继承(single inheritance) 87

访问控制 89

方法重载(method overloading) 91

方法覆盖(method overriding) 93

基于继承的多态实现 94

隐藏(hiding) 95

构造方法在继承中的使用 96

super关键字 97

包装类 97

toString( )方法 100

内容总结 101

独立实践 102

第七章:类的高级特征 103

学习目标 103

static关键字 104

final关键字 106

内部类 106

实例分析 110

抽象类,接口 115

内容总结 120

独立实践 121

第八章:异常 122

学习目标 122

异常的概念 123

异常的分类 123

实例分析 124

自定义异常 126

方法覆盖和异常 127

内容总结 129

第九章:基于文本的应用 131

学习目标 131

程序交互的几种方式 132

常用类方法说明 132

String的方法 132

正则表示式(Regular expression) 133

StringBuffer类 135

StringBuffer与String的区别 136

集合类的使用 136

实例分析 136

内容总结 153

独立实践 154

第十章:JAVA GUI概述 155

学习目标 155

GUI概述及组成 156

Swing优点 157

布局管理器 158

BorderLayout 159

GridLayout 161

CardLayout 162

GridBagLayout 164

实例分析 165

内容总结 172

独立实践 173

第十一章 线程 174

学习目标 174

线程的概念 175

线程状态和调度 ….176

线程中断/恢复的几种方式 178

创建线程的两种方式 179

线程的控制 180

实例分析 182

内容总结 189

独立实践 190

第十二章:高级I/O流 192

学习目标 192

I/O基础知识 193

字节流 193

字符流 194

节点流 194

过程流 194

基本字符流类 198

对象串行化 201

实例分析 203

内容总结 211

独立实践 212

第十三章:网络 213

学习目标 213

TCP/IP协议模型 214

基于Java的网络技术 216

DatagramSocket 219

InetAddress类的使用 221

扩展知识: 227

SocketChannel类 227

内容总结 229

独立实践 230

第十四章:数据结构与算法(上) 231

学习目标 231

算法(algorithm): 232

查找算法: 234

排序算法: 237

递归(recursive): 240

快速排序: 242

内容总结 245

独立实践 246

第十五章:数据结构与算法(下) 247

学习目标 247

数据结构介绍: 248

数组 248

逻辑大小和物理大小 248

链表 248

栈(stack) 249

队列: 250

树: 250

实例分析 252

内容总结 274

独立实践 275

第十六章:数据库(一) 276

学习目标 276

数据库的基本概念 277

数据的描述 277

数据联系的描述 278

数据模型 278

数据库三级模式结构 279

数据库三个范式 280

范式总结 282

创建数据库 283

创建表 287

内容总结 290

独立实践 291

第十七章: 数据库(二) 292

学习目标: 292

查询(从基本到高级) 293

模糊查询 293

排序 295

集合操作-并 296

集合操作-交 297

集合操作-差 297

常用函数 297

数据类型转换函数CAST和CONVERT 298

分组查询 299

五大约束 301

索引 303

数据库的备份与恢复 304

内容总结 308

独立实践 309

第十八章:数据库(三) 310

学习目标: 310

存储过程 311

触发器 313

函数 314

规则 314

事务 315

while语句 317

case语句 317

内容总结: 318

独立实践 319

第十九章: JDBC基础 320

学习目标 320

JDBC的概念 321

连接数据库的几种方式 321

JAVA编程语言和JDBC 323

JDBC编程的步骤 323

实例分析 325

内容总结 335

独立实践 336

第二十章:高级JDBC 337

学习目标 337

使用DDL,DML语言对数据库进行基本操作 338

查询数据库里的数据 339

预编译语句(PreparedStatement) 340

使用事务 341

事务的级别控制 343

使用存储过程 345

操作元数据 347

ResultSetMetaData(结果集元数据) 349

可滚动的和可更新的结果集 350

批处理更新 354

二进制大对象BLOB 357

RowSet 新特性 359

JdbcRowSet 360

FilteredRowSet 361

内容总结 363

独立实践 364

第二十一章:XML基础 366

学习目标 366

XML的概念 367

定义XML文档 368

命名冲突 371

使用前缀解决命名冲突问题 371

使用命名空间 371

命名空间属性 372

统一资源标识符 372

默认的命名空间 372

使用命名空间 373

XML 文档规则 374

XML Schema 379

Schema和DTD的区别: 380

Schema的数据类型 380

样式表(XSL) 384

CSS样式表 384

XSL样式表 385

XSL的基本结构 386

XSL的基本语法 390

节点选择语句<xsl:value-of > 392

循环判断语句<xsl:for-each> 392

条件判断语句<xsl:if> 392

内容总结 395

独立实践 395

第二十二章:使用Java解析XML 397

学习目标 397

解析器的介绍 398

DOM以及广义的基于树的处理具有几个优点 399

文档对象模型(DOM)解析实例 402

DOM对象 404

DOM解析的例子: 406

SAX解析实例 409

DOM4J解析实例 412

JDOM解析实例 413

JAVA操纵XML 实例讲解 414

通过JAVA写数据到XML里面 415

内容总结 418

独立实践 418

第二十三章:HTML基础 419

学习目标 419

知识要点 420

HTML元素 420

标签属性 420

HTML基本标签 422

标题元素 422

HTML格式 427

HTML实体 431

不可拆分的空格 431

HTML表格 439

表格 441

HTML列表 461

HTML图像 469

Img标签和src属性 469

HTML背景 475

内容总结 478

独立实践 479

第二十四章:HTML进阶 480

学习目标 480

知识要点 481

表单 481

HTML框架 488

框架 488

在子窗体中访问并控制父窗体中对象 493

内容总结 495

独立实践 496

第二十五章:JavaScript介绍 497

学习目标 497

JavaScript语言概况 498

JavaScript和Java的区别 499

基于对象和面向对象 499

解释和编译 499

强变量和弱变量 500

静态联编和动态联编 500

实例 500

内容总结 503

独立实践 504

第二十六章:JavaScript基础 505

学习目标 505

基本结构 506

JavaScript代码的加入 506

基本数据类型 506

常量 507

表达式和运算符 509

实例 511

JavaScript程序构成 513

事件驱动及事件处理 516

内容总结 519

独立实践 520

第二十七章:JavaScript进阶 521

学习目标 521

基于对象的JavaScript语言 522

对象的基础知识 522

this关键词 523

new运算符 523

常用对象的属性和方法 525

算术函数的math对象 527

创建新对象 529

JavaScript中的数组 532

实例 535

文档对象功能及其作用 538

document中三个主要的对象 539

文档对象的基本元素 541

窗口及输入输出 544

输出流及文档对象 546

简单的输入、输出例子 547

内容总结 551

独立实践 552

第二十八章: Servlet 553

学习目标 553

Java Servlet概述 554

Servlet能够做什么 554

Servlet的生命周期 557

Java Servlet API 560

Web上使用的HTTP Servlet 560

处理HTTP Servlet的关键方法 560

其它相关接口的说明 561

HTTP协议基本概念及其特点 563

获取Cookie 565

HTTP响应报头--Response 566

会话管理 566

Servlet过滤器 569

Servlet监听器 576

内容总结 579

独立实践 580

第二十九章: Jsp 技术 583

学习目标 583

JSP介绍 584

JSP语法 584

模板元素 588

指令元素 588

页面指令 588

标签库指令 593

脚本元素 593

动作元素 597

<jsp:include> 598

JSP内置对象 605

Session和Application对象 610

JSP的汉字问题的原理 611

自定义标签 613

标准标签的使用 618

内容总结 621

独立实战 622

第三十章:struts入门 623

学习目标 623

Struts简介 624

什么是应用框架 624

WEB框架所要解决的问题 625

建立简单的Struts应用 627

内容总结 636

独立实践 636

第三十一章:Struts基础 637

学习目标 637

MVC 638

struts框架的优势 639

Struts如何实现Model 2, MVC 639

Struts 控制流 639

Struts framework的工作原理和组件 642

Struts ActionServlet控制器对象 642

Struts Action Classes 642

搞定Action对象 643

处理异常 643

Action的分类 643

Struts Action Mapping 646

使用ActionForward导航 647

Struts ActionForm Bean捕获表单数据 648

ActionForm的处理流程 649

Struts的其他组件 652

内容总结 653

独立实践 653

第三十二章:配置Struts组件 654

学习目标 654

三个 XML文件和一个属性文件 655

Web应用部署描述符 web.xml 655

ActionServlet的参数的配置 656

应用资源文件 658

Ant构建文件 659

配置Tiles框架 660

内容总结 661

独立实践 661

第三十三章:Struts标记库 662

学习目标 662

Struts标记库taglib介绍 663

Bean标记 663

逻辑标记 665

转发和重定向标记 668

HTML标记 669

显示错误信息的标记 673

其他HTML标记 673

模板标记 673

内容总结 676

独立实践 676

第三十四章:Hibernate基础 677

学习目标 677

Hibernate简介 678

建立简单的Hibernate应用 678

通过 Hibernate API 操纵数据库 684

Hibernate的初始化 687

访问Hibernate的Session接口 688

Hibernate工作原理图 691

内容总结 693

独立实践 693

第三十五章: 映射继承关系 694

学习目标 694

域模型关系 695

继承关系树的每个具体类对应一个表 696

创建映射文件 696

操纵持久化对象 698

选择继承关系的映射方式 699

映射多对一多态关联 702

内容总结 705

独立实践 705

第三十六章:HQL介绍 706

学习目标 706

HQL的出现 707

进入HQL世界 707

聚合 708

分组 709

在Java中使用HQL 709

内容总结 712

独立实践 712

第三十七章 Spring介绍 713

学习目标 713

Spring简介 714

IOC控制反转 714

Spring的容器 715

AOP面向切面编程 715

AOP的专业术语 715

Spring事务管理 718

Spring与Struts整合 719

Spring与Hibernate整合 721

独立实践 724

第一章: Java开始

学习目标

? Java技术的组成

? Java虚拟机的主要功能

? JAVA内存垃圾自动回收机制

? 运行代码的步骤

? 编写、编译并运行简单Java应用程序

Java历史

1991年,电视机,机顶盒,录象机的开发设计需要一种可移植、方便、高效的计算机语言。为了满足这种需求,由Sun公司的Patrick Haughton和James Gosling领导的技术小组开发了JAVA。

上世纪九十年代中期,Sun推出了Sun Java Development Toolkits 1.0,简称JDK1.0。JDK1.0是一个功能强大的软件包,可以用来开发小应用程序和多种操作系统(Sun Solaris,Windows Nt,Windows 95,Macintosh)的应用程序。

1998年Sun推出Java 2 Platform,它定义了所有Java技术的概念和标准,即包括已经实现的技术也包括尚未实现的技术;即包括Sun的实现的,也包括其它公司的实现。目前,Java 2 SDK1.6是Java 2 Platform的最新定义。这个定义又可以细分为四个版本:

标准版:J2SE(Java SE),用于开发普通的小应用程序和应用程序。它是我们这门课程要讲述的内容。

(Java SE的体系结构)

企业版:J2EE(Java EE),用于企业级应用。

微型版:J2ME(Java ME),用于开发移动电话,机顶盒,个人数字设备等。

JavaCard:适用于智能卡的Java平台。

(Java各版本应用领域)

Java技术概述

任何复杂的事物都可以分解成一些相对简单的组成部分。Java作为一门丰富而复杂的新技术,它由下列这些技术层面组成:

? Java编程语言:定义变量、表达式、逻辑控制等基本规则。

? Java类库:Java 软件工具包(SDK)为程序员提供了几千个类,包括基本的数学函数、数组和字符串、窗口,GUI,I/O,网络等。

? Java运行环境:包括Java字节代码的加载器、校验器以及Java虚拟机。

? Java虚拟机:Java技术出于跨平台,可移植的考虑,没有将程序的源代码编译连接成CPU的指令序列,直接交给计算机执行。Java技术在不同的硬件,不同的操作系统之上,定义了完全相同的支持Java程序运行的虚拟计算机。Java源程序被编译成字节(byte)代码,编译后的文件名后缀是.class文件,在Java虚拟机上运行。

? Java工具:编译器,注释器(interpreter),文档生成器等工具。

? Java小应用程序:小应用程序(Applets)是一种贮存于WWW服务器上的用Java编写的程序,它通常由浏览器下载到客户系统中,并通过支持Java运行环境的浏览器运行。它由超文本标识语言(HTML)的Web页来调用。

? Java应用程序:Java Applications是一种独立的程序,它不需要任何Web浏览器来执行。它们是普通的应用程序;可运行于任何具备Java运行环境的设备中。

? 其它Java程序:JavaBean,Servlet,JSP等。

Java技术的优点

Java技术取得今天这样的成就,依赖于这门技术的先进性,Java技术具有下列优点:

? 简单、健壮:java=(c++)--++,继承了c++语言的优点,去掉了c++的难点,又加入了新的特性;许多高级语言都要由程序员进行指针运算和存储器管理。这项工作即复杂又容易出错。Java不需要程序员进行指针运算和存储器管理,简化了设计,减少了出错的可能性。

? 面向对象:Java是面向对象的程序设计语言。与面向过程的语言相比,面向对象的语言更能反映人类对世界的认识和处理模式,具有良好的代码重用性。处理复杂、庞大而且不断变化的信息系统,必须使用面向对象的程序设计语言。

? 分布式:目前,在所有软件产品中,仅供单个计算机使用的单机版软件所占的比例越来越小。大多数软件都可以运行在网络环境中。Java拥有一个网络协议对象库(TCP/IP,HTTP,FTP等),可以象访问当地文件一样访问Internet上的对象。

? 改进的解释性:为弥补解释语言速度较慢的不足,Java采用预编译的办法,将源程序生成字节代码,减轻运行时的解释工作。另一方面,有些Java运行环境采用Just-In-Time(JIT)编译器将字节代码编译成机器码直接运行,这种运行环境对于重复执行的服务器端软件特别有效,可以达到C语言的速度。

? 安全:Java语言在编译时删除了指针和内存分配,在运行时检查字节代码,拒绝执行非法的内存访问,超越权限的访问等。可以防御黑客攻击。

? 平台无关:Java通过采用虚拟机技术真正实现了与平台无关。Java软件是真正跨平台可移植的。编译过的JAVA文件,即可以在windowns上运行,也可以Linux、Unix等系统上运行。

? 多线程:允许一个应用程序同时做多个任务。

? 动态性:Java允许下载代码模块,因此,当程序运行时也能动态升级。

? 高性能:经过实际的综合评测得出结论,Java是高性能的。

Java虚拟机

Java虚拟机在Java运行环境中处于核心地位。Java虚拟机使Java语言可以跨多种平台运行,保障了SUN提出的"write once, run anywhere"的特性,其组成为:

? 指令集(中央处理器[CPU]

? 寄存器

? 类文件格式

? 栈

? 垃圾收集堆

? 存储区

Java虚拟机隐藏了计算机硬件和操作系统的复杂性。使我们只面对单一的支持Java的计算机。当我们写好Java源程序后,使用编译器将源代码转换成JVM的指令序列(字节代码),保存为.class文件。执行Java程序时,JVM负责解释字节代码, JVM的指令转换成真实的机器指令,并执行。

自动内存回收(垃圾收集)

垃圾回收就是将程序不再使用的内存块释放出来,以提供给其它程序使用。程序运行时需要占用一定内存空间,当程序退出后应该把占用的内存释放,在c, c++语言中由程序保证内存的释放,但如果程序员忘记释放内存,就会在内存中“垃圾”增多,影响其它程序运行;Java语言实现自动垃圾回收处理,减轻了程序员的负担,杜绝了因内存管理而导致的程序中的问题。程序员可以建议垃圾回收,通过调用System.gc()实现。

常见垃圾收集机制介绍

JVM中内存划分为:堆栈(Stack)、堆(Heap)、静态存储(Static)

大多数垃圾收集使用了根集(root set)这个概念;所谓根集就量正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。简单的说,就是没有任何引用指向这块内存空间,该空间的内存就可以被回收。

引用计数法(Reference Counting Collector)

一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。

基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。

Tracing算法(Tracing Collector)

基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器。

compacting算法(Compacting Collector)

为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来 的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

垃圾回收的几个特点

垃圾收集发生的不可预知性:由于实现了不同的垃圾收集算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。

垃圾收集的精确性:(a)垃圾收集器能够精确标记活着的对象;(b)垃圾收集器能够精确地定位对象之间的引用关系。前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。而后者则是实现归并和复制等算法的必要条件。所有不可达对象都能够可靠地得到回收,所有对象都能够重新分配,允许对象的复制和对象内存的缩并,这样就有效地防止内存的支离破碎。

垃圾收集的实现和具体的JVM 以及JVM的内存模型有非常紧密的关系。不同的JVM 可能采用不同的垃圾收集,而JVM 的内存模型决定着该JVM可以采用哪些类型垃圾收集。现在,HotSpot 系列JVM中的内存系统都采用先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的垃圾收集。

代码运行过程

Java源程序通过"编译",转换成字节码。字节码被存储在.class文件中。

构成Java软件程序的字节代码在运行时被加载、校验并在解释器中运行。当运行Applets时,字节码可被下载,然后由浏览器中的JVM进行解释。解释器具备两种功能,一是执行字节代码,二是对底层硬件做适当调用。

在一些使用Java技术的运行环境中,部分校验过的字节码被编译成机器码并直接运行于硬件平台。这就使Java软件代码能够以接近C或C++的速度运行,只是在加载时,因为要编译成机器码而略有延迟。

Java技术运行字节代码包含如下三大步骤:

? 加载代码-由类加载器执行

? 校验代码-由字节码校验器执行

? 执行代码-由运行时的解释器执行

(Java程序运行的三大步骤)

类加载器

类加载器为程序的执行,加载所需要的类。类加载器将本地文件系统的类名空间,与来自网络的类名空间相分离,以增加安全性。当全部类被加载后,可执行文件的存储器格式被确定。这时,特定的存储器地址被分配给变量并创建检索表格。由于存储器格式在运行时出现,因而Java技术解释器增加了保护以防止对限制代码区的非法访问。

JVM中有四种类加载器,分别为(本节以下内容可以在以后再理解):

? BootStrapClassLoader:启动类加载器:虚拟机启动时最先加载系统加载关键的运行类的加载器。

? ExtClassLoader:扩展类加载器,专门用来加载所有java.ext.dirs下的.jar文件。开发者可以通过把自己的.jar文件或库文件加入到扩展目录的classpath,使其可以被扩展类加载器读取。

? AppClassLoader:应用(系统)类加载器,用来读取所有的对应在java.class.path系统属性的路径下的类

以上三个类在JDK中不能查到,他们是JVM底层使用的,我们可以认为这三者关系如下:

BootStrapClassLoader<-ExtClassLoader<-AppClassLoader

JDK中定义了一些ClassLoader类,我们可以观察到继承关系如下:

ClassLoader<-SecureClassLoader<-URLClassLoader<-AppClassLoader

我们也可以继承ClassLoader类制作自己的ClassLoader(用户定义的类加载器)。

一些关于ClassLoader的常用方法:

? sun.misc.Launcher.getBootstrapClassPath().getURLs();//根据以上叙述,应该是先获取启动类路径再转成路径的数组。

? MyClass.class.getClassLoader();//MyClass是类名

? ClassLoader.getSystemClassLoader();//获得系统(应用)级类加载器

? ClassLoader.getSystemClassLoader().getClass();//可以知道这个类加载器的名字等了

? java.lang.Thread.getContextClassLoader();//获取当前线程的上下文类加载器。

字节代码校验器

所有的class文件都要经过字节码校验器。字节码校验器主要测试以下内容:

? 代码段语法格式是否正确?

? 有无违反规则的非法代码?

? 是否违反对象访问权限?

? 是否试图改变对象类型?

字节码校验器对程序代码进行四遍校验,这可以保证代码符合JVM规范并且不破坏系统的完整性。如果校验器在完成四遍校验后未返回出错信息,则下列各项要求会得到保证:

? 程序代码所在的类,符合JVM规范的类文件格式。

? 无访问限制违例,如访问本地文件,访问网络等。

? 代码未引起操作数栈的上溢或下溢。

? 所有操作代码的参数类型将总是正确的。

? 无非法数据转换发生,例如:将整数转换为对象引用。

Java安装配置

各平台安装程序可以到SUN的网站(java.sun.com)下载,在安装完成后应该进行配置,通常集成开发环境(IDE)自带JDK或进行了自动配置,如果我们通过控制台编译或运行Java程序则必须手动配置。

JDK: 是Java开发工具包(J2SE Software Development Kit)的缩写,用Java语言编写applet小程序和应用程序的软件开发环境,Java开发者应该安装JDK,并且通常JDK中已包含一个JRE拷贝。

JRE: 是Java运行环境 (Java Runtime Environment) 的缩写。它基本上就和Java虚拟机是同一个概念,运行Java程序的应该安装JRE。

(JDK安装目录示例)

? bin:java常用开发工具目录,例如javac,java,jar等工具。

? demo: sun提供的一些用于java开发的示范,如有关applets,jfc,plugin等。

? include: Java提供用于c/c++调用的.h文件;

? jre: 对应版本的java运行环境文件

? sample: java案例。

? lib: java类库目录

Windows环境变量

应设置的环境变量包括:JAVA_HOME, PATH, CLASSPATH。

假设J2DK安装在“C:\JDK1.5”,环境变量可设置为:

? JAVA_HOME=C:\JDK1.5

? CLASSPATH=.

? PATH=%JAVA_HOME%\BIN;%PATH%

(Windows下环境变量的设置)

Linux下环境变量

假设jdk安装在/home/jdk1.5目录下,修改配置文件 /etc/profiles ,在文件的尾部加入如下信息:

JAVA_HOME=/home/jdk1.5

CLASSPATH=.

PATH=$JAVA_HOME/bin:$PATH

第一个Java程序

编写一个程序,输出一行文本信息:"Welcome to Java Programming"

编辑器的使用

Java代码的开发可以使用常见的编辑器,如:写字板,Editplus,Ultra Edit,也可以使用专用的开发工具,如:JBuilder, Eclipse, NetBeans,JCreator等。在开始学习时,建议先学习使用带有关键字颜色提示的EditPlus开发Java代码。

Editplus下的Java程序显示

编写程序

1 //这是一行注释

2 public class Welcome {

3

4 public static void main(String[] args) {

5 System.out.println("Welcome to Java Programming!");

6 }//main方法结束

7

8 }//类结束

行1:注释:用来说明程序的作用

行2:开始声明一个类,类名为Welcome, class是JAVA的关键字,用来定义类,public为访问说明。在这里我们声明了一个类,类的名字为Welcome.

行3,7:空行,用来增加程序的可读性

行4:是JAVA应用程序必须包含的方法,被称为入口方法:程序运行时从这里开始执行,其形式必须是:

public static void main(String[] args) 或者

static public void main(String[] args)

上面的语句里,只能修改 args这个字符串数组的名字。

static 表示直接能够加载该方法运行。

void 是方法的返回类型 ,每个方法必须有返回类型(除了构造方法)。

main是方法的名字,是程序的入口调用的方法。

String[] 是方法的参数,必须是String 数组类型的 。

行5: System.out.println("Welcome to Java Programming!");

就是输出字符串的,双引号中的字符串会在控制台输出。

也可以使用以下语句输出一个对话框显示信息:

JOptionPane.showMessageDialog(null, "Welcome\nto\nJava\nProgramming!" );

程序的开头要加入以下语句:

import javax.swing.JOptionPane;

行6:"}"表示方法的结束。

行8:表示类的结束。

编写完成后,以Welcome.java文件名保存到指定目录中。

配置环境和编译

在控制台使用如下命令编译刚才保存的文件Welcome.java

将会生成对应类名的.class文件:Welcome.class,如下图:

在Editplus下也可以直接编译并运行JAVA代码,配置如下:

打开用户工具栏:视图/工具栏/窗口/用户工具栏

选择配置工具栏

配置相应的内容

运行JAVA程序

在控制台使用如下命令运行刚才生成的文件Welcome.class

注意:运行时,不可以加文件的扩展名.class

内容总结

JAVA是一门丰富而且复杂的技术;

JAVA虚拟机处于核心地位,保障了JAVA的平台独立性;

虚拟机可以进行自动内存管理,并且进行安全检查;

JAVA包含三个版本,分别适用于不同的应用需求;

开发JAVA程序的三大步骤:编写JAVA源程序,编译源程序,运行程序。

独立实践

实践1: 编写程序,实现在控制台输出一行文本"I LOVE JAVA!"。

实践2: 编写程序,实现在对话框输出文本"I LOVE JAVA!"。

实践3: 假设:Welcome.java在d:\study,编译后生成Welcome.class.

如何可以在任意路径运行此程序?

实践4:以下main函数定义正确的是:

1. public static String main(String[] args)

2. public static void main(String args[])

3. public void main(String[] args)

4. static public void main(String args[])

5. public void static main(String[] args)

第二章: 面向对象概述

学习目标

? 掌握什么是面向对象以及面向对象的主要特点

? 了解OOA、OOD、OOP的概念以及之间的关联

? 了解面向对象程序语言的发展简史

面向对象(Object Oriented)

面向对象是一个广泛使用但涵义并不清晰的术语。上世纪80年代面向对象主要指一种程序设计方法,但在随后的发展中,面向对象的方法已经渗透到计算机软件领域的许多分支。在了解面向对象之前我们必须得了解两个重要概念:对象和类。

什么是对象?

首先,我们来了解一下什么是对象。对象的概念最早出现于五十年代人工智能的早期著作中,在现实世界中,我们可以将对象定义为:

对象(Object):一个具有明确行为的有形实体,具有行为和状态,有着清晰的边界。

以面向对象的眼光来看待现实世界,现实世界中的每一个具体事物,我们都会将其看作一个对象,比如狗、电视机、自行车等,都可以是对象。狗有特定的名称、毛色、品种、会饿等特点,还有狗叫、尾巴左右摇摆等动作;同样的,自行车也有品牌、重量等特点,同时具备换速、刹车等动作;依此类推,我们不难发现,现实中的每一个对象都会有自己的特点以及相关动作,我们将对象所有特点(属性)称之为对象的状态,而动作称之为行为。

属性(Attribute):对象的一种内在的或独特的性质、特点、质量或特征,它构成了一个独一无二的对象(摘自<<Objects-Oriented Analysis and Design with Applications>>)

每一个学生具备名称、性别、年龄、身高、长相、爱好等属性,通过这些属性我们可以很快地区分所有的学生对象,因为这些属性构成了每一个独一无二的学生对象。比如说,在大部分情况下,我们会通过学生名称来区分每一个学生对象。我们经常会说:某某人很高,某某人很年轻等,高和年轻实际上就表现出某一个对象的内在的特点。因此,可以这样理解,我们是通过每一个对象特有的属性来区分各个对象。

注意:属性都有其对应的值,而且某些属性值是可以动态改变的,比如学生的体重、爱好是可以改变的,而DNA、指纹却是不能改变的。

随着属性值的改变,状态也随之改变了。比如,学生甲的身高值逐渐增长了,那学生甲的状态也发生了变化----长高了!

状态的具体定义如下:

状态(State):包含对象的所有属性以及属性值,会随着属性值的改变而改变。

没有一个对象是孤立存在的,对象之间是相互作用的。行为就是指这种作用。

行为(Behavior):是一个对象根据它的状态改变和消息传送所采取的行动和做出的反应,代表了对象对外的可见、可测试的动作。(摘自<<Objects-Oriented Analysis and Design with Applications>>)

行为和状态是相互的。Dog处于饿的状态时,就会采取进食等行为,进食完毕后,Dog又会处于饱的状态。现实中,大多对象是通过接受另一个对象传递的一个消息(操作)来改变自身的状态。比如:驾驶员扭动汽车启动钥匙至关闭位置,此时,汽车关闭引擎。其中,驾驶员、汽车都是对象,而关闭引擎则是一个特定消息,是由驾驶员传递给汽车的消息,汽车根据这个消息改变状态---从启动状态变为关闭状态。因此,消息实际上就是一个对象对另一个对象施加的操作。

小练习

列举现实生活中5种对象以及相关的属性和可能的行为?

在软件开发中,对象一词的正式应用是在Simula*语言中,在Simula中对象是用来描述现实的某个方面。对象的范围更广,除了现实中描述的对象之外,还包括在软件设计中产生的一些产物,这些产物一般用来实现更高级的行为功能。

Simula--20世纪60年代产生的一种面向对象语言,是所有基于对象和面向对象语言的原型,用于描述系统和开发仿真的语言。

在软件中,对象代表一个单独的、可确认的实体,可以是具体的也可以是抽象的,必须有明确定义的一个这样的角色。相似对象的属性和行为在它们的共同类中定义。

在软件中,对象可能有很清晰的边界,然而却是抽象的;也可能是具体存在的,但边界不明确。注意,软件中的对象和现实中对象同样有其自身的状态和行为!每一个对象在软件中都会拥有自己的空间(内存)和唯一标识名。一般,我们也将对象称之为实例(instance)。通俗地讲,对象指的是客观世界中的实体, 它能"知道一些事情"(即存储数据),"做一些工作"(即封装服务),并"与其它对象协同"(通过交换消息),从而完成(模块化)系统的所有功能。对象反映了系统保存和处理现实世界中某些事物的信息的能力。

什么是类?

在现实世界中,我们会将所有脊椎动物对象进行分类:鸟类、两栖爬行类、哺乳类,比如我们将leopard、tiger、monkey等脊椎对象都归纳为哺乳类,而eagle、crane、swan属于鸟类。那我们又是怎样去判断哪种动物是属于哪一类的?

我们发现,leopard、tiger、monkey都具备viviparity(胎生)、lactation(哺乳)、coat(皮毛)等哺乳动物特征,而eagle、crane、swan却具备feather(羽毛)、flight(飞翔)、beak(喙)等鸟类特有的特征。不难发现,。这些对象都具有一些共同的属性和行为,通过这些属性和行为的划分,我们将leopard、tiger、monkey划分为哺乳类,而eagle、crane、swan划分为鸟类。

类和对象是紧密关联的,没有对象就没有类!

类的定义:类(Class)是一组共享公共属性和公共行为的对象的集合。

类可以是一种抽象,是一个蓝图,即可以不具体存在;它实际上是对一组对象的公共部分的归纳集合,而对象却是一个具体的存在,是实体而非抽象。我们可以这样说:类Mammal代表所有哺乳动物的共有特征,所有的非公有特征都不属于类Mammal,比如皮毛的颜色、喜好的食物等都不会归纳到类Mammal。而当我们想去识别某个特定的哺乳动物时,就必须指定"这个哺乳动物"或者"那个哺乳动物",即对象,也就是说,当我们需要去分析哺乳动物特有的属性和行为时,那就必须针对具体的对象来研究,而不是针对一个抽象的类!

注意:在软件开发中,我们将一个对象称之为类的一个实例。

没有公共结构和公共行为的对象,我们不能将它们组合在一个类中。比如,我们都知道不能将pinaster和squirrel组合成一个类,是因为pinaster和squirrel并没有共享结构和公共行为,比如 pinaster有枝叶而squirrel没有,squirrel能行走而pinaster不能等等。

在现实世界中,我们认识事物的时候都是首先将所见事物进行分类,然后按照每种类别去逐一研究事物对象。同样,在软件领域中,我们也是将问题进行分解分类(将大问题分解成许多小问题,每个小问题可以归纳为一类),然后再针对每一类进行分析设计以及实现,这就是广义的面向对象。

面向对象定义:尽量模仿现实世界,在软件中将复杂问题中的实体都作为一个对象来处理,然后根据这些对象的结构和行为再划分出类(即实现问题的分解),最后实现软件的模拟仿真。

也可以理解为:现实世界中的实体抽象成模型世界、程序世界中的对象和类,设计它们以完成预期任务并具有更好的可维护性和健壮的结构。

注意:在软件领域中,类包含了对象的属性和行为的定义,是一组属性和行为操作的集合!

只有掌握了对象和类的概念,我们才能清晰、准确地明白什么是面向对象!当然,对面向对象的深入了解还必须掌握面向对象的一些相关特性,在下一小节我们将具体学习。

面向对象的主要特性

程序语言发展到如今,已出现了多种风格不一的编程语言,大体主要分为以下两种风格的语言:

? 面向过程(以C语言为代表)

? 面向对象(Java、Smalltalk、C++等)

并不是说哪种语言是最好的,每种风格语言的好坏是针对不同方面的应用程序而言的。比如说,面向过程语言适用于计算密集型操作(复杂算法等)的设计,而面向对象适用于广阔的应用程序范围(这也是Java能迅速发展并占据广大市场的原因之一)。

面向过程(Process Oriented):基于该风格的程序的设计都是在想怎样才能一步一步的解决问题,思维方式是过程,或是说步骤。

那怎么区分面向过程和面向对象的区别,只要是具备以下特点的语言我们都可以将其看作为实现了面向对象的语言:

抽象(Abstraction)

在前面,我们已知道通过抽象创建出了类,类就是抽象的一种表示。抽象是解决问题复杂性的一种基本方法,抽象会重点强调我们应该注意什么,而不应该注意什么!通过抽象我们可以着重实现需关注的细节而忽略一些非实质性或无关紧要的细节,从而能够更好的分析问题解决问题!

抽象:表示一个对象与其它所有对象区别的基本特征,为不同的观察者提供不同的角度来观察对象。

对同一个对象---电视机,维修员只会关注电视机的内部结构:线路是否断裂、显像管是否损坏等,而购买电视的客户却只会关心电视机的画面是否逼真、外观是否好看等等,客户不会也不用关心电视机的内部结构。可以看出,维修员只关心电视机的内部,购买用户只关心电视机的外观及实用性,他们观察电视机的角度不一致,这就是抽象的实现,在现实生活中,到处都存在着抽象!

抽象强调实体的本质、内在的属性,在软件开发中,抽象指的是在决定如何实现对象之前对对象的意义和行为的定义----即类的实现!通过对类的了解,我们也可以将抽象定义为:从许多事物中舍弃个别的、非本质的特征,抽取共同的、本质性的特征。

为什么需要抽象?

首先,问题的复杂性日益增长,其中描述的事物也越来越复杂,如果分析员去了解每一个事物的所有方面,那将是一个穷举耗时、低效率的工作

其次,如果没有识别所有对象的共同特征的行为,那么对这些对象的分类将无法进行!

对象分类是面向对象中非常重要的环节,而抽象则是实现该环节的必须解决方案!抽象的本质就是将问题分解,在面向对象中,所有对象被抽象成各种类,从而让程序员集中关注于功能的实现!

小练习

列举现实生活中的抽象例子,最少3例。

封装(Encapsulation):

我们再来思考上面的例子,对于客户来说,他只会关心电视机的外观及性能是否良好等方面,电视机中的显像管、集成电路板对客户而言是不可见的,即电视机的生产商已将这些配件通过电视机外壳包装了起来,对客户,配件是隐藏的!现实中,这样的例子很多,比如计算机对于购买者而言,其组成部分CPU、Mainboard、HD等PC配件都被封装在机箱内,是不可见的!

封装:是一种信息隐蔽技术,是一个划分抽象的结构和行为的过程。

封装和抽象是互补的,抽象着重于对象的行为,而封装着重于对象的行为的实现。抽象实现抽取众多对象的公共特征和行为,而封装则是在不同的抽象之间设置明显的分隔线,从而导致每一个观察者关注内容的明显分离!没有抽象的封装将无意义;没有封装的抽象是不完整的!

在开发中,封装可以更通俗的理解为:把代码和代码所操作的数据捆绑在一起,使这两者不受外界干扰和误用的机制,封装可被理解为一种用作保护的包装器,以防止代码和数据被包装器外部所定义的其他代码任意访问,对包装器内部代码与数据的访问通过一个明确定义的接口来控制。

封装代码的好处是每个人都知道怎样访问代码,进而无需考虑实现细节就能直接使用它,同时不用担心不可预料的副作用。

继承(Inheritance)

类和类之间的关系之一,是面向对象系统的最基本要素。继承表明为一个"是一种"的关系,在现实中有很多这样的例子:人类是一种哺乳动物;树是一种植物类…….。为什么人类是一种哺乳动物?答案:因为人类具备所有哺乳动物的特征(结构和行为),也就是说,人类继承了哺乳动物的特点!我们经常会这样描述:儿子继承了父(母)亲的优(缺)点!

继承:一个类共享一个或多个类中定义的结构和行为,表示的是一种泛化/特化的层次关系。

注意:类和类之间实现继承的前提是这些类之间有着必然的联系---"类A是一种类B"!假如:我们将树继承于哺乳动物,而人类则继承于植物类!大家想想看,结果会如何?

我们在使用继承的时候必须得遵守这样的检验标准:如果类B"不是一种"类A,那么类B必然不是从类A继承而来!

? 当类B继承于一个类A,那么这种继承称之为单继承。

? 当类B继承于多个类A、C、D等,那么这种继承称之为多继承。

也可以这样理解,当实现单继承时,那类B继承了类A的所有属性和方法(比如人类继承了哺乳动物的所有属性和行为);而实现多继承时,类B继承了A、C、D等多个类的所有属性和方法(比如儿子继承了母亲和父亲的所有优、缺点)。其中,类B称之为子类(派生类)subclass,而类A、C、D则称之为超类(父类、基类)superclass

泛化(generalization):将一些有关联的不同类中的共同结构和行为抽取出来,构成一个新的超类。

特化(specialization):和泛化正好相反,从一个或者多个超类中派生出多个子类。

简单理解就是:超类的创建代表泛化,子类的派生代表特化!

子类除了能通过继承共享父类的属性和行为之外,还能修改继承于父类的属性和行为,最重要的一点就是:能在继承父类的基础上定义属于子类自身的属性和方法,从而实现扩展!以上这段话就是继承的最神奇之处!面向对象使继承得到了充分的体现!

学习技巧:大部分同学在初学时往往对继承、抽象等概念一知半解,建议大家多结合实际例子来学习,可以多想想周边的现实世界中哪些是继承、哪些是抽象。

多态(Polymorphism)

我们来假设思考一下现实中的一个问题:

假设,某条snake和某条cabrite,它们都取名为Jack,注意,snake和cabrite有着共同的超类---爬行类,都具备爬行类的公共行为:爬行。但我们发现,虽然一个名字Jack代表两种不同类对象,但是这两个对象却是用不同的方式来爬行,Jack(snake)通过蠕动身上的鳞片来爬行,而Jack(cabrite)则通过四肢来爬行。

虽然,在现实中我们可以通过给这两个对象取不同名字用以区分,但上面的例子向我们演示了这样一个过程:

一个名字可以表示许多不同类(这些不同类必须拥有一个共同的超类)的对象,从而实现以不同的方式来响应某个共同的操作集。

以上就是多态的定义,多态只有通过继承才能实现!在后面章节我们会详细掌握多态以及其运用。

以上就是面向对象的一些主要特征,具备了这样特征的程序语言我们就可以称之为面向对象语言。

面向对象的优点

先来看一下软件复杂性,软件固有的复杂性:

? 维护过于困难

? 修改每一个地方将会牵涉到许多功能模块的变动,复杂性加大。

? 过于僵硬,扩展困难

? 由于系统的关系错综复杂,很难添加新的功能。

? 重用率过低,导致大量冗余

? 在每个项目中往往会出现大量的代码冗余,不能很好的重用。

? 耦合度过高,导致理解、修改困难

? 每一个程序之间联系过于密切(强耦合),增加了阅读和修改的复杂性。

一个好的系统应该具备如下的性质:

可扩展性、灵活性、可插入性。

--Peter Code[CODE99]

通过面向对象就能很好的处理以上的复杂性。在面向对象过程中,我们根据问题领域中的关键抽象来分解系统,识别关键对象并将其分类,最终达到将复杂问题分解成多方面来分析和实现。

比如,PC机就是一个很复杂的设备,为了更好的了解PC,我们就会将PC的组成部分分解,再逐一了解,从而达到复杂问题简单化。PC包含CPU、display、keyboard、HD等,通过对这些部件的逐一熟悉,PC就不会显得很复杂了。而且,如果我们只想了解PC的处理速度,那么就只需专注于对CPU的了解(通过抽象来实现)。

面向对象是一种尽量模仿现实世界的思想,以上对PC的了解就是我们在现实中对一个复杂事物进行分解熟悉的一个过程,面向对象就是这样的一个过程,只是复杂许多。

面向对象的一些主要优点:

? 和现实世界更接近,更符合我们人类的思维逻辑

? 开发出的系统容易维护和修改

? 适用于开发复杂、庞大的系统

? 开发出的程序更稳定合理

? 由于继承、类的机制,更容易实现重用,减少冗余

? 由于实现数据和逻辑的分离,维护和扩展变得更加容易

面向对象的分析、设计和编程

在面向对象开发中,一般都会遵循三方面来分析解决问题:

? 首先,通过一些好的方法对问题进行分析,寻找问题中的对象实体、类。

? 其次,基于分析结果使用类和对象抽象在逻辑上构建系统,实际上就是使用面向对象的各种分解方法来建立系统的逻辑、物理模型和动态、静态模型。

? 最后,就是运用不同的面向对象语言对设计的结果的实现,即类和对象的实现。

下面我们就这三方面具体来学习。

面向对象分析(Object-Oriented Analysis)

面向对象分析(OOA)方法是建立在对象及其属性、类属及其成员、整体及其部分这些基本概念的基础上。

大英百科全书指出:

人类在认识和理解现实世界的过程中普遍运用着三个构造法则:

a.区分对象及其属性。例如,区分一棵树和树的大小或空间位置关系。

b.区分整体对象及其组成部分。例如,区分一棵树和树枝。

c.不同对象类的形成及区分。例如,所有树的类和所有石头的类的形成和区分。

OOA就是建立在以上三个原则的基础上的,每个软件都建立在特定的现实世界中,OOA阶段产生的模型就是用来形式化该现实世界的"视图"---我们称之为建模。有许多优秀的方法作用于整个OOA阶段,如Shlaer-Mellor, Jacobson, Coad-Yourdon, Charles,Abbott等,还有现今已成为建模主流的UML(Unified Model Language)。

OOA就是仔细的划分系统的各个部分,明确它们之间的层次关系,然后将各个部分作为一个对象进行功能上的分析的一个过程。包括业务分析和需求分析。

OOA:是一种分析方法,它以可在问题域的词汇表中找到的类和对象的观点来理解、审视需求。(摘自<<Objects-Oriented Analysis and Design with Applications>>第二版)

虽然面向对象分析和面向对象设计的侧重点很不相同,但二者之间的界限是很模糊的,在开发中,我们往往不能明确的区分二者的范围。从上面的描述可以知道,在分析中,我们通过发现构成问题域中的词汇表来寻找类和对象,从而模拟现实世界对软件建模。

OOA阶段的方法众多,各有其特点,这里我们只介绍一种最简单的方法:非形式化的语言描述。这个方法是由Abbott提出,他建议用语言(英语)写出问题的描述,接着在名词和动词下面划线;名词代表对象,动词代表对象的操作。

例子

描述一个顾客购买机票的场景。

顾客选择某一个柜台,首先顾客向柜台助手查询某一天的航班信息以及是否有机票预订,柜台助手往电脑里输入查询信息并查看结果,如果有机票,顾客则填写预订表预订机票,柜台助手输入预订信息并打印机票,最后将机票返回给顾客。

单下划线代表对象,双下划线代表操作。通过Abbott方法我们可以分析出该场景中的对象和操作,名词:顾客、柜台助手、航班信息、机票、查询信息、机票;动词:查询、预订、打印。

在使用UML建模时,OOA阶段主要是通过另一种方法---用例分析来实现系统的用例图以及简单类图和时序图等,既UML是将系统用一种统一标准的图形来表示(在后期会专门学习)。

面向对象设计(Object-Oriented Design)

OOD是对OOA的细化,强调的是复杂系统的正确和有效的构建,通过不同的方法来着重于系统的逻辑设计、物理设计。如果说分析是产生系统的类和对象,那么设计则确定类和类之间的关系、对象和对象之间的关系、类的状态转换、模块之间的依赖性等。

OOD特点

? 没有严格的界线

? OOD的结果直接用于编码

? 与OOA的输出一样,只是更加详细完善

OOD:是一种设计方法,包含面向对象分解的过程以及一种表示方法,用来描述设计中系统的逻辑与物理模型和动态与静态模型

OOD步骤

? 细化重组类

? 细化和实现类间关系,明确其可见性

? 增加属性,指定属性的类型与可见性

? 分配职责,定义执行每个职责的方法

? 对消息驱动的系统,明确消息传递方式

? 利用设计模式进行局部设计

? 画出详细的类图与时序图、状态图、对象图等(UML)

OOD与OOA的区别

? OOA偏重于理解问题,描述软件要做什么,而OOD偏重于理解解决方案,描述软件要如何做

? OOA只考虑理想的设计,不关心技术与实现底层的细节,而OOD需要得到更具体详细更接近于真实的代码的设计方案

? 在设计结果的描述上,OOA偏重于描述对象的行为,OOD偏重于描述对象的属性与方法

? OOA只关注功能性需求,OOD还需要关注非功能性需求

一个设计的好坏直接决定程序的成败,因此,在往后的项目实战中,大家一定要全面、仔细、合理的进行设计。

面向对象编程(Object-Oriented Programming)

OOP是对设计的一种代码实现方法,在该阶段,我们将采用合适的面向对象语言来编码实现设计阶段产生的系统架构。

OOP:是一种实现方法,程序被组织成对象的协作集合,每一个对象代表某一个类的实例,而类则是通过继承关系联系在一起的。

OOP阶段必须满足三个标准条件

? 使用对象而不是算法(面向过程则是使用算法)作为其基本逻辑构件

? 任何一个对象都必须是某一个类的实例

? 类通过继承关系和其它类相关

OOP的关键就是面向对象语言的选择以及面向对象特点的实现。实际上,有很多程序员对面向对象和基于对象(Object-Based)的语言概念很模糊,容易混淆。当一种语言满足以下条件时才能称之为面向对象语言:

? 对象必须是数据的抽象

? 每一对象对应相关类

? 子类可以从超类中继承属性和方法

如果某种语言只满足了以上的前两个条件,而第三个条件并没满足(如JavaScript语言),那么我们就将该语言称之为基于对象的语言。

面向对象语言的发展简史

编程语言从最初的复杂机器语言发展到如今的高级面向对象语言,经历了近50年的历史,其中,产生了四代计算机语言:

? 第一代:(1954-1958)

Fortran I、ALCOL 58、Flowmatic等语言,主要应用于数学和工程应用,几乎全部使用数学词汇。

? 第二代:(1959-1961)

Fortran II、ALCOL 60、COBOL、Lisp等语言,重点在于算法抽象,集中于告诉机器应该做什么,比如通过这些语言编写的算法会告诉机器应该在查看员工数据之前进行排序。

? 第三代:(1962-1970)

PL/1(FORTAN+ALCOL+COBOL)、Pascal、Simula(最早出现类和对象的思想的语言),对数据(类)抽象,描述各种类的类型。

? 第四代:(1970-至今)

Smalltalk、C++、Ada、Java、Eiffel、PHP等语言,都是面向对象或基于对象的语言。

发展到至今,高级语言已出现了2千多种,现在仍在不断出现(如Ajax等新语言),呈一个高速发展阶段,而现今主流的语言就是我们所学的面向对象语言,对于复杂的问题面向对象语言能比任何其他种类语言更好的解决,其中的代表性语言是-Java。

Simula是最早出现类和对象的基本思想的语言,后期产生的各种面向对象或者基于对象语言均是从Simula语言的基础上发展而来。比较典型的有:Java(纯面向对象)、Smalltalk 80(纯面向对象)、C++(面向对象)、Object Pascal(面向对象)、Ada(基于对象)等。

对象不是类,类却可以是对象,请思考是否正确

对象之间的关系

? 上下级关系

对象之间的一个物理或概念联系,一个对象通过它与另一个对象之间的上下级关系协作,通过上下级关系,对象和对象之间可以相互传递消息,即一个对象可以通知另一个对象并产生响应。注意:对象之间可以双向传递消息。

? 聚合关系

表示一个整体/部分的层次,表示物理包含和非物理包含。如:PC包括CPU、keyboard、memory bank,这就是物理包含;而股东拥有股票,但股票不是股东身体的一部分,这就是非物理包含。

OOA模型的结构

? 第一层:对象-类层

表示待开发系统的基本构造块。图符的外层边界表示实例边界,实际上它表明对象是非空的。而图符的内层边界则表示类边界。在某些情形下,定义这种对象是很有用的。我们将它们称为模板类或抽象类。模板类可以为结成较高级的聚合体提供一条方便的途径。

? 第二层:属性层

对象的属性和实例连接共同组成了OOA模型的属性层。我们把对象所存储的数据称为对象的属性。类的实例之间互相约束,它们必须遵从应用论域的某些限制条件或事务规则。例如,当定金取消后,相应的订户也应该被取消,这可能是一项事务规则。我们称这些约束为实例连接。

? 第三层:服务层

对象的服务和消息通信组成了OOA模型的服务层。我们把对象所做的工作称为服务或方法。系统的不同对象都分别执行一定的工作或功能,它们之间通过消息通信,即所谓的协同。对象的服务及对象实例之间的消息通信共同组成了OOA模型的服务层。

? 第四层,结构层

该层负责捕捉特定应用论域中的结构关系。泛化、特化等结构,整体-部分结构表达了人类的一种基本组织方式,即自然的整体和部分的结构关系,从而把一些部分的聚合构造成整体。例如,一辆汽车由发动机、传动装置和刹车装置组成。

? 第五层:主题层

相当于全局系统的子系统或子模型。由于OOA模型的结构庞大而复杂,因此众多的对象有时很难处理。于是,可以把对象归到各个主题层中,可以把有关的对象用一个边框框起来加以实现。例如,在一个控制系统中,众多的对象可分为"管理"和"控制"两个主题。

类和类之间的关系

? 关联

? 一对一,保险合同与保险人、订单与客户

? 一对多,订单与商品、职员与部门

? 多对多,学生与课程、项目与程序员

? 继承

? 泛化和特化的实现,分为单继承和多重继承

? 聚合

? 同对象的聚合类似

? 实例化

? 定义类的一个对象

参考书籍:<<Objects-Oriented Analysis and Design with Applications>>(Second Edition)

内容总结

? 对象(Object)是一个具有明确行为的有形实体,具有行为和状态,有着清晰的边界,具备属性、行为、状态。

? 对象的属性及其值表现状态,不同的状态会有不同的属性。

? 对象的状态和行为是相互的,行为可以改变状态,状态可以促使行为的产生。

? 类(才lass)是一组共享公共属性和公共行为的对象的集合,类是一种抽象,是蓝图,不具体存在。一个对象称之为类的一个实例。

? 类包含了对象的属性和行为的定义。

? 面向对象的主要特征:抽象、封装、继承、多态。

? 在OOA中,我们通过发现构成问题域中的词汇表来寻找类和对象,从而模拟现实世界对软件建模。

? OOD是对OOA的细化,强调的是复杂系统的正确和有效的构建,通过不同的方法来着重于系统的逻辑设计、物理设计。

? OOD与OOA的区别。

? 面向对象与基于对象语言的区别。

? 四代程序语言的发展,面向对象语言属于第四代语言。

独立实践

? 观察猫和老鼠,用自然语言描述出猫和老鼠的属性、状态、行为。

? 描述猫抓老鼠的过程。

? 试分析:”螳螂在前,黄雀在后”这句话包含几个类及这些类之间的关系.

第三章:面向对象的程序设计学习目标

? 类和对象的描述

? 类,属性,方法,构造方法定义

? private和public访问权限的介绍

? 源文件的布局

? 包的声明和导入

? 包与目录的布局

? CLASSPATH环境变量的使用

? API文档的使用

类和对象的描述

现实社会中一切皆对象。比如:人,树,石头,猫,鸟,汽车,地球等等。任何一种对象都具有静态的属性,但不一定具有动态的行为。比如:石头。一般情况下,对象既有静态的属性,也有动态的行为。对象本身的属性和行为之间可以相互影响,比如:一个人饿了(属性),就会去吃饭(行为)。相反,这个人吃饭(行为)后,就饱了(属性),体重(属性)也增加了。不同的对象之间也可以相互作用。比如:人看到汽车开过来了,就会沿着路边走。如果这个人站在路中间不动(他不怕死),那么汽车就会停下来。那么怎么用Java语言来实现上述功能呢?后面实例分析有实现。

如同建筑设计师设计建筑图(建筑的蓝图),可以用该图来盖出许许多多这种风格的房子一样。类是对象的蓝图,是用来描述对象的,你可以用该类,来实例化许许多多个该类型的对象,类就是对象的模板。在类中定义了一套数据元素(属性)和一套行为(方法)。数据是用来描述具体的一个对象(静态),行为是用来描述该类型对象的共性,也就是该对象能够做什么(动态),以及完成相关对象之间的交互,从而改变对象的状态。同样对象的状态也能够对象的行为。属性和方法都叫做类的成员。例如杯子装水的时候:最大盛水量和当前盛水量。盛水的方法要始终跟踪这两个属性。 装水时改变了当前盛水量的属性,同样当当前盛水量等于最大盛水量(水装满时),就会影响装水的行为,将不再倒水。

我们下面通过一个具体事例来说明Java中如何实现以上概念。

例1:

编写一个类,描述人吃饭,体重增加这个简单操作。

下图描述了一个“人”

这是一个UML中的类图,我们对它进行简单说明。

? 第一行:是类名Person,代表我们正在说明一个“人”的概念。

? 第二行:是属性,“-” 号代表这个属性只有这个类自己可以访问,weight代表属性的名字,double表示属性的类型,这里意思是“人有一个体重的特性,体重可以是小数,别人不能直接看出人有多重,必须使用某种称量体重的方法”。

? 第三行、第四行:是构建器,“+” 号代表public访问权限,含义是任何人可以访问到它。构建器是外界创造出这个“概念”的实际“例子”的入口,第三行是按照“缺省”方式构建,第四行是按照特定方式构建,特定方式是指按照参数指定的属性构建

? 第五行、第六行:是方法,其中eat方法有参数,参数名字是temp,参数类型是double,该方法的返回类型为void,该方法含义是人可以吃一定数量的食物,吃完不需要给外界任何回馈。第六行的方法getWeight()没有参数,返回double类型,含义是看这个人的重量。

声明类

Java中的语法和C一样,语句都是以分号结束,区分大小写。

在Java技术中采用下列方法声明类:

<modifier> class <name>{

<attribute_declaration>

<constructor_declaration>

<method_declaration>

}

说明:

<modifier>:暂时只用"public",含义为:可以被所有其它类访问。或者不加public, 在修饰类的访问权限的时候,只有两种:1,就是加上public,表示所有类都可以访问。 2,就是什么也不写,表示本包访问权限,在讲到包的含义时再理解。

<name>:任何合法的标识符。它代表所声明类的名称。

Java中的标识符(类名,变量名,方法名)是由字母,数字,下划线(_),美圆符($)组成,数字不能用于标识符的开始。其中长度不受限制,不能使用java中的关键字,并且是区分大小写的。比如:class,void等关键字。Java中的关键字都是由小写的字母组成的,所以在我们并不知道java中有那些关键字的情况下,在定义标识符的时候,只要不全是小写的字母,就不会和java中的关键字相冲突。

<attribute_declaration>:声明属性。也就是说用变量表示事物的状态。

<constructor_declaration>:声明构造函数。也叫构造方法,也叫构造器。是用来实例化该类的实例(对象)的。

<method_declaration>:声明方法。来说明事物能够做的事情,也就是行为。

注意:属性,方法,构造函数在类中的顺序没有固定的约束。一般习惯性地先声明属性,后声明方法(习惯性地把构造方法写普通方法的前面)。

所以Person这个类的初始版本应该是:

public class Person{

}

声明属性

<modifier> <type> <name> [ = <default_value> ];

说明:

<name>:任何合法的标识符。它代表所声明属性的名称。

<modifier>:暂时只用“public”和“private”,其中private含义为:仅能被所属类中的方法访问,这称作封装。

<type>:可以是任何原始类型(基本类型)或其它类(引用类型)。

[=<default_value>]是给属性初始化为给定的值。如果没有的话初始化为默认的值。(基本类型的初始化相应的值:比如:int,short,byte,long,char(Unicode码值)初始化为0,float,double初始化为0.0,boolean初始化为false,所有的引用类型都初始化为null)。

注意:Java语言与其它语言不同,在JAVA中声明属性和初始化属性值必须一句完成。不能分开写:先声明,再另起一行初始化。

例如:

private int a ;

a=5; //错误

private int b=6;//声明一个属性 b,并初始化为6;

在类里面除了声明语句之外,是不能直接写其它执行语句的。 a=5 是赋值语句。如果要执行语句应放在语句块(方法块,初始化块等)里执行。

据此,我们的Person类成为如下样子:

public class Person{

private double weight;

}

声明构造器

<modifier> <class_name> (<parameter>) {

<statement>

}

说明:

<class_name>:是类名,构造器的名字和类名必须一样。

<modifier>:可以是public,指明可以被任何其它代码访问。private,指明仅能被同一个类中的其它代码访问。

<parameter>:向构造器传递参数,可以没有参数。传递多个参数时,参数之间用逗号分开。每个参数由参数类型和标识符组成,这和声明属性的方式很类似,但是不能向参数赋初始值。

注意:构造器必须没有返回类型,并且构造方法的名字和类名必须一样。当一个类里面如果没有声明构造方法,那么虚拟机将自动给该类一个默认的构造器(不带参数的构造器)。如果在该类里面,定义了带参数的构造方法,那么虚拟机将不再为该类提供默认的构造方法,也就是该类不能按缺省方式构建了。

我们的类此时变成:

public class Person {

private double weight;

// 该类的默认的构造器

public Person() {

}

// 带参数的构造器

public Person(double d_weight) {

weight = d_weight; // 实例化对象时,给weight属性赋初始值

}

}

声明成员方法

<modifier> <return_type> <name> ( <parameter> ){

<statement>

}

<name>:任何合法的标识符。

<modifier>:可以是public,指明可以被任何其它代码访问,private:指明仅能被同一个类中的其它代码访问。

< return_type>:指明方法返回值的类型。假如方法不返回值,应被声明为void。

<parameter>:向方法传递参数。传递多个参数时,参数之间用逗号分开。每个参数由参数类型和标识符组成。

注意:方法必须有返回类型,返回类型为void和没有返回类型是不一样的。除了返回类型void外,有其它返回类型的方法块里必须有return语句。

我们的类此时变成(Person.java):

public class Person {

private double weight;

// 该类的默认的构造器

public Person() {

}

// 带参数的构造器

public Person(double d_weight) {

weight = d_weight; // 实例化对象时,给weight属性赋初始值

}

public void eat(double temp) { // 吃饭的方法

weight = weight + temp; // 吃多少,体重就增加多少

}

public double getWeight() {// 得到人的体重属性

return weight; //返回weight属性

}

}

private封装

在类里面声明属性的时候,一般把属性的访问权限定义成private,封装的要求。这样只能在类里面访问该属性,在类的外面是没有访问的权限的,也就是说对于该类的实例(对象),是不能够直接访问该对象的属性的。这样就会保护对象状态不会非法改变。

比如,人的体重是不能直接修改的,通过吃饭可以增加人的体重,如果该人很瘦,是不能直接把20斤牛肉放到该人身上,就算增加该人的体重的。同样的道理,如果该人很胖,也不能够从该人身上割下20斤肉,而让体重下降20斤。

所以我们在以上的类中声明weight属性为private。

public 公共访问

在类里面声明方法的时候,一般把该方法定义成public访问权限。在程序运行的时候,就是通过对象和对象之间的交互来实现的。为了保证对象都能够执行功能(方法),应该把方法的访问权限定义成public。

我们对方法getWeight()的处理就是这样。

下面是测试Person的PersonApp.java

public class PersonApp {

public static void main(String[] args) {

// p1是声明的变量,类型是Person类型的,并且引用了Person类的一个对象,且使用默认的构造器构造对象

Person p1 = new Person();

// p2 同p1,使用带参数的构造器

Person p2 = new Person(120);

// p1所引用的对象(简称p1对象),吃了2.5斤

p1.eat(2.5);

// p2 对象 吃了4.3斤

p2.eat(4.3);

// 打印出p1的体重

System.out.println("p1的体重为:" + p1.getWeight());

// 打印出p2的体重

System.out.println("p2的体重为:" + p2.getWeight());

}

}

编译和运行的过程如下图:

例2

我们知道如果文件名出现重复,那么我们要放在不同的目录中,class文件作为类的字节码载体,如果存在类名重复,那么class文件就应该放到不同的目录下,就类似文件夹的方式来管理文件。在Java里面是通过包的结构来管理类的。下面我们就上面例子修改下,再加个类,定义不同的包,放在不同目录下进行访问。

源文件的布局

Java技术源文件只有三部分,采用下面的布局:

[<package_declaration>]

<import_declaration>

<class_declaration>

说明:

<package_declaration> 声明包的语句,包通常使用小写字母,用.作为分割符,这是一种逻辑结构划分的方法。

<import_declaration> 导入包语句

<class_declaration> 类的声明语句

源文件命名

源文件的名字必须与该文件中声明的公有类的名字相同。一个源文件中可以包含多个类,但是最多只能包含一个公有类,显然,这个文件的名字应该是这个public类的名字后缀是“java”。如果源文件中不含公有类,源文件的名字不受限制。

包的声明

多数软件系统是庞大的。为了方便管理,通常要将功能相似的类组织成包,通过包来管理类文件。在包中可以存放类,也可以存放子包,从而形成具有层次结构的包。包可以根据需要任意组织,通常,要按照类的用途、含义来组织包。如下UML 包图:

Java技术提供了包的机制,以次来组织相关的类。声明包的句法如下:

package <top_pkg_name> [.<sub_pkg_name>];

你可以使用package命令指明源文件中的类属于某个特定的包。例如:

package shenzhen.luohu;

public class Person{

//…

}

package声明必须放在源文件的最前面,或者说可执行代码的第一行,或者除了注释之外的第一行。一个源文件最多只能有一条package声明。一条package声明对源文件中的所有类起作用。如果你的源文件中没有package声明,你的类将在“缺省”包中,这个缺省包的位置就是当前目录。

包的导入

当你想要使用包中的类的时候,可以用import命令告诉编译器类在哪里。import命令的语法:

import <pkg_name>[.<sub_pkg_name>].<class_name | *>;

例如:

import shenzhen.nanshan.*;

import shenzhen.futian.*;

import java.util.List;

import java.io.*;

当你使用import指令时,你并没有将那个包或那个包中的类拷贝到当前文件或当前包中。你仅仅是将你在import指令中选择的类加入到你的当前名字空间。无论你是否导入当前包,当前包都是你的名字空间的一部分。

import命令指明你要访问的类。例如,你需要访问Writer类,你需要下面的命令:

import java.io.Writer;

如果你需要访问一个包中的所有类,你需要下面的命令:

import java.io.*;

但是不会导入java.io下子包的类。

默认情况下,系统将自动导入java.lang包。

import java.lang.*; //源文件里不管有没有写上该句,该句的功能永远存在。

import java.io.Writer;与import java.io.*;的区别如下:

如果写类名,那么虚拟机将直接从所在包里找到该加载执行,如果写*号,编译器会CLASSPATH指定的路径,一个一个路径去找,直到找到该类为止。

javac和java命令都有-classpath 参数,他们就是通知编译器或虚拟机在哪些路径中查找可能用到的类。

包与目录的布局

由于编译后的字节码文件在文件系统中存放,包结构就以目录结构的方式体现,包的名字就是目录的名字。例如,shenzhen.luohu包中的PersonApp.class文件应该在 path\shenzhen\luohu目录中。

*运行的时候进入到path目录下:

path>java shenzhen.luohu.PersonApp 或者

path>java shenzhen/luohu/PersonApp

我们没有使用-CLASSPATH参数,系统是如何工作的呢?

首先,系统级别的CLASSPATH环境变量应该是个“.”,表示当前路径,在运行javac或java的时候如果没有-classpath参数就会使用环境变量中的CLASSPATH环境变量,所以系统将从当前路径开始查找类,也就是在“path”路径下,那么按照shenzhen/luohu/路径查下去,确实能找到PersonApp类,所以运行正常,在这里我们把path路径称为顶层目录。

在这种情况下不能进入到shenzhen目录下运行:path\shenzhen>java luohu.PersonApp或者path\shenzhen>java luohu\PersonApp,也不能进入到上一级目录运行。假如你把shenzhen 放在daima文件夹里,你也不能进入到daima 那个文件所在目录下运行:

path>java daima\shenzhen\luohu\PersonApp 或者

path>java daima.shenzhen.luohu.PersonApp

如果想在任何目录下运行正确,必须有三个限制:

? CLASSPATH环境变量或-CLASSPATH参数已指向顶层目录

? class文件按包路径完整结构存放。

? 类的名字必须是全限定名,就是必须包含包路径的类名。

上面是运行时的目录结构,我们可以通过拷贝的方式将类文件防止为以上形式并运行成功。我们也可以在编译的时候用下面两种方式把包的结构生成出来。

通常我们需要把源文件和类文件完全分离,例如我们我们把源文件按包路径完整的存放到src目录中,我们将来编译的类文件将按照包路径完整的存放到build目录中,如下图:

为了达到以上目的,我们手工开始工作,虽然将来有集成开发环境(IDE)帮助我们,但是这对我们明白包的含义很有帮助。

1、手动建立文件夹。把包的结构创建出来

我们分别使用三个包,在一个我们选定的工作目录下按照上图结构创建好目录,其中build目录下的结构无需创建(但是build自身要创建),src目录中存放源文件,他们分别按照自己的package声明存放到不同目录。

2、自动把包的目录结构生成出来。

进入工作目录(src和build的上级目录),执行以下命令:

就自动把类按包的结构生成到build目录中了。

其中-d指定了生成带有包结构的类的顶层目录,-sourcepath 指定了源文件查找路径,显然,由于PersonApp要使用到其他包里的其他类,而这些类还是以源文件形式存在的时候,编译器必须能查找到他们并同时将他们编译成功才可以编译PersonApp自身,编译器对源文件的查找方式和虚拟机对类的查找方式类似。

<PersonApp.java>

//声明包

package shenzhen.luohu;

//导入包

import shenzhen.nanshan.*;

import shenzhen.futian.*;

//公共类PersonApp

public class PersonApp {

public static void main(String[] args) {

// p1是声明的变量,类型是Person类型,并且引用了Person类的一个对象

Person p1 = new Person();

// p2 同p1

Person p2 = new Person(120);

// c1是声明的变量,类型是Cat类型的,并且引用了Cat类的一个对象

Cat c1 = new Cat();

c1.jiao();

// p1所引用的对象(简称p1对象),吃了2.5斤

p1.eat(2.5);

// p2 对象 吃了4.3斤

p2.eat(4.3);

// 打印出p1的体重

System.out.println("p1的体重为:" + p1.getWeight());

// 打印出p2的体重

System.out.println("p2的体重为:" + p2.getWeight());

}

}

<Person.java>

//声明包

package shenzhen.nanshan;

//声明公共类Person

public class Person {

// 声明该类的一个属性,访问权限为private ,对该属性进行封装,实例化时,给该属性的初始化默认值0。

private double weight;

// 该类的默认的构造器

public Person() {

}

// 带参数的构造器

public Person(double init_weight) {

// 实例化对象时,给weight属性赋初始值

weight = init_weight;

}

// 吃饭的方法

public void eat(double temp) {

// 吃多少,体重就增加多少

weight = weight + temp;

}

// 得到人的体重属性

public double getWeight() {

// 返回weight属性

return weight;

}

}

<Cat.java>

//声明包

package shenzhen.futian;

//声明公共类

public class Cat {

public void jiao() {

System.out.println("cat jiao......");

}

}

运行时情况如下:

当然我们也可以在build目录下,利用CLASSPATH环境变量已经带有的.(当前路径)直接找到要运行的类(shenzhen.luohu.PersonApp),我们也可以利用绝对路径形式制定CLASSPATH,例如:

而且绝对路径相对路径的概念在编译、运行的时刻同样有效。

对于CLASSPATH我们可以通过环境变量方式设置,例如希望从系统级别设置类路径,也可以通过-CLASSPATH参数形式设置,例如希望临时更改类路径。

例3

人看到汽车开过来了,就会沿着路边走。如果这个人站在路中间不动(他不怕死),那么汽车就会停下来

这里涉及到两个对象的相互作用,我们先声明两个类,来描述上述现象,然后再用一个测试类来实现。

〈CarPersonApp.java〉

class Car {

private boolean moving;

public boolean getMoving() {

return moving;

}

public void move(boolean side) {

if (side) {

System.out.println("车继续行驶");

moving = true;

} else {

System.out.println("车停下来");

moving = false;

}

}

};

class Person {

boolean side; // 表示人是否在路边

public boolean walk(boolean car) {

if (car) // 如果有车的话,人往路边走

{

System.out.println("人往路边走");

side = true;

} else {

System.out.println("人直着走");

side = false;

}

return side;

}

};

public class CarPersonApp {

public static void main(String[] args) {

Person p = new Person();

Car c = new Car();

c.move(true); // 车在行驶

p.walk(c.getMoving());// 测试人是否能行走,也就是说车的行为,影响了人的行为

}

}

编译和运行:

API文档的使用

Java API是扩展的Java类库。它为程序员提供了几千个类,包括基本的数学函数、数组和字符串、窗口,图形用户界面,输入/输出,联网等任何你需要的内容。

类库被组织成许多包,每个包都包含多个类。下面列举了一些重要的包:

? java.lang:包含一些形成语言核心的类,如String、Math、Integer和Thread。

? java.awt:包含了构成抽象窗口工具包(AWT)的类,这个包被用来构建和管理应用程序的图形用户界面。

? java.applet:包含了可执行applet特殊行为的类。

? java.net:包含执行与网络相关的操作的类和处理接口及统一资源定位器(URLs)的类。

? java.io:包含处理I/O文件的类。

? java.util:包含为任务设置的实用程序类,如随机数发生、定义系统特性和使用与日期日历相关的函数。

Java API文档详细说明了Java API的使用方法。Java API文档是一组等级制布局的HTML文件,因而主页列出所有的包为超链接。如果选中了一个特殊包的热链接,作为那个包成员的类将被列出。从一个包页选中一个类的热链接将提交一页有关那个类的信息。

下载Java2 SE Version 1.5的文档并解压缩,打开/api/Index.html 文件。

一个类文档的主要部分包括:

? 类层次

? 类和类的一般目的描述

? 成员变量列表

? 构造函数列表

? 方法列表

? 变量详细列表及目的和用途的描述

? 构造函数详细列表及描述

? 方法详细列表及描述

内容总结

? 一个类由属性、方法、构造函数构成。

? private实现信息的封装,只有这个类自身内部可以访问这个属性或方法。

? CLASSPATH环境变量是Java虚拟机查找类的路径,在编译和运行时会使用到它。

? Package:将功能相似的类组织成包,通过包来管理类文件。在包中可以存放类,也可以存放子包,从而形成具有层次结构的包。

? import告诉编译器:导入Java类库中的类或者程序员自己开发的类文件。

? API文档是我们使用已有类完成任务的参考手册。

独立实践

? 练习API文档的使用,查找Date类,根据API文档说明写出一个程序演示输出当前日期。

? 写一个猫吃老鼠的实例。

? 对上面的类增加包,要求猫、老鼠、演示类在不同的包中,练习使用 CLASSPATH环境变量。

? 编写一个类:Person,包含一个属性:name及方法:getName(),画出它的类图。

? 给Person类加上注释,练习javadoc命令,生成文档。

第四章: Java语法基础

学习目标

? 标识符定义

? Java数据类型

? 参数传递:基本型参数,引用类型参数

? 对象实例化和成员的访问

? 变量作用域及程序中逻辑控制

基本语法元素

注 释

注释是程序员用来标记、说明程序的。编译器会忽略注释中的内容,注释中的内容不会对程序的运行产生任何影响。Java语言允许三种风格的注释:

// 单行注释

多用于对属性,变量以及算法重要转折时的提示

/* 多行

注释 */

多用于对类、方法及算法的详细说明,一般在对类的注释中要有以下内容:

1. 类的简要说明

2. 创建者及修改者

3. 创建日期或者最后修改日期

/** JAVA文档

*注释

*/

产生Java文档,使用javadoc命令.

分号

在Java编程语言中,语句是一行由分号(;)终止的代码。

例如:

totals = a + b + c + d + e + f;

语句块(block)

语句块(block)也叫做复合语句。一个语句块(block)是以上括号和下括号{}为边界的语句集合;语句块也被用来组合属于某个类的语句。例如:

public class Date {

private int day = 3;

private int month;

private int year;

public void pri() {

}

public static void main(String[] a) {

}

}

语句块可被嵌套。我们以前见到的main方法就是一个语句块,它是一个独立单元。

下面的语句是合法的:

// a block statement

{

x = y + 1;

y = x + 1;

}

// an example of a block statement nested within another block

// statement

while ( i < large ) {

a = a + i;

if ( a == max ) {

b = b + a; // nested block is here

a = 0;

}

i++;

}

还有一种静态语句块,这个我们将在学习static关键字时介绍.

空白

空白:是空格、tabs和新行(换行符)的统称。

在源代码元素之间允许插入任意数量的空白。空白可以改善源代码的视觉效果,增强源代码的可读性。例如:

{

int x;

x = 23 * 54;

}

{

int x;

x = 23 + 54;

}

标识符定义

标识符是语言元素的名称,是我们在程序中表示变量、类或方法等等的符号。

? 标识符由字母、下划线(_)、美元符号($)或数字组成,但不能以数字开头。另外可以使用中文做标识符,但实际开发中不推荐这样做。

? 标识符是大小写敏感。

? 标识符未规定最大长度,但实际工作中不会对标识符命名过长,10个字符以内合适,标识符的命名尽可能的有意义。

下列标识符是有效的:

idendsafdstifier

ugfdsgName

Udsaf_dsfe

_sys_varldaf

$changdsafe

Java技术源程序采用双字节的"统一字符编码" (Unicode,使用16bit编码)标准,而不是单字节的 ASCII(使用8bit编码)文本。因而,一个字母有着更广泛的定义,而不仅仅是a到z和A到Z。

标识符不能是关键字,但是它可包含一个关键字作为它的名字的一部分。例如,thisone是一个有效标识符,但this却不是,因为this是一个Java关键字。

Java关键字

下面列出了在Java编程语言中使用的关键字。

abstract do implements private throw

boolean double import protected throws

break else instanceof public transient

byte extends int return true

case false interface short try

catch final long static void

char finally native super volatile

class float new switch while

continue for null synchronized default

if package this

关键字对Java技术编译器有特殊的含义,它们可标识数据类型名或程序构造(construct)名。

以下是有关关键字的重要注意事项:

? true、false和null为小写,而不是象在C++语言中那样为大写。

? 无sizeof运算符;所有类型的长度和表示是固定的,不依赖执行。

? goto和const不是Java编程语言中使用的关键字。

基本Java数据类型

Java编程语言定义了八种原始数据类型:

类型 位数(bit) 默认值

逻辑型 boolean 1bit false

文本型 char 16bit(2byte) '\u0000'

整数型 byte 8bit(1byte) 0

short, 16bit(2byte) 0

int, 32bit(4byte) 0

long 64bit(8byte) 0

浮点型 double, 64bit(8byte) 0.0

float 32bit(4byte) 0.0

注意:整数类型默认的是int,浮点型默认的是double

逻辑型--boolean

逻辑值有两种状态,即人们经常使用的 “true”和“false”。这样的值是用boolean类型来表示的。boolean有两个文字值,即true和false。

以下是一个有关boolean类型变量的声明和初始化:

boolean truth = true; //声明变量值为真

注意:在整数类型和boolean类型之间无转换计算。有些语言(特别值得强调的是C和C++)允许将数字值转换成逻辑值, 这在Java编程语言中是不允许的;boolean类型只允许使用boolean值。

字符型--char

使用char类型可表示单个字符。一个char代表一个16-bit无符号的(不分正负的)Unicode字符。一个char文字必须包含在单引号内(’’)。

‘a’

‘\t’ 一个制表符

‘\u????’ 一个特殊的Unicode字符。????应严格按照四个16进制数字进行替换。例如: ’\u03A6’表示希腊字母“Φ”

char类型变量的声明和初始化如下所示:

char ch = ’A’; // 声明并初始化一个char型变量

char ch1,ch2 ; // 声明两个char型变量

char是int兼容的类型,比如可以如下声明:

int a = ‘a’; // a = 97

char c = 65; // c = ‘A’

字符串类--String

String不是原始类型,而是一个类(class),它被用来表示字符序列。字符本身符合Unicode标准。与C和C++不同,String不能用 \0作为结束。

String的文字应用双引号封闭,如下所示:

“The quick brown fox jumped over the lazy dog.”

String类型变量的声明和初始化如下所示:

// 声明两个String型变量并初始化他们

String greeting = "Good Morning !! \n" ;

String err_msg = "Record Not Found !" ;

String str1,str2 ; // 声明两个字符串变量

整数型--byte, short, int, long

在Java编程语言中有四种整数类型,它们分别使用关键字byte, short, int和long中的任意一个进行声明。整数类型的文字可使用十进制、八进制和16进制表示,如下所示:

? 十进制值是2

? 首位的0表示这是一个八进制的数值

? 0xBAAC 首位的0x表示这是一个16进制的数值

注意──所有Java编程语言中的整数类型都是带符号的数字。

整数类型数字被默认为int类型。

整数类型数字后面紧跟着一个字母“L”,可以强制它为long型。

例如:

? 2L 十进制值是2,是一个long

? 077L 首位的0表示这是一个八进制的数值

? 0xBAACL 前缀0x表示这是一个16进制的数值

四种整数类型的长度和范围前面已经列出,这些长度和范围是按Java编程语言规范定义的,是不依赖于平台。

浮点数--float和double

如果一个数字包括小数点或指数部分,则该数字默认为double型浮点数。

例如:

3.14

3.02E23

如果一个数字文字后带有字母F或f,则该数字文字为float型浮点数。

例如:

2.718F

如果一个数字文字后带有字母D或d,则该数字文字为double型浮点数。

例如:

123.4E-306D

浮点变量可用关键字float或double来声明。

Java技术规范的浮点数的格式是由电力电子工程师学会(IEEE)754定义的,是独立于平台的。

变量声明和赋值

变量用于存储信息。一个变量代表一个特殊类型的存储位置,它指向内存的某个单元,而且指明这块内存有多大。变量的值可以是基本类型,也可以是对象类型。

下列程序显示了多种类型的变量,如何进行声明及赋值的。

public class TestAssign {

public static void main(String args[]) {

int a, b; // declare int variables

float f = 5.89f; // declare and assign float

double d = 2.78d; // declare and assign double

boolean b = true;// declare and assign boolean

char c; // declare character variable

String str; // declare String

String str1 = "good"; // declare and assign String variable

c = 'A'; // assign value to char variable

str = "hello ,hello"; // assign value to String variable

a = 8;

b = 800; // assign values to int variables

}

}

引用(Reference)类型

从大的范围来讲,Java中的数据类型就分为两种:基本数据类型和引用类型,前面已经对基本数据类型(也称为主数据类型)进行了讲解,下面我们再来理解引用类型。

创建一个新类型

为克服Java中数据类型的不完整,Java编程语言使用类来创建新类型。例如可以用下面的类表示人:

class Person {

private double height = 1.75;

private double weight = 65;

private String name;

public Person(String aName) {

name = aName;

}

public Person() {

}

}

关键字class是用来声明类的。

Person是指定这个类的名称。

height变量被声明,是类Person的一个属性。 本类Person中包含有3个属性。

而Person zhangsan, lisi; 声明Person类有两个引用,引用名称为:zhangsan、lisi。

zhangsan = new Person();//类Persion构造出实际的内存空间,zhangsan这个引用,指向这个内存空间。

lisi = new Person();//同上。因此这两个变量zhangsan、lisi都是引用类型的,并不实际存储这些数据

创建并初始化一个对象

当任何基本数据类型(如boolean, byte, short, char, int, long, float和double类型) 的变量被声明时,内存空间同时被分配,此分配属栈;

使用非基本数据类型(如class)变量的声明,不为对象同时分配内存空间。事实上,使用class类型声明的变量不是数据本身,而是数据的引用(reference)。引用可以理解为C语言的指针(Pointer),但是不能象C语言那样计算指针。

在使用引用变量之前,必须为它分配实际存储空间。这个工作是通过使用关键字new来实现的。如下所示:

Person pangzi;

pangzi= new Person();

第一个语句仅为引用分配了空间,而第二个语句则通过调用对象的构造函数Person()为对象生成了一个实例。这两个操作被完成后,Person对象的内容则可通过pangzi进行访问。

还可以用一条语句创建并初始化一个对象:

Person pangzi = new Person(“danan”);

使用非基本数据类型(String)变量的声明,分两种情况:

使用String str = “班集”,指向的是内存中的特殊区域,叫字符串池;

而 String str = new String(),则和class变量声明的规则一致。

存储器分配和布局

在一个方法体中,做如下声明:

Person liuxiang;

liuxiang= new Person();

语句Person liuxiang; 仅为一个引用分配存储器,存储空间里的值没有确定。

liuxiang

在语句liuxiang= new Person() 中关键字new意味着为对象分配内存空间,并初始化。

注意:由于使用了缺省构造函数,而缺省构造函数中并未对name属性进行赋值操作,所以name属性是没有初始化的。

引用类型的赋值

在Java编程语言中,一个被声明为类的变量,叫做引用类型变量,这是因为它正在引用一个非基本数据类型,这对赋值具有重要的意义。请看下列代码片段:

int x = 7;

int y = x;

String s = “Hello”;

String t = s;

四个变量被创建:两个基本数据类型 int 和两个引用类型String。x的值是7,而这个值被复制到y。x 和 y是两个独立的变量且其中任何一个的进一步的变化都不对另外一个构成影响。

至于变量 s 和 t,只有一个String 对象存在, 它包含了文本”Hello” ,s 和 t均引用这个单一的对象。

将变量t 重新定义, t= "World";则新的对象World被创建,而 t 引用这个对象。

Java基本数据传递与引用传递

Java中的参数传递,都称为是传值。但传的这个值,到底是什么,这个就是我们需要研究的。传递的是一个基本数据,还是一个引用,这就要仔细区别。

基本数据类型,值是在栈中,引用数据类型,值是在堆中。

特别注意:对基本数据类型是pass by value,而对引用类型则是pass by ref.

例如:

public class TestReference {

int i = 5;

int j = 6;

A a = new A();

public void changeIJ(int m, int n) {// 试图改变基本类型的参数的值

int z = 0;

z = m;

m = n;

n = z;

}

public void changeAB(A a1, A a2){ // 试图改变引用类型的参数的地址

A a = null;

a = a1;

a1 = a2;

a2 = a;

}

public void test(int c){ // 试图改变基本类型的参数的值

c = c + 6;

}

public void testA(A a) {// 试图改变引用类型参数的值

a.i = 100;

}

public static void main(String[] args) {

TestReference t = new TestReference(); // 构造本类的一个对象

int z = 5;

int y = 6;

A aa = new A(); // 定义A类的一个对象aa

t.testA(aa); // 改变对象aa的值

System.out.println(aa.i); // 输出aa的值(属性值)

A bb = new A(); // 再定义A类的一个对象bb

t.changeIJ(z, y); // 试图改变两个基本类型的值

t.changeAB(aa, bb);// 试图改变两个引用类型的地址

System.out.println(z);

System.out.println(y);

System.out.println(aa.i);

System.out.println(bb.i);

}

}

class A {

int i = 5;

};

输出结果如下:

100

5

6

100

5

可见,方法只能改变引用类型的值,而不能改变引用类型的地址和基本类型的值。

this引用

this作为一个Java关键字,有两个作用:

? 代表隐含参数的调用

? 调用本类的其它的构造器

关键字this是用来指向当前对象(类实例)的。这里,this.name指的是当前对象的name字段。

例1

public class Person {

private double height = 1.75;

private double weight = 65;

private String name;

public Person(String aName) {

this.name = aName;// 全称应该是:Person.this.name

}

public Person() {

}

}

例2

public class Person {

private double height = 1.75;

private double weight = 65;

private String name;

public Person(String aName) {

this.name = aName;// 全称应该是:Person.this.name

}

public Person(){

this(“zhangsan”);

}

}

例3

class Lamp {

int watts = 60;

boolean isOn = false;//属性声明

Lamp(boolean startOn) {

isOn = startOn;//这里isOn是上面声明的属性

}

public void setIsOn(boolean isOn) {

for (int dummy = 1; dummy < 1000; dummy++) {

System.out.println("The count is " + dummy);

}

this.isOn = isOn;//注意参数和属性名称相同,必须用this关键字来区分不同作用域

}

}

下面的代码用于调用上面的代码

Lamp aLamp = new Lamp();

aLamp.setIsOn(true);

Java编码约定

虽然任何一个合法的标识符都可以用来当作变量、类、方法等的名称,但是Java编程语言极力推荐它的编码约定:

? classes──类名应该是名词,大小写可混用,但首字母应大写。

例如:

class AccountBook

class ComplexVariable

? interface──界面(接口)名大小写规则与类名相同。

interface Account

? method──方法名应该是动词,大小写可混用,但首字母应小写。在每个方法名内,大写字母将词分隔并限制使用下划线。例如:

balanceAccount()

addComplex ()

? Variables──所有变量都可大小写混用,但首字符应小写。词由大写字母分隔,限制用下划线,限制使用美元符号($),因为这个字符对内部类有特殊的含义。

currentCustomer

变量应该代表一定的含义,通过它可传达给读者使用它的意图。尽量避免使用单个字符, 除非是临时“即用即扔”的变量(例如,用i, j, k作为循环控制变量)

? constant──原始常量应该全部大写并用下划线将词分隔;对象常量可大小写混用。

HEAD_COUNT

MAXIMUM_SIZE

? control structures──当语句是控制结构的一部分时,即使是单个语句也应使用括号({})将语句封闭。例如:

if (condition) {

do something

}else {

do something else

? spacing──每行只写一个语句并使用 tab键缩格使你的代码更易读。

? comments──用注释来说明那些不明显的代码段落;对一般注释使用 // 分隔符, 而大段的代码可使用 /*???*/分隔符。使用 /**???*/将注释形成文档,并输入给javadoc以生成HTML代码文档。

// A comment that takes up only one line.

/* Comments that continue past one line and take up space on multiple lines... */

/**

* A comment for documentation purposes.

*

* @see Another class for more information

*/

变量作用域

每个变量都有一个作用域,也就是说这个变量在哪个程序段中起作用。变量的作用域从它被声明时开始直到遇到声明变量的代码段的结束符“}”为止。只能在变量的作用域内访问它。如果在作用域之外访问变量,编译器将产生一个错误。下面的实例有一定的代表性:

可以注意到,不同作用域内的变量互相不干扰,通常变量是当前最近作用域内定义的该变量,如果要引用更大作用域外的变量,则需要增加限定符,例如我们已经看到过的this.

变量初始化

在Java程序中,任何变量都必须经初始化后才能被使用。当一个对象被创建时,实例变量在分配存储器的同时被下列值初始化:

byte 0

short 0

int 0

long 0L

float 0.0f

double 0.0d

char '\u0000' (NULL)

boolean false

所有引用类型 null

在方法外定义的变量被自动初始化。局部变量必须在使用之前做“手工”初始化。如果编译器能够确认一个变量在初始化之前被使用的情况,编译器将报错。

public void doComputation() {

int x = (int) (Math.random() * 100);

int y;

int z;

if (x > 50) {

y = 9;

}

z = y + x; // 编译出错,有可能在未初始化之前使用变量

}

表达式

在不知不觉中,我们一直在使用表达式。

表达式是执行时返回一个值的语句。

表达式可以是一个文字、变量、方法;或用运算符连接的文字、变量、方法。

例如:

65 + 5

( i<10 )

5 * 100

x = 25 – 5

int x = 100 , y;

y = ( x / 4 ) + 3

运算符的优先级

Java支持一元和二元运算符。一元运算符对单一的操作数起作用,二元运算符对两个操作数起作用。假如没有运算符优先级规则的限制,下面的表达式可能得出几种不同的结果。

x = 15 + 3 * 2 - 14

下表按优先顺序列出了各种运算符:

优先级 结合性 运算符 描述

1 不适用 () 括号:强制次序

2 从右到左 ++ -- 前/后增减1(一元)

从右到左 + - 加法 减法(一元)

从右到左 ~ 一元按位逻辑非

逻辑求补(一元)

从右到左 ! 逻辑非

从右到左 (类名) 类型转换

3 从左到右 * / % 乘法、除法、求余

4 从左到右 + - 加法、减法

5 从左到右 <<

>>

>>> 左移

右移(符号扩展)

右移(零扩展)

6 从左到右 < <=

> >=

instanceof 小于、小于等于

大于、大于等于

该类的一个实例对象

7 从左到右 == != 相等、不相等

8 从左到右 & 按位与

9 从左到右 ^ 按位异或

10 从左到右 | 按位或

11 从左到右 && 与

12 从左到右 || 或

13 从右到左 ?: 三元条件

14 从右到左 = 赋值

15 从右到左 *= /= %= += -= <<= >>= >>>= &= ^= |= 赋值并运算

关于++i和i++:

例如:

Int a = 1;

a = a + 1; //a = 2;

可以变化为:

a += 1; //此时a = 3;

还可以变化为:

++a; // a = 4;

另有:

int b;

b = ++a; //则: b = 5, a = 5; a先自加后然后将a的值赋给b;

如果:

int c;

c = a ++; //则: c = 5; a = 6; a先将自己的值赋给c,然后自加1

逻辑表达式

位运算

Java编程语言支持整数数据类型的位运算,它们的运算符~、&、^和|分别表示位运算的NOT(为求反)、位AND、位XOR和位OR。

例如:

7&5=5

0000 0000 0000 0000 0000 0000 0000 0111

0000 0000 0000 0000 0000 0000 0000 0101

0000 0000 0000 0000 0000 0000 0000 0101

-7&5=1

1111 1111 1111 1111 1111 1111 1111 1001

0000 0000 0000 0000 0000 0000 0000 0101

0000 0000 0000 0000 0000 0000 0000 0001

7|5=7

0000 0000 0000 0000 0000 0000 0000 0111

0000 0000 0000 0000 0000 0000 0000 0101

0000 0000 0000 0000 0000 0000 0000 0111

-7|5=-3

1111 1111 1111 1111 1111 1111 1111 1001

0000 0000 0000 0000 0000 0000 0000 0101

1111 1111 1111 1111 1111 1111 1111 1101

~7=-8

0000 0000 0000 0000 0000 0000 0000 0111

1111 1111 1111 1111 1111 1111 1111 1000

~-7=6

1111 1111 1111 1111 1111 1111 1111 1001

0000 0000 0000 0000 0000 0000 0000 0110

7^5=2

0000 0000 0000 0000 0000 0000 0000 0111

0000 0000 0000 0000 0000 0000 0000 0101

0000 0000 0000 0000 0000 0000 0000 0010

-7^5=-4

1111 1111 1111 1111 1111 1111 1111 1001

0000 0000 0000 0000 0000 0000 0000 0101

1111 1111 1111 1111 1111 1111 1111 1100

逻辑运算符

运算符&& (定义为AND)和||(定义为OR)执行布尔逻辑表达式。请看下面的例子:

MyDate d = null;

if ((d != null) && (d.day() > 31)) {

// do something with d

注意:int到boolean不能自动转换。

int i = 1;

if ( i ) //generates a compile error

if (i != 0) // Correct

字符串连接

运算符 + 能够进行String对象的链接并生成一个新的String:

String salutation = "Dr. ";

String name = "Jack " + "Arthur";

String title = salutation + name;

最后一行的结果是:

Dr. Jack Arthur

如果+运算符中有一个自变量为String对象,则其它自变量将被转换成String。所有对象都可被自动转换成String,尽管这样做的结果可能是意义含糊的。不是串的对象是通过使用toString() 成员函数而转换成串的等价物的。

移位运算符

右移位运算符>>和>>>

Java编程语言提供了两种右移位运算符。

运算符>>进行算术或符号右移位。移位的结果是第一个操作数被2的幂来除,而指数的值是由第二个数给出的。例如:

-7>>3=-1

1111 1111 1111 1111 1111 1111 1111 1001

1111 1111 1111 1111 1111 1111 1111 1111

其结果与-7/(2*2*2)) 完全相同。

-7<<3=-56

1111 1111 1111 1111 1111 1111 1111 1001

1111 1111 1111 1111 1111 1111 1100 1000

其结果与 -7*2*2*2 完全相同。

7>>3=0

0000 0000 0000 0000 0000 0000 0000 0111

0000 0000 0000 0000 0000 0000 0000 0000

其结果与 7/(2*2*2) 完全相同。

无符号右移位运算符>>>主要作用于位图,而不是一个值的算术意义;它总是将零置于最高位。例如:

-7>>>3=536870911

1111 1111 1111 1111 1111 1111 1111 1001

0001 1111 1111 1111 1111 1111 1111 1111

注意:

如果移位运算符左侧操作数是int类型,将它们右侧的操作数模32。

如果移位运算符左侧操作数是long类型,将它们右侧的操作数模64。

>>>运算符仅被允许用在整数类型, 并且仅对int和long值有效。如果用在short或byte值上, 则在应用>>>之前, 该值将通过带符号的向上类型转换被升级为一个int。有鉴于此,无符号移位通常已成为符号移位。

左移位运算符(<<)

运算符<<执行一个左移位。移位的结果是:第一个操作数乘以2的幂,指数的值是由第二个数给出的。例如:

128 << 1 gives 128*21 = 256

16 << 2 gives 16*22 =64

无论正数、负数,它们的右移、左移、无符号右移 32 位都是其本身,比如 -7<<32=-7、-7>>32=-7、-7>>>32=-7。

把 1 左移 31 位再右移 31 位,其结果为 -1。

0000 0000 0000 0000 0000 0000 0000 0001

1000 0000 0000 0000 0000 0000 0000 0000

1111 1111 1111 1111 1111 1111 1111 1111

类型转换

在赋值的信息可能丢失的地方,编译器需要程序员用类型转换(typecast)的方法确认赋值。例如,它可以"挤压"一个long值到一个int变量中。显式转型做法如下:

long bigValue = 99L;

int squashed =(int)bigValue;

在上述程序中,期待的目标类型被放置在圆括号中,并被当作表达式的前缀,该表达式必须被更改。一般来讲,建议用圆括号将需要转型的全部表达式封闭。否则,转型操作的优先级可能引起问题。

升级和表达式的类型转换

当没有信息丢失时,变量可被自动升级为一个较长的形式(如:int至long的升级)

long bigval = 6; // 6 is an int type, OK

int smallval = 99L; // 99L is a long, illegal

double z = 12.414F; // 12.414F is float, OK

float z1 = 12.414; // 12.414 is double, illegal

一般来讲,如果变量类型至少和表达式类型一样大(位数相同),则你可认为表达式是赋值兼容的。

对 + 运算符来说,当两个操作数是原始数据类型时,其结果至少有一个int,并且有一个通过提升操作数到结果类型、或通过提升结果至一个较宽类型操作数而计算的值,这可能会导致溢出或精度丢失。例如:

short a,b,c

a = 1;

b = 2;

c = a + b;

上述程序会因为在操作short之前提升每个short至int而出错。然而,如果c被声明为一个int,或按如下操作进行类型转换:

c = (short)(a + b);

则上述代码将会成功通过。

例1

编写一个测试类,在其中定义基本类型和字符串类型属性。

public class TestVar {

public static void main(String[] args) {

int age = 20;

float fVar = 5.89f;

double dVar = 2.78d;

String str = "good";

char cVar = 'A';

System.out.println(age);

System.out.println(fVar);

System.out.println(dVar);

System.out.println(str);

System.out.println(cVar);

}

}

结果如下:

例2

在TelCom公司中有几百名员工,他们有各自的姓名,部门;tom和bob分别在管理部和开发部,编写代码实现该描述。

编写员工类:Employee,类结构如下图:

public class Employee {

private String name;

private String department;

public Employee() {

this("", "");

}

public Employee(String name, String department) {

this.name = name;

this.department = department;

}

public String getName() {

return name;

}

public String getDepartment() {

return department;

}

public static void main(String[] args) {

Employee tom = new Employee("tom", "Finance");

Employee bob = new Employee("bob", "Dev");

System.out.println(tom.getName() + " : " + tom.getDepartment());

System.out.println(bob.getName() + " : " + bob.getDepartment());

}

}

运行结果如下:

分支语句

条件语句使部分程序可根据某些表达式的值被有选择地执行。Java编程语言支持双路if和多路switch分支语句。

if,else语句

基本句法:

if ( /* 布尔表达式 */ ){

// 语句或块;

} else {

// 语句或块;

在Java编程语言中,if()用的是一个布尔表达式,而不是数字值,这一点与C/C++不同。前面已经讲过,布尔类型和数字类型不能相互转换。因而,如果出现下列情况:

if ( x ) // x is int

你应该使用下列语句替代:

if ( x != 0 )

else部分是可选的,并且当条件为假时如不需做任何事,else部分可被省略。

switch语句

基本句法:

switch (expr1){// int 兼容型

case expr2:

//statements;

break;

case expr3:

//statements;

break;

default:

//statements;

break;

注意:

在switch (expr1) 语句中,expr1必须与int类型是赋值兼容的;byte, short或char类型可被升级;不允许使用浮点或long表达式。

当变量或表达式的值不能与任何case值相匹配时,可选缺省符(default)指出了应该执行的程序代码。如果没有break语句作为某一个case代码段的结束句,则程序的执行将继续到下一个case,而不检查case表达式的值。

例:

switch (colorNum) {

case 0:

setBackground(Color.red);

break;

case 1:

setBackground(Color.green);

break;

default:

setBackground(Color.black);

break;

}

for循环

基本句法:

for (init_expr; boolean testexpr; alter_expr){

//语句或块

例如:

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

System.out.println("Are you finished yet?");

}

System.out.println("Finally!");

注意:

Java编程语言允许在for() 循环结构中使用逗号作为分隔符。 例如,for (i= 0, j = 0; j<10; i++, j++)是合法的;它将i值初始化为零,并在每执行完一次循环体后,增加一次它们的值。

while循环

基本句法:

while(布尔表达式){

//语句或块

例如:

int i = 0;

while (i < 10){

System.out.println("Are you finished yet?");

i++;

}

System.out.println("Finally!");

请确认循环控制变量在循环体被开始执行之前已被正确初始化,并确认循环控制变量是真时,循环体才开始执行。控制变量必须被正确更新以防止死循环。

do循环

do循环的句法是:

do {

//语句或块;

} while (布尔测试)

例如:

int i = 0;

do {

System.out.println("Are you finished yet?");

i++;

} while (i < 10);

像while循环一样,请确认循环控制变量在循环体中被正确初始化和测试并被适时更新。

作为一种编程惯例,for循环一般用在那种循环次数事先可确定的情况,而while和do用在那种循环次数事先不可确定的情况。

break和continue

下列语句可被用在更深层次的控制循环语句中:

break [标注];

continue[标注];

label: 语句; // 语句必须是有效的

break语句被用来从switch语句、loop语句和预先给定了label的块中退出。

continue语句被用来略过并跳到循环体的结尾。

label可标识控制需要转换到的任何有效语句,它被用来标识循环构造的复合语句。

例如:

loop: while (true){

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

switch (c = System.in.read()) {

case -1:

case ' \n ':

break loop;

....

}

} // end for

} // end while

test: for (...) {

....

while (...) {

if (j > 10) {

// jumps to the increment portion of

// for-loop at line #13

continue test;

}

} // end while

} // end for

计算1到100的合计值。

public class Number {

public int sum(){

int totle = 0;

for (int i = 1; i <= 100; i++){

totle += i;

}

return totle;

}

public static void main(String[] args){

Number obj = new Number();

int temp = obj.sum();

System.out.println(temp);

}

}

输出结果

内容总结

? 在源程序中使用注释

? 区分有效和无效标识符,识别Java技术关键字

? 列出八个原始类型

? 定义术语class,object,member,variable,referrence variable

? 为一个的包含原始成员变量的类创建一个类定义

? 使用new构造一个对象,对象缺省初始化

? 使用点符号访问一个对象的成员变量

? 描述引用变量的意义

? 区分实例变量和局部变量;

? 描述实例变量是如何被初始化的;

? 辨认、描述并使用Java运算符;

? 区分合法和非法原始类型赋值;

? 确认boolean表达式和它们在控制构造中的要求;

? 辨认赋值兼容性和在基本类型中的必要转型;

? 使用if, switch,for,while和do句型结构和break和continue的标注形式作为程序的流程控制结构。

独立实践

? 实践1:

按照以下步骤创建一个类和相应的对象.

1.一个点可用x和y坐标描述。定义一个称为Point的类来表达上述想法。

2.在另一个类MyPoint中声明两个Point变量,将变量称为start和end;用new Point()创建对象并分别将引用值赋予变量start和end;

3.将值10赋予对象start的成员x和y;

4.将值20赋予对象end的x值,将值20赋予对象end的y值。

5.分别打印start和end的成员值(x和y)。

? 实践2:

使用你在前一个练习中MyPoint类,增加代码到main()方法,以完成下列事项:

1.为类型Point声明一个新的变量,称之为stray。将现存变量end的引用值赋予stray;

2.打印end和stray变量的成员x和y的值;

3.赋予变量stray的成员x和y新的值;

4.打印end和stray的成员的值;编译并运行MyPoint类。end的值反映了stray内的变化,表明两个变量都引用了同一个Point对象;

5.将start变量的成员x和y赋予新的值;

6.打印start和end的成员值; 再次编译并运行MyPoint类,start的值仍然独立于stray和end的值,表明start变量仍然在引用一个Point对象,而这个对象与stray和end引用的对象是不同的。

? 实践3:

判断一个给定的字符是否是元音字符.

? 实践4:

求n!的阶乘.

? 实践5:

已知直角三角形的两个直角边长,求第三边长度.

第五章:数组

学习目标

? 数组的声明

? 创建数组

? 多维数据(数组的数组)

? 数组的界限

? 数组的拷贝

数组的描述

数组是我们接触的第一个容器,数组是长度固定的容器。一但定义好大小,将不能改变。由于Java中的数据类型分为两种:基本类型和引用类型。所以数组也有两种类型的:基本类型的数组和引用类型的数组(数组的数组也是引用类型的数组)

数组声明

语法:

<modifier> <type> <name>[];

<modifier> <type>[] <name>;

说明:

<modifier> 目前可以用private,public或着默认的修饰,private是封装的访问权限。

<type> 可以是任何原始类型(基本类型)或其它类(引用类型)。

<name>:任何合法的标识符。它代表所声明属性的名称。

举例:

char s[ ];

Point p? ?; // where point is a class

在Java编程语言中,无论数组元素由原始类型构成,还是由类构成,数组都是一个对象。声明不能创建对象本身,而创建的是一个引用,该引用可被用来引用数组。数组元素使用的实际存储器可由new语句或数组动态初始化来分配。

上述这种将方括号置于变量名之后的声明数组的格式,是用于C、C++和Java编程语言的标准格式。但这种格式会使数组的声明复杂难懂,因而,Java编程语言允许一种替代的格式,该格式中的方括号位于变量名的左边,意思是:声明一类型的数组,这个数组有一个引用名叫什么?JAVA推荐使用此格式。

char[ ] s;

Point[ ] p;

创建一组基本数据类型 char 的数组,其引用名为 s

创建一组对象 Point 的数组,其引用名为 p

数组声明不指出数组的实际大小。

注意:当数组声明的方括号在左边时,该方括号可应用于所有位于其右的变量。

char[ ] s1, s2; // s1,s2都是字符数组。

char s1[ ],s2; // s1是字符数组,s2是字符。

创建数组

语法:

<name> = new <type>[<int>];

<name> = new <type>[]{};

说明:

<name>:任何合法的标识符。它代表所声明属性的名称。

<type>:可以是任何原始类型(基本类型)或其它类(引用类型)。

<int>: 一个整数值,数组的大小

用来指示单个数组元素的下标必须总是从0开始,并保持在合法范围之内--大于或等于0,并小于数组长度。任何访问在上述界限之外的数组元素的企图都会引起运行时出错。

public int[] createIntArray() {

int[] k = null;

k = new int[5];

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

k[i] = i + 1;

return k;

}

你可以象创建对象一样,使用关键字new 创建一个数组。

k = new int ?5?;

创建了一个5个int值元素的数组。

内存中数组初始化情形如下:

对于应用类型元素的数组,示例如下:

public Point[] createArray() {

Point[] p= null;

p = new Point[5];

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

p[i] = new Point(i,i+1);

return p;

}

数组在内存中的映像则如下:

当创建一个数组时,每个元素都被初始化。在上述int数组k的例子中,每个值都被初始化为0;在数组p的例子中, 每个值都被初始化为null,表明它还未引用一个Point对象。在经过赋值 p?0? = new Point(i,i+1)之后,数组的第一个元素引用为实际Point对象。

Java编程语言允许使用下列形式快速创建数组:

String[] names = ?

“Georgianna”,

“Jen”,

“Simon”,

?;

其结果与下列代码等同:

String[] names;

names = new String ?3?;

names ?0? = “Georgianna”;

names ?1? = “Jen”;

names ?2? = “Simon”;

这种”速记”法可用在任何元素类型。例如:

MyDate[] Dates = ?

new MyDate (22,7,1964),

new MyDate (1,1,2000),

new MyDate (22,12,1964)

?;

适当类型的常数值也可被使用:

Color[] palette = ?

Color.blue,

Color.red,

Color.white

?;

例1

public class TestArray1 {

public static void main(String[] args) {

// 声明并初始化一个数组

int[] num = new int[40];

// 记录数组下标

int k = 0;

// 循环找素数

for (int i = 2; i <= 100; i++) {

int j = 2;

for (; j < i; j++) {

if (i % j == 0)

break;

}

if (j == i) // 表示是素数

{

num[k++] = i;

}

}

// 输出所有素数,5个一行

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

// 如果是小于10的数,多输出个空格,整齐

if (num[i] < 10)

System.out.print(num[i] + " ");

else

System.out.print(num[i] + " ");

// 够5个数换行

if ((i + 1) % 5 == 0)

System.out.println();

}

}

}

结果如下:

例2

把一个班的名字(String类型,引用类型)保存起来,并进行输出。

public class TestStudents {

public static void main(String[] args) {

// 定义3个空间大小的数组

String[] stu = new String[3];

// 数组是通过下标来赋值,下标从0开始

stu[0] = "java";

stu[1] = "c++";

stu[2] = "c#";

// 下面的赋值将产生异常

stu[3] = "j2me";

// 输出数据

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

System.out.println(stu[i]);

}

}

运行的结果如下:

如果把stu[3] = "j2me"; 这句去掉,运行正常,结果如下:

例3

保存1到100之间的素数(除了1和它本身不能被其他整数整除的数)。

public class TestArray3 {

public static void main(String[] args) {

// 声明并初始化一个数组

int[] num = new int[40];

// 记录数组下标

int k = 0;

// 循环找素数

for (int i = 2; i <= 100; i++) {

int j = 2;

for (; j < i; j++) {

if (i % j == 0)

break;

}

if (j == i) {// 表示是素数

num[k++] = i;

}

}

// 输出所有素数,5个一行

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

// 如果是小于10的数,多输出个空格,整齐

if (num[i] < 10)

System.out.print(num[i] + " ");

else

System.out.print(num[i] + " ");

// 够5个数换行

if ((i + 1) % 5 == 0)

System.out.println();

}

}

}

运行结果如下:

多维数组

Java编程语言没有象其它语言那样提供多维数组。因为一个数组可被声明为具有任何类型,所以你可以创建数组的数组(和数组的数组的数组,等等)。一个二维数组(JAVA中没有二维数组的概念,二维数组其实就是数组的数组)如下例所示:

Int[][] twoDim = new int[4][];

twoDim[0] = new int[5];

twoDim[1] = new int[5];

首次调用new而创建的对象是一个数组,它包含4个元素,每个元素的类型也是int数组型,必须将数组的每个元素分别初始化。

注意:

尽管声明的格式允许方括号在变量名左边或者右边,但此种灵活性不适用于数组句法的其它方面。

例如: new int ? ??4?是非法的。

因为这种对每个元素的分别初始化,所以有可能创建非矩形数组的数组。也就是说,twoDim的元素可按如下方式初始化:

twoDim?0? = new int ?2?

twoDim?1? = new int ?4?;

twoDim?2? = new int ?6?;

twoDim?3? = new int ?8?;

由于此种初始化的方法烦琐乏味,而且矩形数组的数组是最通用的形式,因而产生了一种”速记”方法来创建二维数组。例如:

Int[][] twoDim = new int ?4??5?;

可被用来创建一个每个数组有5个整数类型的4个数组的数组。

数组界限

在Java编程语言中,所有数组的下标都从0开始。 一个数组中元素的数量存储在length属性中; 这个值被用来检查所有运行时访问的界限。如果发生了一个越出界限的访问,那么运行时的报错也就出现了。

使用length属性的例子如下:

Int[] list = new int ?10?;

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

System.out.println(list?i?);

}

使用length属性使得程序的维护变得更简单。

例4

存放java班,c++班,j2me班,嵌入式班四个班学生的名字,并打印出来。

public class TestFourClass {

public static void main(String[] args) {

// 声明4个空间大小,类型是String数组的数组

String[][] stu = new String[4][];

// 初始化java班的6个人

stu[0] = new String[6];

for (int i = 0; i < stu[0].length; i++)

stu[0][i] = "java" + (i + 1);

// 初始化c++班的3个人

stu[1] = new String[3];

for (int i = 0; i < stu[1].length; i++)

stu[1][i] = "c++" + (i + 1);

// 初始化j2me班的5个人

stu[2] = new String[5];

for (int i = 0; i < stu[2].length; i++)

stu[2][i] = "j2me" + (i + 1);

// 初始化潜入式班的4个人

stu[3] = new String[4];

for (int i = 0; i < stu[3].length; i++)

stu[3][i] = "嵌入式" + (i + 1);

// 打印各个班级的名单

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

switch (i) {

case 0:

System.out.println("java班:");

break;

case 1:

System.out.println("c++班:");

break;

case 2:

System.out.println("j2me班:");

break;

case 3:

System.out.println("嵌入式班:");

}

System.out.print(" ");

for (int j = 0; j < stu[i].length; j++) {

System.out.print(stu[i][j] + " ");

}

System.out.println();

}

}

}

运行的结果如下:

拷贝数组

数组一旦创建后,其大小不可调整。然而,你可使用相同的引用变量来引用一个全新的数组:

int[] myArray = new int ?6?;

myArray = new int?10?;

在这种情况下,第一个数组被丢弃,除非对它的其它引用保留在其它地方。

Java编程语言在System类中提供了一种特殊方法拷贝数组,该方法被称作arraycopy()。例如,araycopy可作如下使用:

int[] myArray = { 1, 2, 3, 4, 5, 6 }; // 原始数组

int[] hold = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; // 新的更大的数组

System.arraycopy(myArray, 0, hold, 0, myArray.length); // 从没有Array拷贝所有元素到hold,从下标0开始

这时,数组hold有如下内容:1,2,3,4,5,6,4,3,2,1。

注意:如果数组保存的是基本类型的话直接把值拷贝过来。如果数组存放的是引用类型(类类型,数组类型(多维数组的拷贝)等),那么拷贝的是引用类型的地址,请看下面的例子:

例5

class AA {

int i;

public AA(int ii) {

i = ii;

}

}

public class TestArrayCopy {

public static void main(String[] args) {

// 声明数组并初始化,源数组

AA str1[] = new AA[] { new AA(1), new AA(2), new AA(3), new AA(4) };

// 拷贝的目的数组

AA str2[] = new AA[str1.length];

// 完全拷贝,array方法参数的介绍看api

System.arraycopy(str1, 0, str2, 0, str1.length);

// 改变目的数组

str2[1].i = 5;

// 打印原始数组,如果没有改变说明是两个数组

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

System.out.print(str1[i].i + " ");

}

}

}

输出的结果如下:

显然,数组发生了改变,也就是经过拷贝操作后,原始的数组和新拷贝的数组没有分离,因为所拷贝的将是元素的引用。

对于多维数组,由于数组本身是引用类型,所以其拷贝特性与引用类型数组相同。

例6

public class TestMutipleDemensionArrayCopy {

public static void main(String[] args) {

// 定义数组的数组

int[][] source = new int[5][];

// 定义目的数组

int[][] target1 = new int[5][];

int[][] target2 = new int[5][];

// 给源数组赋值

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

source[i] = new int[i + 1];

// int temp=i;

for (int j = 0; j < source[i].length; j++)

source[i][j] = j + 1;

}

// 打印源数组的数据

System.out.println("-------------源数据-------------");

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

for (int j = 0; j < source[i].length; j++)

System.out.print(source[i][j] + " ");

System.out.println();

}

// 数组的拷贝(浅拷贝)

System.arraycopy(source, 0, target1, 0, source.length);

// 改变目的1数组的值

target1[1][0] = 100;

// 打印源数组的信息,可以看到值改变,说明没有深拷贝

System.out.println("-----------浅拷贝后输出-----------");

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

for (int j = 0; j < source[i].length; j++)

System.out.print(source[i][j] + " ");

System.out.println();

}

// 数组的深拷贝,先拷贝”第一维“的

System.arraycopy(source, 0, target2, 0, source.length);

// 再深拷贝

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

target2[i] = new int[i + 1];

System.arraycopy(source[i], 0, target2[i], 0, i + 1);

}

// 改变目的2数组的数据

target2[1][0] = 999;

// 打印源数组的信息,可以看到值没有改变,说明是深拷贝

System.out.println("-----------深拷贝后输出未把100改成999-----------");

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

for (int j = 0; j < source[i].length; j++)

System.out.print(source[i][j] + " ");

System.out.println();

}

}

}

输出的结果如下:

内容总结

? 数组是具有相同数据类型的,存储空间连续的变量的集合

? 数组使用new操作符初始化

? 数组长度具有不可变性

? 数组初始化后其每个元素得到缺省值

? 二维数组可以认为是数组的数组;

? 可以使用System.arraycopy方法对数组进行拷贝,数组拷贝时对其元素进行浅拷贝,对于引用类型元素将只按照引用方式拷贝。

独立实践

1、基本数组的使用

1) 创建一个称作BasicArray的类,在...main()方法中声明两个变量,一个是thisArray,另一个是thatArray,它们应属类型array of int。

2) 创建一个数组,它有10个int值,范围从1至10。分配这个第三数组的引用给变量thisArray。

3) 使用for()循环打印thisArray的所有值。如何控制循环的次数?

4) 编译并运行程序。多少值被打印?这些值是什么?

5) 对每个thisArray的元素,建立它的值为索引值的阶乘。打印数组的值。

6) 编译并运行程序。

7) 分配thisArray的引用给变量thatArray。打印thatArray的所有元素。

8) 编译并运行程序。tyatArray的多少值被显示?这些值是什么?它们来自何处。

9) 修改thisArray的某些元素,打印thatArray的值。

10) 编译并运行程序;在thatArray的值中,你注意到了什么?

11) 创建一个有20个int值的数组。分配新数组的引用给变量thatArray,打印thatArray的值。

12) 编译并运行程序。每个数组有多少值被显示? 这些值是什么?

13) 拷贝thisArray的值给thatArray。你将使用什么方法调用? 你将如何限制拷贝元素的数量? thatArray的元素10至19有什么变化?

14) 打印thatArray的值。

15) 编译并运行程序。你所显示的值都是正确的吗?如果不是,你知道有那些内容理解得不对吗?

16) 改变thatArray的某些值;打印thisArray和thatArray。

17) 编译并运行程序。这些值是你所期待的吗?

2、数组的数组

1) 创建一个称作Array2D的类,在main()方法中声明一个称作twoD的变量,它应属类型array of array of int。

2) 创建一个元素类型为int的数组,该数组应包括4个元素并被赋值到变量twoD的elements?0?。

3) 编写两个嵌套for()循环语句以打印twoD的全部值。以矩阵的格式安排输出 (可采用System.out.print()方法)。

4) 编译并运行程序。 你应该能发现此时出现了运行错误(空指针异常) ,这是因为twoD的elements?1? 至 ?3?未被初始化。

5) 分别创建包括5个、6个和7个元素的int数组,将这些数组的引用分别赋予twoD的elements ?1?,?2?和?3?;确认完成上述操作的代码是在第3步所描述的嵌套for()循环之前插入的。

6) 编译并运行程序。这次你应该看到一个零值的非矩形布局。

7) 赋予twoD数组的每个元素一个明显的非零值(提示:使用Math.random() 以获得随机值)。

3、编写一个程序,输入一个99乘法表(提示:使用二维数组);

4、请在一个类中编写一个方法,这个方法搜索一个字符数组中是否存在某个字符,如果存在,则返回这个字符在字符数组中第一次出现的位置(序号从0开始计算),否则,返回-1。要搜索的字符数组和字符都以参数形式传递给该方法,如果传递的数组为null,直接返回-1

5、随机产生一维10位长度的整数类型数组,删除本数组中有重复的数字。例如:

new int{11,1,5,6,8,8,9,7,9,2},删除数组中重复的数字,8和9。

第六章:继承

学习目标:

? 单继承(single inheritance)

? 访问控制

? 方法重载(overloading)

? 方法覆盖(overriding)

? 基于继承的多态实现

? 隐藏(hiding)

? 构造方法在继承中的使用

? super关键字

? 包装类

? ==与equals()

? toString()方法

单继承(single inheritance)

在面向对象一章中我们学习了OO的特征之一:继承,是任何面向对象的语言必然实现的特性,java也不例外,但我们应该注意的是,java和某些面向对象语言(如c++)在实现继承的不同之处在于java只支持单继承,不支持多重继承。

即,java中一个类只能继承于另一个类。我们将被继承的类称之为父类(基类),继承类称之为子类(派生类)。在java中用关键字extends来实现单继承,语法如下:

class subclass extends superclass{...}

在前面所讲已知,实现继承关系的类之间有着必然的联系,不能将不相关的类实现继承,就象人类不能继承于鸟类!

那怎么去判断类和类之间是否有着必然联系呢?实际上,在第一章里面,我们已知当某类A和类B之间有着共同的属性和行为时,那么类A和类B之间就可能是继承关系或者有着共同的父类。

下面,假设我们开发某公司的员工管理系统,已知类Manager和类Employee,代码如下:

class Employee

{

public String f_name;

public String l_name;

public float salary = 0.0f;

public String getEmpDetails()

{....}

}

class Manager

{

public String f_name;

public String l_name;

public float salary;

public String dept;

public String getEmpDetails()

{....}

}

通过分析得知,在类Employee和类Manager中存在许多共同的属性和行为,在现实生活中,Manager是公司Employee之一,因此,我们可以将Manager类定义成Employee类的子类,修改类Manager如下:

class Manager extends Employee

{

public String dept;

public String getEmpDetails()

{

return "This is Manager!";

}

}

UML中类图表示为:

大家可能会质疑:Manager类重新定义后,原有的属性减少了(f_name,f_name,salary),岂不是违背了需求?!

当类A继承于类B时,子类A拥有父类B的所有成员变量和方法,换句话说,子类继承了父类的所有成员属性和方法,在父类中已定义的属性和方法,在子类中可以无需定义(除非方法覆盖)。

所以,在子类Manager中已继承了父类Employee中的属性和方法,无需再定义在父类中已有的属性(f_name,f_name,salary)。

好,我们已经学会了基本的继承语法,下面就来探讨一下继承带来的一些好处:

a.减少代码冗余

从上面的例子就可以看出,类Manager通过继承而无需再定义属性(f_name,f_name,salary),从而减少了代码量,试想一下,当公司员工分为许多不同级别员工时(如定义秘书、工程师、CEO等员工类),如果没有继承,那将是怎样的结果?

b.维护变得简单

假设公司要求给所有员工添加生日这一属性,那么,在没有继承时,我们的维护将变得困难(需修改每一个级别的员工类)。

c.扩展变得容易

当一个新的级别员工类需创建时,我们只需将该类继承所有员工父类Employee,接着再定义属于该员工的特有属性即可。

当然,以上所举例子只是继承带来的好处的部分体现,在下面的学习中我们将逐渐深入体会继承带来的优势。

小测试:以下代码是否正确?

class B{...}

class C{...}

class D{...}

class A extends B,C,D

{.....}

java中一个类只能继承一个类,但一个类可以被多个类所继承。如:

class Engineer extends Employee{...}

class Secretary extends Employee {...}

class Manager extends Employee

{

public String dept;

...

}

以上三个不同类分别继承了Employee类,即三个类拥有从父类继承过来的共同属性和方法。但是,请注意:这仍旧是单继承!以UML类图表示为:

提醒:构造方法不能被继承!一个类得到构造构造方法只有两种途径:自定义构造方法;使用JVM分配的缺省构造方法。但是,可以在子类中访问父类的构造方法,后面我们会深入。

访问控制

在java中是通过各种访问区分符来实现数据封装的,共分为四种访问级别(由高到低):private(私有)、friendly(缺省)、protected(受保护)、public(公共)。

注意:以上四种访问修饰符可以作用于任何变量和方法,类只可以定义为默认或公共级别(嵌套类除外)。

? public(公共)

当变量或方法被public修饰时,该变量和方法可以在任何地方(指的是任何包中)的任何类中被访问。

//类PublicSample中的构造方法、成员变量及方法均被定义为公共的访问级别

package com.itjob;

class PublicSample {

public PublicSample(){....}

public int num1;

public byte bt1;

public char ch1;

public void method1(){....}

}

通过以上定义,类PublicSample中的成员方法和变量可以在任何包中的任何类中访问,即访问是公共的,不受限制的。以下访问都是允许的:

访问1

class PublicSample {

......

public static void main(String[] args) {

new PublicSample().num1 = 100;

new PublicSample().ch1 = '\u0000';

new PublicSample().bt1 = 10;

new PublicSample().method1();

}

}

访问2

package com.java;

import com.itjob.*;

class A {

public void method1() {

new PublicSample().ch1 = 'a';

....

}

}

? protected(受保护的)

当类的变量或方法被protected修饰时,该变量和方法只可以在同包中的任何类、不同包中的任何当前类的子类中所访问。即不同包中的任何不是该类的子类不可访问级别为protected的变量和方法。

//受保护的变量

protected Stirng str = "";

//受保护的方法

protected String get(){return "";}

? friendly(缺省的)

当类的变量和方法没有显式地被任何访问区分符修饰时,该变量和方法的访问级别是缺省的。缺省的变量和方法只能在同包的类中访问。

//缺省访问级别的变量和方法、类

Float f1 = null;

void method(){...}

class C1{...}

? private(私有的)

被private所修饰的所有变量和方法只能在所属类中被访问。即类的私有成员和变量只能在当前类中被访问。

//私有的构造方法和成员变量、方法

private ClassName(){....}

private int num = 0;

private void method(){....}

通过以上学习,我们可以通过以下表格来描述四种访问区分符的访问限制:

方法重载(method overloading)

? 成员方法重载

学习重载之前,我们来了解一下在java中方法的特征。

在java中,每一个方法都有自己的特征,其特征主要是指方法名以及方法的参数。

void method1(){}

void method2(){}

method1()和method2()可以被理解为是两个方法名不同的方法,即方法的特征不一致。

void method1(int x){}

void method1(){}

第一个method1()与第二个method1()虽然名字一样,但是却有不同的参数,因此,这两个同名方法仍有着不同的特征。

对于java编译器来说,它只依据方法的名称、参数列表的不同来判断两个方法是否相同,如果出现两个名称相同、参数也完全一致的方法,那么编译器就认为这两个方法是完全一样的,也就是说方法被重复定义!

以下定义是错误的:

class ClassName {

void m1(){}

void m1(){}

}

对于以上两个方法定义语句,java解释器认为这两个方法完全相同,当执行到第二条语句时,它会告诉你方法m1()已在类ClassName中被定义!

可以这样理解,当我们在一个类中不能定义相同名称的多个方法,除非这些方法具有不同的方法特征(参数的不一致)。将上面语句修改为:

class ClassName {

void m1(int x){}

void m1(){}

}

这样,虽然方法名相同,但由于两个方法的参数不一致,因此,编译器就认为这是两个不同的方法,从而不会产生歧义。

好,在这里,大家可能就会质疑,把其中的某个方法换成不同的名称不也可以正常运行吗?

对,这样确实可以解决问题,但我们知道,在现实中,往往一个类会实现复杂的功能,其中定义的多种方法可能实现的功能意义都是一样,比如我们已经熟悉的System类中的静态对象中方法println(),在该类中println()被定义了多个,每一个方法都有不同的参数,现在我们已知道每一个println()都具有相同的功能:在控制台上输出内容!我们来假想一下,如果按照每个方法定义一个不同名称,那么我们将在System类中定义十多种不同名称的打印方法,虽然功能实现了,首先,我们是否需要编写代码前给这十几种方法取不同名称,并且还得保证名称唯一,这就会增加我们的工作量;其次我们还得记住每一个方法名对应的功能,如果稍有记错,那就会得到错误的结果!

因此,我们有更好的解决办法,通过重载,可以在一个类中定义相同名称、不同参数的实现相同功能的多个方法,这样就避免了给每个方法取不同名称、熟记每个不同名的方法对应的功能的额外工作量,提高了我们的开发效率。

当一个类中的多个同名方法满足以下条件时之一时,即实现了方法重载:

a.不同的参数个数

b.不同的参数类型

c.不同的参数顺序

小测试:以下哪几组方法实现了重载,满足了重载的那一个条件?

组一:

void m1(int x){}

void m1(int x, int y){}

组二:

void m1(int x, String str){}

void m1(String str, int x){}

组三:

void m1(int x, int y){}

void m1(int y, int x){}

组四:

void m1(int x){}

int m1(int x, int y){}

组五:

void m1(int x){}

void m2(int x){}

? 构造方法重载

如果有一个类带有几个构造函数,那么也许会想复制其中一个构造函数的某些功能到另一个构造函数中。可以通过使用关键字this作为一个方法调用来达到这个目的。

public class Employee {

private String name;

private int salary;

public Employee(String n, int s) {

name = n;

salary = s;

}

public Employee(String n) {

this(n, 0);

}

public Employee() {

this(" Unknown ");

}

}

在第二个构造函数中,有一个字符串参数,调用this(n,0)将控制权传递到构造函数的另一个版本,即采用了一个String参数和一个int参数的构造函数中。

在第三个构造函数中,它没有参数,调用this("Unknownn")将控制权传递到构造函数的另一个版本,即采用了一个String参数的构造函数中。

注:对于this的任何调用,如果出现,在任何构造函数中必须是第一个语句。

构造函数中调用另一构造函数,其调用(this()、super())有且只能有一次,并不能同时出现调用。

分析例题3(Example3.java)的执行结果。

提醒:方法的重载都是基于同一个类!

方法覆盖(method overriding)

覆盖是基于继承的,没有继承就没有覆盖。在java中,覆盖的实现是在子类中对从父类中继承过来的非私有方法的内容进行修改或扩展的一个动作(注意:不能违反访问级别的限制,即子类方法的访问级别不能低于父类方法的访问级别)。

下面我们来分析一个例子:

class A {

public void method() {

System.out.println("SuperClass method()");

}

}

class B extends A {

public static void main(String[] args) {

new B().method();

}

}

执行以上代码得到的打印结果:"SuperClass method()"。

结果分析:

子类B继承了父类A中的公共方法method(),调用子类B的method()方法实际就是调用从父类中继承过来的method()的方法。

修改子类B:

class B extends A {

public void method() {

System.out.println("SubClass method()");

}

// main()

..............

}

再次执行以上代码得到的结果是:"SubClass method()"。

结果分析:

在类B中实现方法覆盖,父类方法method()被子类B覆盖了。在子类B定义了一个返回类型、方法名、方法参数列表都和从父类中继承过来的方法method()一样的方法,那么,新定义的方法就会覆盖原有的方法,实际上就是对从父类继承过来的方法重写(覆盖)!

实现方法的覆盖必须满足以下所有条件:

a. 覆盖方法的返回类型必须与父类中被覆盖方法的返回类型相同

b. 覆盖方法的参数列表类型、次序和方法名称必须与被覆盖方法的参数列表类型、次序和方法名称相同

c. 覆盖方法的访问级别不能比被覆盖方法访问级别低

d. 覆盖方法不能比它所覆盖的方法抛出更多的异常。(异常将在下一个模块中讨论)

父类中定义的方法:public void m1(int x, String str){...}

以下是在各个子类中定义的方法:

子类1:public String m1(int x, String str){...}

分析:没有实现覆盖,方法的返回类型不同

子类2:public void m1(String str, int x){...}

分析:没有实现覆盖,方法的参数类型不同

子类3:void m1(int x, String str){...}

分析:没有实现覆盖,方法的访问级别不能被降低

子类4:public void m1(int x, String str){...}

分析:已实现覆盖,满足覆盖的所有条件

基于继承的多态实现

多态:一个名字可以表示许多不同类(这些不同类必须拥有一个共同的超类)的对象,从而实现以不同的方式来响应某个共同的操作集。

在java中,名字指的就是变量名,我们可以认为每个对象都有一个名字---引用该对象的变量名。比如String str = "abcd";我们就称之为对象str。因此,在java中的多态就体现在一个变量可以引用多个不同类对象,前提是这些不同类必须有者共同的父类,从而该变量可以且只能调用每个不同对象之间的公共操作集(方法)。

很明显,多态的实现是基于继承的。比如,前面所说的Manager类继承了父类Employee的所有属性和方法,这就是说,任何在Employee上的合法操作在Manager上也合法!

一个变量只能有一个类型!这个类型是在编译时指定的,按照多态的定义,一个变量可以指向不同类型的对象,在java中,当这个条件成立时,那这个变量就是多态性的!我们先来看看java中多态的实现。

父类变量可以引用子类对象!这在java是允许的,即Employee e = new Manager()是合法的,但是请注意,变量e只能调用共同的成员属性与方法,即e只能访问子类从父类中继承过来的成员!

//error! 父类变量不能访问属于子类特有的成员(即非公有成员)

e.dept = "";

以上只是实现了多态的一部分:一个父类变量引用许多不同子类对象。

接着,我们再来分析以下例题:

class Engineer extends Employee {

public String getEmpDetails() {

return "Engineer getEmpDetails()";

}

}

class Manager extends Employee {

public String getEmpDetails() {

return "Manager getEmpDetails()";

}

public void m1() {

}

}

通过以上定义,我们已知类Engineer和类Manager分别继承了类Employee,并且都覆盖了从父类中继承过来的方法getEmpDetails(),在main方法中执行以下代码:

1 Employee e = new Manager();

2 System.out.println(e.getEmpDetails());

3 e = new Engineer();

4 System.out.println(e.getEmpDetails());

结果是执行语句2打印:" Manager getEmpDetails()",执行语句4打印:" Engineer getEmpDetails()"。

分析:首先,通过语句1和3可以得知父类变量e可以引用不同类型的子类对象,并且,当e引用子类Manager对象时调用方法getEmpDetails()时,返回的是子类Manager已覆盖的方法内容,因此,现在我们可以肯定一点,当子类覆盖父类方法时,子类对象访问的将是已被覆盖的内容!

其次,通过语句2和4的执行结果对比,父类变量引用子类对象时,调用的将是每一个被引用对象的成员方法,即引用不同的子类对象则调用该子类的成员方法,互不干扰!

如果增加以下语句:e.m1();

将会是编译错误,因为该父类变量调用了一个不属于公有的方法,m1()是Manager类的特有成员,而不是从父类继承下来的。Manager的特殊部分是隐藏的。这是因为编译者应意识到,e 是一个Employee的引用,而不是一个Manager的引用

因此,通过以上分析,我们可以得出结论:父类引用可以引用子类对象,同时该父类引用只能访问所有子类的公有操作集(从父类继承过来的成员);当子类中已覆盖继承方法时,父类变量调用的将是子类中的已覆盖方法!

可以创建具有共同类的对象的收集(如数组)。这种收集被称作同类收集。

就是因为有了java的这种多态机制,我们因而可以实现异类收集!java拥有一个顶层父类java.lang.Object,该类是所有类的顶级父类,在我们平常定义各种类时,虚拟机会自动在类的声明语句后加上继承Object类,如下:

class Employee 与 class Employee extends Object是等同的!extends Object是JVM自动给任意类加上去的,从而保证java中的所有类都具备一个通用父类。

既然Object是所有类的父类,那么我们就可以通过Object数组来实现异类收集,由于多态性,Object数组就能收集所有种类的元素,如:

Object[] obj = {"123", new Employee(), new Manager()}; //收集了三种不同类型对象

Employee[] e = {new Employee(), new Manager(), new Engineer()}; //收集了三种不同子类对象

可以分解为: e[0] = new Employee();

e[1] = new Manager();

e[2] = new Engineer();

以上语句恰好满足了java中的父类变量可以引用子类对象的定义,因此,以上语句都是合法的。

异类收集就是不相同的对象的收集。

请参考例题4(Example4.java)来详细了解多态的使用。

隐藏(hiding)

通过上面的学习,我们发现,父类的私有方法对于子类来说是不可见的,注意,不可见不等于没有,子类仍旧继承了父类所有的成员,那么这些私有的父类成员去哪了?

实际上,它们都被隐藏,对子类来说,这些父类的私有成员都被隐藏了起来,从而导致子类中的不可见。

分析以下例题:

class A {

private void method(String str, int i) {

System.out.println("SuperClass method()");

}

}

class B extends A {

public static void main(String[] args) {

// error! 父类中的私有方法对于子类来说是隐藏的,不可在子类中访问已被隐藏的成员

// new B().method("",0);

}

}

构造方法在继承中的使用

分析例题:

class A {

A() {

System.out.println("A()");

}

}

class B extends A {

B() {

System.out.println("B()");

}

public static void main(String[] args) {

new B();

}

}

执行结果为分别打印:

A()

B()

分析得知,父类的对象是优先于子类对象而存在的(在现实生活中也是如此),也就是说,父类对象的构造在子类对象构造之前,先调用父类构造方法创建父类对象,再调用子类构造方法创建子类对象。

因此,在继承中,父类和子类的构造方法的调用次序如下:先调用父类构函再调用子类构函!

当然,不管是构造父类对象还是子类对象,都必须遵循以下步骤执行:

a.静态语句的执行

b.成员变量的初始化

c.语句块的初始化

d.构造方法的执行

分析例题5()的执行结果。

super关键字

构造方法不能被继承,因此,在子类中调用父类构造方法只能通过super关键字来实现。super可以理解为父类在子类中的一个对象,我们可以象使用父类对象一样使用子类对象。

例:

class A {

A() {

System.out.println("A()");

}

}

class B extends A {

B() {

super(); // 调用父类A的构造方法,打印"A()"语句

System.out.println("B()");

}

}

当执行new B()语句时,结果仍是:

A()

B()

因此,我们通过super关键字显示地调用父类的构造函数,当然,也可以象使用this对象一样通过super调用父类的成员方法。比如:super.m1()等

但是,必须得注意:父类构造函数只能在子类构造函数中通过super显示调用,并且必须是第一句!

下面语句是错误的:

B()

{

System.out.println("B()");

//error! super()语句必须是方法中的第一条语句!

super();

}

包装类

对于每一个java基本数据类型,java都提供了对应的包装类,如:Boolean、Character、Integer、Double等8种包装类。

在java中,包装类主要是用于将基本数据类型与对象之间进行转换连接。

int num = 10;

String str = (String)num;

当执行第二条语句时,就会产生转换异常,因为我们在将一个非对象类型转换成一个对象类型!问题就是,我们应该怎样才能把基本类型转换成对象,很幸运的是,java的包装类就是为了专门解决这个问题而诞生的,我们修改以上语句,修改结果如下:

int num = 10;

Integer numObj = new Integer(10);

String str = (String)numObj;

这样,就能正常实现转换,因此,可以得知,包装类就是解决基本类型转换成对象的专用类。

在5.0中,java已实现了自动装、拆箱,即基本类型和对应的包装类之间可以直接赋值,转换的工作由虚拟机代劳了!

Integer numObj = 10;语句是正确的!其中基本类型到包装类的自动转换我们称之为自动装箱,反过来就是自动拆箱,比如:

Integer numObj = 10; //自动装箱

int num = numObj; //自动拆箱

因为java是面向对象语言,我们在许多场合都会将基本类型作为一个对象来处理,因此,包装类就起到了很方便的作用!

包装类有许多共同的方法和静态常量:

valueof(***) --static方法,将对应的基本类型转换成包装类

parseXXX(String) --static方法,将字符串转换成对应的基本类型

toString() --static方法,将基本类型转换成字符串对象。

SIZE --静态常量,返回对应基本类型占用的字节位大小。

TYPE --静态常量,返回对应的基本数据类型名称

== 与 equals()

在java中,==与equals()都是用来比较引用,只是==即可以比较基本类型,也可以比较对象,而equals()则只能在对象之间进行引用比较。

先来了解一下==运算符,分析一下语句执行结果:

int num1 = 10;

int num2 = 10;

System.out.println(num1 == num2); //打印true

String str1 = new String("123");

String str2 = new String("123");

System.out.println(str1 == str2); //打印false

可以看出,由于基本类型不是对象,即不存在引用,所以==运算符只会比较两个基本类型的值,但当==作用于对象时,我们可以通过第二条打印语句发现,虽然str1与str2都是引用相同的值"123",但是这两个对象分别是引用两个不同内存地址的值,即引用不相同,因此str1 == str2返回false,==作用于对象时,比较的是两个对象的引用是否相同!

==运算符进行等值比较。也就是说,对于任何引用值X和Y,当且仅当X和Y指向同一对象时, X==Y返回真。

Java.lang包中的Object类有public boolean equals(Object obj)方法。它也比较两个对象是否相等。仅当被比较的两个引用指向同一对象时,对象的equals()方法返回true。

Object类的equals()方法很少被使用,因为,多数情况下我们希望比较两个对象的内容,而不是判断两个引用是否指向同一对象。

String类中的覆盖equals()方法返回true,当且仅当参数是一个不为null 的String对象,该对象与调用该方法的String对象具有相同的字符顺序。例如:

String s1 = new String("JDK1.2");

String s2 = new String("JDK1.2");

方法s1.equals(s2)返回真,尽管s1和s2指向两个不同的对象。

下面的例子使用equals方法测试雇员的名字和生日:

public class Employee {

private String name;

// private Mydate birthDate;

private float salary;

public Employee(String name, float salary) {

this.name = name;

// this.birthDate = Dob;

this.salary = salary;

}

public boolean equals(Object o) {

boolean result = false;

if ((o != null) && (o instanceof Employee)) {

Employee e = (Employee) o;

if (name.equals(e.name)) {

result = true;

}

}

return result;

}

public int hashCode() {

return (name.hashCode());

}

}

我们覆盖了hashCode方法。这样做保证了相同的雇员对象有相同的hashCode。

下面的程序判断两个雇员对象引用是否相同:

public class TestEquals {

public static void main(String[ ] args){

Employee emp1 = new Employee("Fred Smith", 25000.0F);

Employeeemp2 = new Employee("Fred Smith", 25000.0F);

if(emp1 == emp2){

System.out.println("emp1 is identical to emp2")'

}else{

System.out.println("emp1 is not identical to emp2")'

}

if(emp1.equals(emp2) ){

System.out.println("emp1 is equals to emp2")'

}else{

System.out.println("emp1 is not equals to emp2")'

}

emp2 = emp1;

System.out.println("set emp2 = emp1");

if(emp1 == emp2){

System.out.println("emp1 is identical to emp2")'

}else{

System.out.println("emp1 is not identical to emp2")'

}

}

}

执行结果为:

emp1 is not identical to emp2

emp1 is equals to emp2

set emp2 = emp1

emp1 is identical to emp2

toString( )方法

toString方法被用来将一个对象转换成String表达式。当自动字符串转换发生时,它被用作编译程序的参照。例如:

Date now = new Date()

System.out.println(now)

将被翻译成:

System.out.println(now.toString());

对象类定义缺省的toString()方法,它返回类名称和它的引用的地址(通常情况下不是很有用)。许多类覆盖toString()以提供更有用的信息。例如,所有的包装类覆盖toString()以提供它们所代表的值的字符串格式。甚至没有字符串格式的类为了调试目的常常实现toString()来返回对象状态信息。

覆盖toString()方法之前:

class A {

public static void main(String[] args) {

System.out.println(new A());

}

}

执行结果是完整类名称+ @ +引用地址!(垃圾数字)

覆盖toString()方法之后:

class A {

public String toString() {

return "This is object of Class A";

}

public static void main(String[] args) {

System.out.println(new A());

}

}

执行结果变为:"This is object of Class A"

内容总结

? java是通过关键字extends来实现继承,子类extends父类。

? 四种访问区分符:public,protected,friendly,private,访问限制级别由低到高。

? 基于同类的同名方法重载,重载须满足下列条件之一:

? 方法同名

? 不同的方法特征(参数个数、次序、类型)

? 可以在子类中覆盖从父类继承过来的方法,覆盖须满足以下条件:

? 覆盖方法名、返回类型、参数必须与被覆盖方法一致

? 覆盖方法的访问限制级别不能低于被覆盖方法的访问级别

? 覆盖方法抛出异常不能多于被覆盖方法

? 从父类中继承过来的私有成员对于子类来说是隐藏的

? 创建子类对象时,遵循先创建父类对象,再创建子类对象的原则

? 通过super关键字在子类构造方法中调用父类构函,也可以通过super调用父类的普通成员。

? Object是java中所有类的顶级父类,所有类都继承了Object类的方法,包括toString()、equals()、hashcode()等方法,toString()方法实现将对象用指定字符串来描述的功能

? ==与equals()方法都是比较对象的引用,但包装类的equals()则是比较两个对象的值是否相等(通过方法覆盖实现)。

? 父类变量可以引用子类对象,通过多态可以在运行的时候决定变量引用的是什么类型的对象,从而实现不同的操作。

独立实践:

1、某公司有总经理,经理,员工三个对象,用继承的概念设计这个公司的结构。

2、员工的工资由经理来确定,请添加这个行为。

3、总经理负责奖金的发放,经理的奖金为员工的1.5倍,实现这个功能。

4、子类继承父类的哪些成员变量和方法?子类在什么情况下隐藏父类的成员变量和方法?在子类中是否允许有一个方法和父类的方法名字相同,而类型不同?

5、编写一个父类,一个子类,各类分别包含各自的静态初始化块、定义初始化块和构造函数,要求回答出类对象生成时,执行的顺序。

第七章:类的高级特征

学习目标:

? static关键字

? final关键字

? 内部类

? 成员内部类

? 静态内部类(也叫顶层类)

? 方法内部定义内部类

? 匿名内部类

? 实例分析

? 接口

static关键字

static关键字用来声明成员属于类,而不是属于类的对象。

1. static (类)变量

类变量可以被类的所有对象共享,以便与不共享的成员变量区分开来。

2. static (类)方法

静态方法可以通过类名直接调用该方法,而不用通过对象调用。

例如:

class PersonCount {

private int personID;

private static int num = 0;

public PersonCount() {

num++;

personID = num;

}

public static String getPersonDes() {

return "this is a policeman";

}

}

class TestPersonCount {

public static void main(String[] args) {

// 直接用类名来访问该静态方法,而不需要该类的对象

String s = PersonCount.getPersonDes();

System.out.println(s);

}

}

main()是静态的,因为它必须在任何实例化发生前被顺序地访问,以便应用程序的运行。

静态方法不能被覆盖成非静态。同样,非静态方法也不能被覆盖成静态方法。

3 "单态 "设计模式

单态 设计模式,也就是说一个类只产生一个对象。那么怎么才能做到这一点呢?我们知道构造器是用来构造对象的。首先要对构造器入手。既然只产生一个对象,那么我们就干脆先一刀砍断,把构造器的访问权限定义成私有,不能在类的外面再构造该类的对象。也就是说只能在类的里面调用该类的构造器来产生对象。那么在该类里应该定义一个静态的属性,并初始化该类的一个对象。(原因是静态的东西只执行一次,也就是说该属性只初始化一次。那么每次得到的应该是同一个实例)

class TestSC {

public static void main(String[] args) {

SingleClass sc1 = SingleClass.sc;

SingleClass sc2 = SingleClass.sc;

sc1.test();

sc2.test();

}

}

class SingleClass {

int i = 0;

static SingleClass sc = new SingleClass();

private SingleClass() {

}

public void test() {

System.out.println("hello " + (++i));

}

}

运行的结果为:

hello 1

hello 2

说明是同一个实例。

在类的设计的时候,我们也应该遵守封装的要求。把属性定义成私有的。再定义一个共有的方法来去到该属性的值。

改后的代码:

class TestSC {

public static void main(String[] args) {

SingleClass sc1 = SingleClass.getSingleClass();

SingleClass sc2 = SingleClass.getSingleClass();

sc1.test();

sc2.test();

}

}

// 型态1:此时,还是可以直接调用类静态变量,得到类对象,还不完全达到我们的设定

class SingleClass {

int i = 0;

private static SingleClass sc = new SingleClass();

private SingleClass() {

}

/*

* 因为在类的外面不能来构造给类的实例了, 所有该方法定义成静态的,通过类名直接可以调用。

*/

public static SingleClass getSingleClass() {

return sc;

}

public void test() {

System.out.println("hello " + (++i));

}

}

// 型态2:此时,只能调用类静态方法,得到类对象

class SingleClass {

int i = 0;

private static SingleClass sc = null;

private SingleClass() {

}

/*

* 因为在类的外面不能来构造给类的实例了, 所有该方法定义成静态的,通过类名直接可以调用。

*/

public static SingleClass getSingleClass() {

if (sc == null) {

sc = new SingleClass();

}

return sc;

}

public void test() {

System.out.println("hello " + (++i));

}

}

final关键字

1 、final类

Java编程语言允许关键字final修饰类。如果这样做了,类便不能被继承。比如,类Java.lang.String就是一个final类。这样做是出于安全原因,因为它保证,如果方法有字符串的引用,它肯定就是类String的字符串,而不是某个其它类的字符串。

2 、final方法

方法也可以被标记为final。被标记为final的方法不能被覆盖。这是由于安全原因。如果方法具有不能被改变的实现,而且对于对象的一致状态是关键的,那么就要使方法成为final。

被声明为final的方法有时被用于优化。编译器能产生直接对方法调用的代码,而不是通常的涉及运行时查找的虚拟方法调用。

被标记为static或private的方法被自动地final。

3 、final变量

如果变量被标记为final,其结果是使它成为常数。想改变final变量的值会导致一个编译错误。下面是一个正确定义final变量的例子:

public final int PI = 3.14;

扩展知识:

内部类

内部类,有时叫做嵌套类。内部类允许一个类定义被放到另一个类定义里。内部类是一个有用的特征,因为它们允许将逻辑上同属性的类组合到一起,并在另一个类中控制一个类的可视性。内部类可以访问外部类的属性和方法。你可以把内部类看作"方法"一样,在使用的时候调用执行。你也可以把内部类看作"属性"一样,在构造内部类对象的时候,也会在堆里为内部类的属性分配存储空间。所以内部类也有类似像修饰属性,方法那样的修饰符,比如:public,private,static 等等。当一个类没有用static 关键字修饰的时候,这个内部类就叫做成员类,类似属性,方法,作为类的成员。

成员内部类:

public class OC1 {

private int size;

public class IC {

public void addSize() {

size++;

}

}

public void testTheIC() {

IC i = OC1.new IC();

i.addSize();

}

}

内部对象拥有一个外部对象的引用(如图7.7):

例2:

这个例子阐述了如何在其它类(外部类的外部)实例化内部类:

class OC2 {

private int size;

public class IC {

public void addSize() {

size++;

}

}

}

public class TestIC // Test Inner Class

{

public static void main(String[] args) {

OC2 outer = new OC2();

// 因为是成员内部类,所以必须用外部类的对象来构造内部类的对象,类似调用方法一样。

OC2.IC inner = outer.new IC();

inner.addSize();

}

}

内部类要在外部类实例的上下文中实例化(如图7.8):

例3

this的一个作用是调用本类的其它构造器,另外一个作用就是做隐含参数的调用,代表当前的实例。完整的写法应该是 该类的类名.this 如下例:

本例阐述如何区分同名变量:

public class OC3 {

private int size;

public class IC // Inner Class

{

private int size;

public void addSize(int size) {

// 方法里的临时变量,当方法执行完自动消失

size++;

this.size++;

// 代表本类的当前对象,全称是IC.this.size++;

OC3.this.size++;

}

}

}

例4

静态内部类(也叫顶层类):

class OC4 {

private static int size;

// 声明一个内部类 叫 "IC"

public static class SIC // Static Inner Class

{

public void addSize() {

// 访问外部类的属性

size++;

}

}

}

public class TestSIC // Test Static Inner Class

{

public static void main(String[] args) {

// 因为内部类是静态内部类,所以直接可以构造内部类的一个对象,与调用静态方法类似

OC4.SIC inner = new OC4.SIC();

inner.addSize();

}

}

例5

方法内部定义内部类:

class OC5 {

// 内部类访问,应该定义成final

public Object makeObject(final int i) {

class MIC // Methord Inner Class

{

int k = i;

public String toString() {

return ("属性k :" + k);

}

}

return new MIC();

}

public static void main(String[] args) {

OC5 oc = new OC5();

Object o = oc.makeObject(5);

System.out.println(o);

}

}

注意:在方法中定义的内部类的方法,不能访问外方法的运行时变量空间(line 10),可以访问外方法的非运行时变量空间(line 11)。

例6

匿名内部类:有时候定义一个类,并不需要提供名字。所以叫匿名类。

class OC6 {

// 多态,传递的参数应该是实现该接口的任何类产生的对象

public void testFly(Fly f) {

f.fly();

}

public static void main(String[] args) {

OC6 oc = new OC6();

// 画下划线的代码就是构造了实现Fly接口的某个类的对象,类名并不需要知道,只

// 知道该对象具有接口的功能就行。

oc.testFly(new Fly() {

public void fly() {

System.out.println("fly higher and higher");

}

});

}

}

interface Fly {

public void fly();

}

匿名类具有广泛性。不只是对接口才有,对抽象类或者具体的类都适用。例如:

class OC7 {

public static void main(String[] args) {

System.out.println(new Person() {

public String toString() {

return "this is a person";

}

});

}

}

class Person {

}

注意:如果是接口或者抽象类的话,在匿名类里面必须实现接口或者抽象类里所有的抽象方法。如果是具体的类的话就没必要了,需要的话可以覆盖方法,不需要时可以不写任何代码。看下例:

class OC7 // Outer Class 7

{

public static void main(String[] args) {

// 这里其实是Person类里的一个子类,只不过该子类并没有扩展功能

System.out.println(new Person() {

});

}

}

class Person {

public String toString() {

return "this is a person";

}

}

内部类有如下属性:

1 内部类只在定义他们的代码段内可见。

2 内部类可以被定义在方法中。如果方法中的变量被标记为final,那么,就可以被内部类中的方法访问。

3 内部类可以使用所嵌套类的类和实例变量以及所嵌套的块中的本地变量。

4 内部类可以被定义为abstract.

5 只有内部类可以被声明为private或protected,以便防护它们不受来自外部类的访问。

6 一个内部类可以作为一个接口,由另一个内部类实现。

7 被声明为static的内部类自动地成为顶层类。这些内部类失去了在本地范围和其它内部类中使用数据或变量的能力。

8 内部类不能声明任何static成员;只有顶层类可以声明static成员。因此,一个需求static成员的内部类必须使用来自顶层类的成员。

实例分析

例1:

ChinaSoft的员工都必须被初始化为统一的公司名称,并且改变一个员工的公司名称信息可以修改所有员工信息,设计一个员工类解决该问题.

解决方案:

1 问题分析

2 使用static说明符

3 变量初始化分析

4 编写代码并编译运行

问题分析

本问题涉及变量共享,可以采用static变量来完成,并编写必要的构造函数,静态代码块来初始化变量.

使用static说明符

static (类)变量

static类变量可以被类的所有对象共享,以便与不共享的成员变量区分开来。

例如:

class PersonCount {

private int personID;

private static int num = 0;

public PersonCount() {

num++;

personID = num;

}

}

在这个例子中,被创建的每个对象被赋于一个独特的序号,从1开始并继续往上。变量num在所有对象中共享,所以,当一个对象的构造函数增加num时,被创建的下一个对象接受增加过的值。

static变量在某种程度上与其它语言中的全局变量相似。Java编程语言没有这样的全局语言,但static变量是可以从类的任何对象访问的单个变量。

如果static变量没有被标记成private,它可能会被从该类的外部进行访问。要这样做,不需要类的实例,可以通过类名指向它。

例如:

class MathVar {

public static double pi = 3.14;

}

class TestMath {

public double CirArea(double i) {

double area = i * i * MathVar.pi;

return area;

}

}

结果:

public static String company = "china soft";

变量初始化

当类被装载时,静态块代码只执行一次。类中不同的静态块按它们在类中出现的顺序被执行。

class TestSIB // static initialize block

{

static {

System.out.println("hello,I am a static block programe");

}

public static void main(String args[]) {

System.out.println("I am main methord");

}

}

将打印出:

hello,I am a static block programe

I am main methord

从结果可以看出,在main 方法执行前,先执行了静态块。

结果:

static{

company = "china soft";

}

编写代码并编译运行

public class Employee {

public static String company = "china soft";

private String name;

private String phone = "0755-51595599";

// 静态代码块

static {

company = "china soft";

System.out.println("Employee Company:" + company);

}

// 代码块

{

System.out.println("Employee phone:" + phone);

}

// 默认构造函数

public Employee() {

this("Unknown");

System.out.println("Empoloyee()");

}

public Employee(String name) {

System.out.println("Employee(String)");

this.name = name;

}

public static void main(String args[]) {

new Employee();

}

}

运行结果:

Employee Company:china soft

Employee phone:0755-51595599

Employee(String)

Empoloyee()

例2:

ChinaSoft的销售部经理都必须被初始化为统一的部门名称,并且改变一个经理的部门名称信息可以修改其它该部门经理信息,设计一个经理类解决该问题.

解决方案:

1 问题分析

2 继承Employee

2 使用static说明符

3 变量初始化分析

4 编写代码并编译运行

问题分析

本问题涉及变量共享,可以采用static变量来完成,并编写必要的构造函数,静态代码块来初始化变量.

经理本身就是员工,可以继承员工类来完成经理类的编写。

继承Employee

结果:

public class Manager extends Employee {

}

使用static说明符

结果:

public static String department;

变量初始化

结果:

static{

department = "sale";

}

编写代码并编译运行

public class Manager extends Employee {

public static String department;

private int salary = 8000;

// 静态代码块

static{

department = "sale"

System.out.println("Manager department:" + department);

}

// 代码块

{

System.out.println("Manager salary:" + salary);

}

// 默认构造函数

public Manager() {

this("Unknown");

System.out.println("Manager()");

}

public Manager(String name) {

System.out.println("Manager(String)");

}

public static void main(String args[]) {

new Manager();

}

}

运行结果:

Employee Company:china soft

Manager department:sale

Employee phone:0755-51595599

Employee(String)

Empoloyee()

Manager salary:8000

Manager(String)

Manager()

总结

1 如果存在继承关系,就先父类后子类;

2 如果在类内有静态变量和静态块,就先静态后非静态,最后才是构造函数;

3 继承关系中,必须要父类初始化完成后,才初始化子类。

例3:

在轮船公司例子中,假设系统需要提供周报,列出公司的交通工具清单及其燃料需求情况。让我们假设Shipping系统中有一个ShippingMain类,这个类为我们生成交通工具清单及其燃料需求报告。如下代码:

public class ShippingMain {

public static void main(String[] args) {

Company c = Company.getCompany();

c.addVehicle(new truck(10000.0));

c.addVehicle(new truck(15000.0));

c.addVehicle(new RiverBarge(500000.0));

c.addVehicle(new truck(9500.0));

c.addVehicle(new RiverBarge(750000.0));

FuelNeedsReport report = new FuelNeedsReport();

report.generateText(System.out);

}

}

如图显示了Company和它的交通工具关系。

生成报告代码:

public class FuelNeedsReport {

public void generateText(PrintStream output){

Company c = Company.getCompany();

Vehicle v;

double fuel;

double total_fuel = 0.0

for(int i = 0; i < c.getFleetSize(); i++){

v = c.getVehicle(i);

fuel = v.calcTripDistance() / v.calcFuelEfficiency( );

output.println("Vehicle "+ v.getName() + " needs " + fuel + " liters of fuel.");

total_fuel += fuel;

}

output.println("Total fuel needs is " + total_fuel +" liters.");

}

}

问题:卡车与船舶计算燃料效率的方法是不同的,如何设计设计超类Vehicle,使卡车与驳船燃油计算合理?

解决方案:

1 问题分析

2 抽象类,接口

4 编写代码

问题分析

在本例中可以使用抽象类或者接口来解决问题.

抽象类,接口

抽象类 Java语言允许你在一个类中声明一些方法,然而又不实现它。这种方法叫抽象方法。包含一个或多个抽象方法的类叫做抽象类。你可以在抽象类的子类中实现抽象方法。抽象类只能被其它类继承,不能用来创建实例。

如图设计抽象类:

计算fuel调用了Vehicle类的两个抽象方法,Vehicle类的抽象方法在它的子类中实现。这就是"模板方法"

接口

接口是用关键字interface来定义的,接口是客户端代码与提供服务的类之间的"规约"。接口是抽象类的变体。接口中的所有方法都是抽象的,没有一个有程序体。接口只可以定义static final成员变量。

接口的使用,弥补了Java技术单继承规则的不足。一个类可以实现多个接口。接口的实现与子类相似,当类实现某个接口时,它必须定义这个接口的所有方法。

例如:一些对象都具有飞行的能力,可以定义一个Flyer接口,它支持三种操作:

takeoff() 、land()、fly()。类Airplane可以实现Flyer。

interface Flyer {

public void takeoff();

public void land();

public void fly();

}

public class Airplane implents Flyer{

public void takeoff(){

// accelerate until lift-off

// raise landing gear

}

public void land(){

// lower landing gear

// decelerate . . .

}

public void fly(){

// keep those engines running

}

}

Java允许多个类实现同一个接口:

飞机是一种交通工具,并且能够飞行。鸟是一种动物,也能够飞行。这些例子说明继承与实现接口可以混合使用。(如图7.5)

一个类可以实现多个接口:

类能实现许多接口。由类实现的接口出现在类声明的末尾以逗号分隔的列表中,如下所示:

public class MyApplet extends Applet implements Runnable, MouseListener{ }

什么时候使用接口

对于下述情况,应该使用接口:

声明方法,期望一个或更多的类来实现该方法。

揭示一个对象的编程接口,而不揭示类的实际程序体。(当将类的一个包输送到其它开发程序中时它是非常有用的。)

捕获无关类之间的相似性,而不强迫类关系。

描述"似函数"对象,它可以作为参数被传递到在其它对象上调用的方法中。它们是"函数指针"(用在C和C++中)用法的一个安全的替代用法.

结果:

public abstract class Vehicle{

public abstract double calcFuelEfficiency();

public abstract double calcTripDistance();

}

编写代码

public class Truck extends Vehicle {

public Truck(double max_load) {

}

public double calcFuelEfficiecy() {

// calculate

}

public double calcTripDistance() {

// calculate

}

}

public class RiverBarge extends Vehicle {

public RiverBarge(double max_load) {

}

public double calcFuelEfficiecy() {

// calculate

}

public double calcTripDistance() {

// calculate

}

}

内容总结

? 描述static变量、方法和初始化程序

? 描述final类、方法和变量

? 列出访问控制级别

? 在Java软件程序中,确认:

? static方法和变量

? public,private,protected和缺省变量

? 使用abstract类和方法

? 如何及何时使用内部类

? 如何及何时使用接口

独立实践

? 实践1:China soft管理系统中的公司类的设计必须保障无论何时创建该对象都只能是同一对象,设计Company类实现要求。

? (提示:使用单态 设计模式)

? 实践2:埃及的金字塔前的“狮身人面”像是人类文明的象征,从程序开发的角度如何构建出该对象。

? (提示:接口与抽象类技术使用)

? 实践3:按照以下类之间的关系,使用接口和抽象类,编写出以下对象的类代码:

? 汽车,玩具汽车,玩具飞机,阿帕奇直升机,

? 实践4:构建一成员内部类,理解学习,内部类的构造,内部类对外部类数据的访问方式,和内部类自身数据的访问方式。

? 实践5:新建一接口,声明所包含的方法,新建一类,其私有成员方法中包含内部类,此内部类实现接口,要求,传入任一数据,返回本数据是否包含有特殊字符。

第八章:异常

学习目标

? 异常的概念

? 异常的分类

? 公共异常

? 实例分析

? 自定义异常

? 方法覆盖和异常

异常的概念

在Java编程语言中,异常类定义程序中可能遇到的轻微的错误条件。你可以写代码来处理异常,并继续执行程序,而不需要让程序中止。

在程序执行中,任何中断正常程序流程的条件都是异常。例如,发生下列情况时,会出现异常:

想打开的文件不存在

网络连接中断

操作数超出预定范围

正在装载的类文件丢失

在Java编程语言中,错误类定义被认为是不能恢复的严重错误条件。在大多数情况下,当遇到这样的错误时,建议让程序中断。

在程序中发生错误时,发现错误的方法抛出一个异常到其调用程序,给出已经发生问题的信号。然后,调用方法捕获抛出的异常,在可能时,再恢复回来。这个方案给程序员一个写处理程序的选择,来处理异常。

通过浏览API,可以决定方法抛出的是什么样的异常。

异常的分类

在Java编程语言中java.lang.Throwable类充当所有对象的父类,可以使用异常处理机制将这些对象抛出并捕获。在Throwable类中定义方法来检索与异常相关的错误信息,并打印显示异常发生的栈跟踪信息。它有Error和Exception两个基本子类。

Throwable类不能直接使用,我们使用其子类来捕获和描述异常信息。

异常结构如图:

Error表示严重的错误问题。比如说内存溢出。不可能指望程序能处理这样的情况。

Exception 则是我们关心和需要处理的错误。

RuntimeException表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。比如,如果数组索引扩展不超出数组界限,那么,ArrayIndexOutOfBoundsException异常从不会抛出。比如,这也适用于取消引用一个空值对象变量。因为一个正确设计和实现的程序从不出现这种异常,通常对它不做处理。这会导致一个运行时信息,应确保能采取措施更正问题,而不是将它藏到谁也不注意的地方。

其它异常表示一种运行时的困难,它通常由环境效果引起,可以进行处理。例子包括文件未找到或无效URL异常(用户打了一个错误的URL),如果用户误打了什么东西,两者都容易出现。这两者都可能因为用户错误而出现,这就鼓励程序员去处理它们。

预定义异常

Java编程语言提供几种预定义的异常。下面是可能遇到的更具共同性的异常中的几种:

ArithmeticException:整数被0除,运算得出的结果。

int i = 12 / 0;

NullPointerException:当对象没被实例化时,访问对象的属性或方法的尝试:

Date d = null;

System.out.println(d.toString());

NegativeArraySizeException:创建带负维数大小的数组的尝试。

ArrayIndexoutofBoundsException:访问超过数组大小范围的一个元素的尝试。

SecurityException:典型地被抛出到浏览器中,SecurityManager类将抛出applets的一个异常,该异常企图做下述工作(除非明显地得到允许):

访问一个本地文件

打开主机的一个socket,这个主机与服务于applet的主机不是同一个。

在运行时环境中执行另一个程序

异常的处理机制

一般来说,异常的处理机制有以下三种:

try-catch-finally 主动异常处理

throws 消极异常处理

throw 引入异常

实例分析

例1

问题的描述:

写一个常见的异常

解决方案:

请看下例:

public class TestException {

public static void main(String args[]) {

int i = 0;

String ss[] = { "Hello world!", "您好,世界!", "HELLO WORLD!!" };

for (; i < 6; i++) {

System.out.println(ss[i]);

}

}

}

运行的结果为如图。

要处理异常,将能够抛出异常的代码放入try块中,然后创建相应的catch块的列表,每个可能被抛出异常都有一个。如果生成的异常与catch中提到的相匹配,那么catch条件的块语句就被执行。在try块之后,可能有许多catch块,每一个都处理不同的异常。

请看下例:

class TestException2 {

public static void main(String args[]) {

int i = 0;

String ss[] = { "Hello world!", "您好,世界!", "HELLO WORLD!!" };

for (; i < 5; i++) {

try {

System.out.println("第" + (i + 1) + "次循环:");

System.out.println(ss[i]);

} catch (Exception e) {

System.out.println("数组越界");

} finally {

System.out.println("finally execute");

}

}

}

}

运行的结果如图

总结:从上例可以看出:不管出现不出现异常,finally语句块都会执行。在try语句块里除了System.exit(int)语句外,finally语句块必须执行。

Throws则是自己不处理异常,而交给上级处理,俗称“异常上抛”,比如:

public static void main(String args) throws RuntimeException

这样,一旦在代码里出现RuntimeException,在本类里不做任何处理动作,而是交由上次程序或者虚拟机去处理。

自定义异常

用户定义异常是通过扩展Exception类来创建的。这种异常类可以包含一个"普通"类所包含的任何东西。下面就是一个用户定义异常类例子,它包含一个构造函数、几个变量以及方法

请看下例:

class TestMyException {

public static void main(String[] args) {

ABC abc = new ABD();// ABD 是 ABC的子类

try {

abc.a(5);

} catch (EA e) {

e.test();

}

}

}

创建自己的运行时异常,如果改成Exception,就是创建编译时的异常了,编译的时候就应该对异常处理。

class EA extends RuntimeException {

String s;

public EA(String s) {

this.s = s;

}

public EA() {

}

public void test() {

System.out.println(s);

}

};

class EA1 extends EA // 创建自己的异常的子异常

{

public EA1(String s) {

super(s);

}

};

class EA2 extends EA // 创建自己的异常的子异常

{

public EA2(String s) {

super(s);

}

};

class ABC {

public void a(int i) {

if (i < 0)

System.out.println("normal");

else

throw new EA("no normal");

}

};

class ABD extends ABC {

public void a(int i) {

if (i < 0)

System.out.println("dfsdafds");

else if (i == 0)

throw new EA1("参数 == 0");

else

throw new EA2("参数 > 0");

}

};

运行的结果为:

参数 > 0

方法覆盖和异常

方法覆盖要注意两点:

? 访问权限不能比父类的弱

? 抛出的异常不能比父类的多。(注意多并不是数量上的多,而是父类方法抛出的异常必须包含子类覆盖方法抛出的异常)

请看下例:

class TestMyException2 {

public static void main(String[] args) {

ABC abc = new ABD();

try {

abc.a(5);

} catch (EA e) {

e.test();

}

}

}

class EA extends Exception // 创建自己的编译时异常

{

String s;

public EA(String s) {

this.s = s;

}

public EA() {

}

public void test() {

System.out.println(s);

}

};

class EA1 extends EA // 创建自己的异常的子异常

{

public EA1(String s) {

super(s);

}

};

class EA2 extends EA // 创建自己的异常的子异常

{

public EA2(String s) {

super(s);

}

};

class ABC {

public void a(int i) throws EA {

if (i < 0)

System.out.println("normal");

else

throw new EA("no normal");

}

};

class ABD extends ABC {

public void a(int i) throws EA1, EA2 // 虽然抛出的异常比父类多,但是都是父类异常的子类

{

if (i < 0)

System.out.println("dfsdafds");

else if (i == 0)

throw new EA1("参数 == 0");

else

throw new EA2("参数 > 0");

}

};

运行结果为:

参数 > 0

内容总结

? 了解java异常的处理机制

? 掌握java异常的分类

? 使用try,catch,finnaly对异常进行处理

? 创建并使用自己定义异常

第八章:异常 122

学习目标 122

异常的概念 123

异常的分类 123

实例分析 124

自定义异常 126

方法覆盖和异常 127

内容总结 129

独立实践

? 1、给客户建立一个帐户,当取的钱大于余额的话,就要抛出异常------显示余额不足的信息

? 2、编写User类,其年龄必须在18-60,否则产生异常。

? 3、思考,在多个异常被定义需捕获时,是否所有的异常信息都会被打印出来呢?

? 4、思考,一个异常被捕获,这个异常之后的代码是否还可运行?

? 5、编写一个异常类MyException,再编写一个类Student,该类有一个产生异常的方法public void speak(int m) throws MyException,要求参数m的值大于500时,方法抛出一个MyException对象。编写一个主类,在主类main方法中用Student创建一个对象,该对象调用speak方法。

第九章:基于文本的应用

学习目标

? 程序交互的几种方式

? String的方法

? 正则表示式

? StringBuffer类

? String与StringBuffer类的区别

? 集合类的使用

? 命令行参数

? 系统属性

? 标准输入

? 程序中实现文件的创建,读,写

? Set容器

? List容器

? 迭代器

? 映射(Map)

? 排序

程序交互的几种方式

程序在运行的时候,我们要给程序输入数据,程序根据输入的数据作出响应。

常见的输入方式有:

? 命令行参数

? 系统属性

? 标准的输入

? 在程序中实现文件的创建,读,写

常用类方法说明

Math类是用来支持数学计算的,它打包在java.lang包中,包含一组静态方法和两个常数,是终态(final)的,它不能被实例化。它主要包括下列方法:

分割

int ceil(double d):返回不小于d的最小整数。

int floor(double d):返回不大于d的最大整数。

int round(float f) :返回最接近f的整数(int)。

long round(double d) :返回最接近d的整数(long)。

极值、绝对值

double abs(double d):返回d的绝对值。对于float,int,long有类似的函数。

double min(double d1, double d2) :返回d1与d2中的小者。对于float,int,long有类似的函数。

double max(double d1, double d2) :返回d1与d2中的大者。对于float,int,long有类似的函数。

三角函数

double sin(double d):返回d正弦。对于余弦和正切有类似的函数cosine,tangent。

double asin(double d):返回d反正弦。对于反余弦和反正切有类似的函数acos,atan。

double toDegrees(double r):将弧度换算成度。

double toRadians(double d):将度换算成弧度。

对数、指数

double log(double d):返回d的自然对数。

double exp(double d):返回以e为底的指数。

其它函数

double sqrt(double d):返回d的平方根。

double pow(double d1, double d2):返回d1的d2次幂。

double random():返回0 至 1 的随机数。

常数

PI:圆周率,double。

E:自然对数的底,double。

String的方法

String concat(String s):返回一个新的String,即,在原来的String后面追加上s。

String replace(String old, String new):返回一个新的String,将原来的String中的old替换成new。

String substring(int start, int end):返回一个新的String,它是原来的String中从start到end的一部分。

String toLowerCase():返回一个新的String,它将原来的String中的大写字母变成小写。

String toUpperCase():返回一个新的String,它将原来的String中的小写字母变成大写。

查找方法

boolean endsWith(String s):如果原来的String以s为结尾,则返回true。

boolean startsWith(String s) :如果原来的String以s为开始,则返回true。

int indexOf(String s):返回String中第一次出现s偏移量。类似有lastindexOf,从串尾开始查找。

int indexOf(int ch):返回String中第一次出现ch偏移量。类似有lastindexOf,从串尾开始查找。

int indexOf(String s, int offset):返回String中从offset开始查找,第一次出现s的偏移量。类似有lastindexOf,从串尾开始查找。

int indexOf(int ch, int offset) 返回String中从offset开始查找,第一次出现ch的偏移量。类似有lastindexOf,从串尾开始查找。

比较方法

boolean equals(String s):如果原String与s逐字符比较都相等,则返回true。

boolean equalsIgnoreCase(String s):如果在忽略大小写的情况下,原String与s逐字符比较都相等,则返回true。

int compareTo(String s):进行词汇比较,如果原String 小于s则返回负数;如果原String 大于s则返回正数;如果原String 等于s则返回零。

其它方法

char charAt(int index):返回index处的字符。

int length():返回String的长度。

正则表示式(Regular expression)

正则表示式的功能是J2SE 1.4之后加入的新功能。String的matches()、replaceAll()等方法,所传入的参数就是正则表示式(Regular expression)的字符串;可以在API文件的 java.util.regex.Pattern 类中找到有关正则表示式的相关信息。

如果您使用String类定义字符串对象,可以使用简易的方法来使用正则表示式,并应用于字符串的比较或替换等方法中,以下先介绍几个简单的正则表示式。

例如一些常用的范围,我们可以使用预先定义的字符类别:

. 表示任一字符

\d 表示 [0-9] 数字

\D 表示 [^0-9] 非数字

\s 表示 [ \t\n\x0B\f\r] 空格符

\S 表示 [^ \t\n\x0B\f\r] 非空格符

\w 表示 [a-zA-Z_0-9] 数字或是英文字

\W 表示 [^a-zA-Z_0-9] 非数字与英文字

. 表示任一字符。例如有一字符串abcdebcadxbc,使用.bc来比对的话,符合的子字符串有abc、ebc、xbc三个。

以上的例子来根据字符比对,您也可以使用「字符类」(Character class)来比较一组字符范围,例如:

[abc] a、b或c

[^abc] 非a、b、c的其它字符

[a-zA-Z] a到z或A到Z(范围)

[a-d[m-p]] a到d或m到p(联集)

[a-z&&[def]] d、e或f(交集)

[a-z&&[^bc]] a到z,除了b与c之外(差集)

[a-z&&[^m-p]] a到z且没有m到p(a-lq-z)(差集)

可以指定字符可能出现的次数:

X? X出现一次或完全没有

X* X出现零次或多次

X+ X出现一次或多次

X{n} X出现n次

X{n,} X出现至少n次

X{n,m} X出现至少n次,但不超过m次

在String类别中,matches()方法可以让您验证字符串是否符合指定的正规表示式,这通常用于验证使用者输入的字符串数据是否正确,例如电话号码格式;replaceAll()方法可以将符合正规表示式的子字符串置换为指定的字符串;split()方法可以让您依指定的正规表示式,将符合的子字符串分离出来,并以字符串数组传回。

下面这个程序示范几个正则表示式的应用:

UseRegularExpression.java

import java.util.Scanner;

public class UseRegularExpression {

public static void main(String args[]) {

Scanner scanner = new Scanner(System.in);

String str = "abcdefgabcabc";

System.out.println(str.replaceAll(".bc", "###"));

System.out.print("输入手机号码: ");

str = scanner.next(); // 简单格式验证

if (str.matches("[0-9]{4}-[0-9]{6}"))

System.out.println("格式正确");

else

System.out.println("格式错误");

System.out.print("输入href标签: ");

// Scanner的next()方法是以空白为区隔

// 我们的输入有空白,所以要执行两次

str = scanner.next() + " " + scanner.next();

// 验证href标签

if (str.matches("<a.+href*=*['\"]?.*?['\"]?.*?>"))

System.out.println("格式正确");

else

System.out.println("格式错误");

System.out.print("输入电子邮件: ");

str = scanner.next();

// 验证电子邮件格式

if (str.matches("^[_a-z0-9-]+([.][_a-z0-9-]+)*@[a-z0-9-]+([.][a-z0-9-]+)*$"))

System.out.println("格式正确");

else

System.out.println("格式错误");

}

}

执行结果:

StringBuffer类

StringBuffer对象是一个可以改变的统一编码字符串。String与StringBuffer之间没有继承关系。

构造函数

StringBuffer():创建一个空的StringBuffer。

StringBuffer(int capacity) :创建一个空的StringBuffer,容量是capacity。

StringBuffer(String initialString) :创建一个StringBuffer,其内容是initialString。

修改方法

StringBuffer append(String s):在原来的StringBuffer后面追加上s。对于下列参数类型有重载的方法:

boolean,char,char[],double,float,int,long,Object。

StringBuffer insert(int offset, String s):在原来的StringBuffer的offset处插入s。对于下列参数类型有重载的方法:boolean, char, char[], double, float, int, long, Object。

StringBuffer reverse():颠倒原StringBuffer中字符的顺序。

void setCharAt(int index, char ch):将StringBuffer中的index处设为ch。

void setlength(int newLength):设定StringBuffer的长度。

另外,在JDK5.0中新加入了StringBuilder类,它类似于StringBuffer类,只是该类的方法是非线程安全的;因此在不需要考虑线程安全时可以考虑这个类替换StringBuffer类。

StringBuffer与String的区别

主要是效率上的区别。它们都可以表示一个字符串对象,只不过String类型的字符串对象,只要值改变,存储的空间也会改变,而StringBuffer是在原来的空间上进行修改的。所以要进行大量的字符串操作,要用StringBuffer.

而如果不是频繁操作字符串对象,两者都可以,只不过String 更方便些,因为用两个双引号就可以表示一个Sting对象。

集合类的使用

集合(或容器)是代表一个对象组的单个对象,其它对象被认为是它的元素。集合用于处理多种类型对象的问题,所有的类型都有一个特殊的种类(也就是说,它们都是从一个共同父类继承来的)。Java编程语言支持集合Vector,List,Map,Stack等等。例如,Stack实现后进先出(LIFO)的顺序,Hashtable提供一个相关的对象数组。

集合可用于保存,处理Object类型的对象。这允许在收集中贮存任何对象。它还可以,在使用对象前、从集合中检索到它之后,使用正确的类型转换为我们所需要的对象类型。

Collections API的体系结构

集合是代表一组对象的单个对象。集合中的对象叫元素。

我们在程序设计中经常会用到各种各样的数据结构,如:表、映射、清单、树、集合等。显然,这些数据结构都满足集合的定义。为了方便我们处理各种各样的数据结构,Java在Java.util包中提供了一组API。这组API中的大部分类都实现了Collection接口,因此,被称作Collections API。

API还包括诸如HashSet, ArraySet, ArrayList, LinkedList和Vector等等的类,它们实现这些接口。API还提供了支持某些算法的方法,如:排序,二进制搜索,计算列表中的最小和最大等。

集合根据它们处理的不同种类的数据结构,Collections API可分为三类:

(Collection)收集-没有具体顺序的一组对象

(Set)设定-没有重复的一组对象

(List)列表-有序对象组,允许重复

实例分析

命令行参数

例1

问题的描述:

通过命令行参数给程序输入数据

解决方案:

当一个Java应用程序从终端启动时,用户可以提供零个或多个命令行参数。这些命令行参数都是字符串,这些字符串可以是独立的记号(如:arg1),也可以是引号之间的多个符号("another arg")。参数序列跟在程序类的名字后面输入;然后被存放在String 对象的数组中,传递给main 方法。

请看下例:

class TestArgs {

public static void main(String[] args) {

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

System.out.println(args[i]);

}

}

}

编译,运行以及输出的结果为:

例2

系统属性

问题的描述:

通过系统属性给程序传入数据

解决方案:

系统属性是另外一种在运行时向程序传递参数的机制。每个属性都是一个属性名和属性值的映射对。属性名和属性值都是字符串。Properties 类表示这样的映射。System.getProperties方法返回系统的属性对象。System.getProperties(String)方法返回String属性的值。System.getProperties(String, String)方法允许你在属性名不存在时返回默认值。你可以使用递归调用PropertyNames方法遍历全部属性名的集合;对每个属性名调用getProperty方法得到所有属性的值。

请看下例:

import java.util.*;

class TestSP // System Properties

{

public static void main(String[] args) {

Properties p = System.getProperties(); // 第六行

Enumeration e = p.propertyNames(); // 第七行

while (e.hasMoreElements()) {

String name = (String) e.nextElement();

if (name.equals("aaa")) {

String value = p.getProperty(name);

System.out.println("name: " + name + " value: " + value);

}

}

}

}

分析:第六行取得系统属性的集合,第七行从属性集合中得到属性名的枚举。枚举对象允许程序循环遍历其中的所有元素。这一点与迭代相似。hasMoreElements 方法判断枚举中是否还有后续元素,nextElement方法返回枚举中的下一个元素。

运行:

java -Daaa=345 TestSP //-D后面是属性的名字,=后面是属性的值 ,注意是大写的D

输出结果摘录如下:

例3

标准的输入

问题的描述:

标准的输入

解决方案:

多数应用都会发生人机交互,人机交互经常通过控制台文本输入/输出来完成。

Java 2 SDK 用公有类java.lang.System支持控制台I/O。

System.out是一个PrintStream对象,它指向运行Java应用程序的终端窗口。

System.in是一个InputStream对象,它指向用户的键盘。

请看下例:

import java.io.*;

class TestKI // Keyboard Input

{

public static void main(String[] args) {

String s;

// 进行字符串的包装,就可以读取一行字符串

InputStreamReader isr = new InputStreamReader(System.in);

BufferedReader br = new BufferedReader(isr);

System.out.println("按 ctrl+c 键 或者输入exit 退出程序!");

try {

s = br.readLine();

// 如果按 ctrl+c 键,那么s==null,所以要先进行s!=null判断,不然

// 会出现空指针异常(调用s.equals("exit"))

while (s != null && !s.equals("exit")) {

System.out.println("Read: " + s);

s = br.readLine();

}

br.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

编译和运行的结果为:

JDK1.5新特性:java.util.Scanner类,如下代码:

import java.util.Scanner;

public class ScannerDemo {

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);

System.out.print("请输入你的名字:");

String name = scanner.nextLine();

System.out.print("请输入你的年龄:");

int age = scanner.nextInt();

System.out.println(name + "年龄是:" + age);

}

}

JDK1.6新特性:java.io.Console类,如下代码:

import java.io.Console;

public class ConsoleDemo {

public static void main(String[] args) {

Console console = System.console();

System.out.print("请输入你的名字:");

String name = console.readLine();

System.out.print("请输入你的密码:");

char[] password = console.readPassword();

System.out.println(name + "密码是:" + new String(password));

}

}

例4

程序中实现文件的创建,读,写

问题的描述:

在程序中实现文件的创建,读,写

解决方案:

Input/Output(I/O)是程序中最重要的元素之一。Java技术包含了一套丰富的I/O流。这一节我们要讲解文件中字符数据的读写。我们将讲述:

创建文件对象。

操作文件对象。

读写文件流。

创建一个新的File对象

File类提供了若干处理文件和获取它们基本信息的方法。在java技术中,目录也是一个文件。你可以创建一个代表目录的文件,然后用它定义另一个文件:

File myFile;

myFile = new File("FileName");

myFile = new File("/", "FileName");

File myDir = new File("/");

myFile = new File(myDir, "FileName");

你所使用的构造函数经常取决于你所使用的其他文件对象。例如,如果你在你的应用程序中只使用一个文件,那么就会使用第一个构造函数。如果你使用一个公共目录中的若干文件,那么使用第二个或者第三个构造函数可能更容易。

File类提供了独立于平台的方法来操作由本地文件系统维护的文件。然而它不允许你存取文件的内容。

注意:你可以使用一个File对象来代替一个String作为FileInputStream和FileOutputStream对象的构造函数参数。这是一种推荐方法,因为它独立于本地文件系统的约定。

文件测试和工具

当你创建一个File对象后,你可以使用下面任何一种方法来获取有关文件的信息:

文件名:

String getName()

String getPath()

String getAbsolutePath()

String getParent()

boolean renameTo(File newName)

文件测试:

boolean exists()

boolean canWrite()

boolean canRead()

boolean isFile()

boolean isDirectory()

boolean isAbsolute()

通用文件信息和工具:

long lastModified()

long length()

boolean delete()

目录工具:

boolean mkdir()

String[] list()

请看下例:

import java.io.*;

class TestReader {

public static void main(String[] args) {

//读取当前目录下的,该类的原文件

File f = new File("TestReader.java");

//输出到当前目录下的out.java,系统会自动创建该文件

File f1 = new File("out.java");

FileReader fr = null;

FileWriter fw = null;

BufferedReader br = null;

PrintWriter pw = null;

try {

fr = new FileReader(f);

br = new BufferedReader(fr);

fw = new FileWriter(f1);

pw = new PrintWriter(fw, true);//true 代表自动刷新到磁盘里

String ss = br.readLine();

while (ss != null && !ss.equals("exit")) {

pw.println(ss);

ss = br.readLine();

}

} catch (Exception e) {

e.printStackTrace();

} finally {

//不管发生不发生异常,都要进行IO流的关闭

try {

//防止发生空指针异常,也就是说在创建文件IO流之前发生异常。

if (br != null)

br.close();

if (pw != null)

pw.close();

} catch (Exception e) {

e.printStackTrace();

}

}

// 以上是关闭IO流最完整的代码

}

}

运行完之后,会在当前目录下生成out.java文件,跟上面的源文件一样。

例5

问题的描述:

通过控制台写数据到文件里

解决方案:

1,建立控制台的输入流

2,建立文件的输出流

请看下例:

import java.io.*;

class TestKWF {

public static void main(String[] args) {

File file = new File("keyout.java");

BufferedReader br = null;

// 要习惯性的给声明的引用类型变量初始化为null

PrintWriter pw = null;

try {

br = new BufferedReader(new InputStreamReader(System.in));

pw = new PrintWriter(new FileWriter(file));

System.out.println("按 ctrl+c 键 或者输入exit 退出程序!");

String s = null;

s = br.readLine();

// 如果按 ctrl+c 键,那么s==null,所以要先进行s!=null判断,不然

// 会出现空指针异常(调用s.equals("exit"))

while (s != null && !s.equals("exit")) {

pw.println(s);

s = br.readLine();

}

} catch (IOException e) {

e.printStackTrace();

} finally {

try {

if (br != null)

br.close();

if (pw != null)

pw.close();

} catch (Exception e) {

e.printStackTrace();

}

}

}

}

运行完之后,会在当前目录下生成keyout.java文件,跟控制台输入的一样。

例6

问题的描述:

String 类的使用

解决方案:

String 对象是一个不可变的统一编码字符串。该类是JDK中最特殊的一个类。只要一个String对象值改变,系统会在内存堆里寻找新的空间来存放改变后的值。

请看下例:

class TestString {

public static void main(String[] args) {

String s1 = "abc";

String s2 = "abc";

/*

* 如果该成 String s2 = new String("abc"); 会在内存中再生成一个"abc"对象 那么将第一次将输出!=

* 如果要想输出== 把(s1 == s2)改成(s1.equals(s2)) ==只能判断基本类型的值和引用类型的地址相等不相等

* equals方法可以判断引用类型的值相等不相等。

*/

if (s1 == s2) // 地址相等的判断

System.out.println("==");

else

System.out.println("!=");

s2 = s2 + "d"; // 改变对象"abc"的值

if (s1 == s2) // 地址相等的判断,如果输出!= 说明s2的地址改变了。

System.out.println("==");

else

System.out.println("!=");

}

}

编译和运行的结果为:

==

!=

例7

Set容器

问题的描述:

Set容器的使用

解决方案:

在下面的例题中,程序声明了一个Collections API的Set 型对象,并且用它的子类HashSet初始化它。然后向Set中添加元素,打印结果。

请看下例:

import java.util.*;

public class TestSet {

public static void main(String[] args) {

Set set = new HashSet();

set.add("abc");

set.add("abd");

set.add("abe");

set.add(new Integer(4));

set.add("abe"); // 插入了相同的数据,所以会失败

set.add(new Integer(4)); // 同上

System.out.println(set);

}

}

编译和运行的结果为:

[4,abd,abc,abe]

从结果可以看出Set容器的特点:不能保存重复的数据,而且保存的是无序的数

请思考下面的问题:这里的重复数据是根据什么判断两个数据是否是同一个数据?

是由对象的hashCode()和equals()方法共同决定的。

请看下例:

import java.util.*;

class TestSet1 {

public static void main(String[] args) {

HashSet hs = new HashSet();

// 保存了10个数据

for (int i = 0; i < 10; i++)

hs.add(new Data());

// 只打印一个数据,如果Data没有覆盖hashCode()、equals()其中一个方法,那么会输出10个数据

System.out.println(hs);

}

}

class Data {

// 覆盖hashCode()方法,得到一样的hashcode

public int hashCode() {

return 12;

}

// 覆盖equals()方法,是每个对象比较相等

public boolean equals(Object o) {

return true;

}

}

输出结果为:

[Data@c]

例8

List 容器

问题的描述:

List 容器的使用

解决方案:

在下面的例题中,程序声明了一个Collections API的List 型对象,并且用它的子类ArrayList初始化它。然后向List中添加元素,打印结果。

注意:List允许重复的元素。

请看下例:

import java.util.*;

class TestList {

public static void main(String[] args) {

// 建立一个List类型的容器

ArrayList al = new ArrayList();

// 放11个数据,其中有重复的

for (int k = 0; k < 10; k++)

al.add("abc" + k);

al.add("abc" + 4);

// 打印出容器里的数据

System.out.println(al);

}

}

编译和运行的结果为:

[abc0,abc1,abc2,abc3,abc4,abc5,abc6,abc7,abc8,abc9,abc4]

从结果可以看到List容器的特点:有序的数据,可重复的数据

例9

迭代器

问题的描述:

迭代器的用法

解决方案:

请看下例:

可以用迭代扫描一个集合。基本的Iterator接口可以向前扫描任何集合。对于一个Set来说,它的迭代是没有顺序的。对于一个List来说,它的迭代的顺序与List中元素的顺序相同,只能向前,不能后退。此外,List对象还支持ListIterator接口,允许迭代向后移动。

示例:

import java.util.*;

public class TestList2 {

public static void main(String[] args) {

List list = new ArrayList();

list.add("one");

list.add("second");

list.add("third");

list.add(new Integer(4));

list.add(new Float(5.0F));

list.add("second"); // true

list.add(new Integer(4)); // true

// 使用迭代器,来迭代容器里的所以数据,我们把Iterator叫做迭代器

Iterator iterator = list.iterator();

while (iterator.hasNext())

System.out.println(iterator.next());

}

}

输出结果为:

one

second

third

4

5.0

second

4

上面例子只用了Iterator,这种迭代器只能向后迭代容器里的数据,有时候需要向前迭代数据,那么就得用ListIterator(列表迭代器),它可以前后滚动容器里的数据。请看下面的例子:

import java.util.*;

public class TestList3 {

public static void main(String[] args) {

List list = new ArrayList();

list.add("one");

list.add("second");

list.add("third");

list.add(new Integer(4));

list.add(new Float(5.0F));

list.add("second"); // true

list.add(new Integer(4)); // true

ListIterator iterator = list.listIterator();

System.out.println("向下迭代容器里的数据:");

while (iterator.hasNext())

System.out.println(iterator.next());

System.out.println("向上迭代容器里的数据:");

while (iterator.hasPrevious())

System.out.println(iterator.previous());

}

}

输出结果为:

向下迭代容器里的数据:

one

second

third

4

5.0

second

4

向上迭代容器里的数据:

4

second

5.0

4

third

second

one

用迭代器还可以修改容器里的数据,只不过只能用当前的迭代器修改,不能两个迭代器同时修改一个容器里的数据!

请看下面事例:

import java.util.*;

public class TestList4 {

public static void main(String[] args) {

List list = new ArrayList();

list.add("one");

list.add("second");

list.add("third");

list.add(new Integer(4));

list.add(new Float(5.0F));

list.add("second"); //true

list.add(new Integer(4)); //true

ListIterator listIterator = list.listIterator();

Iterator iterator = null;

iterator = list.iterator();

if (iterator.hasNext())

iterator.next();

iterator.remove();//会删除该next()返回的元素。

//用另个迭代器操作上一个迭代器改变后的数据

while (listIterator.hasNext())

System.out.println(listIterator.next());

}

}

会发生如下异常:

java.util.ConcurrentModificationException

JDK5.0新特性:增强性for循环

package com.itjob;

import java.util.*;

public class TestList

{

public static void main(String[] args)

{

List<String> list = new LinkedList<String>();

list.add("tom");

list.add("mary");

list.add("bob");

for (String name: list)

{

System.out.println(name);

}

}

}

例10

映射(Maps)

问题的描述:

映射(Maps)的用法

解决方案:

映射这种数据结构含有两个部分:关键字和值。对于每个关键字都有一个值,也就是说,一个关键字映射一个值。映射允许通过关键字访问数据结构。

Java 在Collections API的Map接口中声明了一对一的映射。当向映射中插入一对关键字和值时,如果出现关键字重用,则用新值替换映射中原有的值,不在映射中增加一个元素。

请看下例:

import java.util.Map;

import java.util.HashMap;

public class TestMap {

public static void main(String[] args) {

Map map = new HashMap();

map.put("abc", new Integer(5));

map.put("abd", new Integer(6));

map.put("abf", new Integer(7));

// 会覆盖原来的值7

map.put("abf", new Integer(8));

System.out.println(map.get("abf"));

}

}

编译和运行的结果为:

8

在应用中可以把该容器里关键字单独取出来放到一个实现Set接口的容器里,然后过滤出你所需要的关键字,然后根据这个关键字把对应的值取出来。

import java.util.Map;

import java.util.*;

public class TestMap1 {

public static void main(String[] args) {

Map map = new HashMap();

map.put("abc", new Integer(5));

map.put("abd", new Integer(6));

map.put("abf", new Integer(7));

map.put("abf", new Integer(8));

// 得到关键字的集合

Set s = map.keySet();

Iterator i = s.iterator();

String str = null;

while (i.hasNext()) {

str = (String) i.next();

if (str.equals("abf"))

System.out.println(map.get(str));

}

}

}

结果还是8

注意:这里的关键字对象还是由hashCode()和equals()方法共同决定的

例11

排序

问题的描述:

排序的用法

解决方案:

对于复杂的应用,尤其是生成报表时,排序是一种必不可少的基本操作。在Java 2 Collections API中,已经实现了几种排序。

给数组排序可以使用Arrays对象的一套sort方法。

对于boolean以外的基本类型,Arrays.sort有两种变化:

Arrays.sort(<type>[] array)用来为整个数组排序。

Arrays.sort(<type>[] array, int fromIndex, int toIndex)用来为数组的一部分排序。

对于引用类型,Arrays.sort有四种种变化:

Arrays.sort(Object[] array)用默认的比较器为整个数组排序。

Arrays.sort(Object[] array, int fromIndex, int toIndex)用默认的比较器为数组的一部分排序。

Arrays.sort(Object[] array, Comparator comparator)使用指定得比较器为整个数组排序。

Arrays.sort(Object[] array, int fromIndex, int toIndex, Comparator comparator)使用指定得比较器为数组的一部分排序。

由于排序算法必须比较两个对象,所以使用Arrays.sort方法时要传递比较器参数。比较器有两种类型:一种是实现Comparable接口的, 另一种是实现Comparator接口的。

Comparable接口定义了一个方法:int compareTo(element, Object)。这个方法比较 Object 与element。如果Object小于element,则返回负值;如果Object大于element,则返回正值;如果Object等于element,则返回零。

Comparator接口定义了两个方法:

compare(T o1, T o2),返回值int。如果o1小于o2,则返回负值;如果o1大于o2,则返回正值;如果o1等于o2,则返回零。

equals(Object obj),返回值boolean。用于判断两个比较器对象是否相等。该方法在继承Object类时已经被实现。

请看下例:

import java.util.*;

public class TestCompareable1 {

public static void main(String[] args) {

Cat[] c = new Cat[5];

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

c[i] = new Cat((int) (Math.random() * 100));

// 随机生成一个整数,作为猫对象的属性值,以区分不同的对象

System.out.println(c[i]);

}

Arrays.sort(c);

System.out.println("----------------");

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

System.out.println(c[i]);

}

}

}

class Cat implements Comparable {

int w = 0;

public Cat(int i) {

w = i;

}

public String toString() {

return "Cat: " + w;

}

public int compareTo(Object o) {

Cat c = (Cat) o;

if (this.w == c.w) // 相等的话返回0

{

return 0;

} else if (this.w > c.w) // 大于的话返回正数

{

return 6;

} else

return -3; // 小于的话返回负数

}

}

输出结果如图

也可以定义动态的比较器,如下事例:

import java.util.*;

public class TestCompareable2 {

public static void main(String[] args) {

Cat[] c = new Cat[5];

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

c[i] = new Cat((int) (Math.random() * 100));

// 随机生成一个整数,作为猫对象的属性值,以区分不同的对象

System.out.println(c[i]);

}

Arrays.sort(c, new Comparator() // 匿名类实现

{

public int compare(Object o1, Object o2) {

Cat c1 = (Cat) o1;

Cat c2 = (Cat) o2;

if (c1.w > c2.w) // 如果大于的话返回正数

return 4;

else if (c1.w < c2.w)

return -2; // 小于的话返回负数

else

return 0; // 相等的话返回0

}

/*

* 注意该接口还有一个方法叫 equals(Object o)方法在

* 这里并没有实现,也会通过,原因是Object类也有那个方法 所以每个类都有该方法。

*/

});

System.out.println("----------------");

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

System.out.println(c[i]);

}

}

}

class Cat {

int w = 0;

public Cat(int i) {

w = i;

}

public String toString() {

return "Cat: " + w;

}

}

输出结果如图

内容总结

? 程序交互的几种方式包括:

常见的输入方式有:命令行参数

系统属性

标准的输入

在程序中实现文件的创建,读,写

? 数学类是用来支持数学计算的,它打包在java.lang包中,包含一组静态方法和两个常数,是终态(final)的,它不能被实例化

? String类是一个字符串对象

? StringBuffer类是一个可以改变的统一编码字符串。String与StringBuffer之间没有继承关系

? String与StringBuffer类的区别在于String类型的字符串对象,只要值改变,存储的空间也会改变,而StringBuffer是在原来的空间上进行修改的

? 使用集合,列表。收集(或容器)是代表一个对象组的单个对象,其它对象被认为是它的元素。收集用于处理多种类型对象的问题,所有的类型都有一个特殊的种类(也就是说,它们都是从一个共同父类继承来的)。收集可用于保存,处理Object类型的对象。这允许在收集中贮存任何对象。它还可以,在使用对象前、从收集中检索到它之后,使用正确的类型转换为我们所需要的对象类型。

? 迭代器的用法:可以用迭代扫描一个集合。基本的Iterator接口可以向前扫描任何集合。对于一个Set来说,它的迭代是没有顺序的。对于一个List来说,它的迭代的顺序与List中元素的顺序相同,只能向前,不能后退。此外,List对象还支持ListIterator接口,允许迭代向后移动。

? 映射(Map):映射这种数据结构含有两个部分:关键字和值。对于每个关键字都有一个值,也就是说,一个关键字映射一个值。映射允许通过关键字访问数据结构。Java 在Collections API的Map接口中声明了一对一的映射。当向映射中插入一对关键字和值时,如果出现关键字重用,则用新值替换映射中原有的值,不在映射中增加一个元素。

? 排序:对于复杂的应用,尤其是生成报表时,排序是一种必不可少的基本操作。在Java 2 Collections API中,已经实现了几种排序。

独立实践

? 1、使用集合表示银行与客户的关系,以及客户与他的账户的关系。

? 2、为客户排序

? 3、在上面那个例子中分别使用实现Comparable和Comparator接口的比较器进行比较。

? 4、在上面那个例子中使用动态比较器进行比较

? 5、程序模拟微软CMD中的COPY命令

第十章:JAVA GUI概述

学习目标

? GUI概述及组成

? SWING优点

? 布局管理器

? FlowLayout

? BorderLayout

? GridLayout

? CardLayout

? GridBagLayout

? GUI实例分析

GUI概述及组成

Java1.0刚出现时,包含一个用于基本GUI编程的类库,Sun把它叫做抽象窗口工具箱(Abstract Window Toolkit,AWT)。AWT库处理用户界面元素的方法是把这些元素的创建及其行为委托给每个目标平台(Windows,Solaris,Macintosh等)的本地GUI工具进行处理。不同平台的AWT用户界面存在着不同的bug。程序员们必须在每一个平台上测试他们的应用程序,他们因此嘲笑AWT是"一次编写,到处调试"。

1996年,Netscape开发了一个工作方式完全不同的GUI库,并把它叫做IFC(Internet Foundation Classes,因特网基础类集)。包括用户界面元素,如按钮,菜单等。并且使用IFC部件的程序运行在所有平台上看起来都一样。Sun和Netscape合作完善了这种方法,创建了一个新的用户界面库,它的代码名是"Swing"。从此才真正实现了"一次编写,到处运行"的口号。Swing只是提供了更好的用户界面组件。AWT的基本体系结构,尤其是事件处理模型,从Java1.1版后并没有改变。

组成Swing的类如图

Swing优点

? Swing具有更丰富,更方便的用户界面元素集合。

? Swing对低层平台的依赖更少;因此和平台有关的bug也少的多。

? Swing给不同平台上的用户一致的感觉。

布局管理器

由Swing开发的GUI界面通常由两种组件构成:

? 容器组件:用于管理其他界面组件的组件,例如:JFrame,JPanel等。

? 元素组件:用于构成各种用户界面的组件,例如:JLabel,JTextField等。

容器中组件出现的位置和组件的大小通常由布局管理器控制。每个Container(比如一个JPanel或一个JFrame)都有一个缺省布局管理器,它可以通过调用setLayout()来改变。

布局管理器负责决定布局策略以及其容器的每一个子组件的大小。

Java编程语言包含下面的布局管理器:

? FlowLayout- Panel和Applets的缺省布局管理器

? BorderLayout- Window、Dialog及Frame的缺省管理程序

? GridLayout

? CardLayout

? GridBagLayout

? GridBagLayout

下图描述了容器的默认布局管理器

FlowLayout

前面所用的FlowLayout布局管理器对组件逐行地定位。每完成一行,一个

新行便又开始。与其它布局管理器不一样,FlowLayout布局管理器不限制

它所管理的组件的大小,而是允许它们有自己的最佳大小。Flow布局构造函

数参数允许将组件左对齐或右对齐(缺省为居中)。如果想在组件之间创建一

个更大的最小间隔,可以规定一个界限。

当用户对由Flow布局管理的区域进行缩放时,布局就发生变化。如图10.3:

下面的例子说明如何用容器类的setLayout()方法来创建Flow布局对象并设置它们。

setLayout(new FlowLayout(int align, int hgap, int vgap));

align的值必须是FlowLayout.LEFT, FlowLayout.RIGHT,或

FlowLayout.CENTER。例如:

setLayout(new FlowLayout(FlowLayout.RIGHT, 20, 40));

下述程序构造并设置一个新Flow布局,它带有规定好的对齐方式以及一个缺

省的5单位的水平和垂直间隙。

setLayout(new FlowLayout(FlowLayout.LEFT));

下述程序构造并安装一个新Flow布局,它带有规定好的居中对齐方式和一个

缺省的5单位的水平和垂直间隙。

setLayout(new FlowLayout());

下面的代码将几个按钮添加到框架中的一个Flow布局中:

import java.awt.*;

public class MyFlow {

private Frame f;

private Button button1, button2, button3;

public static void main(String args[]) {

MyFlow mflow = new MyFlow();

mflow.go();

}

public void go() {

f = new Frame("Flow Layout");

f.setLayout(new FlowLayout());

button1 = new Button("Ok");

button2 = new Button("Open");

button3 = new Button("Close");

f.add(button1);

f.add(button2);

f.add(button3);

f.setSize(100, 100);

f.setVisible(true);

}

}

BorderLayout

BorderLayout布局管理器为在一个Panel或Window中放置组件提供一

个更复杂的方案。BorderLayout布局管理器包括五个明显的区域:东、南、

西、北、中。

北占据面板的上方,东占据面板的右侧,等等。中间区域是在东、南、西、

北都填满后剩下的区域。当窗口垂直延伸时,东、西、中区域也延伸;而当

窗口水平延伸时,东、西、中区域也延伸。

BorderLayout布局管理器是用于Dialog和Frame的缺省布局管理器。

其划分界面如图

下面这一行构造并安装一个新Border布局管理器,在组件之间没有间隙:

setLayout(new BorderLayout());

这一行构造并安装一个Border布局,在组件之间有由hgap和 vgap 规定的间隙:

setLayout(new BorderLayout(int hgap, int vgap);

在向使用BorderLayout布局管理器管理的界面添加组件时,默认的将加入

到中间,如果添加的其它组件未指定添加方位将互相覆盖,只有最后添加上

去的组件方可看见。

下面的代码对前例进行了修改,表示出了Border布局管理器的特性。可以

用从Container类继承的setLayout()方法来将布局设定为Border布

局。

import java.awt.*;

public class ExGui2 {

private Frame f;

private Button bn, bs, bw, be, bc;

public static void main(String args[]) {

ExGui2 guiWindow2 = new ExGui2();

guiWindow2.go();

}

public void go() {

f = new Frame("Border Layout");

bn = new Button("B1");

bs = new Button("B2");

be = new Button("B3");

bw = new Button("B4");

bc = new Button("B5");

f.add(bn, BorderLayout.NORTH);

f.add(bs, BorderLayout.SOUTH);

f.add(be, BorderLayout.EAST);

f.add(bw, BorderLayout.WEST);

f.add(bc, BorderLayout.CENTER);

f.setSize(200, 200);

f.setVisible(true);

}

}

这段代码产生如图10.5效果

GridLayout

GridLayout布局管理器为放置组件提供了灵活性。用行和列来创建管

理器。然后组件就填充到由管理器规定的单元中。比如,由语句new

GridLayout(3,2)创建的有三行两列的GridLayout布局能产生如图

的六个单元:

像BorderLayout布局管理器一样,GridLayout布局管理器中的组件相应的位置不随区域的缩放而改变。只是组件的大小改变。GridLayout布局管理器总是忽略组件的最佳大小。所有单元的宽度是相同的,是根据单元数对可用宽度进行平分而定的。同样地,所有单元的高度是相同的,是根据行数对可用高度进行平分而定的。将组件添加到网格中的命令决定它们占有的单元。单元的行数是从左到右填充,就象文本一样,而列是从上到下由行填充。

程序行:setLayout(new GridLayout());创建并安装一个Grid布局,

仅有一行一列。

程序行:setLayout(new GridLayout(int rows, int cols));创

建并安装一个带有规定好行数和栏数的Grid布局。

程序行:setLayout(new GridLayout(int rows, int cols, int

hgap, int vgap); 创建并安装一个带有规定好行数和栏数的网格布局。

hgap和vgap规定组件间各自的间隙。水平间隙放在左右两边及栏与栏之间。

垂直间隙放在顶部、底部及每行之间。

import java.awt.*;

public class GridEx {

private Frame f;

private Button b1, b2, b3, b4, b5, b6;

public static void main(String args[]) {

GridEx grid = new GridEx();

grid.go();

}

public void go() {

f = new Frame("Grid example");

f.setLayout(new GridLayout(3, 2));

b1 = new Button("1");

b2 = new Button("2");

b3 = new Button("3");

b4 = new Button("4");

b5 = new Button("5");

b6 = new Button("6");

f.add(b1);

f.add(b2);

f.add(b3);

f.add(b4);

f.add(b5);

f.add(b6);

f.pack();

f.setVisible(true);

}

}

CardLayout

Card布局管理器能将界面看作一系列的卡片,在任何时候都仅能看到其中的

一个。用add()方法来将卡添加到Card布局中。Card布局管理器的show()

方法将请求转换到一个新卡中。下图就是一个带有5张卡片的框架。

鼠标点击左面板将视图转换到右面板,等等。

用来创建上图框架的代码段如下所示:

import java.awt.*;

import java.awt.event.*;

public class CardExample implements MouseListener {

Panel p1, p2, p3, p4, p5;

Label l1, l2, l3, l4, l5;

private CardLayout myCard;

private Frame f;

public CardExample() {

f = new Frame("Card Test");

myCard = new CardLayout();

p1 = new Panel();

p2 = new Panel();

p3 = new Panel();

p4 = new Panel();

p5 = new Panel();

l1 = new Label("This is the first Panel");

l2 = new Label("This is the second Panel");

l3 = new Label("This is the third Panel");

l4 = new Label("This is the fourth Panel");

l5 = new Label("This is the fifth Panel");

}

public void launchFrame() {

f.setLayout(myCard);

p1.setBackground(Color.yellow);

p1.add(l1);

p2.setBackground(Color.green);

p2.add(l2);

p3.setBackground(Color.magenta);

p3.add(l3);

p4.setBackground(Color.white);

p4.add(l4);

p5.setBackground(Color.cyan);

p5.add(l5);

p1.addMouseListener(this);

p2.addMouseListener(this);

p3.addMouseListener(this);

p4.addMouseListener(this);

p5.addMouseListener(this);

f.add(p1, "First");

f.add(p2, "Second");

f.add(p3, "Third");

f.add(p4, "Fourth");

f.add(p5, "Fifth");

myCard.show(f, "First");

f.setSize(200, 200);

f.setVisible(true);

}

//用于处理鼠标点击事件

public void mousePressed(MouseEvent e) {

myCard.next(f);

}

public void mouseReleased(MouseEvent e) {}

public void mouseClicked(MouseEvent e) {}

public void mouseEntered(MouseEvent e) {}

public void mouseExited(MouseEvent e) {}

public static void main(String args[]) {

CardExample ct = new CardExample();

ct.launchFrame();

}

}

GridBagLayout

除了Flow、Border、Grid和Card布局管理器外,核心Java.awt也提

供GridBag布局管理器。

GridBag布局管理器在网格的基础上提供复杂的布局,但它允许单个组件在

一个单元中而不是填满整个单元那样地占用它们的最佳大小。网格包布局

理器也允许单个组件扩展成不止一个单元。

实例分析

例1:组成用户登录的界面包括用户名和密码输入并可以确认提交,请使用Swing组件编写

解决方案

? 问题分析

? 使用组件分析

? 编写代码

? 编译并运行

问题分析

可以使用Swing中的GUI类构建登录界面,注意区别元素组件和容器组件

使用组件分析

基本元素组件

? JLabel: 用于短文本字符串或图像或二者的显示区。

? JTextField: 用于单行输入文本。

? JButton: 按钮的实现。

? JTextArea: 显示纯文本的多行区域。

? JComboBox: 下拉列表组合的组件。

? JRadioButton: 实现一个单选按钮,此按钮项可被选择或取消选择.

? JCheckBox: 复选框的实现

创建框架

Java中的顶层窗口(即:那些没有包含在其他窗口中的窗口)被称为框架,

也称为容器组件。

? JPanel: 一般轻量级容器,不能直接显示.

? JFrame: 是带有标题和边界的顶层窗口.

? JApplet:一种不适合单独运行但可嵌入在其他应用程序中的小程序。

注解:大部分Swing组件类的名字都是以"J"开头,如JButton,JFrame,JTextField等等。Java中也有着Button,Frame类,不过它们都是AWT组件。如果你不小心忘了写Swing组件前的"J",程序还是很可能能够编译和运行,不过由于混杂了Swing和AWT组件,在视觉和响应上可能会有不一致的地方。

给框架定位

JFrame类本身只有几个用来改变框架外观的类。当然,通过继承,JFrame

从不同的超类中继承来很多用于处理框架大小和位置的方法。下面列出几个

可能最为重要的方法:

? dispose方法----关闭窗口并回收用于创建窗口的任何资源;

? setIconImage方法----当窗口最小化时,把一个Image对象用作图标。

? setTitle方法-----改变标题栏中的文字。

? setResizable方法-使用boolean参数来决定框架大小是否能被用户改变。

? setLocation方法----用来显示组件在容器中的位置(对于框架来说,方法中的参数坐标是相对于整个屏幕而言的,对于容器内的组件,坐标是相对于容器的)。

? setBounds方法----同上,只不过它带有四个参数(其中前两个参数同上,都是设置组件的位置,后两个参数用来设置组件的大小)。

在面板中显示信息

JFram与Frame不同的是,在JFrame中加组件是加在内容窗格里的。如:

Container contentPane=frame.getContentPane(); //用上例中的 frame对象

JComponent c=…;

contentPane.add(c);

如果你只需要在框架中显示一个Swing组件,那么你可以用下面的方式把组

件放置到内容窗格中。

Frame.setContentPane(c);

面板是也是个容器。它可以再放其他的组件,我们可以设计自己的面板:

? 定义一个扩展JPanel的新类

? 覆盖paintComponent方法

注意:paintComponent方法实际上定义在JComponent中,这个类是所有非窗口Swing组件的父类。该方法有一个Graphics类型的参数。Graphics 对象存储了一个用于绘制图形和文本的设置集合(比如字体和当前颜色)。Java中的所有绘制都必须用Graphics对象。它拥有绘制,图案,图象和文本的方法。

例如:SimpleJFrameTest.java

import javax.swing.*;

import java.awt.*;

public class SimpleJFrameTest {

public static void main(String[] args) {

SimpleJFrame fram = new SimpleJFrame();

fram.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

fram.show();

// fram.setVisible(true);

}

}

class SimpleJFrame extends JFrame {

public static final int WIDTH = 400;

public static final int HEIGHT = 300;

public SimpleJFrame() {

setSize(WIDTH, HEIGHT);

Container contentPane = getContentPane();

contentPane.add(new JPanelOne());

}

}

class JPanelOne extends JPanel {

public void paint(Graphics g) {

super.paint(g);

g.fillRect(30, 10, 200, 100);

g.clearRect(60, 30, 80, 40);

g.drawRect(70, 45, 35, 20);

g.drawLine(10, 60, 250, 60);

}

}

结果

定义元素组件:

JLabel lblUsername;

JLable lblPassword;

JTextField txtUsername;

JPasswordField txtPassword;

JButton bLogin;

定义容器组件:

JPanel panel;

编写代码

import java.awt.*;

import javax.swing.*;

public class UserLogin extends JFrame {

JLabel lblUsername;

JLabel lblPassword;

JTextField txtUsername;

JPasswordField txtPassword;

JButton bLogin;

JPanel panel;

public UserLogin() {

panel = (JPanel) getContentPane();

// 设置布局管理器

panel.setLayout(new FlowLayout());

lblUsername = new JLabel("Usernanme:");

lblPassword = new JLabel("Password:");

txtUsername = new JTextField(10);

txtPassword = new JPasswordField(10);

bLogin = new JButton("Login");

panel.add(lblUsername);

panel.add(txtUsername);

panel.add(lblPassword);

panel.add(txtPassword);

panel.add(bLogin);

setTitle("User Login");

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

setSize(250, 150);

setVisible(true);

}

public static void main(String[] args) {

new UserLogin();

}

}

编译并运行(图10。9)

例2:组成用户登录的界面已经设计完成,使用合理的布局管理器管理界面元

素,请使用Swing组件编写

解决方案

? 问题分析

? 使用布局管理器分析

? 编写代码

? 编译并运行

问题分析

可以使用使用GridBagLayout管理布局

使用布局管理器分析

GridBagLayout

与GridLayout类似,GridBagLayout将组件在一个矩形的"单元"网格中对齐。GridLayout和GridBagLayout的区别在于,GridBagLayout使用了一种称为GridBagConstraints的对象。

通过在GridBagConstraints对象中设置值,组件可水平或垂直地对齐(带

有或不带有小插图和填充),被告知扩展以填充给定的区域,以及被指示如何

对窗口大小的改变采取相应的行动。

GridBagConstraints

GridBagConstraints的成员变量用于控制组件布局它们是:

? gridx和gridy

这些变量指定了位于组件显示区域左上方的单元的特征(也就是组件出现的

x,坐标值),其中,最左上方的单元具有地址gridx=0,gridy=0。

? gridwidth和gridheight

这些变量指定了在组件的显示区域中,行(gridwidth)或列(gridheight)

中的单元数目。缺省值为1。你可用GridBagConstraints.REMAINDER

来指定某组件在其行(gridwidth)或列(gridheight)中为最后一个。

用GridBagConstraints.RELATIVE可指定某组件在其行(gridwidth)

或列(gridheight)中与最后一个相邻。

? fill

fill在某组件的显示区域大于它所要求的大小时被使用;fill决定了是否

(和怎样)改变组件的大小。有效值为GridBagConstraints.NONE(缺

省),GridBagConstraints.HORIZONTAL(使该组件足够大以填充其显

示区域的水平方向,但不改变其高度),GridBagConstraints.VERTICAL

(使该组件足够大,以填充其显示区域的垂直方向,但不改变其宽度),

GridBagConstraints.BOTH(使该组件填充其整个显示区域)。

? ipadx和ipady

这些变量指定了内部填充;即,在该组件的最小尺寸基础上还需增加多少。

组件的宽度必须至少为其最小宽度加ipadx*2个象素(因为填充作用于组件

的两边)。同样地,组件的高度必须至少为其最小高度加ipady*2个象素。

? insets

insets指定了组件的外部填充;即,组件与其显示区域边界之间的最小空间大小。

? anchor

本变量在组件小于其显示区域时使用;anchor决定了把组件放置在该区域中的位置。有效值为GridBagConstraints.CENTER(缺省),.NORTH,.NORTHEAST,.EAST,.SOUTHEAST,.SOUTH,.SOUTHWEST,.WEST,和.NORTHWEST。

? weightx和weighty

这些变量用来决定如何分布空白和改变它的大小。除非你为一行(weightx)

和一列(weighty)中至少一个组件指定了重量,否则,所有组件都会集中

在容器的中央。这是因为,当重量为0(缺省值)时,GridBagLayout在其

单元网格间以及容器边界加入一些额外的空白。

结果

GridBagLayout gb;

GridBagConstraints gbc;

....

gb = new GridBagLayout();

gbc = new GridBagConstraints();

panel.setLayout(gb);

...

gbc.gridx = 1;

gbc.gridy = 1;

gb.setConstraints(lblUsername,gbc);

panel.add(lblUsername);

编写代码

import java.awt.*;

import javax.swing.*;

public class UserLoginFrame extends JFrame {

JLabel lblUsername;

JLabel lblPassword;

JTextField txtUsername;

JPasswordField txtPassword;

JButton bLogin;

JPanel panel;

GridBagLayout gb;

GridBagConstraints gbc;

public UserLoginFrame() {

panel = (JPanel) getContentPane();

gb = new GridBagLayout();

gbc = new GridBagConstraints();

// 设置布局管理器

panel.setLayout(gb);

lblUsername = new JLabel("Usernanme:");

lblPassword = new JLabel("Password:");

txtUsername = new JTextField(10);

txtPassword = new JPasswordField(10);

bLogin = new JButton("Login");

gbc.anchor = GridBagConstraints.NORTHWEST;

gbc.gridx = 1;

gbc.gridy = 1;

gb.setConstraints(lblUsername, gbc);

panel.add(lblUsername);

gbc.gridx = 2;

gbc.gridy = 1;

gb.setConstraints(txtUsername, gbc);

panel.add(txtUsername);

gbc.gridx = 1;

gbc.gridy = 2;

gb.setConstraints(lblPassword, gbc);

panel.add(lblPassword);

gbc.gridx = 2;

gbc.gridy = 2;

gb.setConstraints(txtPassword, gbc);

panel.add(txtPassword);

gbc.gridx = 2;

gbc.gridy = 3;

gb.setConstraints(bLogin, gbc);

panel.add(bLogin);

setTitle("User Login");

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

setSize(250, 250);

setVisible(true);

}

public static void main(String[] args) {

new UserLoginFrame();

}

}

编译并运行

内容总结

? 描述swing包及其组件

? 定义Container组件及布局管理器等术语,以及它们如何一起工作来建立GUI的

? 使用布局管理器

? 使用Flow、Border、Grid和Card布局管理器来获得所需的动态布局

? 添加组件到容器

? 正确使用框架和面板容器

? 描述复杂布局与嵌套容器是如何工作的

独立实践

1、创建计算器GUI(如图10.8)

2、创建用户注册的GUI,要求注册内容为:姓名,密码,电话,性别,地址,国家

3、创建聊天室主界面的GUI

4、创建一GUI界面,能响应按钮的事件

5、创建一树型结构,能对节点进行响应

第十一章 线程

学习目标

? 线程的概念

? 线程状态和调度

? 线程中断/恢复的几种方式

? 创建线程的两种方式

? 线程的控制

? 线程的同步

? 实例分析

1.线程的概念

一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器,在JAVA中也叫堆栈)、包含程序所要操作的数据的RAM(随机存储器,在JAVA中也叫堆)。在这个简化视图中,只能执行一个作业。一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。

你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程(或执行上下文),被认为是带有自己的程序代码和数据的虚拟处理机的封装。java.lang.Thread类允许用户创建并控制他们的线程。在单CPU的情况下,一个时刻只能运行一个进程,那么进程在运行时,也只能运行一个线程来代表该进程的执行。

进程是正在执行的程序。一个或更多的线程构成了一个进程(操作系统是以进程为单位的,而进程是以线程为单位的,进程中必须有一个主线程)。一个线程(执行上下文)由三个主要部分组成:

? 一个虚拟CPU

? CPU执行的代码

? 代码操作的数据

如图所示

代码可以由多个线程共享,它不依赖数据。如果两个线程执行同一个类的实例的代码时,则它们可以共享相同的代码。

类似地,数据可以由多个线程共享,而不依赖代码。如果两个线程共享对一个公共对象的访问,则它们可以共享相同的数据。

在Java编程中,虚拟处理机封装在Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。Java线程分守护线程和用户线程,由创建时设置。

线程状态和调度

在Java中,线程的调度是基于时间片基础上的优先级优先原则 。

抢占式调度模型(优先级优先)是指可能有多个线程是可运行的,但只有一个线程在实际运行。这个线程会一直运行,直至它不再是可运行的(运行时间到,时间片原则,或者,另一个具有更高优先级的线程抢占,优先级优先原则)。对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。

线程的代码可能执行了一个Thread.sleep()调用,要求这个线程暂停一段固定的时间。这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。

所有可运行线程根据优先级保存在池中。当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。优先级最高的非空池中的线程会得到处理机时间(被运行)。

一个Thread对象在它的生命周期中会处于各种不同的状态。

下图形象地说明了这点:

线程进入"可运行"状态,并不意味着它立即开始运行。在一个只有一个CPU的机器上,在一个时刻只能进行一个动作。(下节将描述:如果有一个以上可运行线程时,系统如何分配CPU。)

因为Java线程是抢占式的,所以你必须确保你的代码中的线程会不时地给其它线程运行的机会。这可以通过在各种时间间隔中发出sleep()调用来做到。

class ThreadA implements Runnable {

public void run() {

while (true) {

//线程的执行代码部分

try {

//给其他的线程提供机会运行

Thread.sleep(7);

} catch (Exception e) {

}

}

}

}

try和catch块的使用。Thread.sleep()和其它使线程暂停一段时间的方法是可中断的。线程可以调用另外一个线程的interrupt()方法,这将向暂停的线程发出一个InterruptedException。

Thread类的sleep()方法对当前线程操作,因此被称作Thread.sleep(x),它是一个静态方法。sleep()的参数指定以毫秒为单位的线程最小休眠时间。除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。使用该方法只是使当前线程中断多少毫秒,并不是创建多线程。

例如:

class TestTS {//Thread sleep

public static void main(String[] args) {

try {

Thread.sleep(5000);//中断当前线程(main)5秒,并没有创建新的线程。

} catch (Exception e) {

e.printStackTrace();

}

System.out.println("Hello World!");

}

}

线程中断/恢复的几种方式

一个线程可能因为各种原因而不再是可运行的。

? 该线程调用Thread.sleep() 进入中断状态必须经过规定的毫秒数才能从中断状态进入可运行状态

? 该线程进行了IO操作 而进入中断状态必须等待IO操作完成,才能 进入可运行状态

? 该线程调用了其它线程的join()方法,而使自己进入中断状态必须等待调用的线程执行完,才能进入可运行状态

? 该线程试图访问被另一个线程锁住的对象 而进入中断状态必须等待另一个线程释放对象锁,该线程才能进入可运行状态该线程调用wait()方法而进入中断状态必须通过其他线程调用notify()或者notifyAll()方法才能进入可运行状态

创建线程的两种方式(实现接口的方式请看实例分析5)

? 实现Runnable接口

? 继承Thread类

实现Runnable的优点

从面向对象的角度来看,Thread类是一个虚拟处理机的严格封装,因此只有当处理机模型修改或扩展时,才应该继承类。

由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类。

继承Thread的优点

当一个run()方法体出现在继承Thread的类中,用this指向实际控制运行的Thread实例。因此,代码不再需要使用如下控制:

Thread.currentThread().join();而可以简单地用:join();

因为代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。

线程的控制

终止一个线程:

当一个线程结束运行并终止时,它就不能再运行了。可以用一个标志来指示run()方法,必须退出一个线程。

public class Runner implements Runnable {

private boolean timeToQuit = false; //终止标志

public void run() {

while(! timeToQuit) { //当结束条件为假时运行

...

}

}

//停止运行的方法

public void stopRunning() {

timeToQuit = true;

}

}

//控制线程类

public class ControlThread {

private Runnable r = new Runner();

private Thread t = new Thread(r);

public void startThread() {

t.start();

}

public void stopThread() {

r.stopRunning();

}

}

线程的优先级

使用getPriority方法测定线程的当前优先级。使用setPriority方法设定线程的当前优先级。线程优先级是一个整数(1到10)。Thread类包含下列常数:

Thread.MIN_PRIORITY 1 (最低级别)

Thread.NORM_PRIORITY 5 (默认的级别)

Thread.MAX_PRIORITY 10 (最高级别)

延迟线程

sleep()方法是使线程停止一段时间的方法。在sleep时间间隔期满后,线程不一定立即恢复执行。这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非

(a)"醒来"的线程具有更高的优先级

(b)正在运行的线程因为其它原因而阻塞

线程同步

为了保证共享数据在任何线程使用它完成某一特定任务之前是一致的,Java使用关键字synchronized,允许程序员控制共享数据的线程。

对象锁

在Java技术中,每个对象都有一个和它相关联的标志。这个标志可以被认为是"锁标志"。 synchronized关键字使线程能和这个标志的交互,即允许独占地存取对象。

当线程运行到synchronized语句,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。

持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。

关键字synchronized

public void push(char c) {

synchronized(this) {// synchronized语句块

. . .

}

}

// synchronized方法

public synchronized void push(char c) {

. . .

}

死锁

当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。

避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按照与获取相反的次序释放锁。

实例分析

例1: 创建四个线程对同一个数据操作,其中两个线程对该数据执行加1操作,两个线程对该数据减1操作

? 创建数据类

//数据类

class Data {

private int k;

public void add() {

k++;

}

public void sub() {

k--;

}

public int getK() {

return k;

}

}

? 创建加数据的线程

//加数据的线程

class ThreadAdd extends Thread {

//线程操作的数据

Data data;

public ThreadAdd(Data data, String name) {

//给当前线程命名

super(name);

this.data = data;

}

//线程执行时所调用的方法

public void run() {

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

data.add();

//打印出哪个线程执行的加操作

System.out.println(Thread.currentThread().getName() + " "

+ data.getK());

//每循环一次,让该线程中断5毫秒

try {

Thread.sleep(5);

} catch (Exception e) {

e.printStackTrace();

}

}

}

};

? 创建减数据的线程

//减数据的线程

class ThreadSub extends Thread {

//线程所操作的关键数据

Data data;

public ThreadSub(Data data, String name) {

//给当前线程命名

super(name);

this.data = data;

}

//线程执行时所调用的方法,即线程所执行的代码

public void run() {

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

data.sub();

//打印出哪个线程执行的加操作

System.out.println(Thread.currentThread().getName() + " "

+ data.getK());

//每循环一次,让该线程中断5毫秒

try {

Thread.sleep(5);

} catch (Exception e) {

e.printStackTrace();

}

}

}

};

? 启动四个线程运行

class TestThread {

public static void main(String[] args) {

Data data = new Data();

//创建四个线程

Thread thadd1 = new ThreadAdd(data, "thadd1");

Thread thadd2 = new ThreadAdd(data, "thadd2");

Thread thsub1 = new ThreadSub(data, "thsub1");

Thread thsub2 = new ThreadSub(data, "thsub2");

//启动四个线程

thadd1.start();

thadd2.start();

thsub1.start();

thsub2.start();

}

}

? 编译,运行以及输出的结果为:

例2 通过join()方法中断一个线程

需要修改上例的代码(只修改main()方法):

class TestThread {

public static void main(String[] args) {

Data data = new Data();

//创建四个线程

Thread thadd1 = new ThreadAdd(data, "thadd1");

Thread thadd2 = new ThreadAdd(data, "thadd2");

Thread thsub1 = new ThreadSub(data, "thsub1");

Thread thsub2 = new ThreadSub(data, "thsub2");

//启动四个线程

thadd1.start();

try {

thadd1.join(); //thadd1执行完后才输出“join() 已经执行完毕”

} catch (Exception e) {

e.printStackTrace();

}

System.out.println("join() 已经执行完毕");

thadd2.start();

thsub1.start();

thsub2.start();

}

}

运行的结果如图4:

例3 通过IO中断线程

class TestThread {

public static void main(String[] args) {

Data data = new Data();

//创建四个线程

Thread thadd1 = new ThreadAdd(data, "thadd1");

Thread thadd2 = new ThreadAdd(data, "thadd2");

Thread thsub1 = new ThreadSub(data, "thsub1");

Thread thsub2 = new ThreadSub(data, "thsub2");

//启动四个线程

thadd1.start();

try {

//等待用户从控制台输入数据

int k = System.in.read();

} catch (Exception e) {

e.printStackTrace();

}

System.out.println("io 已经执行完毕");

thadd2.start();

thsub1.start();

thsub2.start();

}

}

运行的结果如图5:

例4 线程的同步

先看线程不同步的情况:

//数据类

package com.itjob;

public class Person {

private String name = "王非";

private String sex = "女";

public void put(String name, String sex)

{

this.name = name;

this.sex = sex;

}

public void get()

{

System.out.println(name + "----->" + sex);

}

}

//用来显示Person数据

package com.itjob;

public class Consumer implements Runnable {

Person person;

public Consumer(Person person)

{

this.person = person;

}

public void run() {

while(true)

{

person.get();

}

}

}

// 用来修改Person数据

package com.itjob;

public class Producer implements Runnable {

Person person;

public Producer(Person person)

{

this.person = person;

}

public void run()

{

int i = 0;

while(true)

{

if(i==0)

{

person.put("刘祥", "男");

}else{

person.put("王非", "女");

}

i = (i+1)%2;

}

}

}

//主程序类

package com.itjob;

public class ThreadCom {

public static void main(String[] args)

{

Person person = new Person();

new Thread(new Producer(person)).start();

new Thread(new Consumer(person)).start();

}

}

运行的结果如图:

修改为线程同步

只需要修改Person类就OK了,同步只是对数据加锁,与线程无关,当多个线程访问同一个数据的时候,就要对数据加锁(同步)。

修改后的代码如:

//数据类

package com.itjob;

public class Person {

private String name = "王非";

private String sex = "女";

public synchronized void put(String name, String sex)

{

this.name = name;

this.sex = sex;

}

public synchronized void get()

{

System.out.println(name + "----->" + sex);

}

}

运行的结果不会有男女不分的情况!

例5: 通过wait()和notify()/notifyAll()方法进行同步

Object类中提供了用于线程通信的方法:wait()和notify(),notifyAll()。如果线程对一个指定的对象x发出一个wait()调用,该线程会暂停执行,此外,调用wait()的线程自动释放对象的锁,直到另一个线程对同一个指定的对象x发出一个notify()调用。

为了让线程对一个对象调用wait()或notify(),线程必须锁定那个特定的对象。也就是说,只能在它们被调用的实例的同步块内使用wait()和notify()。

根据以上内容,修改Person类如下:

package com.itjob;

public class Person {

private String name = "王非";

private String sex = "女";

//为true时,修改数据;为false时,显示数据

boolean flag = false;

public synchronized void put(String name, String sex)

{

if(flag)

{

try {

wait();

} catch (InterruptedException e1) {

// TODO Auto-generated catch block

e1.printStackTrace();

}

}

this.name = name;

try {

Thread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

this.sex = sex;

flag = true;

notifyAll();

}

public synchronized void get()

{

if(!flag)

{

try {

wait();

} catch (InterruptedException e1) {

// TODO Auto-generated catch block

e1.printStackTrace();

}

}

System.out.println(name + "----->" + sex);

flag = false;

notifyAll();

}

}

内容总结

? 线程与进程的区别

? 产生线程的两种方式

? 线程的生命周期和状态

? 线程同步方法和同步块

? 多线程同步机制

独立实践

1、创建一个可以容纳10个整数类型的数组,数据的加入在数组尾部,删除在头部,并保证线程操作的安全性。

2、主线程创建两个子线程,一个线程每次往缓冲区里写入一个整数,另一个线程每次从缓冲区里读出一个整数。要确保当前缓冲区无数据时不能读,并且读写不能同时进行。

3、创建两个线程,线程A和线程B,各线程运行打印数据要求(0-100),要求,线程A必须等待线程B完成后,才能再进行打印动作。

4、有三个线程,stu1、stu2、teacher,其中stu1准备休眠10分钟后运行,而stu2准备休眠1小时后再运行。Teacher发出命令“上课”,叫醒stu1线程,stu1线程醒来后,stu1线程再叫醒stu2线程。通过程序返应出来。

5、编写一个应用程序,有两个线程,一个负责模仿垂直上抛运动,另一个模仿45度的抛物运动。

第十二章:高级I/O流

学习目标

? I/O基础知识

? 字节流

? 字符流

? 过程流

? URL输入流

? 使用RandomAccessFile随机访问文件

? 实例分析

I/O基础知识

Java语言中数据流是发送或接收数据的管道。通常,你的程序是流的一个端点,其它程序或文件是流的另一个端点。

流是:one dimension one direction 一维单向的。

数据源端点和数据目的端点分别叫做input stream(输入流)和output stream(输出流)。你可以从输入流读,但你不能对它写;同样,你可以向输出流写,但不能从输出流读。

数据流的分类如图

InputStream和OutputStream:字节流。其它字节流都是InputStream或OutputStream的子类。

Reader和 Writer:字符流。其它字符流都是Reader或Writer的子类。

字节流

InputStream

InputStream有三个方法访问它的数据:

int read():简单读方法,返回一个int值,它是从流里读出的一个字节。如果遇到文件结束则返回-1。

int read(byte []):将数据读入到字节数组中,并返回所读的字节数。

int read(byte[], int offset,int length) 将数据读入到字节数组中,并返回所读的字节数。Offset是数组的偏移量,length是读取的长度。

void close() 你完成流操作之后,就关闭这个流。如果你有一个流所组成的栈,使用过滤器流,就关闭栈顶部的流。这个关闭操作会关闭其余的流。

int available()

这个方法报告立刻可以从流中读取的字节数。在这个调用之后的实际读操作可能返回更多的字节数。

skip(long)这个方法丢弃了流中指定数目的字符。

boolean markSupported()

void mark(int)

void reset()

如果流支持"回放"操作,则这些方法可以用来完成这个操作。如果mark()和reset()方法可以在特定的流上操作,则markSupported()方法将返回ture。mark(int)方法用来指明应当标记流的当前点和分配一个足够大的缓冲区,它最少可以容纳参数所指定数量的字节。在随后的read()操作完成之后,调用reset()方法来返回你标记的输入点。

OutputStream

void write(int)

void write(byte[])

void write(byte[], int, int)

这些方法写输出流。和输入一样,总是尝试以实际最大的块进行写操作。

void close()当你完成写操作后,就关闭输出流。如果你有一个流所组成的栈,就关闭栈顶部的流。这个关闭操作会关闭其余的流。

void flush()

有时一个输出流在积累了若干次之后才进行真正的写操作。flush()方法允许你强制执行写操作。

字符流

Reader

int read()

int read(char[])

int read(char[], int offset, int length)

简单读方法返回一个int值,它包含从流里读出的一个字符或者-1,其中-1表明文件结束。其它两种方法将数据读入到字符数组中,并返回所读的字符数。第三个方法中的两个int参数指定了所要填入的数组的子范围。

void close()

boolean ready()

void skip(long)

boolean markSupported()

void mark(int)

void reset()

这些方法与InputStream中的对应方法相似

Writer

void write(int c)

void write(char [])

void write(char [], int offset, int length)

void write(String string)

void write(String string, int offset, int length)

void close()

void flush()

所有这些方法与OutputStream中的方法类似。

节点流

Java 2 SDK中有三种基本类型的节点:文件(file)、内存(memory)、管道(pipe)。如

过程流

过程流在其它流之上,完成排序、变换等操作。过程流也被称做过滤流。当你需要改变输入流的原始数据时,你可以将一个过滤输入流连接到一个原始的输入流上。用过滤流将原始数据变换成你需要的格式。

其分类如图

基本字节流类

分类如图

FileInputStream和FileOutputStream

这两个节点流用来操纵磁盘文件。这些类的构造函数允许你指定它们所连接的文件。要构造一个FileInputStream,所关联的文件必须存在而且是可读的。如果你要构造一个FileOutputStream而输出文件已经存在,则它将被覆盖。主要用于操作二进制或者带有格式的文件:如压缩文件,可执行文件等。

FileInputStream infile = new FileInputStream("myfile.dat");

FileOutputStream outfile = new FileOutputStream("results.dat");

BufferInputStream和BufferOutputStream

带有缓冲区的流,BufferInputStream一次可以读入一定长度的数据(默认2048字节),BufferOutputStream一次可以一定长度的数据(默认512字节),可以提高I/O操作的效率。需要和其它的流类配合使用。

BufferOutputStream在使用时,为了确保把数据写出去,建议最后执行flush()将缓冲区中的数据全部写出去。

PipedInputStream和PipedOutputStream

管道流用来在线程间进行通信。一个线程的PipedInputStream对象从另一个线程的PipedOutputStream对象读取输入。要使管道流有用,必须有一个输入方和一个输出方。

DataInputStream和DataOutputStream

用来对java的基本数据类型读写的类

DataInputStream方法

byte readByte()

long readLong()

double readDouble()

String readUTF(DataInput in)

DataOutputStream方法

void writeByte(byte)

void writeLong(long)

void writeDouble(double)

void writeUTF(String str)

PrintStream

可以自动进行字符转换的动作,默认会使用操作系统的编码处理对应的字符。

import java.io.*;

public class PrintStreamDemo {

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

PrintStream out = new PrintStream(new FileOutPutStream("1.txt"));

out.println(1);

out.close();

}

}

基本字符流类

阐述了Reader和Writer字符流的体系结构。

InputStreamReader 和 OutputStreamWriter

用于字节流与字符流之间的转换接口。

当你构造一个InputStreamReader或OutputStreamWriter时,转换规则定义了16位Unicode和其它平台的特定表示之间的转换。

InputStreamReader从一个数据源读取字节,并自动将其转换成Unicode字符。如果你特别声明,InputStreamReade会将字节流转换成其它种类的字符流。

OutputStreamWriter将字符的Unicode编码写到输出流,如果你的使用的不是Unicode字符,OutputStreamWriter会将你的字符编码转换成Unicode编码。

BufferedReader和BufferedWriter

因为在各种格式之间进行转换和其它I/O操作很类似,所以在处理大块数据时效率最高。在InputStreamReader和OutputStreamWriter的结尾链接一个BufferedReader和BufferedWriter是一个好主意。记住对BufferedWriter使用flush()方法。

FileReader和FileWriter

以字符的方式操作文件的类,主要用于操作文本文件。

PrintWriter

与PrintStream相类似,使用println()输出内容。

URL输入流

除了基本的文件访问之外,Java技术提供了使用统一资源定位器(URL)来访问网络上的文件。当你使用Applet的getDocumentBase()方法来访问声音和图象时,你已经隐含地使用了URL对象。

String imageFile = new String ("images/Duke/T1.gif");

images[0] = getImage(getDocumentBase(), imageFile);

当然,你也可以直接使用URL如下:

java.net.URL imageSource;

try{

imageSource = new URL("http://mysite.com/~info");

}catch(MalformedURLException e) {}

images[0] = getImage(imageSource, "Duke/T1.gif");

使用RandomAccessFile随机访问文件

你经常会发现你只想读取文件的一部分数据,而不需要从头至尾读取整个文件。你可能想访问一个作为数据库的文本文件,此时你会移动到某一条记录并读取它的数据,接着移动到另一个记录,然后再到其他记录――每一条记录都位于文件的不同部分。Java编程语言提供了一个RandomAccessFile类来处理这种类型的输入输出。

创建一个随机访问文件

你可以用如下两种方法来打开一个随机存取文件:

用文件名

myRAFile = new RandomAccessFile(String name, String mode);

用文件对象

myRAFile = new RandomAccessFile(File file, String mode);

mode参数决定了你对这个文件的存取是只读(r)还是读/写(rw)。

例如,你可以打开一个数据库文件并准备更新:

RandomAccessFile myRAFile;

myRAFile = new RandomAccessFile("db/stock.dbf","rw");

存取信息

RandomAccessFile对象按照与数据输入输出对象相同的方式来读写信息。你可以访问在DataInputStrem和DataOutputStream中所有的read()和write()操作。

Java编程语言提供了若干种方法,用来帮助你在文件中移动。

long getFilePointer();返回文件指针的当前位置。

void seek(long pos); 设置文件指针到给定的绝对位置。这个位置是按照从文件开始的字节偏移量给出的。位置0标志文件的开始。

long length()返回文件的长度。位置length()标志文件的结束。

添加信息

你可以使用随机存取文件来得到文件输出的添加模式。

myRAFile = new RandomAccessFile("java.log","rw");

myRAFile.seek(myRAFile.length());

对象串行化

java.io.Serializable接口支持将一个Java技术对象存放到一个流中。

将一个对象存放到某种类型的永久存储器上称为"保持"。如果一个对象可以被存放到磁盘或磁带上,或者可以发送到另外一台机器并存放到存储器或磁

盘上,那么这个对象就被称为可保持的。

java.io.Serializable接口没有任何方法,它只作为一个"标记",用来表明实现了这个接口的类可以串行化。类中没有实现Serializable接口的对象不能被保持。

当一个对象被串行化时,只有对象的数据被保存;方法和构造函数不属于串行化流。如果一个数据变量是一个对象引用,那么这个对象的数据成员也会被串行化。树或者对象数据的结构,包括这些子对象,构成了对象图。

因为有些对象类所表示的数据在不断地改变,所以它们不会被串行化;例如,java.io.FileInputStream、java.io.FileOutputStream和java.lang.Thread等流。如果一个可串行化对象包含某个不可串行化元素的引用,那么整个串行化操作就会失败,而且会抛出一个NotSerializableException。

如果对象包含一个不可串行化的引用,只要这个引用已经用transient关键字进行了标记,那么对象仍然可以被串行化。

public class MyClass implements Serializable {

public transient Thread myThread;

private String customerID;

private int total;

}

域存取修饰符对于被串行化的对象没有任何作用。写入到流的数据是字节格式,而且字符串被表示为UTF(文件系统安全的通用字符集转换格式)。transient关键字防止对象被串行化。

public class MyClass implements Serializable {

public transient Thread myThread;

private transient String customerID;

private int total;

}

实例分析

例1:从第一个命令行参数代表的文件中读字符,然后写入第二个参数代表的文件。

问题分析

本题中需要从文件读,写数据,需要使用到与文件有关的流FileReader/FileWriter。

可以通过运行时参数提供文件的名称。

使用带有Buffer功能的流

为了提高读写数据的效率,可以使用带有buffer功能的流完成文件读写,并且可以以行为单位读写数据。

使用类BufferedReader,BufferedWriter

I/O流的链

在程序中很少使用单独一个流对象,实际做法是将几个流对象串联起来处共同理数据。这样做会提高程序的效率。

数据源-> FileInputStream -> BufferedInputStream -> DataInputStream -> 程序

数据源<- DataOutputStream <- BufferedOutputStream <- FileOutputStream <-程序

编写代码

import java.io.*;

public class TestBufferedStreams {

public static void main(String[] args) {

try {

FileReader input = new FileReader(args[0]);

BufferedReader bufInput = new BufferedReader(input);

FileWriter output = new FileWriter(args[1]);

BufferedWriter bufOutput = new BufferedWriter(output);

String line = bufInput.readLine();

while (line != null) {

bufOutput.write(line, 0, line.length());

bufOutput.newLine();

line = bufInput.readLine();

}

bufInput.close();

bufOutput.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

编译运行

javac TestBufferedStreams.java

java TestBufferedStreams user.bat userbak.bat

例2:使用管道流完成线程之间的通讯。

1 问题分析

本题中需要一个线程向管道写入数据,另外一个线程从管道中读出数据,需要使用到与管道有关的流PipedInputStream和PipedOutputStream。

2 使用管道流

PipedInputStream和PipedOutputStream

管道流用来在线程间进行通信。一个线程的PipedInputStream对象从另一个线程的PipedOutputStream对象读取输入。要使管道流有用,必须有一个输入方和一个输出方。

PipedInputStream in=new PipedInputStream();

PipedOutputStream out=new PipedOutputStream(in);

3 编写代码

import java.io.*;

public class TestPipeStream {

public static void main(String[] args) {

try {

PipedInputStream in1 = new PipedInputStream();

PipedOutputStream out1 = new PipedOutputStream(in1);

PipedInputStream in2 = new PipedInputStream();

PipedOutputStream out2 = new PipedOutputStream(in2);

ThreadC tc = new ThreadC(out1);

ThreadZ tz = new ThreadZ(out2, in1);

ThreadQ tq = new ThreadQ(in2);

tc.start();

tz.start();

tq.start();

} catch (Exception e) {

e.printStackTrace();

}

}

static class ThreadC extends Thread {

DataOutputStream dos = null;

public ThreadC(OutputStream os) {

dos = new DataOutputStream(os);

}

public void run() {

while (true) {

try {

double d = Math.random();

dos.writeDouble(d);

sleep(2);

} catch (Exception e) {

e.printStackTrace();

}

}

}

};

static class ThreadZ extends Thread {

DataOutputStream dos = null;

DataInputStream dis = null;

public ThreadZ(OutputStream os, InputStream is) {

dos = new DataOutputStream(os);

dis = new DataInputStream(is);

}

public void run() {

while (true) {

try {

double d = dis.readDouble();

dos.writeDouble(d);

} catch (Exception e) {

e.printStackTrace();

}

}

}

};

static class ThreadQ extends Thread {

DataInputStream dis = null;

public ThreadQ(InputStream is) {

dis = new DataInputStream(is);

}

public void run() {

while (true) {

try {

double d = dis.readDouble();

System.out.println(d);

} catch (Exception e) {

e.printStackTrace();

}

}

}

};

}

编译运行

javac TestPipeStream.java

java TestPipeStream

例3:保存所有的Person对象到文件并以对象的方式读出来

1 问题分析

本题中需要对文件读,写对象数据,需要使用到与对象有关的流ObjectInputStream/ObjectOutputStream。

2 使用对象的读写流

ObjectOutputStream用于将一个对象输出,输出对象使用的方法为writeObject(Object obj)

ObjectInputStream用于读取一个对象,读取对象使用的方法为readObject()

注意: 被读写的对象必须是已序列化的类的对象,即要实现要Serializable接口。

3 编写代码

import java.io.*;

import java.util.*;

class Person implements Serializable {

String name = null;

public Person(String s) {

name = s;

}

public String toString() {

return name;

}

}

public class TestObjectStream {

public static void main(String[] args) {

ObjectOutputStream oos = null;

ObjectInputStream ois = null;

try {

File f = new File("date.ser");

oos = new ObjectOutputStream(new FileOutputStream(f));

oos.writeObject(new Person("andy"));

oos.close();

ois = new ObjectInputStream(new FileInputStream(f));

Person d = (Person) ois.readObject();

System.out.println(d);

ois.close();

} catch (Exception e) {

e.printStackTrace();

}

}

}

4 编译运行

javac TestObjectStream.java

java TestObjectStream

例4:创建GZIP压缩格式文件

在JDK API中,定义了多种类型用于创建和解除GZIP压缩格式数据文件的通用对象和方法,用于基于JDK编写GZIP压缩数据管理程序。

由于在数据压缩过程中可以采用多种类型的压缩算法,因此,压缩文件的压缩比很高。在JDK API中,只定义了GZIPInputStream和GZIPOutputStream两种类型的流(Stream)对象,用于在基于流的数据传输过程中实现数据压缩。

以下程序完成压缩功能

package com.itjob;

import java.io.*;

import java.util.zip.*;

public class GZIPDemo {

public static void main(String[] args) {

if (args.length != 2) {

System.out.println("Usage:java GZIPDemo SourceFile DestnFile"

+ args.length);

System.exit(1);

}

try {

int number;

// 建立需压缩文件的输入流

FileInputStream fin = new FileInputStream(args[0]);

// 建立压缩文件输出流

FileOutputStream fout = new FileOutputStream(args[1]);

// 建立GZIP压缩输出流

GZIPOutputStream gzout = new GZIPOutputStream(fout);

// 设定读入缓冲区尺寸

byte[] buf = new byte[1024];

while ((number = fin.read(buf)) != -1)

gzout.write(buf, 0, number);

gzout.close();

fout.close();

fin.close();

} catch (IOException e) {

System.out.println(e);

}

}

}

以下程序完成解压缩:

package com.itjob;

import java.io.*;

import java.util.zip.*;

public class UnGZIPDemo {

public static void main(String[] args) {

if (args.length != 2) {

System.out.println("Usage:java UnGZIPDemo GZIPFile DestnFile");

System.exit(1);

}

try {

int number;

// 建立GZIP压缩文件输入流

FileInputStream fin = new FileInputStream(args[0]);

// 建立GZIP解压工作流

GZIPInputStream gzin = new GZIPInputStream(fin);

// 建立解压文件输出流

FileOutputStream fout = new FileOutputStream(args[1]);

// 设定读入缓冲区尺寸

byte[] buf = new byte[1024];

while ((number = gzin.read(buf, 0, buf.length)) != -1)

fout.write(buf, 0, number);

gzin.close();

fout.close();

fin.close();

} catch (IOException e) {

System.out.println(e);

}

}

}

对象通过内存压缩,解压缩的例子:

package com.itjob;

import java.io.Serializable;

public class Data implements Serializable {

String name = "张三";

int age = 12;

float height = 1.83f;

}

package com.itjob;

import java.util.zip.*;

import java.io.*;

public final class CompressObject {

// 将Data类型数据对象序列化对象压缩,返回字节数组,压缩后的对象数组可写入文件保存或用于网络传输

public static byte[] writeCompressObject(Data object_) {

byte[] data_ = null;

try {

// 建立字节数组输出流

ByteArrayOutputStream o = new ByteArrayOutputStream();

// 建立gzip压缩输出流

GZIPOutputStream gzout = new GZIPOutputStream(o);

// 建立对象序列化输出流

ObjectOutputStream out = new ObjectOutputStream(gzout);

out.writeObject(object_);

out.close();

gzout.close();

// 返回压缩字节流

data_ = o.toByteArray();

o.close();

} catch (IOException e) {

System.out.println(e);

}

return (data_);

}

// 将压缩字节数组还原为Data类型数据对象

public static Data readCompressObject(byte[] data_) {

Data object_ = null;

try {

// 建立字节数组输入流

ByteArrayInputStream i = new ByteArrayInputStream(data_);

// 建立gzip解压输入流

GZIPInputStream gzin = new GZIPInputStream(i);

// 建立对象序列化输入流

ObjectInputStream in = new ObjectInputStream(gzin);

// 按制定类型还原对象

object_ = (Data) in.readObject();

i.close();

gzin.close();

in.close();

} catch (ClassNotFoundException e) {

System.out.println(e);

} catch (IOException e) {

System.out.println(e);

}

return (object_);

}

}

package com.itjob;

import java.io.*;

import java.util.zip.*;

public class Test {

public static void main(String[] args) {

Data testData_ = new Data();

// 未压缩数据对象内容

System.out.println("name=" + testData_.name + " age=" + testData_.age

+ " height=" + testData_.height);

// 压缩

byte[] i_ = CompressObject.writeCompressObject(testData_);

/*

* 可执行保存或网络传输,需要时还原或在对端还原

*/

// 解压缩

Data o_ = CompressObject.readCompressObject(i_);

// 解压缩后对象内容

System.out.println("name=" + o_.name + " age=" + o_.age + " height="

+ o_.height);

}

}

内容总结

? java.io包内类:Java语言中数据流是发送或接收数据的管道。通常,你的程序是流的一个端点,其它程序或文件是流的另一个端点。流的单向性:源端点和目的端点分别叫做input stream(输入流)和output stream(输出流)。你可以从输入流读,但你不能对它写;同样,你可以向输出流写,但不能从输出流读。

? 理解流的概念和节点流,过滤流的使用:Java 2 SDK中有三种基本类型的节点:文件(file)、内存(memory)、管道(pipe);过程流在其它流之上,完成排序、变换等操作。过程流也被称做过滤流。当你需要改变输入流的原始数据时,你可以将一个过滤输入流连接到一个原始的输入流上。用过滤流将原始数据变换成你需要的格式。

? 流与字符流并恰当使用和目录:Reader和Writer字符流的体系结构

? 更新文本和数据文件:RandomAccessFile随机访问文件及对象的串行化

? 用Serialization接口序列化编码对象的状态

独立实践

1、读取一个文件名称,如果是目录,显示该目录下的所有文件名称;如果是文件,显示文件的内容

2、将一个文件拷贝到另一个位置。

3、文本文件中有10条数据,请编写程序读取第4行的信息。

4、将10个学生信息写入文件,并重新读取显示。

5、使用输入、输出流将一个文本文件的内容按行读出,每读一行就添加一行号,并写入到另一个文件中。

第十三章:网络

学习目标

? ?TCP/IP协议模型

? 网络通信协议

? 通信端口

? 基于Java的网络技术

? ServerSocket

? ?Socket

? UDP套接字

? DatagramSocket

? InetAddress类的使用

? 扩展知识

TCP/IP协议模型

计算机网络的组成部分:计算机系统、数据通信系统、网络软件。其中,网络软件充当网络管理者的角色,它提供并实现各种网络服务,包括网络协议软件(TCP/IP、IPX等协议驱动)、网络服务软件、网络工具软件、网络操作系统(Netware、NT局域网系统)。

网络通信协议

网络协议是构成网络的基本组件之一,协议是若干规则和协定的组合,一般指机器1的第n层与机器2的第n层的对话,这种对话中所使用的若干规则和约束便称为第n层网络协议。TCP/IP网络体系结构模型就是遵循TCP/IP协议进行通信的一种分层体系,现今,Internet和Intranet所使用的协议一般都为TCP/IP协议。

TCP/IP协议是一个工业标准协议套件,专为跨大广域网(WAN)的大型互联网络而设计。在了解该协议之前,我们必须掌握基于该协议的体系结构层次,而TCP/IP体系结构分为四层,具体结构如下图:

可以看出,TCP/IP体系模型分为4层结构,其中有3层对应于ISO参考模型中的相应层。这4层概述如下:

第一层 网络接口层

包括用于协作IP数据在已有网络介质上传输的协议,提供TCP/IP协议的数据结构和实际物理硬件之间的接口。比如地址解析协议(Address Resolution Protocol, ARP )等。

第二层 网络层

对应于ISO模型的网络层,主要包含了IP、RIP等相关协议,负责数据的打包、寻址及路由。还包括网间控制报文协议(ICMP)来提供网络诊断信息。

第三层 传输层

对应于ISO的传输层,提供了两种端到端的通信服务,分别是TCP和UDP协议。

第四层 应用层

对应于ISO的应用层和表达层,提供了网络与应用之间的对话接口。包含了各种网络应用层协议,比如Http、FTP等应用协议。

TCP/IP体系模型相对于ISO模型的7层结构来说更简单更实用!现已成为因特网之间的标准协议模型。

TCP/IP网络体系主要包含两种协议:TCP/IP、UDP协议。其中,IP(Internet Protocol)协议是一种低级路由协议,该协议主要实现将传输数据分解成许多小数据包,接着通过网络将这些数据包传到一个指定地址,但是,请注意,IP协议并不会保证传输的数据包一定到达目的地,或者是数据包的完整性!

TCP(Thransfer Control Protocol)协议正好弥补了IP协议的不足,属于一种较高级的协议,它实现了数据包的有力捆绑,通过排序和重传来确保数据传输的可靠(即数据的准确传输以及完整性)。排序可以保证数据的读取是按照正确的格式进行,重传则保证了数据能够准确传送到目的地!

UDP协议与TCP协议类似,它们之间的区别在于TCP协议是面向连接的可靠数据传输协议,而UDP协议是面向数据报的不可靠数据传输协议;UDP协议可以要求数据传输的目的地可以没有连接甚至不存在,数据传输效率更快,但可靠性低,TCP正好相反。

注意,TCP与UDP协议均属于传输层协议,而IP协议属于网络层协议。

应用层各种协议提供了应用程序访问其他层的服务,并定义应用程序用于交换数据的协议。以下应用协议是广泛被使用的交换用户信息的协议:

? 超文本传输协议(HTTP): 用于传输组成万维网Web页面的文件,大部分Web项目都是基于该协议实现用户数据的传输。

? 文件传输协议(FTP): 交互式文件传输

? 简单邮件传输协议(SMTP): 用于传输邮件消息和连接

? 终端访问协议(Telnet): 远程登录到网络主机

? 域名系统(DNS)

? 路由选择信息协议(RIP)

通信端口

每一个应用协议都有其特定的端口,通过端口实现服务器同时能够服务于很多不同的客户端,服务器进程通过监听端口来捕获客户端连接。一个服务器允许在同个端口接受多个客户,一个服务器也能开启多个端口接受客户请求。能够接受并管理多个客户连接的服务器进程必须是支持多线程的(或者采用同步输入/输出处理多路复用技术的其他方法)。

每一个端口都有特定的端口号,TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。同时该号一般对应一些特定协议,TCP/IP为特定协议保留了低端的1024个端口,其中,端口80是HTTP协议的,21是FTP协议的,23是Telnet协议的等等,客户和服务器必须事先约定所使用的端口。如果系统两部分所使用的端口不一致,那就不能进行通信。

HTTP是网络浏览器及服务器用来传输超文本网页和图像的协议,一般的Web服务器也提供了HTTP服务器的功能,当客户端向HTTP服务器请求一个资源时,HTTP协议会将相关数据以特定格式传给其缺省端口80,服务器在该端口接受请求并将处理结果返回给客户。可以这样理解,端口是基于某种特定协议的一个点(一般是服务器)与另一个点之间的对话窗口,在通话的过程中两点必须在同一窗口才能实现数据对话。

基于Java的网络技术

TCP/IP套接字

套接字是网络软件中的一个抽象概念,套接字允许单个计算机同时服务于很多不同客户,并能够提供不同类型信息的服务。该技术由引入的端口处理,该端口既是一个特定机器上的一个被编号的套接字---通信端口.TCP/IP套接字用于在主机和Internet之间建立的可靠、双向、点对点、持续的流式连接。

在java中,TCP/IP Socket连接是用java.net包中的类实现的,这些类实现了建立网络连接和通过连接发送数据的复杂过程,我们只需使用其简单接口就能实现网络通信!在java中有两类套接字,一种是服务器端套接字--java.net.ServerSocket,另一种是客户端套接字--java.net.Socket。

ServerSocket

其中,ServerSocket被设计成在等待客户建立连接之前不做任何事情的监听器,构造方法的版本如下:

public ServerSocekt(int port) throws IOException

--在服务器指定端口port创建队列长度为50的服务器套接字,当port为0则代表创建一个基于任意可用端口的服务器套接字。队列长度告诉系统多少与之连接的客户在系统拒绝连接之前可以挂起。

public ServerSocekt(int port, int maxQueue) throws IOException

--在指定端口创建指定队列长度的服务器套接字

public ServerSocket(int port, int maxQueue, InetAddress bindAddr ) throws IOException

在多地址主机上,我们除了可以指定端口之外,还可以通过InetAddress类来指定该套接字约束的IP地址。InetAddress在后面将学习。

ServerSocket还定义了以下一些常用的方法:

public Socket accept() throws IOException

--该方法用于告诉服务器不停地等待,直到有客户端连接到该ServerSocket指定的端口,一旦有客户端通过网络向该端口发送正确的连接请求,该方法就会返回一个表示服务器与客户端连接已建立的Socket对象,接下来我们就可以通过这个返回的Socket对象实现服务器与指定客户端的通信。注意:accept()方法会让服务器中的当前线程暂停下来,直到有客户端的正确连接发送过来。

public void bind(SocketAddress endpoint) throws IOException

--绑定该ServerSocket到指定的endpoint地址(IP地址和端口)

public void close() throws IOException

--关闭当前ServerSocket。任何当前被锁定的线程将在accept()方法中抛出IOException。

从jdk1.4开始,java提供了关于ServerSocket的ServerSocketChannel,jdk建议用管道来实现客户端连接的监听以及关闭服务器套接字会更安全,因此,现在我们应该通过ServerSocket来得到其套接字管道,通过管道来实现服务监听以及关闭,可以通过ServerSocket的getChannel()方法来得到当前ServerSocket的相关管道。

Socket

该类为建立连向服务器套接字及启动协议交换而设计,当进程通过网络进行通信的时候,java技术使用流模型来实现数据的通信。一个Socket包括两个流,分别为一个输入流和一个输出流,一个进程如果要通过网络向另一个进程发送数据,只需简单地写入与Socket相关联的输出流,同样,一个进程通过从与Socket相关联的输入流来读取另一个进程所写的数据。如果通过TCP/IP协议建立连接,则服务器必须运行一个单独的进程来等待连接,而某一客户机必须试图到达服务器,就好比某人打电话,必须保证另一方等待电话呼叫,这样才能实现两人之间的通信。

分析以下代码:

import java.io.*;

import java.net.*;

......

try{

// 在服务器的8000端口创建一个ServerSocket

ServerSocket server = new ServerSocket(8000);

// accept()方法使当前服务器线程暂停,之前创建的server套接字

//将通过该方法不停的监听指定端口是否有客户端请求连接发送过来,

//如果有正确连接发送过来,

// 则该方法返回一个表示连接已建立的Socket对象

Scoket fromSocket = server.accept();

if (fromSocket != null){

// 通过套接字实现数据传输,得到套接字的输入流输出流

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

}

}catch(IOException e){

e.printStackTrace();

}

......

通过上面的代码,我们分析得知,套接字只是实现数据传输的接口,真正实现数据传输的是封装在套接字中的输入、输出流。以上代码是服务器端程序,该程序开启了一个端口号为8000的特定端口,并且开启了服务器套接字在该端口上监听客户请求,accept()方法阻塞当前线程直到有客户端请求发送过来,当请求正确时,该方法将客户端套接字引用传递出来,这样,网络之间的数据发送就好像是本地数据调用。

Socket类的相关方法:

Socket()创建一个无参数套接字,该套接字会随即取一个可用端口、可用IP来建立连接。

Socket(String host, int port)创建一个指定远程服务器、端口号的套接字

Socket(InetAddress net, int port)创建一个指定InetAddress类封装IP、指定端口号的套接字。即该套接字只能往指定IP的服务器以及服务器指定端口发送数据。

public OutputStream getOutputStream() throws IOException 得到套接字的输出流,接下来就可以使用得到的输出流去写数据至服务器或客户端

public InputStream getInputStream() throws IOException得到套接字的输入流,接下来就可以使用得到的输入流去读取来自于服务器或客户端的数据。

public SocketChannel getChannel()得到套接字的管道,在1.4之后新增了io功能,在java.nio包中定义,通过流中的SocketChannel来实现数据的读取会比直接使用流来读取更安全可靠。(扩展内容)

public void close() throws IOException关闭当前套接字,注意,关闭套接字的同时也会关闭该套接字中的所有流。

下面是客户端的部分代码:

import java.io.*;

import java.net.*;

......

try{

// 创建套接字,该套接字连接IP地址为192.168.0.2的服务器,端口为8000 Socket server = new Socket("192.168.0.2",8000);

if(server != null){

// 通过套接字实现数据传输输入流

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

}

}catch(IOException e){

e.printStackTrace();

}

以上即是简单的基于Tcp/IP协议、使用套接字实现数据传输的服务器和客户端代码(注意:在运行时,先执行服务器端代码,接着执行客户端代码),但是我们发现,通过以上代码,客户端和服务器端只能进行一次数据对话,通过更改以上代码,我们才能实现一个客户端与服务器端的真正对话,直到某一方终止通话为止,修改后代码如下:

服务器端:

......

ServerSocket server = new ServerSocket(8000);

while (true){

Socket fromSocket = server.accept();

if (fromSocket != null){

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

.....

}

}

.....

客户端:

......

Socket server = new Socket("192.168.0.2",8000);

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

while (true){

// 数据的读写操作

......

}

......

实际上,我们只要将服务器端每一个客户的请求套接字的获取以及流的获取、数据的读写放入至一个无限循环,这样就可以保证服务器可以随时接受任意客户发送过来的套接字,从而实现对话。客户端也可以通过一个无限循环实现对服务器端的任意时刻的数据传输。

当然,这只是实现了一个客户和服务器之间的对话,但是服务器一般是对应许多客户的,因此,为了实现服务器对应多个客户,必须将每一次accept()方法返回的每一个客户套接字保存起来,这样才可以实现服务器与多个客户对应,具体代码在后面讨论。

注意,数据的读写应该是在一个独立于主线程的单独线程中运行,因为accept方法会阻塞当前线程,为了不影响主线程的其余功能,我们应该启用多线程来实现高效的数据传输。

当通过代码实现服务器与客户之间的套接字发送之后,我们就可以使用流的IO操作来实现服务器与客户端之间的基于特定协议的远程数据传输。注意,基于TCP/IP协议的数据传输,前提是必须得保证接受数据的一方是连通的,也就是说,如果服务器端没有运行,那么客户端是无法对其发送信息,反之亦然。那有时候我们只在乎信息的发送,并不理会接受的人是否连通,甚至不理会接受人是否存在,那就不能使用TCP/IP协议,而得通过我们下面所学的UDP协议来实现。

UDP套接字

UDP(User Datagrams Protocol)用户数据报协议,是一种使用数据报的机制来传递信息的协议。数据报(Datagrams)是一种在不同机器之间传递的信息包,该信息包一旦从某一机器被发送给指定目标,那么该发送过程并不会保证数据一定到达目的地,甚至不保证目的地的存在真实性。反之,数据报被接受时,不保证数据没有受损,也不保证发送该数据报的机器仍在等待响应。

由此可见,UDP协议是一种基于数据报的快速的(因为它无需花时间去保证数据是否损坏,无需花时间确定接受方是否存在并等待响应)、无连接的、不可靠的信息包传输协议。

在java中,通过两个特定类来实现UDP协议顶层数据报,分别是DatagramPacket和DatagramSocket,其中DatagramPacket是一个数据容器,用来保存即将要传输的数据;而DatagramSocket实现了发送和接收DatagramPacket的机制,即实现了数据报的通信方式。 1 、atagramPacket

该类主要有四个常用构造方法,分别如下:

DatagramPacket(byte[] buff, int length)

DatagramPacket(byte[] buf, int offset, int length)

DatagramPacket(byte[] buf, int length, InetAddress address, int port)

DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)

第一个构造方法用于创建一个指定数据缓冲区大小和信息包的容量大小的DatagramPacket,第二个构造方法用于创建一个长度大小为length的缓冲区,并指定数据存储(读取)的偏移地址为offset的DatagramPacket。第三个创建一个指定缓冲区大小、传送(接受)IP地址、端口号的DatagramPacket。一般情况下,发送地址是由DatagramSocket指定。

常用的几个方法如下:

byte[] getData()

用于得到发送过来的DatagramPacket中的数据

void setData(byte[] buf)

用于将buf中的数据写入DatagramPacket中,以备发送。

InetAddress getAddress()

返回目标的InetAddress,一般用于发送。

DatagramSocket

DatagramSocket()

创建一个以当前计算机的任意可用端口为发送端口的数据报连接

DatagramSocket(int port)

创建一个以当前计算机的port端口为发送端口的数据报连接

DatagramScoket(int port, InetAddress address)

创建一个以当前计算机的port端口为发送端口,向IP地址为address的计算机发送数据报连接

常用的几个方法:

void close() throws IOException

关闭数据报连接

void recieve(DatagramPacket packet)

接收来自于packet数据报的信息

void send(DatagramPacket packet)

发送packet数据报

void connect(InetAddress address, int port)

以当前计算机指定端口port为发送端口,向IP地址为address的计算机发送数据报连接

void disconnect()

断开连接

DatagramChannel getChannel()

和SocketChannel类似

结合以上两个类的方法,创建一个简单的UDP服务器端如下:

....

// 指定连接端口

DatagramSocket socket = new DatagramSocket(8001)

byte[] buff = new byte[256];

// 指定接受数据报缓冲区大小为字节数组buff大小

DatagramPacket fromPacket = new DatagramPacket(buff,buf.length);

// 接受客户端请求,并将请求数据存储在数据报packet中

packet.recieve(fromPacket);

// 读取数据报中数据

byte rec = packet.getData();

.....

buff = "hello world".toBytes();

InetAddress addr = .....

// 指定发送数据报缓冲区大小为字节数组buff大小,

//发送目的地为addr,端口为8001,数据报内容为数组buff内容

DatagramPacket toPacket = new DatagramPacket(buff,buf.length,addr,8001);

// 发送服务器端数据报toPacket

packet.send(toPacket);

.......

客户端代码与服务器段类似,只要注意接受与发送应该分别是两个不同的数据报。

InetAddress类的使用

java.net.InetAddress类是java的IP地址封装类,内部隐藏了IP地址,可以通过它很容易的使用主机名以及IP地址。一般共各种网络类使用。直接由Object类派生并实现了序列化接口。该类用两个字段表示一个地址:hostName与address。hostName包含主机名,address包含IP地址。InetAddress支持ipv4与ipv6地址。

一些常用方法如下:

byte[] getAddress()

返回指定对象的IP地址的以网络字节为顺序的4个元素的字节数组

static InetAddress getByName(String hostname)

使用DNS查找hostname的IP地址,并返回

static InetAddress getLocalHost()

返回本地计算机的InetAddress。

String getHostName()

返回指定InetAddress对象的主机名。

String getHostAddress()

返回指定InetAddress对象的主机地址的字符串形式

分析:

InetAddress addr = InetAddress.getByName("java.sun.com");

System.out.println(addr);

以上代码将打印网址域名为java.sun.com的对应IP地址

因此,在网络编程中,我们可以很方便的使用InetAddress类实现Ip地址的各种操作。

例题1:服务器端:

package com.itjob.net;

import java.awt.*;

import javax.swing.*;

import java.awt.event.*;

import java.io.*;

import javax.swing.border.*;

import java.net.*;

public class ServerApp extends JFrame implements Runnable, ActionListener {

JPanel mainPanel;

JPanel bottomPanel;

GridBagLayout gbl;

GridBagConstraints gbc;

Border border;

JTextArea txtChatMess;

JScrollPane scroll;

JTextField txtMess;

JLabel lblEnterMess;

JButton cmdSend;

JButton cmdReset;

ServerSocket server = null;

Socket socket = null;

Scanner read = null;

public ServerApp() {

super("服务器");

mainPanel = new JPanel();

bottomPanel = new JPanel();

mainPanel.setLayout(new BorderLayout());

mainPanel.add(bottomPanel, BorderLayout.SOUTH);

gbl = new GridBagLayout();

gbc = new GridBagConstraints();

bottomPanel.setLayout(gbl);

txtChatMess = new JTextArea(15, 20);

txtChatMess.setEditable(false);

scroll = new JScrollPane(txtChatMess);

mainPanel.add(scroll);

border = BorderFactory.createRaisedBevelBorder();

txtMess = new JTextField(15);

lblEnterMess = new JLabel("请输入消息: ");

cmdSend = new JButton("发 送");

cmdSend.setPreferredSize(new Dimension(50, 20));

cmdSend.setEnabled(false);

cmdSend.addActionListener(this);

cmdReset = new JButton("清 空");

cmdReset.setPreferredSize(new Dimension(50, 20));

cmdReset.addActionListener(this);

cmdSend.setBorder(border);

cmdReset.setBorder(border);

gbc.gridx = 3;

gbc.gridy = 10;

gbl.setConstraints(lblEnterMess, gbc);

bottomPanel.add(lblEnterMess);

gbc.gridx = 10;

//gbc.fill = gbc.BOTH;

gbl.setConstraints(txtMess, gbc);

bottomPanel.add(txtMess);

gbc.gridx = 3;

gbc.gridy = 30;

gbl.setConstraints(cmdSend, gbc);

bottomPanel.add(cmdSend);

gbc.gridx = 10;

gbl.setConstraints(cmdReset, gbc);

bottomPanel.add(cmdReset);

getContentPane().add(mainPanel);

pack();

setVisible(true);

}

public void actionPerformed(ActionEvent evt) {

if (evt.getSource() == cmdSend) {

try {

write.println(txtMess.getText());

write.flush();

} catch (Exception e) {

e.printStackTrace();

}

}

if (evt.getSource() == cmdReset) {

txtMess.setText("");

}

}

public void run() {

try {

server = new ServerSocket(2005);

//反复接收客户端请求

while (true) {

socket = server.accept();

if (socket != null) {

txtChatMess.append("服务器消息:客户已连接!" + "\n");

cmdSend.setEnabled(true);

} else {

txtChatMess.append("服务器消息:客户未能连接!" + "\n");

cmdSend.setEnabled(false);

}

//为每一个客户启动一个读取数据的单独线程

Connections con = new Connections(socket);

Thread thread = new Thread(con);

thread.start();

}

} catch (Exception e) {

e.printStackTrace();

}

}

class Connections implements Runnable {

Socket clientSock = null;

Connections(Socket s) {

clientSock = s;

}

public void run() {

try {

read = new Scanner(socket.getInputStream()));

write = new

PrintWriter(socket.getOutputStream());

String result = read.readLine();

if (result == null){

return;

}

while(!result.trim().equals("Exit!")) {

txtChatMess.append("客户端消息: " + result + "\n");

if (read.hasNextLine()){

result = read.nextLine();

}else{

Thread.sleep(100);

}

}

}catch(Exception e){

e.printStackTrace();

}finally{

try {

read.close();

write.close();

socket.close();

server.close();

cmdSend.setEnabled(false);

}catch(Exception e){

}

}

}

}

public static void main(String[] args) {

ServerApp serverApp = new ServerApp();

Thread thread = new Thread(serverApp);

thread.start();

}

}

客户端:

package com.itjob.net;

import java.awt.*;

import javax.swing.*;

import java.awt.event.*;

import java.io.*;

import javax.swing.border.*;

import java.net.*;

public class ClientApp extends JFrame implements Runnable, ActionListener {

JPanel mainPanel;

JPanel bottomPanel;

GridBagLayout gbl;

GridBagConstraints gbc;

Border border;

JTextArea txtChatMess;

JScrollPane scroll;

JTextField txtMess;

JLabel lblEnterMess;

JButton cmdSend;

JButton cmdReset;

ServerSocket server = null;

Socket socket = null;

Scanner read = null;

PrintWriter write = null;

public ClientApp() {

super("客户端");

mainPanel = new JPanel();

bottomPanel = new JPanel();

mainPanel.setLayout(new BorderLayout());

mainPanel.add(bottomPanel, BorderLayout.SOUTH);

gbl = new GridBagLayout();

gbc = new GridBagConstraints();

bottomPanel.setLayout(gbl);

//new GBC();

txtChatMess = new JTextArea(15, 20);

txtChatMess.setEditable(false);

scroll = new JScrollPane(txtChatMess);

mainPanel.add(scroll);

border = BorderFactory.createRaisedBevelBorder();

txtMess = new JTextField(15);

lblEnterMess = new JLabel("请输入消息: ");

cmdSend = new JButton("发 送");

cmdSend.setPreferredSize(new Dimension(50, 20));

cmdSend.setEnabled(false);

cmdSend.addActionListener(this);

cmdReset = new JButton("清 空");

cmdReset.setPreferredSize(new Dimension(50, 20));

cmdReset.addActionListener(this);

cmdSend.setBorder(border);

cmdReset.setBorder(border);

gbc.gridx = 3;

gbc.gridy = 10;

gbl.setConstraints(lblEnterMess, gbc);

bottomPanel.add(lblEnterMess);

gbc.gridx = 10;

//gbc.fill = gbc.BOTH;

gbl.setConstraints(txtMess, gbc);

bottomPanel.add(txtMess);

gbc.gridx = 3;

gbc.gridy = 30;

gbl.setConstraints(cmdSend, gbc);

bottomPanel.add(cmdSend);

gbc.gridx = 10;

gbl.setConstraints(cmdReset, gbc);

bottomPanel.add(cmdReset);

getContentPane().add(mainPanel);

pack();

setVisible(true);

}

public void actionPerformed(ActionEvent evt) {

if (evt.getSource() == cmdSend) {

try {

write.println(txtMess.getText());

write.flush();

} catch (Exception e) {

e.printStackTrace();

}

}

if (evt.getSource() == cmdReset) {

txtMess.setText("");

}

}

public void run() {

try {

InetAddress add = InetAddress.getLocalHost();

socket = new Socket(add, 2005);

read = new Scanner(socket.getInputStream());

write = new PrintWriter(socket.getOutputStream());

if (socket != null) {

txtChatMess.append("客户消息:服务器已连接!" + "\n");

cmdSend.setEnabled(true);

} else {

txtChatMess.append("客户消息:服务器未能连接!" + "\n");

cmdSend.setEnabled(false);

}

} catch (Exception e) {

e.printStackTrace();

}

Connections con = new Connections();

Thread thread = new Thread(con);

thread.start();

}

class Connections implements Runnable {

public void run() {

try {

String result = read.readLine();

if (result == null) {

return;

}

while (!result.trim().equals("Exit!")) {

txtChatMess.append("服务器端消息: " + result + "\n");

if (read.hasNextLine()) {

result = read.nextLine();

} else {

Thread.sleep(100);

}

}

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

read.close();

write.close();

socket.close();

server.close();

//cmdSend.setEnabled(false);

} catch (Exception e) {

}

}

}

}

public static void main(String[] args) {

ClientApp client = new ClientApp();

Thread thread = new Thread(client);

thread.start();

}

}

注意:以上代码是基于客户端与服务器在同一台服务器上的,而且服务器不能实现对应多个客户,请大家思考并修改,从而实现一个服务器对应多个客户端。

扩展知识:

套接字超时

在使用套接字的时候,读取数据之前当前线程会被阻塞,直到数据的到达为止,在这个过程中间可能由于被访问的主机不可达,从而使你的应用将等待很长的时间,最终受操作系统的底层影响而导致超时产生。也就是说,虽然最终会导致超时的产生,但我们在设计一个套接字的时候应该给定一个合理的超时时间,从而保证在读、写操作未完成之前可以有一个限定时间,当超出该限定时间时操作仍未完成,那么套接字就会抛出SocketTimeoutException异常,这样,我们就可以捕获该异常并编写超时后的程序逻辑。这样,通过设置套接字的超时时间(对套接字内所有的操作而言)就可以保证程序因为某些原因而导致程序长时间的无作用等待。

通过调用套接字的setSoTimeout(int milliseconds)方法来设置套接字的超时时间,比如:

Socket s = new Socket(...);

s.setSoTimeout(10000);

上面方法将设置套接字的超时时间为10秒,当10秒内数据仍未读取时,套接字将抛出异常并停止操作。注意,setSoTimeout方法会抛出SocketTimeoutException异常,并且实现了线程同步。

在上面我们所说的是当操作被阻塞的情况下,还有一种情况也会导致程序长期阻塞直至建立服务器的连接为止。回顾一下套接字的构造方法,其中一构造方法如下:Socket(String host, int port),如果该构造方法在连接制定的host由于某些特殊原因(网络)导致阻塞,那么该套接字将无限阻塞,直到建立和主机的连接为止。为了避免这种可能,我们还应该设置套接字的连接超时,当在指定时间内仍不能建立和主机的连接,则抛出异常并终止操作。解决步骤如下:

//先建立一个无连接套接字

Socket s = new Socket();

//在调用connect方法去指定套接字连接主机时给定连接超时参数,这里超时时间设置为10秒

s.connect(new InetSocketAddress(host,port),10000);

(InetSocketAddress类是一个专门封装套接字地址的特定类,可以通过该类将一个Scoket与指定主机及端口连接)

SocketChannel类

不知大家有没有发现,虽然给线程设置中断标志可以终止一个被阻塞线程,但是当线程是被套接字等网络因素而阻塞的情况下,设置中断标志是无法终止阻塞的。在1.4之后提供的java.nio.channels包中SocketChannel类则很好的解决了这个问题,即如果我们是通过该类来创建一个套接字连接,那么就可以使用线程的中断标志来终止被阻塞的线程,具体操作如下:

//首先,我们应该获取套接字连接对象,通过SocketChannel类的静态方法open可以得到一个指定主机连接对象

SocketChannel channel = SocketChannel.open(new InetSocketAddress("192.168.0.1",8001));

//对SocketChannel的对象进行的操作和对Socket的操作类似,可以通过Channels类的静态方法newOutputStream()于newInputStream()来分别得到通道中的输出和输入流

OutputStream out = Channels.newOutputStream(channel);

InputStream input = Channels.newInputStream(channel);

//或者通过Scanner来得到输入流

Scanner scan = new Scanner(channel);

注意,通过Scanner来得到输入流于通过Channels.newInputStream(channel)得到的输入流有所不同,实际上channel并没有与之关联的流,其所拥有的read和write方法都是通过Buffer对象来实现的。也就是说,Channels类的newInputStream方法得到的流实际上是通过Buffer来读取数据,而Scanner则是直接去读数据而不使用缓存。因此,读取大数据时使用缓冲可能更高效,否则反之。

SocketChannel实现了两个接口:ReadableByteChannel和WritableByteChannel,分别实现了read与write方法,这两个方法均接收ByteBuffer(java.nio)对象并对其操作,即使用SocketChannel的同时我们需结合nio包中的字节缓冲对象共同实现数据的读写操作。在使用SocketChannel时,假设线程正在执行打开、读取、写入操作,此时如果对线程进行中断,那么这些操作不会导致线程阻塞而是以抛出异常的方式结束。

内容总结

? IP(Internet Protocol)协议是一种低级路由协议,该协议主要实现将传输数据分解成许多小包,接着通过网络传到一个指定地址,但是,请注意,IP协议并不会保证传输的数据包一定到达目的地!TCP(Thransfer Control Protocol)协议正好弥补了IP协议的不足,属于一种较高级的协议,它实现了数据包的有力捆绑,通过排序和重传来确保数据传输的可靠!

? TCP与UDP协议均属于传输层协议,而IP协议属于网络层协议

? TCP/IP套接字用于在主机和Internet之间建立的可靠、双向、点对点、持续的流式连接

? UDP协议是一种基于数据报的快速的(因为它无需花时间去保证数据是否损坏,无需花时间确定接受方是否存在并等待响应)、无连接的、不可靠的信息包传输协议。

? ServerSocket被设计成在等待客户建立连接之前不做任何事情的监听器,Socket类为建立连向服务器套接字及启动协议交换而设计,当进程通过网络进行通信的时候,java技术使用流模型来实现数据的通信

? 数据报(Datagrams)是一种在不同机器之间传递的信息包,该信息包一旦从某一机器被发送给指定目标,那么该发送过程并不会保证数据一定到达目的地,甚至不保证目的地的存在真实性。

? DatagramPacket是一个数据容器,用来保存即将要传输的数据;DatagramSocket实现了发送和接收DatagramPacket的机制,即实现了数据报的通信方式。

? java.net.InetAddress类是java的IP地址封装类,内部隐藏了IP地址,可以通过它很容易的使用主机名以及IP地址

独立实践:

1、 编写一个应用,该应用应能实现基于HTTP协议的数据传送,分为服务器与客户端(应有对应的简单界面),要求在正常实现数据读写的同时,每一端应能判断服务器(或客户端)的关闭并做出相应的处理。还应能判别正常关闭(关闭窗口或点击退出)与非正常关闭(断电)。实现服务器一对多客户端。

2、 编写一个应用,实现服务器端,该服务器应能处理当前时间并显示,通过telnet协议应能访问服务器并查看当前时间。

3、 通过HTTP协议,写明数据客户端和服务器端数据的传输。

4、 通过UDP协议,写明数据客户端和服务器端数据的传输。

5、 使用多线程,使用HTTP协议或UDP协议,写明多客户端和服务器之间数据的通讯。

第十三章:网络

学习目标

? ?TCP/IP协议模型

? 网络通信协议

? 通信端口

? 基于Java的网络技术

? ServerSocket

? ?Socket

? UDP套接字

? DatagramSocket

? InetAddress类的使用

? 扩展知识

TCP/IP协议模型

计算机网络的组成部分:计算机系统、数据通信系统、网络软件。其中,网络软件充当网络管理者的角色,它提供并实现各种网络服务,包括网络协议软件(TCP/IP、IPX等协议驱动)、网络服务软件、网络工具软件、网络操作系统(Netware、NT局域网系统)。

网络通信协议

网络协议是构成网络的基本组件之一,协议是若干规则和协定的组合,一般指机器1的第n层与机器2的第n层的对话,这种对话中所使用的若干规则和约束便称为第n层网络协议。TCP/IP网络体系结构模型就是遵循TCP/IP协议进行通信的一种分层体系,现今,Internet和Intranet所使用的协议一般都为TCP/IP协议。

TCP/IP协议是一个工业标准协议套件,专为跨大广域网(WAN)的大型互联网络而设计。在了解该协议之前,我们必须掌握基于该协议的体系结构层次,而TCP/IP体系结构分为四层,具体结构如下图:

可以看出,TCP/IP体系模型分为4层结构,其中有3层对应于ISO参考模型中的相应层。这4层概述如下:

第一层 网络接口层

包括用于协作IP数据在已有网络介质上传输的协议,提供TCP/IP协议的数据结构和实际物理硬件之间的接口。比如地址解析协议(Address Resolution Protocol, ARP )等。

第二层 网络层

对应于ISO模型的网络层,主要包含了IP、RIP等相关协议,负责数据的打包、寻址及路由。还包括网间控制报文协议(ICMP)来提供网络诊断信息。

第三层 传输层

对应于ISO的传输层,提供了两种端到端的通信服务,分别是TCP和UDP协议。

第四层 应用层

对应于ISO的应用层和表达层,提供了网络与应用之间的对话接口。包含了各种网络应用层协议,比如Http、FTP等应用协议。

TCP/IP体系模型相对于ISO模型的7层结构来说更简单更实用!现已成为因特网之间的标准协议模型。

TCP/IP网络体系主要包含两种协议:TCP/IP、UDP协议。其中,IP(Internet Protocol)协议是一种低级路由协议,该协议主要实现将传输数据分解成许多小数据包,接着通过网络将这些数据包传到一个指定地址,但是,请注意,IP协议并不会保证传输的数据包一定到达目的地,或者是数据包的完整性!

TCP(Thransfer Control Protocol)协议正好弥补了IP协议的不足,属于一种较高级的协议,它实现了数据包的有力捆绑,通过排序和重传来确保数据传输的可靠(即数据的准确传输以及完整性)。排序可以保证数据的读取是按照正确的格式进行,重传则保证了数据能够准确传送到目的地!

UDP协议与TCP协议类似,它们之间的区别在于TCP协议是面向连接的可靠数据传输协议,而UDP协议是面向数据报的不可靠数据传输协议;UDP协议可以要求数据传输的目的地可以没有连接甚至不存在,数据传输效率更快,但可靠性低,TCP正好相反。

注意,TCP与UDP协议均属于传输层协议,而IP协议属于网络层协议。

应用层各种协议提供了应用程序访问其他层的服务,并定义应用程序用于交换数据的协议。以下应用协议是广泛被使用的交换用户信息的协议:

? 超文本传输协议(HTTP): 用于传输组成万维网Web页面的文件,大部分Web项目都是基于该协议实现用户数据的传输。

? 文件传输协议(FTP): 交互式文件传输

? 简单邮件传输协议(SMTP): 用于传输邮件消息和连接

? 终端访问协议(Telnet): 远程登录到网络主机

? 域名系统(DNS)

? 路由选择信息协议(RIP)

通信端口

每一个应用协议都有其特定的端口,通过端口实现服务器同时能够服务于很多不同的客户端,服务器进程通过监听端口来捕获客户端连接。一个服务器允许在同个端口接受多个客户,一个服务器也能开启多个端口接受客户请求。能够接受并管理多个客户连接的服务器进程必须是支持多线程的(或者采用同步输入/输出处理多路复用技术的其他方法)。

每一个端口都有特定的端口号,TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。同时该号一般对应一些特定协议,TCP/IP为特定协议保留了低端的1024个端口,其中,端口80是HTTP协议的,21是FTP协议的,23是Telnet协议的等等,客户和服务器必须事先约定所使用的端口。如果系统两部分所使用的端口不一致,那就不能进行通信。

HTTP是网络浏览器及服务器用来传输超文本网页和图像的协议,一般的Web服务器也提供了HTTP服务器的功能,当客户端向HTTP服务器请求一个资源时,HTTP协议会将相关数据以特定格式传给其缺省端口80,服务器在该端口接受请求并将处理结果返回给客户。可以这样理解,端口是基于某种特定协议的一个点(一般是服务器)与另一个点之间的对话窗口,在通话的过程中两点必须在同一窗口才能实现数据对话。

基于Java的网络技术

TCP/IP套接字

套接字是网络软件中的一个抽象概念,套接字允许单个计算机同时服务于很多不同客户,并能够提供不同类型信息的服务。该技术由引入的端口处理,该端口既是一个特定机器上的一个被编号的套接字---通信端口.TCP/IP套接字用于在主机和Internet之间建立的可靠、双向、点对点、持续的流式连接。

在java中,TCP/IP Socket连接是用java.net包中的类实现的,这些类实现了建立网络连接和通过连接发送数据的复杂过程,我们只需使用其简单接口就能实现网络通信!在java中有两类套接字,一种是服务器端套接字--java.net.ServerSocket,另一种是客户端套接字--java.net.Socket。

ServerSocket

其中,ServerSocket被设计成在等待客户建立连接之前不做任何事情的监听器,构造方法的版本如下:

public ServerSocekt(int port) throws IOException

--在服务器指定端口port创建队列长度为50的服务器套接字,当port为0则代表创建一个基于任意可用端口的服务器套接字。队列长度告诉系统多少与之连接的客户在系统拒绝连接之前可以挂起。

public ServerSocekt(int port, int maxQueue) throws IOException

--在指定端口创建指定队列长度的服务器套接字

public ServerSocket(int port, int maxQueue, InetAddress bindAddr ) throws IOException

在多地址主机上,我们除了可以指定端口之外,还可以通过InetAddress类来指定该套接字约束的IP地址。InetAddress在后面将学习。

ServerSocket还定义了以下一些常用的方法:

public Socket accept() throws IOException

--该方法用于告诉服务器不停地等待,直到有客户端连接到该ServerSocket指定的端口,一旦有客户端通过网络向该端口发送正确的连接请求,该方法就会返回一个表示服务器与客户端连接已建立的Socket对象,接下来我们就可以通过这个返回的Socket对象实现服务器与指定客户端的通信。注意:accept()方法会让服务器中的当前线程暂停下来,直到有客户端的正确连接发送过来。

public void bind(SocketAddress endpoint) throws IOException

--绑定该ServerSocket到指定的endpoint地址(IP地址和端口)

public void close() throws IOException

--关闭当前ServerSocket。任何当前被锁定的线程将在accept()方法中抛出IOException。

从jdk1.4开始,java提供了关于ServerSocket的ServerSocketChannel,jdk建议用管道来实现客户端连接的监听以及关闭服务器套接字会更安全,因此,现在我们应该通过ServerSocket来得到其套接字管道,通过管道来实现服务监听以及关闭,可以通过ServerSocket的getChannel()方法来得到当前ServerSocket的相关管道。

Socket

该类为建立连向服务器套接字及启动协议交换而设计,当进程通过网络进行通信的时候,java技术使用流模型来实现数据的通信。一个Socket包括两个流,分别为一个输入流和一个输出流,一个进程如果要通过网络向另一个进程发送数据,只需简单地写入与Socket相关联的输出流,同样,一个进程通过从与Socket相关联的输入流来读取另一个进程所写的数据。如果通过TCP/IP协议建立连接,则服务器必须运行一个单独的进程来等待连接,而某一客户机必须试图到达服务器,就好比某人打电话,必须保证另一方等待电话呼叫,这样才能实现两人之间的通信。

分析以下代码:

import java.io.*;

import java.net.*;

......

try{

// 在服务器的8000端口创建一个ServerSocket

ServerSocket server = new ServerSocket(8000);

// accept()方法使当前服务器线程暂停,之前创建的server套接字

//将通过该方法不停的监听指定端口是否有客户端请求连接发送过来,

//如果有正确连接发送过来,

// 则该方法返回一个表示连接已建立的Socket对象

Scoket fromSocket = server.accept();

if (fromSocket != null){

// 通过套接字实现数据传输,得到套接字的输入流输出流

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

}

}catch(IOException e){

e.printStackTrace();

}

......

通过上面的代码,我们分析得知,套接字只是实现数据传输的接口,真正实现数据传输的是封装在套接字中的输入、输出流。以上代码是服务器端程序,该程序开启了一个端口号为8000的特定端口,并且开启了服务器套接字在该端口上监听客户请求,accept()方法阻塞当前线程直到有客户端请求发送过来,当请求正确时,该方法将客户端套接字引用传递出来,这样,网络之间的数据发送就好像是本地数据调用。

Socket类的相关方法:

Socket()创建一个无参数套接字,该套接字会随即取一个可用端口、可用IP来建立连接。

Socket(String host, int port)创建一个指定远程服务器、端口号的套接字

Socket(InetAddress net, int port)创建一个指定InetAddress类封装IP、指定端口号的套接字。即该套接字只能往指定IP的服务器以及服务器指定端口发送数据。

public OutputStream getOutputStream() throws IOException 得到套接字的输出流,接下来就可以使用得到的输出流去写数据至服务器或客户端

public InputStream getInputStream() throws IOException得到套接字的输入流,接下来就可以使用得到的输入流去读取来自于服务器或客户端的数据。

public SocketChannel getChannel()得到套接字的管道,在1.4之后新增了io功能,在java.nio包中定义,通过流中的SocketChannel来实现数据的读取会比直接使用流来读取更安全可靠。(扩展内容)

public void close() throws IOException关闭当前套接字,注意,关闭套接字的同时也会关闭该套接字中的所有流。

下面是客户端的部分代码:

import java.io.*;

import java.net.*;

......

try{

// 创建套接字,该套接字连接IP地址为192.168.0.2的服务器,端口为8000 Socket server = new Socket("192.168.0.2",8000);

if(server != null){

// 通过套接字实现数据传输输入流

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

}

}catch(IOException e){

e.printStackTrace();

}

以上即是简单的基于Tcp/IP协议、使用套接字实现数据传输的服务器和客户端代码(注意:在运行时,先执行服务器端代码,接着执行客户端代码),但是我们发现,通过以上代码,客户端和服务器端只能进行一次数据对话,通过更改以上代码,我们才能实现一个客户端与服务器端的真正对话,直到某一方终止通话为止,修改后代码如下:

服务器端:

......

ServerSocket server = new ServerSocket(8000);

while (true){

Socket fromSocket = server.accept();

if (fromSocket != null){

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

.....

}

}

.....

客户端:

......

Socket server = new Socket("192.168.0.2",8000);

InputStream input = fromSocket.getInputStream();

OutputStream out = fromSocket.getOutputStream();

while (true){

// 数据的读写操作

......

}

......

实际上,我们只要将服务器端每一个客户的请求套接字的获取以及流的获取、数据的读写放入至一个无限循环,这样就可以保证服务器可以随时接受任意客户发送过来的套接字,从而实现对话。客户端也可以通过一个无限循环实现对服务器端的任意时刻的数据传输。

当然,这只是实现了一个客户和服务器之间的对话,但是服务器一般是对应许多客户的,因此,为了实现服务器对应多个客户,必须将每一次accept()方法返回的每一个客户套接字保存起来,这样才可以实现服务器与多个客户对应,具体代码在后面讨论。

注意,数据的读写应该是在一个独立于主线程的单独线程中运行,因为accept方法会阻塞当前线程,为了不影响主线程的其余功能,我们应该启用多线程来实现高效的数据传输。

当通过代码实现服务器与客户之间的套接字发送之后,我们就可以使用流的IO操作来实现服务器与客户端之间的基于特定协议的远程数据传输。注意,基于TCP/IP协议的数据传输,前提是必须得保证接受数据的一方是连通的,也就是说,如果服务器端没有运行,那么客户端是无法对其发送信息,反之亦然。那有时候我们只在乎信息的发送,并不理会接受的人是否连通,甚至不理会接受人是否存在,那就不能使用TCP/IP协议,而得通过我们下面所学的UDP协议来实现。

UDP套接字

UDP(User Datagrams Protocol)用户数据报协议,是一种使用数据报的机制来传递信息的协议。数据报(Datagrams)是一种在不同机器之间传递的信息包,该信息包一旦从某一机器被发送给指定目标,那么该发送过程并不会保证数据一定到达目的地,甚至不保证目的地的存在真实性。反之,数据报被接受时,不保证数据没有受损,也不保证发送该数据报的机器仍在等待响应。

由此可见,UDP协议是一种基于数据报的快速的(因为它无需花时间去保证数据是否损坏,无需花时间确定接受方是否存在并等待响应)、无连接的、不可靠的信息包传输协议。

在java中,通过两个特定类来实现UDP协议顶层数据报,分别是DatagramPacket和DatagramSocket,其中DatagramPacket是一个数据容器,用来保存即将要传输的数据;而DatagramSocket实现了发送和接收DatagramPacket的机制,即实现了数据报的通信方式。 1 、atagramPacket

该类主要有四个常用构造方法,分别如下:

DatagramPacket(byte[] buff, int length)

DatagramPacket(byte[] buf, int offset, int length)

DatagramPacket(byte[] buf, int length, InetAddress address, int port)

DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)

第一个构造方法用于创建一个指定数据缓冲区大小和信息包的容量大小的DatagramPacket,第二个构造方法用于创建一个长度大小为length的缓冲区,并指定数据存储(读取)的偏移地址为offset的DatagramPacket。第三个创建一个指定缓冲区大小、传送(接受)IP地址、端口号的DatagramPacket。一般情况下,发送地址是由DatagramSocket指定。

常用的几个方法如下:

byte[] getData()

用于得到发送过来的DatagramPacket中的数据

void setData(byte[] buf)

用于将buf中的数据写入DatagramPacket中,以备发送。

InetAddress getAddress()

返回目标的InetAddress,一般用于发送。

DatagramSocket

DatagramSocket()

创建一个以当前计算机的任意可用端口为发送端口的数据报连接

DatagramSocket(int port)

创建一个以当前计算机的port端口为发送端口的数据报连接

DatagramScoket(int port, InetAddress address)

创建一个以当前计算机的port端口为发送端口,向IP地址为address的计算机发送数据报连接

常用的几个方法:

void close() throws IOException

关闭数据报连接

void recieve(DatagramPacket packet)

接收来自于packet数据报的信息

void send(DatagramPacket packet)

发送packet数据报

void connect(InetAddress address, int port)

以当前计算机指定端口port为发送端口,向IP地址为address的计算机发送数据报连接

void disconnect()

断开连接

DatagramChannel getChannel()

和SocketChannel类似

结合以上两个类的方法,创建一个简单的UDP服务器端如下:

....

// 指定连接端口

DatagramSocket socket = new DatagramSocket(8001)

byte[] buff = new byte[256];

// 指定接受数据报缓冲区大小为字节数组buff大小

DatagramPacket fromPacket = new DatagramPacket(buff,buf.length);

// 接受客户端请求,并将请求数据存储在数据报packet中

packet.recieve(fromPacket);

// 读取数据报中数据

byte rec = packet.getData();

.....

buff = "hello world".toBytes();

InetAddress addr = .....

// 指定发送数据报缓冲区大小为字节数组buff大小,

//发送目的地为addr,端口为8001,数据报内容为数组buff内容

DatagramPacket toPacket = new DatagramPacket(buff,buf.length,addr,8001);

// 发送服务器端数据报toPacket

packet.send(toPacket);

.......

客户端代码与服务器段类似,只要注意接受与发送应该分别是两个不同的数据报。

InetAddress类的使用

java.net.InetAddress类是java的IP地址封装类,内部隐藏了IP地址,可以通过它很容易的使用主机名以及IP地址。一般共各种网络类使用。直接由Object类派生并实现了序列化接口。该类用两个字段表示一个地址:hostName与address。hostName包含主机名,address包含IP地址。InetAddress支持ipv4与ipv6地址。

一些常用方法如下:

byte[] getAddress()

返回指定对象的IP地址的以网络字节为顺序的4个元素的字节数组

static InetAddress getByName(String hostname)

使用DNS查找hostname的IP地址,并返回

static InetAddress getLocalHost()

返回本地计算机的InetAddress。

String getHostName()

返回指定InetAddress对象的主机名。

String getHostAddress()

返回指定InetAddress对象的主机地址的字符串形式

分析:

InetAddress addr = InetAddress.getByName("java.sun.com");

System.out.println(addr);

以上代码将打印网址域名为java.sun.com的对应IP地址

因此,在网络编程中,我们可以很方便的使用InetAddress类实现Ip地址的各种操作。

例题1:服务器端:

package com.itjob.net;

import java.awt.*;

import javax.swing.*;

import java.awt.event.*;

import java.io.*;

import javax.swing.border.*;

import java.net.*;

public class ServerApp extends JFrame implements Runnable, ActionListener {

JPanel mainPanel;

JPanel bottomPanel;

GridBagLayout gbl;

GridBagConstraints gbc;

Border border;

JTextArea txtChatMess;

JScrollPane scroll;

JTextField txtMess;

JLabel lblEnterMess;

JButton cmdSend;

JButton cmdReset;

ServerSocket server = null;

Socket socket = null;

Scanner read = null;

public ServerApp() {

super("服务器");

mainPanel = new JPanel();

bottomPanel = new JPanel();

mainPanel.setLayout(new BorderLayout());

mainPanel.add(bottomPanel, BorderLayout.SOUTH);

gbl = new GridBagLayout();

gbc = new GridBagConstraints();

bottomPanel.setLayout(gbl);

txtChatMess = new JTextArea(15, 20);

txtChatMess.setEditable(false);

scroll = new JScrollPane(txtChatMess);

mainPanel.add(scroll);

border = BorderFactory.createRaisedBevelBorder();

txtMess = new JTextField(15);

lblEnterMess = new JLabel("请输入消息: ");

cmdSend = new JButton("发 送");

cmdSend.setPreferredSize(new Dimension(50, 20));

cmdSend.setEnabled(false);

cmdSend.addActionListener(this);

cmdReset = new JButton("清 空");

cmdReset.setPreferredSize(new Dimension(50, 20));

cmdReset.addActionListener(this);

cmdSend.setBorder(border);

cmdReset.setBorder(border);

gbc.gridx = 3;

gbc.gridy = 10;

gbl.setConstraints(lblEnterMess, gbc);

bottomPanel.add(lblEnterMess);

gbc.gridx = 10;

//gbc.fill = gbc.BOTH;

gbl.setConstraints(txtMess, gbc);

bottomPanel.add(txtMess);

gbc.gridx = 3;

gbc.gridy = 30;

gbl.setConstraints(cmdSend, gbc);

bottomPanel.add(cmdSend);

gbc.gridx = 10;

gbl.setConstraints(cmdReset, gbc);

bottomPanel.add(cmdReset);

getContentPane().add(mainPanel);

pack();

setVisible(true);

}

public void actionPerformed(ActionEvent evt) {

if (evt.getSource() == cmdSend) {

try {

write.println(txtMess.getText());

write.flush();

} catch (Exception e) {

e.printStackTrace();

}

}

if (evt.getSource() == cmdReset) {

txtMess.setText("");

}

}

public void run() {

try {

server = new ServerSocket(2005);

//反复接收客户端请求

while (true) {

socket = server.accept();

if (socket != null) {

txtChatMess.append("服务器消息:客户已连接!" + "\n");

cmdSend.setEnabled(true);

} else {

txtChatMess.append("服务器消息:客户未能连接!" + "\n");

cmdSend.setEnabled(false);

}

//为每一个客户启动一个读取数据的单独线程

Connections con = new Connections(socket);

Thread thread = new Thread(con);

thread.start();

}

} catch (Exception e) {

e.printStackTrace();

}

}

class Connections implements Runnable {

Socket clientSock = null;

Connections(Socket s) {

clientSock = s;

}

public void run() {

try {

read = new Scanner(socket.getInputStream()));

write = new

PrintWriter(socket.getOutputStream());

String result = read.readLine();

if (result == null){

return;

}

while(!result.trim().equals("Exit!")) {

txtChatMess.append("客户端消息: " + result + "\n");

if (read.hasNextLine()){

result = read.nextLine();

}else{

Thread.sleep(100);

}

}

}catch(Exception e){

e.printStackTrace();

}finally{

try {

read.close();

write.close();

socket.close();

server.close();

cmdSend.setEnabled(false);

}catch(Exception e){

}

}

}

}

public static void main(String[] args) {

ServerApp serverApp = new ServerApp();

Thread thread = new Thread(serverApp);

thread.start();

}

}

客户端:

package com.itjob.net;

import java.awt.*;

import javax.swing.*;

import java.awt.event.*;

import java.io.*;

import javax.swing.border.*;

import java.net.*;

public class ClientApp extends JFrame implements Runnable, ActionListener {

JPanel mainPanel;

JPanel bottomPanel;

GridBagLayout gbl;

GridBagConstraints gbc;

Border border;

JTextArea txtChatMess;

JScrollPane scroll;

JTextField txtMess;

JLabel lblEnterMess;

JButton cmdSend;

JButton cmdReset;

ServerSocket server = null;

Socket socket = null;

Scanner read = null;

PrintWriter write = null;

public ClientApp() {

super("客户端");

mainPanel = new JPanel();

bottomPanel = new JPanel();

mainPanel.setLayout(new BorderLayout());

mainPanel.add(bottomPanel, BorderLayout.SOUTH);

gbl = new GridBagLayout();

gbc = new GridBagConstraints();

bottomPanel.setLayout(gbl);

//new GBC();

txtChatMess = new JTextArea(15, 20);

txtChatMess.setEditable(false);

scroll = new JScrollPane(txtChatMess);

mainPanel.add(scroll);

border = BorderFactory.createRaisedBevelBorder();

txtMess = new JTextField(15);

lblEnterMess = new JLabel("请输入消息: ");

cmdSend = new JButton("发 送");

cmdSend.setPreferredSize(new Dimension(50, 20));

cmdSend.setEnabled(false);

cmdSend.addActionListener(this);

cmdReset = new JButton("清 空");

cmdReset.setPreferredSize(new Dimension(50, 20));

cmdReset.addActionListener(this);

cmdSend.setBorder(border);

cmdReset.setBorder(border);

gbc.gridx = 3;

gbc.gridy = 10;

gbl.setConstraints(lblEnterMess, gbc);

bottomPanel.add(lblEnterMess);

gbc.gridx = 10;

//gbc.fill = gbc.BOTH;

gbl.setConstraints(txtMess, gbc);

bottomPanel.add(txtMess);

gbc.gridx = 3;

gbc.gridy = 30;

gbl.setConstraints(cmdSend, gbc);

bottomPanel.add(cmdSend);

gbc.gridx = 10;

gbl.setConstraints(cmdReset, gbc);

bottomPanel.add(cmdReset);

getContentPane().add(mainPanel);

pack();

setVisible(true);

}

public void actionPerformed(ActionEvent evt) {

if (evt.getSource() == cmdSend) {

try {

write.println(txtMess.getText());

write.flush();

} catch (Exception e) {

e.printStackTrace();

}

}

if (evt.getSource() == cmdReset) {

txtMess.setText("");

}

}

public void run() {

try {

InetAddress add = InetAddress.getLocalHost();

socket = new Socket(add, 2005);

read = new Scanner(socket.getInputStream());

write = new PrintWriter(socket.getOutputStream());

if (socket != null) {

txtChatMess.append("客户消息:服务器已连接!" + "\n");

cmdSend.setEnabled(true);

} else {

txtChatMess.append("客户消息:服务器未能连接!" + "\n");

cmdSend.setEnabled(false);

}

} catch (Exception e) {

e.printStackTrace();

}

Connections con = new Connections();

Thread thread = new Thread(con);

thread.start();

}

class Connections implements Runnable {

public void run() {

try {

String result = read.readLine();

if (result == null) {

return;

}

while (!result.trim().equals("Exit!")) {

txtChatMess.append("服务器端消息: " + result + "\n");

if (read.hasNextLine()) {

result = read.nextLine();

} else {

Thread.sleep(100);

}

}

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

read.close();

write.close();

socket.close();

server.close();

//cmdSend.setEnabled(false);

} catch (Exception e) {

}

}

}

}

public static void main(String[] args) {

ClientApp client = new ClientApp();

Thread thread = new Thread(client);

thread.start();

}

}

注意:以上代码是基于客户端与服务器在同一台服务器上的,而且服务器不能实现对应多个客户,请大家思考并修改,从而实现一个服务器对应多个客户端。

扩展知识:

套接字超时

在使用套接字的时候,读取数据之前当前线程会被阻塞,直到数据的到达为止,在这个过程中间可能由于被访问的主机不可达,从而使你的应用将等待很长的时间,最终受操作系统的底层影响而导致超时产生。也就是说,虽然最终会导致超时的产生,但我们在设计一个套接字的时候应该给定一个合理的超时时间,从而保证在读、写操作未完成之前可以有一个限定时间,当超出该限定时间时操作仍未完成,那么套接字就会抛出SocketTimeoutException异常,这样,我们就可以捕获该异常并编写超时后的程序逻辑。这样,通过设置套接字的超时时间(对套接字内所有的操作而言)就可以保证程序因为某些原因而导致程序长时间的无作用等待。

通过调用套接字的setSoTimeout(int milliseconds)方法来设置套接字的超时时间,比如:

Socket s = new Socket(...);

s.setSoTimeout(10000);

上面方法将设置套接字的超时时间为10秒,当10秒内数据仍未读取时,套接字将抛出异常并停止操作。注意,setSoTimeout方法会抛出SocketTimeoutException异常,并且实现了线程同步。

在上面我们所说的是当操作被阻塞的情况下,还有一种情况也会导致程序长期阻塞直至建立服务器的连接为止。回顾一下套接字的构造方法,其中一构造方法如下:Socket(String host, int port),如果该构造方法在连接制定的host由于某些特殊原因(网络)导致阻塞,那么该套接字将无限阻塞,直到建立和主机的连接为止。为了避免这种可能,我们还应该设置套接字的连接超时,当在指定时间内仍不能建立和主机的连接,则抛出异常并终止操作。解决步骤如下:

//先建立一个无连接套接字

Socket s = new Socket();

//在调用connect方法去指定套接字连接主机时给定连接超时参数,这里超时时间设置为10秒

s.connect(new InetSocketAddress(host,port),10000);

(InetSocketAddress类是一个专门封装套接字地址的特定类,可以通过该类将一个Scoket与指定主机及端口连接)

SocketChannel类

不知大家有没有发现,虽然给线程设置中断标志可以终止一个被阻塞线程,但是当线程是被套接字等网络因素而阻塞的情况下,设置中断标志是无法终止阻塞的。在1.4之后提供的java.nio.channels包中SocketChannel类则很好的解决了这个问题,即如果我们是通过该类来创建一个套接字连接,那么就可以使用线程的中断标志来终止被阻塞的线程,具体操作如下:

//首先,我们应该获取套接字连接对象,通过SocketChannel类的静态方法open可以得到一个指定主机连接对象

SocketChannel channel = SocketChannel.open(new InetSocketAddress("192.168.0.1",8001));

//对SocketChannel的对象进行的操作和对Socket的操作类似,可以通过Channels类的静态方法newOutputStream()于newInputStream()来分别得到通道中的输出和输入流

OutputStream out = Channels.newOutputStream(channel);

InputStream input = Channels.newInputStream(channel);

//或者通过Scanner来得到输入流

Scanner scan = new Scanner(channel);

注意,通过Scanner来得到输入流于通过Channels.newInputStream(channel)得到的输入流有所不同,实际上channel并没有与之关联的流,其所拥有的read和write方法都是通过Buffer对象来实现的。也就是说,Channels类的newInputStream方法得到的流实际上是通过Buffer来读取数据,而Scanner则是直接去读数据而不使用缓存。因此,读取大数据时使用缓冲可能更高效,否则反之。

SocketChannel实现了两个接口:ReadableByteChannel和WritableByteChannel,分别实现了read与write方法,这两个方法均接收ByteBuffer(java.nio)对象并对其操作,即使用SocketChannel的同时我们需结合nio包中的字节缓冲对象共同实现数据的读写操作。在使用SocketChannel时,假设线程正在执行打开、读取、写入操作,此时如果对线程进行中断,那么这些操作不会导致线程阻塞而是以抛出异常的方式结束。

内容总结

? IP(Internet Protocol)协议是一种低级路由协议,该协议主要实现将传输数据分解成许多小包,接着通过网络传到一个指定地址,但是,请注意,IP协议并不会保证传输的数据包一定到达目的地!TCP(Thransfer Control Protocol)协议正好弥补了IP协议的不足,属于一种较高级的协议,它实现了数据包的有力捆绑,通过排序和重传来确保数据传输的可靠!

? TCP与UDP协议均属于传输层协议,而IP协议属于网络层协议

? TCP/IP套接字用于在主机和Internet之间建立的可靠、双向、点对点、持续的流式连接

? UDP协议是一种基于数据报的快速的(因为它无需花时间去保证数据是否损坏,无需花时间确定接受方是否存在并等待响应)、无连接的、不可靠的信息包传输协议。

? ServerSocket被设计成在等待客户建立连接之前不做任何事情的监听器,Socket类为建立连向服务器套接字及启动协议交换而设计,当进程通过网络进行通信的时候,java技术使用流模型来实现数据的通信

? 数据报(Datagrams)是一种在不同机器之间传递的信息包,该信息包一旦从某一机器被发送给指定目标,那么该发送过程并不会保证数据一定到达目的地,甚至不保证目的地的存在真实性。

? DatagramPacket是一个数据容器,用来保存即将要传输的数据;DatagramSocket实现了发送和接收DatagramPacket的机制,即实现了数据报的通信方式。

? java.net.InetAddress类是java的IP地址封装类,内部隐藏了IP地址,可以通过它很容易的使用主机名以及IP地址

独立实践:

1、 编写一个应用,该应用应能实现基于HTTP协议的数据传送,分为服务器与客户端(应有对应的简单界面),要求在正常实现数据读写的同时,每一端应能判断服务器(或客户端)的关闭并做出相应的处理。还应能判别正常关闭(关闭窗口或点击退出)与非正常关闭(断电)。实现服务器一对多客户端。

2、 编写一个应用,实现服务器端,该服务器应能处理当前时间并显示,通过telnet协议应能访问服务器并查看当前时间。

3、 通过HTTP协议,写明数据客户端和服务器端数据的传输。

4、 通过UDP协议,写明数据客户端和服务器端数据的传输。

5、 使用多线程,使用HTTP协议或UDP协议,写明多客户端和服务器之间数据的通讯。

第十四章:数据结构与算法(上)

学习目标

? 算法(algorithm)

? 算法的类型

? 查找算法

? 排序算法

? 递归(recursive)

? 阶乘递归(factorial recursive)

? 快速排序

算法(algorithm):

对一个现有的问题我们采取的解决过程及方法,可简单可复杂,可高效可低效。一个用算法实现的程序会耗费两种资源:处理时间和内存。很显然,一个好的算法应该是耗费时间少、所用内存低,但是,在实际中,我们往往不能两方面顾全!

算法的效率分析标准:

衡量算法是否高效主要是从以下几个方面来分析:

? 简单性和清晰度

一般我们都希望算法越简单越清晰就越好,但是要保证效率为前提。可是,往往我们在复杂的项目开发中所遇见的问题比较复杂,对时间和空间效率的要求也较高,因此,算法一般都会比较复杂。

? 空间效率

注意:这里的空间效率并不是指算法代码占用的内存指令空间,而是指代码中的数据分配(变量与变量所引用值的分配)以及方法调用所使用的内存(调用栈的空间分配)。比如,我们常用的递归,虽然会使代码清晰简单,但是内存的使用也会大大提高。理想的,程序所使用的内存应该和数据及方法调用 所占用内存相等。但事实总是会有些额外的开销!因此,空间效率也是我们衡量算法的方面之一。

? 时间效率

针对同一任务所使用的不同算法所执行的时间都会不同。

比如:在一个数据集合中查找数据,我们会从第一个数据开始查找,一直找到需要的数据为止,如果查找数据存在,则这种查找方式(称之为线性查找)一般要查找半个列表!然而,如果数据的排放是有序的,则通过另一种查找方法会更有效,即二分查找法,首先从集合的中间开始,如果查找值在中间值的前面,则从集合的前一半重复查找,否则从后一半查找,每执行一次则将查找的集合减少为前一次的一半。

算法的类型:

所有的算法可以大概分为以下三种类型:

1.贪婪算法(greedy algorithm)

该算法每一步所做的都是当前最紧急、最有利或者最满意的,不会考虑所做的后果,直到完成任务。这种算法的稳定性很差,很容易带来严重后果,但是,如果方向正确,那该算法也是高效的。

2.分治算法(divide-and-conquer algorithm)

该算法就是将一个大问题分解成许多小问题,然后单独处理这些小问题,最终将结果结合起来形成对整个问题的解决方案。当子问题和总问题类型类似时,该算法很有效,递归就属于该算法。

3.回溯算法(backtracking algorithm)

也可以称之排除算法,一种组织好的试错法。某一点,如果有多个选择,则任意选择一个,如果不能解决问题则退回选择另一个,直到找到正确的选择。这种算法的效率很低,除非运气好。比如迷宫就可以使用这种算法来实现。

实际上,我们对算法的效率高低评价,主要是在时间和内存之间权衡。根据实际情况来决定,比如有的客户不在乎耗用的内存是多少,他在乎的是执行的速度,那么一个用内存来换取更高执行时间的算法可能是更好的。同样,有的客户可能不想耗用过多内存同时对速度也不是特别要求。不管怎样,效率是算法的主要特性,因此关注算法的性能尤其重要!标准的测量方法就是找出一个函数(增长率),将执行时间表示为输入大小的函数。选择处理的输入大小来说增长率比较低的算法!

计算增长率的方式:

1.测量执行时间

通过System.currentTimeMillis()方法来测试

部分代码:

// 测量执行时间

static void calculate_time(){

long test_data = 1000000;

long start_time = 0;

long end_time = 0;

int testVar = 0;

for (int i = 1; i <= 5; i++){

// 算法执行前的当前时间

start_time = System.currentTimeMillis();

for(int j = 1; j <= test_data; j++){

testVar++;

testVar--;

}

// 算法执行后的当前时间

end_time = System.currentTimeMillis();

// 打印总共执行时间

System.out.println("test_data = " + test_data + "\n" +

"Time in msec = " + (end_time - start_time) + "ms");

//环后将循环次数加倍

test_data = test_data * 2;

}

}

以上代码将分别计算出1000000、2000000、4000000...次的循环时间。

缺点:

? 不同的平台执行的时间不同

? 有些算法随着输入数据的加大,测试时间会变得不切实际!

2.指令计数

指令---指编写算法的代码.对一个算法的实现代码计算执行指令次数。两种类型指令:不管输入大小,执行次数永远不变;执行次数随着输入大小改变而改变。一般,我们主要测试后一种指令。

例:计算指令执行次数

static void calculate_instruction(){

long test_data = 1000;

int work = 0;

for (int i = 1; i <= 5; i++){

int count = 0;

for (int k = 1; k <= test_data; k++){

for(int j = 1; j <= test_data; j++) {

// 指令执行次数计数

count++;

work++;

work--;

}

}

System.out.println("test_data = " + test_data + "\n" +

"Instr. count = " + count );

test_data = test_data * 2;

}

}

3.代数计算

代码1:

long end_time = 0; t1

int testVar = 0; t2

for (int i = 1; i <= test_data; i++){ t3

testVar++; t4

testVar--; t4

}

假设t1 --- t4分别代表每条语句的执行时间,那么,以上代码的总执行时间为:t1 + t2 + n(t3 + 2t4).其中n = test_data,当test_data增大时,t1和t2可以忽略不计,也就是说,对于很大的n,执行时间可以近似于:n(t3 + 2t4)

4.测量内存使用率

一个算法中包含的对象和引用的数目,越多则内存使用越高,反之越低。

比较增长率:

1.代数比较法

条件1:c≦ f(n)/g(n) ≦ d (其中c和d为正常数,n代表输入大小)

当满足以上条件1时,则f(n)和g(n)具备相同的增长率,或者两函数复杂度的阶相同!

如:f(n) = n + 100 和 g(n) = 0.1n + 10两函数就具备相同的增长率。

条件2: 当n增大时,f(n)/g(n)趋向于0

当满足此条件2时,则该两个增长函数有不同的增长率。

比如:f(n) = 10000n + 20000 和 g(n) = n?2 + n + 1 。请大家比较以上两函数增长率是否一样,如果不一样,谁的增长率小?

2.大O表示法

如果f的增长率小于或者等于g的增长率,则我们可以用如下的大O表示法:

f = O(g)

O表示on the order of

将代码1的代数增长率函数用大O表达式如下:

f(n) = t1 + t2 + n(t3 + 2t4)

= a1*n + a

= O(n)

其中a1 = t3 + 2t4; a = t1 + t2

3.最佳、最差、平均性能

对每一个算法不能只考虑单一的增长率,而应该给出最佳、最差、平均的增长率函数

查找算法:

1.线性查找

从数组的第一个元素开始查找,并将其与查找值比较,如果相等则停止,否则继续下一个元素查找,直到找到匹配值。

注意:要求被查找的数组中的元素是无序的、随机的。

比如,对一个整型数组的线性查找代码:

static boolean linearSearch(int target, int[] array) {

// 遍历整个数组,并分别将每个遍历元素与查找值对比

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

if (array[i] == target){

return true;

}

}

return false;

}

分析该算法的三种情况:

a.最佳情况

要查找的值在数组的第一个位置。也就是说只需比较一次就可达到目的,因此最佳情况的大O表达式为:O(1)

b.最差情况

要查找的值在数组的末尾或者不存在,则对大小为n的数组必须比较n次,大O表达式为:O(n)

c.平均情况

估计会执行:(n + (n - 1) + (n - 2) + ….. + 1)/n = (n + 1) / 2次比较,复杂度为O(n)

2.二分查找

假设被查找数组中的元素是升序排列的,那我们查找值时,首先会直接到数组的中间位置(数组长度/2),并将中间值和查找值比较,如果相等则返回,否则,如果当前元素值小于查找值,则继续在数组的后面一半查找(重复上面过程);如果当前元素值大于查找值,则继续在数组的前面一半查找,直到找到目标值或者无法再二分数组时停止。

注意:二分查找只是针对有序排列的各种数组或集合

代码:

static boolean binarySearch(int target, int[] array){

int front = 0;

int tail = array.length - 1;

// 判断子数组是否能再次二分

while (front <= tail){

// 获取子数组的中间位置,并依据此中间位置进行二分

int middle = (front + tail) / 2;

if (array[middle] == target){

return true;

}

else if (array[middle] > target){

tail = middle - 1;

}

else{

front = middle + 1;

}

}

return false;

}

最佳情况:

中间值为查找值,只需比较一次,复杂度为O(1)

最差、平均:

当我们对一个数组执行二分查找时,最多的查找次数是满足n < 2^k的最小整数k,比如:当数组长度为20时,那么使用二分法的查找次数最多为5次,即:2^5 > 20因此可以得出二分法的最差及平均情况的复杂度为O(logn)。

分析:1,2,3,4,5,6,7,8,9

在上面数组中查找7需要比较多少次?

查找2.5需要比较多少次?(假设存储的数值都是双精度数据类型)

显然,对于一个有序数组或集合,使用二分查找会比线性查找更加有效!但是注意,虽然二分法效率更高,但使用的同时系统也会增加额外的开销,为什么?

排序算法:

1.选择排序

首先在数组中查找最小值,如果该值不在第一个位置,那么将其和处在第一个位置的元素交换,然后从第二个位置重复此过程,将剩下元素中最小值交换到第二个位置。当到最后一位时,数组排序结束。

代码:

static void selectionSort(int[] array) {

for (int i = 0; i < array.length - 1; i++){

int min_idx = i;

for (int j = i + 1; j < array.length; j++){

if (array[j] < array[min_idx]){

min_idx = j;

}

}

if (min_idx != i) {

swap(array,min_idx,i);

}

}

}

从上面代码我们可以看出,假设数组大小为n,外循环共执行n-1次;那么第一次执行外循环时,内循环将执行n-1次;第二次执行外循环时内循环将执行n-2次;最后一次执行外循环时内循环将执行1次,因此我们可以通过代数计算方法得出增长函数为:(n - 1) + (n - 2) + (n - 3) + ….. + 1 = n(n - 1) / 2 = 1/2 * n^2 + 1/2 * n,即可得出复杂度为:O(n^2)。我们可以分析得知,当数组非常大时,用于元素交换的开销也相当大。这都属于额外开销,是呈线性增长的。注意:如果是对存储对象的集合进行排序,则存储对象必须实现Comparable接口,并通过compareTo()方法来比较大小。

2.冒泡排序

冒泡排序法是运用数据值比较后,依判断规则对数据位置进行交换,以达到排序的目的。具体算法是将相邻的两个数据加以比较,若左边的值大于右边的值,则将此两个值互相交换;若左边的值小于等于右边的值,则此两个值的位置不变。右边的值继续和下一个值做比较,重复此操作,直到比较到最后一个值。此方法在每比较一趟就会以交换位置的方式将该趟的最大者移向数组的尾端,就像气泡从水底浮向水面一样,到水面时气泡最大,故称为冒泡排序法。

冒泡和选择的复杂度很相似,对于大小为n的数组,对于最佳、最差还是平均,冒泡的复杂度都是O(n^2)。注意:冒泡的最差情况是高于线性的

大家可以发现,冒泡的效率是比较低的,因为它不论是那种情况复杂度都是O(n^2),但是我们可以改进一下,来实现当冒泡处于最佳情况时只会执行一次外循环,即实现线性。我们可以推断,如果执行一次外循环,结果并没有发生元素交换(调用swap()),那么我们就能判定该数组是已经排序好的,而通过上面的冒泡程序得知,不管是否已经排序,外循环会执行n-1次,而最佳情况就是发生在第一次外循环,因此,我们可以改良以上程序,通过使用一个布尔型的值来记录是否有元素交换的状态,是就为true,否就为false,如果内循环没有交换元素(没有改变布尔值),那么直接返回。

改后代码:

public void bubbleSort(int[] array) {// 冒泡排序算法

int out, in;

// 外循环记录冒泡次数

for (out = nElems - 1; out > 1; out--) {

boolean flag = false;

// 进行冒泡

for (in = 0; in < out; in++) {

// 交换数据

if (array[in] > array[in + 1]){

swap(in, in + 1);

flag=true;

}

}

if(!flag){break;}

}

} // end bubbleSort()

private void swap(int one, int two) {// 交换数据

int temp = array[one];

array[one] = array[two];

array[two] = temp;

}

注意:以上改良程序只会提高最佳情况的性能,而对于平均和最差来说,复杂度还是O(n^2)。该改良程序适合于对已经排序好的数组或者只需稍微重排的数组,比选择排序性能更好。

3.插入排序

插入排序是对于欲排序的元素以插入的方式寻找该元素的适当位置,以达到排序的目的。插入排序法的优点是利用一个一个元素的插入比较,将元素放入适当的位置,所以是一种很简单排序方式。但因每次元素插入都必须与之前已排序好的元素做比较,故需花费较长的排序时间。

步骤如下:(假设数组长度为n)

a.对数组的每次(第i次)循环,下标值为i的元素应该插入到数组的前i个元素的正确位置(如果是升序,则i元素应插入到小于它的元素之后,大于它的元素之前,降序则反之)

b.每次循环(第i次)结束时,应保证前i个元素排序是正确的!

c.包含两个循环,外循环(循环变量为i)遍历下标从1到n-1的元素,保存每次循环的所遍历的元素的值,内循环(循环变量为k)从i -1开始,即遍历前将k赋值为i-1,每次k--,直到k < 0。在内循环中,将第i个元素和该元素之前的所有元素一一对比,并将元素插入到合适的位置,如果第i个元素的位置是正确的,那么就跳出内循环,重新开始外循环。

代码:

public void insertSort(){// 插入排序算法

int in, out;

for (out = 1; out < nElems; out++){// 外循环是给每个数据循环

int temp = array[out]; // 先取出来保存到临时变量里

in = out; // in是记录插入数据之前的每个数据下标

// while内循环是找插入数据的位置,并且把该位置之后的数据(包括该位置)

// 依次往后顺移。

while (in > 0 && array[in - 1] >= temp) {

array[in] = array[in - 1]; // 往后顺移

--in; // 继续往前搜索

}

array[in] = temp; // 该数据要插入的位置

} // end for

} // end insertionSort()

分析:内循环在第一次外循环时执行1次,第二次外循环时执行2次,。。。。第n - 1次外循环时执行n - 1次,因此,插入排序的最差和平均情况的性能是O(n^2)。但是,在最佳情况下(即数组中的元素顺序是完全正确的),插入排序的性能是线性的。注意:插入排序适合针对于已排序元素多的数组,即数组中已排序的元素越多,插入排序的性能就越好。

递归(recursive):

定义函数1:sum(1) = 1

定义函数2:sum(n) = n + sum(n - 1)

假设n = 5,那么sum(5) = 5 + sum(4)

=5 + 4 + sum(3)

=5 + 4 + 3 + sum(2)

=5 + 4 + 3 + 2 + sum(1)

=5 + 4 + 3 + 2 + 1

以上这种在自身中使用自身定义函数的过程称之递归。

阶乘递归(factorial recursive):

阶乘!4 = 4 * 3 * 2 * 1

可以用递归来表示为:

factorial(1) = 1

factorial(n) = n * factorial(n - 1)

其中n>1。

斐波纳契递归(fibonacci recursive)

1,1,2,3,5,8,13,21,34,55,89,144…………

斐波纳契数列的第一个和第二个数字都定义为1,后面的每个数字都为前两个数之和。

用递归表示为:

fibonacci(1) = fibonacci(2) = 1

fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)

其中n>2。

实现递归必须满足两个条件:

1.基本条件(base case)的成立

实际上就是定义递归应该什么时候终止,比如在上面两个例子中,factorial(1) = 1和fibonacci(1) = fibonacci(2) = 1就是递归的基本条件,一旦当递归执行到满足基本条件时就是结束递归。

2.递归步骤

对于所有的n值,函数都是以其自身通过n值较小的函数来定义,也就是说,所有n值函数通过许多步骤来达到最终用n值较小的函数(基本条件)来定义。可以得知,递归函数就是反复执行递归步,直到n达到基本条件为止。

factorial的递归代码实现:

factorial:

static int factorial(int n) {

// 基本条件

1 if (n <= 1){

2 return 1;

}else{

// 递归步,执行n-1次

3 return n * factorial(n - 1);

}

}

分析:

语句3将被执行n-1次,一直到n<=1时结束递归。

假设n=4,那么我们可以得知:

第一步:调用factorial(4)

第二步:调用4 * factorial(3) n = 4

第三步:调用3* factorial(2) n = 3

第四步:调用2 * factorial(1) n = 2

第五步:返回1给factorial(2)

第六步:返回2给factorial(3)

第七步:返回6给factorial(4)

第八步:返回值24。

fibonacci:

static int fibonacci(int n){

// 基本条件

if (n <= 2){

return 1;

}else{

// 递归步,执行n-2次

return fibonacci(n-1) + fibonacci(n-2);

}

}

分析:可以用递归调用树来描述。每次都会两次调用到自己。

编写递归应注意事项:

----避免死循环,一般都是由于没有基本条件而造成,也可能是递归步的逻辑错误导致。

递归方法的运行实现原理:

我们发现,递归就是一个不停调用方法自身的一个过程,即方法的反复调用!

计算机是通过栈的机制来实现方法调用的。首先,我们来了解下计算机的方法调用机制:

1.程序执行前,计算机会在内存中创建一个调用栈 ,一般会比较大

2.当调用某个方法时,会有一个和该被调用方法相关的记录信息被推入到栈中

3.被推入到栈中的记录信息包括内容:传递到被调用方法中的参数值、该方法的局部变量、该方法的返回值。

4.当返回某个方法或者方法结束时,会从栈中取出对应的方法记录信息

栈的使用机制:后进先出(LIFO)。注意:最然递归方法简洁,但是效率不是完全就比迭代高,有时甚至低。因为我们考虑算法不仅要从时间、增长率来考虑,还要考虑空间(一般指内存)问题,递归的栈空间是我们必须考虑的,因为每次方法的调用都需额外的空间来存储相关信息。

递归和查找:

在前面,我们已使用迭代来实现了线性和二分查找,同样,这两种算法也能用递归来实现,但是线性查找用递归来实现并没任何优势,因此一般线性不采用递归。

线性查找的递归实现:

static boolean linearSearch(int[] array, int target, int pos){

if (pos >= array.length){

return false;

}

else if (array[pos] == target){

return true;

}else{

return linearSearch(array,target,pos + 1);

}

}

注意:pos = 0

二分查找的递归实现:

static boolean binarySearch(int[] array, int target, int front, int tail){

if (front > tail) {

return false;

}else{

int middle = (front + tail) / 2;

if (array[middle] == target){

return true;

}

else if (array[middle] < target){

return binarySearch(array,target,middle + 1,tail);

}else{

return binarySearch(array,target,front,middle - 1);

}

}

}

二分查找的递归方法在平均和最差情况下的复杂度和用迭代实现二分的一样,都是O(logn)。其空间复杂度也为O(nlogn)。所以,用递归实现的二分查找也是可行的。

递归和迭代的选择:

在我们选择一个算法来实现一个方法时,我们应该对多个理由进行对比,高效+简洁+易维护就是最好的。一般,递归由于方法调用的时间和空间的开销,往往比相应的非递归方法效率低,但有时递归的精确和简洁有时称为用户首选。

尾递归:

递归调用之后算法不用再做任何工作,那这个算法就是尾递归。尾递归的主要特点就是编译器可将尾递归代码转换成机器语言形式的循环,从而不会产生与递归有关的开销。

例:阶乘的尾递归实现

static int factIter(int n, int result){

if (n <= 1){

return result;

}else{

return factIter(n-1, n*result);

}

}

递归和排序:

前面的几种排序的复杂度都是O(n^2),效率不高,这里我们要学习两种通过分治算法(递归)来实现的比较高效的排序方法,分别是快速排序和归并排序(扩展内容)。注意:效率越高其算法的编写复杂性自然也会提高

快速排序:

快速排序的思想其实就是,先找个参照物(一般以最后一个数据作为参照物)然后用两个变量遍历该数据,左变量从左边找第一个比参照物大的数据,右变量从右边开始找第一个比参照物小的数据,然后交换这两个数据。然后左变量继续往右边找一个比参照物大的数据,右变量继续往左找比参照物小的数据,然后再交换这两个数据。依次直到左变量和右变量出现交差为止。然后把左变量所指的值和参照物进行交换。交换完之后,从左变量的位置开始分成两半,然后先从前半部分按照上面的方法进行排序,排完后在从后半部分进行同样的排序,直到排完为止,整个思想就是个递归调用!

原理:

1.首先,寻找数组的中间值,然后将该中间值看成一个枢轴(pivot)

2.接着开始划分数据,将所有小于枢轴的数据项都排放在它的位置之前,所有大于枢轴的数据项都排放在它的位置之后。枢轴(中间值)最终所在位置是由其自身的实际值大小来决定的,如果它是数组最大值,那么它就处于数组的最右侧,否则处于最左侧。

3.上面的划分过程递归应用于所有的根据枢轴划分出的子数组中。每一个子数组必须拥有自己的枢轴。每个子数组由枢轴左、右的数据元素组成。

4.当子数组小到不能在划分的时候(比如数组元素个数小于2),快速排序结束。

分析:通过以上原理可以理解,快速排序是基于二分法的基础上实现的一个复杂而又高效的排序算法,在排序算法中,最为关键的步骤就是枢轴的位置控制、数组的划分。如果我们把划分的原理理解了,那也就基本掌握了快速排序算法。下面我们来重点分析一下划分是怎样实现的:

划分:

第一步:将枢轴与子数组中的最后一个数据项交换。

第二步:建立一个边界(boundary),最初该边界在数组的第一个元素之前。该边界主要是用于区分大于枢轴的数据元素和小于枢轴的数据元素。

第三步:从划分出的子数组的第一个元素开始,逐步扫描整个数组,在扫描过程中,如果遇见小于枢轴的元素,那么就将该元素与边界后的第一个元素交换,同时边界得往前移动一个位置。

第四步:当扫描完整个数组之后,得将枢轴与边界后的第一个元素交换,这时,划分过程完成了。

代码如下:

static int partition(int[] array, int front, int tail){

// 用于保存中间位置

int middle;

// 保存边界位置

int boundary;

// 保存枢轴位置

int pivot;

// 保存临时值,用于值交换

int temp;

// 获取中间值的位置

middle = (front + tail) / 2;

// 得到枢轴

pivot = array[middle];

// 执行第一步,将枢轴与子数组中的最后一个数据项交换

array[middle] = array[tail];

array[tail] = pivot;

// 执行第二步,建立一个边界(boundary),最初该边界在数组的第一个元素之前

boundary = front;

// 执行第三步,遍历子数组,并将每个元素和枢轴对比,并改变元素位置

for (int i = front; i < tail; i++) {

// 如果当前元素小于枢轴,则将该元素和边界互换,并将交换后的边界往后移一位

if (array[i] < pivot){

temp = array[boundary];

array[boundary] = array[i];

array[i] = temp;

boundary++;

}

}

// 执行第四步,将枢轴与边界后的第一个元素交换

temp = array[tail];

array[tail] = array[boundary];

array[boundary] = temp;

// 返回边界位置

return boundary;

}

快速排序实现如下:

static void quickSort(int[] array, int front, int tail){

if (front < tail) {

int pivotPosition = partition(array,front,tail);

// 递归实现子数组的划分,每次根据不同边界来划分

// 划分枢轴的左数组

quickSort(array,front,pivotPosition - 1);

// 划分枢轴的右数组

quickSort(array,pivotPosition + 1,tail);

}

}

注意:快速排序在最好情况下(每个枢轴在它的子数组的中间划分之后就会终止,即枢轴为每个子数组的中值)的最大运行时间是O(nlogn),最差情况下(在每个阶段,枢轴恰好是它的子数组的最小数据项-升序)的运行时间是O(n^2),幸好,快速排序的平均情况下的时间是O(nlogn);快速排序的空间需求在平均情况下是O(nlogn),最坏情况下是O(n)。因此,快速排序比插入、选择、冒泡排序的效率都要高,但是,在n值较小的情况下,其它排序方法会比快速更快。高效的排序法应该是结合快速排序和其它排序来实现数组排序。当数组很大时,我们先采用快速排序,但一旦划分出子数组变得很小时(这时数组元素已大部分被排序),我们应该停止递归快速排序,而采用另一种非递归排序算法。

内容总结

? 算法的效率分析标准从三方面:简单清晰、空间、时间。

? 算法增长率的计算:时间、指令执行次数、代数计算

? 每种算法的三种情况分析,最佳、平均、最差。

? 代数表述法与大O表示法

? 查找算法:线性查找、二分查找,排序算法:插入排序、选择排序、冒泡排序、快速排序等。

? 递归的方法调用栈原理

? 尾递归的优化

独立实践

? 实现对员工数据的二分查找、排序(普通排序算法和高级算法各用一种)。

? 编写递归实现Fibonacci和Factorial;

? 编写迭代实现Fibonacci和Factorial;

? 编写测试类,使用该类来测试某个方法执行N次的最大时间,最小时间和平均时间。

第十五章:数据结构与算法(下)

学习目标

? 数据结构介绍

? 数组的实现

? 堆栈和队列的实现

? 单链表的实现

? 双链表的实现

? 二叉树的结构与实现

? 遍历二叉树

数据结构介绍:

软件(software)是计算机系统中与硬件(hardware)相互依存的一部分,它包括程序(program)、相关数据(data)及其说明文档(document)。程序来操作数据,如果数据组织的不好就会造成效率问题,甚至造成程序不能运行。因此数据结构就诞生了。数据结构从概念上讲有数据的逻辑结构和数据的物理结构(也叫存储结构)。数据的逻辑结构就是数据表面上的关系。例如:如果每个数据节点,除了首节点和尾节点外,每个节点都只有一个直接前驱和一个直接后继,那么就是个线性表。该类型常见的结构有:数组,队列,堆栈,链表等。如果除了首节点外,每个节点都只有一个直接前驱和多个直接后继,那么该结构就是一个树型结构。如果每个节点都有多个直接前驱和多个直接后继,那么就是图形结构。数据的物理结构只有两个:一个是顺序存储,一个是链接式存储。下面将使用JAVA语言来实现各个数据结构以及算法。

数组

通过索引(下标)来访问数据元素。索引是一种随机访问操作。随机访问的特点就是不需要从数组的第一个元素开始访问要查找的元素,而是可以直接访问查找的元素,在随机访问中,查找第一个元素和查找最后一个元素所需时间是一样的。数组中的数据是保存在一个地址连续的内存中,它们是紧密相邻的。当查找第i个元素时,计算机是将数组的基地址加上数据的偏移量的值来确定第i个元素的。基地址指第一个元素的地址,偏移量等于第i个元素的索引乘以一个常量,这个常量是数组中一个元素所需的内存单元数目(字节)。

在java和c++中,所有的数组都是动态数组,即数组的大小可以在运行的时候才确定。

比如:

void m1(int i){

int[] arra = new int[i];

}

逻辑大小和物理大小

物理大小指数组的容量大小,而逻辑大小则指数组中实际已存储的数据元素个数。Java中通过数组的length属性来得到数组大小

如果数组的逻辑大小等于物理大小,则代表数组已满;如果数组的逻辑大小为0,则代表数组为空;在数组已满的情况下,数组的最后一个元素的索引(下标)值为逻辑大小减一。

链表

由多个节点(对象)组成,其中每个节点在内存中是散乱存放的,即存放在一个叫做对象堆的不连续地址内存中。其中每个节点(除了尾节点外)都有一个特定的引用下一个节点的变量,从而实现一个完整链表。

链表中的每个元素都称为节点,每个节点包含数据内容和引用后一个节点的变量(通常叫做指针)。

数组中的元素是存储在地址连续的内存中,而链表中的元素则是散乱存放在内存中的。

不能通过索引来访问链表,查找链表中的数据则必须从链表的一端开始沿着链一直移动直到找到查询的数据为止。

由于链表的不连续地址内存机制,在往链表插入和删除数据时,不会象数组那样移动其他数据项,而是在空闲内存中为新的节点对象分配空间,然后将该节点和链表中特定位置的节点链接起来。

链表类型

单链表

单链表节点包含一个数据内容和指向后一个节点的引用。其中,单链表的最后一个节点的指针指向null,第一个节点用一个特殊的头节点来指向。

单链表只能以向后的方式访问链表节点!

单链表的实现及各种操作:

见例题。

双向链表

双向链表节点不但包含单链表节点有的内容之外,还包含一个指向前一个节点的引用,通过向前指针和向后指针来实现双向访问链表节点!

同样,双向链表也有头部和尾部,有两个特殊链表指针,分别指向第一个节点和最后一个节点。

双向链表和单链表的区别在于,双向链表可以前后访问节点!

双向链表的实现和操作:

见例题。

循环链表

最后一个节点的指针指向第一个节点,其余和单链表类似。

实现和操作:

见例题。

三、栈(stack)

一个仅在一端访问的线性集合,这端叫做top(栈顶)。

遵循后进先出的协议(LIFO),即最后加入栈的数据会最先被取出。

push----下推数据到栈

pop----从栈顶取出最后一个被推进来的数据

栈的应用:

? 中缀表达式到后缀表达式的转换,以及对后缀表达式的求值

? 回溯算法

? 方法调用

? 文本编辑器中的撤销功能

? web浏览器的链接历史信息的保留

中缀表达式到后缀表达式的转换:

? 从左至右读取表达式

? 若读取的是操作数,则添加到后缀表达式中

? 若读取的是运算符:

? 如果运算符为"(",则直接推入栈中

? 如果运算符为")",则取出栈中的右运算符,添加到后缀表达式中,直到取出左括号为止。

? 如果运算符为非括号运算符,则与栈顶的运算符做比较,如果比栈顶运算符优先级高或相等,则直接推入栈,否则取出栈中运算符,添加到后缀表达式中。

? 当表达式读取完成,栈中还有运算符时,则依序取出栈中运算符,并分别追到后缀表达式中,直到栈为空。

后缀表达式的求值:

? 从左至右读取表达式:

? 若读取的是操作数,则将其推入栈中

? 若是运算符,则从栈中取出两个操作数进行计算,并将结果推入栈中。

? 重复以上步骤,直到表达式读取完毕。

队列:

线性集合,只允许在表的一端进行插入,即队尾(rear),删除则在另一端,即队头(front)。支持先进先出(FIFO)。

队列应用:

? CPU访问

? 磁盘访问

? 打印机访问

树:

由一个或多个节点组成的有限集合。每一颗树必须有一个特定节点,叫做根节点。根节点下可以有零个以上的子节点。而且各子节点也可以为子树,拥有自己的子节点。

若一棵树中的节点最多可以有n个节点,则称该树为n元树。

? 树的相关名称

? 根节点(root node):树中没有父节点的节点即为根节点

? 叶节点(leaf node):没有子节点的节点

? 非叶节点:叶节点以外的节点

? 父节点(parent)和子节点(child)

? 兄弟节点(sibling):同一个父节点的节点

? 分支度(degree):每个节点所拥有的子节点个数,树中的最大的分支度值即为该树的分支度

? 阶层(level):根节点的阶层为1,其子节点为2,依次类推

? 高度和深度:树的最大阶层值即为树的高度或深度

? 祖先(ancestor):由某子节点到根节点的路径上的所有节点,均称为该节点的祖先

? 二叉树

树的一种,节点最多只能有两个节点

? 由有限个节点组成的集合,集合可以为空

? 根节点下可有两个子树,为左子树和右子树

? 二叉树与树的区别:

? 二叉树可以为空,而树不可以(至少要有根节点)

? 二叉树的子树有顺序关系

? 二叉树的分支度必须为0、1或2,而树的分支度可以大于2

? 二叉树类型:

? 左(右)歪斜树

? 所有节点的左子树均不存在,则此二叉树为右歪斜树

? 反之,则称之为左歪斜树。

? 满二叉树

? 所有叶节点均在同一阶层,其他非叶节点的分支度为2

? 若此树的高度为n,则该树的节点数为2^n - 1.

? 完全二叉树

? 一个二叉树除掉最大阶层后为满二叉树,且最大阶层的节点均向左靠齐。

? 二叉树的节点插入规则:

? 均转换成满二叉树的形式来插入节点数据。

? 对各阶层的节点由低阶层到高阶层,从左至右,由1开始编号,再根据编号将节点数据存入相应索引编号的数组(链表)中

? 如果某编号没有节点存在,则数组对应位置没有值存入。

? 插入的第一个元素为根节点

? 满足左小右大的二叉查找树规则

提问:依次输入数据6,3,8,5,2,9,4,7建立一个二叉树,请描述该二叉树节点的排列次序。

? 二叉树的三种表示法:

数组表示法

见例题。

优点:

查找容易且每个节点占用空间不大

缺点:

当二叉树的深度和节点数的比例偏高时,会造成空间浪费

数据的插入和删除涉及到移动大量数据的问题

节点数组表示法

包含三个数组:

一个数组存放节点数据内容

一个数组存放左子节点在数组中的下标

一个数组存放右子节点在数组中的下标

见例题

改良后的数组表示法,在插入和删除方面需移动的数据大大减少

链表表示法

在修改二叉树方面效率比数组实现高。

? 二叉树的遍历:

前序遍历(preorder traversal)

先遍历中间节点,再遍历左子树,最后遍历右子树

伪码表示:

If 指向根节点的指针 == null

Then 此为空树,遍历结束

Else

(1) 处理当前节点

(2) 往左走,递归处理preorder(root ' left)

(3) 往右走,递归处理preorder(root 'right)

中序遍历(inorder traversal)

先遍历左子树,再遍历中间节点,最后遍历右子树

伪码表示:

If 指向根节点的指针 == null

Then 此为空树,遍历结束

Else

(1)往左走,递归处理preorder(root ' left)

(2)处理当前节点

(3)往右走,递归处理preorder(root 'right)

后序遍历(postorder traversal)

先遍历左子树,再遍历右子树,最后遍历中间节点

伪码表示:

? 二叉树的查找:

先将二叉树转换成二叉查找树,即左小右大,接着可以采用二分查找方式来查找。

对二叉查找树的查找效率高于对非二叉查找树的查找

见例题。

? 二叉树的删除:

见例题。

分为几种情况:

1. 删除节点既无左子树也无右子树

? 根节点

? 非根节点

2. 删除节点有左子树,无右子树

3. 删除节点有右子树,无左子树

4. 删除节点既有左子树,也有右子树

实例分析

数组的实现

数组是常用的数据结构。几乎每种编程语言里面都有该结构。数组的优点是快速的插入数据,如果下标(索引值)知道,可以很快地存取数据。数组的缺点是查找数据慢,删除数据慢,固定大小。

请看下例:

public class ArrayApp {

public static void main(String[] args) {

int[] arr;

arr = new int[100];

int nElems = 0; // 记录元素的个数

int j; // 循环变量

int searchKey; // 要查找的数据

// 插入10个元素

arr[0] = 7;

arr[1] = 2;

arr[2] = 4;

arr[3] = 5;

arr[4] = 9;

arr[5] = 3;

arr[6] = 1;

arr[7] = 0;

arr[8] = 6;

arr[9] = 8;

nElems = 10;

System.out.println("---------遍历-----------");

for (j = 0; j < nElems; j++)

// 打印所有的数据

System.out.print(arr[j] + " ");

System.out.println("");

System.out.println("-------查找5------------");

searchKey = 5;

for (j = 0; j < nElems; j++)

if (arr[j] == searchKey) // 如果找到跳出循环

break;

if (j == nElems) // 如果是通过break跳出循环,则n值不等于nElems

System.out.println("找不到 " + searchKey); // yes

else

System.out.println("找到 " + searchKey); // no

System.out.println("------删除6------------");

searchKey = 6; // 删除 6

for (j = 0; j < nElems; j++)

// 找到6的位置

if (arr[j] == searchKey)

break;

for (int k = j; k < nElems; k++)

// 6位置后面的数据依次往前顺移

arr[k] = arr[k + 1];

nElems--; // 个数减一

System.out.println("删除成功");

System.out.println("-----遍历---------------");

for (j = 0; j < nElems; j++)

// 遍历所有数据

System.out.print(arr[j] + " ");

System.out.println("");

} // end main()

} // end class ArrayApp

上面的代码类似于C语言的编程风格。在面向对象的编程思维里,应该按模块化的设计方式,用类来描述一个对象的信息,定义方法来封装该对象的功能,定义属性来区别不同的对象。请看下例:

public class TestMyArray {

public static void main(String[] args) {

MyArray ma = new MyArray();

ma.add(3);

ma.add(1);

ma.add(9);

ma.add(5);

ma.add(7);

System.out.println(ma);

// ------------------------------------------------

System.out.println("---------------------");

if (ma.find(5) != -1)

System.out.println("找到5");

else

System.out.println("没找到5");

// -------------------------------------------------

System.out.println("---------------------");

if (ma.delete(5))

System.out.println("删除成功");

else

System.out.println("删除失败");

// -----------------------------------------------

System.out.println("---------------------");

System.out.println(ma);

}// end main()

}// end class TestMyArray

class MyArray {

int[] arr; // 声明int类型的数组的引用

int nElements; // 记录数组里面元素的个数

int size; // 数组的大小

public MyArray() {

// 默认情况下,该数组对象的大小为10

this(10);

}

public MyArray(int size) {

nElements = 0;

this.size = size;

arr = new int[size];

}

// 增加的方法

public boolean add(int val) {

if (nElements < size) {// 判断是否数组已经满了

arr[nElements++] = val; // nElements 既是下标,又记录元素个数

return true;

} else {

return false;

}// end if

}// end add(int val)

public int get(int index) {

return arr[index];

}

public int find(int key) { //找到返回改值所在的下标,否则返回-1

int i = 0;

for (; i < nElements; i++)

// 循环查找

if (arr[i] == key) // 如果找到跳出循环

break;

if (i == nElements) // 如果i==nElements表示不是通过break跳出循环的,找不到!

return -1; // -1表示找不到

else

return i; // i是找到值所在的下标

}

public boolean delete(int key) { // true 表示删除成功,false表示失败

int k = find(key);

if (k == -1) {

return false;

} else {

for (int i = k; i < nElements; i++)

arr[i] = arr[i + 1];

nElements--;

return true;

}

}

public String toString() {

StringBuffer sb = new StringBuffer();

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

if (i != nElements - 1)

sb.append(arr[i] + ",");

else

sb.append(arr[i]);

}// end for

return sb.toString();

}

public int getSize(){ // 得到该数组的大小

return nElements;

}// end getSize()

};// end MyClass

运行结果

堆栈和队列的实现

堆栈和队列都是线性表结构。只不过堆栈的逻辑数据特点是先进后出(FILO),而队列的逻辑数据特点是先进先出(FIFO)。我们先用数组来存放这两种数据结构,也就是线性的存储结构。

请看下例:

public class StackT {

private int maxSize; // 堆栈的大小

private int[] stackArray;

private int top; // 堆栈顶部指针

public StackT(int s) {

maxSize = s; // 初始化数组大小

stackArray = new int[maxSize]; // 初始化一个数组

top = -1;

}

public void push(int j){

if (!isFull())

stackArray[++top] = j;

else

return;

}

public int pop(){

return stackArray[top--];

}

public int peek(){// 得到栈顶的数据而不是出栈

return stackArray[top];

}

public boolean isEmpty(){

return (top == -1);

}

public boolean isFull(){

return (top == maxSize - 1);

}

public String toString(){

StringBuffer sb = new StringBuffer();

for (int i = top; i >= 0; i--)

sb.append("" + stackArray[i] + "\n");

return sb.toString();

}

} // end class StackX

class StackApp {

public static void main(String[] args) {

StackT theStack = new StackT(10); // 初始化一个堆栈

theStack.push(20);

theStack.push(40);

theStack.push(60);

theStack.push(80);

System.out.println(theStack);

System.out.println("");

} // end main()

} // end class StackApp

运行的结果如图:

队列的实现(循环队列):

public class Queue {

private int maxSize; // 表示队列的大小

private int[] queArr; // 用数组来存放有队列的数据

private int front; // 取数据的下标

private int rear; // 存数据的下标

private int nItems; // 记录存放的数据个数

public Queue(int s) {

maxSize = s;

queArr = new int[maxSize];

front = 0;

rear = -1;

nItems = 0;

}

public void insert(int j) { // 增加数据的方法

if (isFull())

return;

// 如果下标到达数组顶部的话,让rear指向数组的第一个位置之前

if (rear == maxSize - 1)

rear = -1;

queArr[++rear] = j; // increment rear and insert

nItems++; // one more item

}

public int remove(){

int temp = queArr[front++];

// 如果下标到达数组顶部的话,让front指向数组的第一个位置

if (front == maxSize)

front = 0;

nItems--;

return temp;

}

public int peekFront(){// 只是返回最前面那个元素的值,并不删除

return queArr[front];

}

public boolean isEmpty() {

return (nItems == 0);

}

public boolean isFull() {

return (nItems == maxSize);

}

public int size() {

return nItems;

}

} // end class Queue

class QueueApp {

public static void main(String[] args) {

Queue theQueue = new Queue(5);

theQueue.insert(10); // 插入4个数据

theQueue.insert(20);

theQueue.insert(30);

theQueue.insert(40);

theQueue.remove(); // 删除(10, 20, 30)

theQueue.remove();

theQueue.remove();

theQueue.insert(50); // 再插入4个数据

theQueue.insert(60);

theQueue.insert(70);

theQueue.insert(80);

while (!theQueue.isEmpty())

// 取出所有数据

System.out.println(theQueue.remove());

}

}

运行的结果如图:

单链表的实现

链表也是一种线性表。它主要体现在物理结构上,节点跟节点之间是通过连接的方式来组织所有数据的。单链表是上一个节点存放了下一个节点在内存里面的地址。而下一个节点并没有存储上一个节点的地址。

代码的实现:

/**

* @see 链表类,实现了遍历、反转、插入、删除

*/

public class LinkedOfSample {

// 定义链表的最大长度

private int MAXLENGTH = 20;

// 长度计数

private int count;

// 定义引用第一个节点的start对象

Node start = null;

// 定义用来指向前一个节点变量及指向当前节点变量

Node prev, curr;

/**

* @see 判断长度是否达到最大值

* @return boolean

*/

public boolean count() {

if (count > MAXLENGTH) {

return false;

} else {

return true;

}

}

/**

* @see 添加新的节点至链表的末端

* @param dataID(String)

* --新节点的数据部分

* @param dataName(String)

* --新节点的数据部分

*/

public void addNodeToTail(String dataID, String dataName) {

if (count()) {

// 创建新的链表

if (start == null) // start为NULL代表链表中无节点

{

// 创建新节点并让start指向新接点

start = new Node(dataID, dataName, start);

}else {

// 指向链表的开始处(即第一个节点)

prev = curr = start;

// 遍历链表

while (curr != null) {

prev = curr;

curr = curr.next;

}

//创建新节点,并将新节点的NEXT指向NULL ,

//做为链表的最后一个节点添加

Node n = new Node(dataID, dataName, curr);

// 将原链表最后一个节点的NEXT指向新节点

prev.next = n;

}

// 添加计数

count++;

} else {

System.out.println("链表已满!");

}

}

/**

* @see 从链表的中间加入节点

* @param keyID(String)

* --用户指定要插入的链表位置

* @param dataID(String)

* --新节点的数据部分

* @param dataName(String)

* --新节点的数据部分

*/

public void insert(String keyID, String dataID, String dataName) {

if (count()) {

// 指向链表的开始处(即第一个节点)

curr = prev = start;

while (curr != null) {

// 如果找到匹配值就停止遍历,curr定位在插入位置

if (curr.cust.getNumID() == keyID) {

break;

}

prev = curr;

curr = curr.next;

}

// 新节点内next变量指向新节点插入位置的下个节点

Node n = new Node(dataID, dataName, curr);

// 上一个节点的next变量指向新节点

prev.next = n;

// 长度加一

count++;

} else {

System.out.println("链表已满!");

}

}

/**

* @see 添加新节点至链表的头部

* @param cust

*/

public void addNodeToHeader(Customer cust) {

if (count()) {

// 得到第一个节点的位置

curr = start;

//实现了节点的添加 1.创建新节点 2.将start指向新节点

// 3.将新节点的next变量指向了原链表的第一个节点

start = new Node(cust, curr);

count++;

} else {

System.out.println("链表已满!");

}

}

/**

* @see 遍历整个链表并显示

*

*/

public void traverse() {

if (count == 0) {

System.out.println("链表为空!");

return;

}

curr = start;

while (curr != null) {

// 显示链表中的节点数据

curr.cust.dis();

// 让curr读取下一个节点

curr = curr.next;

System.out.println();

}

}

/**

* @see 反转整个链表

*/

public void reverse() {

// 只有一个节点则不需反转

if (count == 1) {

return;

}

//curr,temp指向当前节点, prev指向前一个节点

Node temp;

prev = curr = temp = start;

while (temp != null) {

prev = curr;

curr = temp;

// 处于链表头部时的反转

if (prev == start) {

// 让curr指向下一个节点

curr = curr.next;

// 将链表头节点的next指向空

prev.next = null;

}

// 将下个节点的引用值保存在temp变量中

temp = curr.next;

// 将下个节点的next指向上个节点(反转)

curr.next = prev;

}

// 最后节点变为第一个节点,完成反转

start = curr;

}

/**

* @see 删除指定节点

* @param ID

*/

public void deleteNodeByCust(String ID) {

prev = curr = start;

// 找到删除节点的位置

while (curr != null) {

if (curr.cust.getNumID() == ID) {

break;

}

prev = curr;

curr = curr.next;

}

if (curr == null) {

System.out.println("没有您要删除的数据!");

return;

}

if (prev == curr) {

start = start.next;

} else {

// 将节点从链表中解链

prev.next = curr.next;

}

System.out.println("删除后的队列内容:");

traverse();

}

/**

* @param args

*/

public static void main(String[] args) {

// TODO 自动生成方法存根

LinkedOfSample list = new LinkedOfSample();

String[][] strTmp = { { "1", "2", "3" }, { "001", "002", "003" } };

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

// 在链表尾部线序添加节点

list.addNodeToTail(strTmp[0][i], strTmp[1][i]);

}

// 从指定位置2插入值4

list.insert("2", "4", "004");

list.addNodeToHeader(new Customer("5", "005"));

list.addNodeToTail("6", "006");

list.traverse();

list.deleteNodeByCust("5");

}

}

/**

* @see 组成链表的节点类

* @author Alan

*/

class Node {

// 节点数据部分

Customer cust;

// 节点的next变量,链表中的链线

Node next;

public Node(String _numID, String _strName, Node n) {

cust = new Customer(_numID, _strName);

next = n;

}

public Node(Customer _cust, Node n) {

cust = _cust;

next = n;

}

}

/**

* @see 节点的客户数据类

* @author Alan

*/

class Customer {

private String numID;

private String strName;

public Customer(String _numID, String _strName) {

numID = _numID;

strName = _strName;

}

public String getNumID() {

return numID;

}

public String getStrName() {

return strName;

}

public void dis() {

System.out.println("Customer details are : ");

System.out.println("Customer ID: " + numID);

System.out.println("Customer Name: " + strName);

}

}

运行结果:

双链表的实现

上一章里介绍了单链表,单链表是上一个节点存放了下一个节点在内存里面的地址。而下一个节点并没有存储上一个节点的地址。双链表是除了首节点和尾节点之外,每个节点都存放了上一个节点和下一个节点在内存里的地址。换句话说,双链表里每个节点有三个属性构成:数据,上一个节点的引用和下一个节点的引用。要想实现双链表,首先要创建一个节点类,包含上面的三个属性:

请看下例:

public class Node {

public Node next;// 引用下一个节点

public Node previous;// 引用上一个节点

private int data;// 节点中存放的数据

public Node(int data) {

this.data = data;

}

public String toString(){// 覆盖该方法,用来打印该节点

return data + "";

}

public int getData() {

return data;

}

public void setData(int data) {

this.data = data;

}

public boolean equals(Object other) {// 进行两个节点的比较

Node temp = null;

if (other instanceof Node) {// 判断是否是Node类型

temp = (Node) other;

if (temp.getData() == this.getData())// 进行数据的比较

return true;

else

return false;

} else {

return false;

}

}

}

// 下一步就是把节点联系起来组成一个双链表

class DoubleLink {

private Node first;

private Node end;

public void addFirst(int data) {

Node node = new Node(data);

if (first != null) {

node.next = first;

first.previous = node;

first = node;

} else {

first = node;

end = node;

}

}

public void addEnd(int data) {

Node node = new Node(data);

if (end != null) {

end.next = node;

node.previous = end;

end = node;

} else {

first = node;

end = node;

}

}

public Node find(int data) {

Node temp = new Node(data);// 以该数据创建一个节点进行比较

Node f = first; // 从头节点开始找

while (f != null) {

if (f.equals(temp))

break;

else

f = f.next;

}

return f;

}

public void delete(int data) {

Node node = find(data); // 首先查找容器中有无该数据

if (node != null) { // 如果找到要删的数据,进行指针的移位,从而删除

node.previous.next = node.next;

node.next.previous = node.previous;

}

}

public void update(int ydata, int mdata) {

Node node = find(ydata);// 查找数据

node.setData(mdata);// 修改数据

}

public String toString() {

// 不要直接使用String类,存在效率问题

StringBuffer sb = new StringBuffer();

Node temp = first;

while (temp != null) {

if (temp == end)

sb.append("[" + temp.getData() + "]");

else

sb.append("[" + temp.getData() + "],");

temp = temp.next;

}

return sb.toString();

}

}

// 下面写个类来测试运行的结果:

public class TestDL {

public static void main(String[] args) {

DoubleLink dl = new DoubleLink();

dl.addFirst(30);

dl.addFirst(40);

dl.addFirst(50);

dl.addFirst(60);

dl.addEnd(70);

dl.addEnd(80);

dl.addEnd(90);

dl.delete(50);

System.out.println("result:");

System.out.println(dl);

}

}

运行结果如图

二叉树的结构与实现

树型结构是用来存取数据的效率比较好的一种数据结构,增,删,改效率都比前面介绍的数据结构要高。缺点就是实现起来比较复杂。下面以二叉树为例子,来说明数型结构的特点:

请看下例:

二叉树的实现:

class JD {

int data;// 数据

JD left; // 左儿子

JD right;// 右儿子

public JD(int data) {

this.data = data;

}

public String toString() {

return data + "";

}

};

// 该类实现了增,删,改,查等特性

class Tree{

JD root;

JD parrent;

boolean b;

public boolean add(int d) {// 增加数据的方法

JD jd = new JD(d);

if (root == null){ // 如果根节点为空,那么把新节点加给根节点

root = jd;

}else {

JD current = root;

while (current != null) {// 是找到一个位置加新节点

if (d == current.data)// 如果已经存在,则直接返回false 表示加失败

return false;

else if (d > current.data) {// 如果该值大于当前节点,那么应该往右边找

parrent = current; // 记录要加新节点的父节点

b = true; // 记录是左边还是右边,

current = current.right;// current.right=current

} else if (d < current.data) {

parrent = current;

b = false;

current = current.left;// current.left=current

}

}// end while

if (b)// 如果是右儿子为空 ,就加父节点的右边

parrent.right = jd;

else

parrent.left = jd;

}

return true;

}

public JD find(int d) {// 查询的方法

JD current = root;

while (current != null) {

if (current.data == d)

return current;

else {

parrent = current;// 记录找到节点的父节点,以方便删除操作

if (d > current.data) {

current = current.right;

b = true;

} else if (d < current.data) {

current = current.left;

b = false;

}

}

}// end while

return current;

}

public boolean delete(int d) {

JD current = find(d);

if (current == null){

return false;

}else if (current.left == null && current.right == null) // 如果要删除的节点是页节点

{

if (current == root)// 如果是根节点(也是页节点),直接让根节点==null

root = null;

else if (b)

parrent.right = null;

else

parrent.left = null;

} else if (current.left == null) {// 如果删除节点只有右节点

if (b)

parrent.right = current.right;

else

parrent.left = current.right;

} else if (current.right == null) {// 如果删除节点只有左节点

if (b)// 该变量记录了要删除的节点,在其父节点的左边还是右边

parrent.right = current.left;

else

parrent.left = current.left;

} else {

JD temp = fenlie(current); // 分裂节点

if (b)

parrent.right = temp;

else

parrent.left = temp;

}

return true;

}

public JD fenlie(JD c) {

JD temp = c.right;

JD p = temp;// 用来记录要删除节点右儿子那边的最小节点

JD pp = temp;// 用来记录要删除节点右儿子那边的最小节点的父节点

while (temp != null) {// 找到要删除节点右儿子那边的最小节点

pp = p;

p = temp;

temp = temp.left;

}

if (pp == p) {// 如果删除节点的右儿子节点没有左儿子

pp.left = c.left;// 把删除节点左儿子加到删除节点的右儿子的左边

return pp;

} else {

pp.left = p.right;// 把找到的节点的右儿子部分加到该节点父节点的左边

p.left = c.left;// 把删除节点的左儿子加到分裂节点的左边

p.right = c.right;// 把删除节点的右儿子加到分裂节点的右边

return p;

}

}

public boolean modify(int s, int m) {// 修改数据的方法=先删除后增加,这样还是有//顺序的

delete(s);

return add(m);

}

public void print(JD jd) {// 递归中序遍历该有序二叉树

if (jd != null) {

print(jd.left);

System.out.print(jd + " ");

print(jd.right);

}

}

public void p() {

print(root);

}

}

class TestTree {

public static void main(String[] args) {

Tree t = new Tree();

t.add(5);

t.add(7);

t.add(3);

t.add(9);

t.add(1);

t.add(8);

t.add(13);

t.add(4);

t.p();

System.out.println("\n" + "-------------改变----------");

t.modify(8, 18);// 把接点值为8的接点,把该值改为18

t.delete(9);// 删除接点值为9的接点

t.p();

}

}

运行的结果如图

(图 10)

遍历二叉树

遍历二叉树有很多种方式。大的范围分两种:横向遍历和纵向遍历(深度遍历)。纵向遍历又分为三种:前序,中序,后序。先遍历根接点是前序遍历,根接点中间遍历是中序遍历,根接点在后面遍历就是后续。如下图二叉树的前序,中序,后序遍历结果:

前序遍历结果为:

57,42,35,23,9,24,48,45,43,51,70,62,61,68,66,69,

72,78,77,91

中序遍历结果为:

9,23,24,35,42,43,45,48,51,57,61,62,66,68,69,70,

72,77,78,91

后序遍历结果为:

9,24,23,35,43,45,51,48,42,61,66,69,68,62,77,91,

78,72,70,57

也就是说该二叉树是按中序有序遍历的二叉树。

package com.itjob;

public class TreeNode {

TreeNode leftNode;

int data;

TreeNode rightNode;

public TreeNode(int nodeData) {

data = nodeData;

}

public synchronized void insert(int insertValue) {

if (insertValue < data) {

if (leftNode == null)

leftNode = new TreeNode(insertValue);

else

leftNode.insert(insertValue);

}else if (insertValue > data) {

if (rightNode == null)

rightNode = new TreeNode(insertValue);

else

rightNode.insert(insertValue);

}

}

}

package com.itjob;

public class MyTree {

private TreeNode root;

public MyTree() {

root = null;

}

// 插入节点到树

public synchronized void insertNode(int insertValue) {

if (root == null)

root = new TreeNode(insertValue);

else

root.insert(insertValue);

}

// 开始先序遍历

public synchronized void preorderTraversal() {

preorderHelper(root);

}

// 执行先序遍历的方法

private void preorderHelper(TreeNode node) {

if (node == null)

return;

System.out.print(node.data + " ");

preorderHelper(node.leftNode);

preorderHelper(node.rightNode);

}

// 开始中序遍历

public synchronized void inorderTraversal() {

inorderHelper(root);

}

// 执行中序遍历的方法

private void inorderHelper(TreeNode node) {

if (node == null)

return;

inorderHelper(node.leftNode);

System.out.print(node.data + " ");

inorderHelper(node.rightNode);

}

// 开始后序遍历

public synchronized void postorderTraversal() {

postorderHelper(root);

}

// 执行后序遍历的方法

private void postorderHelper(TreeNode node) {

if (node == null)

return;

postorderHelper(node.leftNode);

postorderHelper(node.rightNode);

System.out.print(node.data + " ");

}

}

package com.itjob;

public class TreeTest {

public static void main(String args[]) {

MyTree tree = new MyTree();

int value;

System.out.println("Inserting the following values: ");

// 插入10个0-99的随机数

for (int i = 1; i <= 10; i++) {

value = (int) (Math.random() * 100);

System.out.print(value + " ");

tree.insertNode(value);

}

System.out.println("先序遍历");

tree.preorderTraversal();

System.out.println("中序遍历");

tree.inorderTraversal();

System.out.println("后序遍历");

tree.postorderTraversal();

System.out.println();

}

}

内容总结

? 数据结构在概念上有数据的逻辑结构和数据的物理结构;数据的逻辑结构就是数据表面上的关系,物理结构包括顺序存储和链接存储

? 数组是常用的数据结构。几乎每种编程语言里面都有该结构。其优点是快速的插入数据,如果下标(索引值)知道,可以很快地存取数据。数组的缺点是查找数据慢,删除数据慢,固定大小。

? 链表是由多个节点(对象)组成,其中每个节点在内存中是散乱存放的。其中每个节点(除了尾节点外)都有一个特定的引用下一个节点的变量,从而实现一个完整链表。链表分为单向链表,循环链表和双向链表;栈和队列是链表的特殊形式。

? Stack(栈)具有先进后出(FILO)的特性,其操作只可以在链表的头部实现

? Queue(队列) 具有先进先出(FIFO)的特性,其进对操作只可以在链表的头部实现,出队操作在链表的尾部实现。

? 树是由一个或多个节点组成的有限集合。每一颗树必须有一个特定节点,叫做根节点。根节点下可以有零个以上的子节点。而且各子节点也可以为子树,拥有自己的子节点。

? 树的基本概念

? 树的主要应用是二叉树

? 树的遍历:先序遍历,中序遍历,后序遍历

独立实践

? 使用递归算法写一个二叉树的创建与遍历。

? 使用单链表创建一个栈和队列数据结构。

? 使用数组创建一个栈和队列数据结构。

? 创建链表,在其中插入学生信息,链表的学生对象需要按学生的名字有序排列(由小到大)。

第十六章:数据库(一)

学习目标

? 数据库的基本概念

? 数据联系的描述

? 数据模型

? 数据库三级模式结构

? 数据库三个范式

? 创建数据库

? 创建表

数据库的基本概念

数据:描述事物的符号记录。可以是数字、字符、图形、图像、声音等。

元数据:描述数据的数据

数据库:数据库长期存放在计算机内的、有组织的、可共享的数据集合。

数据库管理系统:数据管理软件,功能包括:

? 数据定义功能,如定义一个表的结构;

? 数据操纵功能:查询、插入、删除、修改

? 数据库的运行管理;保证系统的正常运行

? 数据库的建立和维护功能。

数据库系统(DBS)

由数据库、数据库管理系统、应用系统和数据库用户构成的系统。

关系数据库的特点

采用数据模型表示复杂的数据结构

数据模型不仅描述了数据本身的特征,还要描述数据之间的联系。这种联系通过存取路径实现。通过存取路径表示自然的数据联系是数据库与传统文件系统的根本区别。文件系统只是记录的内部有结构,一个文件的记录之间是个线性序列,记录之间无联系。数据反映了客观事物间的本质联系,而不是着眼于面向某个应用,是有结构的数据。

数据冗余度小

数据面向整个系统,而不是面向某一应用,数据集中管理,数据共享,因此冗余度小的数据节省存储空间,减少存取时间,且可避免数据之间的不相容性和不一致性。每个应用选用数据库的一个子集,只要重新选取不同子集或者加上一小部分数据,就可以满足新的应用要求,这就是易扩充性。

有较高的数据独立性

数据的逻辑结构和物理结构可以有很大的差别。用户以简单的逻辑结构操作数据,不需要考虑复杂的物理结构。数据库的结构分成用户的局部逻辑结构、数据库的整体逻辑结构和物理结构三级。数据的存取由系统管理,用户不必考虑存取路径等细节,从而简化了应用程序。应用程序与数据库的数据结构之间相互独立。数据库物理结构的变化不能影响整体逻辑结构、用户逻辑结构和应用程序。

提供数据控制功能

数据控制功能 功能描述 措施

安全性控制 保护数据以防止不合法的使用所造成的数据泄露和破坏 用户验证与授权,存取控制

完整性控制 数据的正确性、有效性、相容性 完整性约束条件定义和检查

恢复控制 在数据库遭到破坏或数据不可靠时,系统有能力把数据库恢复到最近的某个正确的状态。 备份

并发控制 对多用户的并发操作加以控制、协调,防止其互相干扰而得到错误的结果并使数据库完整性遭到破坏。 加锁

为用户提供方便的用户接口

用户可以使用查询语言或终端命令操作数据库,也可以使用程序的方式来操作数据库。用户接口一般都是通用接口,不同的编程语言可以使用相同的用户接口。

数据的描述

概念设计中的数据描述

数据库的概念设计是根据用户的需求设计数据库的概念结构,一般有以下几个部

分需要考虑:

实体:现实世界中客观存在的事物。

实体集:具有相同属性的实体的集合。

属性:实体的特征。

实体标识符:能够唯一的确定一个实体的属性。

逻辑设计中的数据描述

逻辑设计是根据概念设计得到的概念结构来进行数据库的逻辑结构设计。

字段:用于表示实体的属性,每一个属性可以对应一个字段。

记录:字段的集合称为记录。每一个记录代表一个实体。

文件:同一类记录的集合组成一个文件。文件用于描述实体集。

关键码:能够唯一标识文件中每一条记录的字段或字段集。对应于实体标识符。

物理介质的数据描述

物理介质中的数据描述指的是数据在计算机中的存储形式,包括以下一些信息:

位(Bit)

字节(Byte)

字(Word)

块(Block)

桶(Bucket)

卷(Volume)

数据联系的描述

现实世界中的事物存在着联系,数据库中在存储信息时必须反映这种联系,也就是说由现实事物抽象出来的实体不是单独存在的,而是存在者必然的联系。

联系的定义:联系是实体之间的相互关系,与一个联系有关的实体集个数,称为联系的元数。

联系的三种类型

1∶1联系 —— 实体集E1中的每一个实体至多和E2中的一个实体有联系,反之亦然。

1∶N联系 —— 实体集E1中的每一个实体与E2中任意个实体有联系,而E2中每个实体至多和E1中的一个实体有联系。

M∶N联系 —— 实体集E1中的每一个实体与E2中任意个实体有联系,反之亦然。

数据模型

数据模型是能够表示实体模型和实体之间联系的模型。

数据模型包括两种:一种是独立于计算机系统的数据模型;另一种是直接面向数据库逻辑结构的数据模型。前者最著名的有实体关系(ER)模型;后者有层次模型、网状模型、关系模型。以及目前正在研究的面向对象的数据模型。

本节介绍第一种中的实体联系模型和第二种中的关系模型。

数据模型的三要素:

数据结构

数据结构是指对实体类型和实体之间联系的表达和实现。

数据操作

数据操作是指对数据库的查询、修改、删除和插入等操作。

数据完整性约束

数据完整性约束定义了数据及其联系应该具有的制约和依赖规则。

概念数据模型:实体联系模型

实体联系模型简称为E-R模型,该模型直接从现实世界中抽象出实体类型和实体之间的联系,然后用实体联系图(E-R图)表示数据模型。E-R图由以下一些部分组成:

矩形框:表示实体类型。

菱形框:表示联系类型。

椭圆形框:表示实体类型和联系类型的属性。

连线:实体与属性之间,联系与属性之间用直线连接;联系类型与其涉及的实体类型用直线连接,并标注联系的类型。

如下图所示:

ER图

结构数据模型:关系模型

关系模型的主要特点是以二维表的形式来表达实体集。它是由若干个关系模式组成的集合。二维表格简单易懂,用户只需要使用简单的查询语句就可以对数据库进行操作,并不涉及存储结构、访问技术等细节。

对关系的理论和实验研究集中于20世纪70年代,80年代初形成产品,并很快得到了广泛的应用和普及,并最终取代基于层次模型、网状模型的数据库而成为商用数据库系统的主流。关系数据库是本课程介绍的主要内容。

数据库三级模式结构

数据库的体系结构分成三级:外部级、概念级和内部级。

外部级

外部级最接近用户是单个用户所能看到的数据特征,单个用户使用的数据视图的描述称为“外模式”。

概念级

概念级涉及到所有用户的数据定义,也就是全局性的数据视图,全局数据视图的描述称为“概念模式”。

内部级

内部级最接近于物理存储设备,涉及到物理数据存储的结构。物理视图的描述称为“内模式”。

数据库的三级模式结构是对数据的三个抽象级别。它把数据的具体组织留给数据库管理系统去做,用户只要抽象地处理数据,而不需要关心数据在计算机中的表示和存储,这样就减轻了用户使用系统的负担。但是数据库的三级结构有很大的差别,为了实现三个抽象级别的相互转换,系统在三级结构中提供两个层次的映像:外模式/概念模式映像和概念模式/内模式映像。其中概念模式经常简称为模式。

三级结构的五个要素

概念模式

概念模式是数据库中全部数据的整体逻辑结构的描述,它由若干个概念记录类型组成,还包括记录之间的联系、数据的完整性和安全性等要求。描述概念模式的数据定义语言是“模式DDL”。

外模式

外模式是用户与数据库系统的接口,是用户用到的部分数据的描述。外模式由若干个外部记录类型组成。用户使用数据操作语言(DML)对数据库进行操作。描述外模式的数据定义语言称为“外模式DDL”。一般程序员不关心概念模式而只关心外模式。

内模式

内模式是数据库在物理存储方面的描述,定义所有内部记录类型、索引和文件的组织方式,以及数据控制方面的细节。事实上内部记录也不涉及物理设备的约束。有关物理方面的操作都是由操作系统完成的。

模式/内模式映像

模式/内模式映像存在于概念级和内部级之间,用于定义概念模式和内模式之间的对应性。

外模式/模式映像

外模式/模式映像存在于外部级和概念级之间,用于定义外模式与概念模式之间的对应性。

两级数据的独立性

数据的独立性是指应用程序和数据库的数据结构之间相互独立,不受影响。

物理数据独立性(物理独立性)

数据库内模式的修改尽量不影响概念模式、外模式和应用程序,只需要修改模式/内模式映像即可。

逻辑数据独立性(逻辑独立性)

数据库概念模式的修改不影响外模式和应用程序,只需要修改外模式/模式映像即可。

数据库三个范式

关系数据库设计之时是要遵守一定的规则的。尤其是数据库设计范式 现简单介绍1NF(第一范式),2NF(第二范式),3NF(第三范式)和BCNF,另有第四范式和第五范式留到以后再介绍。 在你设计数据库之时,若能符合这几个范式,你就是数据库设计的高手。

第一范式(1NF):在关系模式R中的每一个具体关系r(表)中,如果每个属性值 都是不可再分的最小数据单位,则称R是第一范式的关系。例:如职工号,姓名,电话号码组成一个表(一个人可能有一个办公室电话 和一个家里电话号码) 规范成为1NF有三种方法:

一是重复存储职工号和姓名。这样,关键字只能是电话号码。

二是职工号为关键字,电话号码分为单位电话和住宅电话两个属性

三是职工号为关键字,但强制每条记录只能有一个电话号码。

以上三个方法,第一种方法最不可取,按实际情况选取后两种情况。

第二范式(2NF):如果关系模式R(U,F)中的所有非主属性都完全依赖于任意一个候选关键字,则称关系R 是属于第二范式的。

例:选课关系 SCI(SNO,CNO,GRADE,CREDIT)其中SNO为学号, CNO为课程号,GRADEGE 为成绩,CREDIT 为学分。 由以上条件,关键字为组合关键字(SNO,CNO)

在应用中使用以上关系模式有以下问题:

a.数据冗余,假设同一门课由40个学生选修,学分就 重复40次。

b.更新异常,若调整了某课程的学分,相应的元组CREDIT值都要更新,有可能会出现同一门课学分不同。

c.插入异常,如计划开新课,由于没人选修,没有学号关键字,只能等有人选修才能把课程和学分存入。

d.删除异常,若学生已经结业,从当前数据库删除选修记录。某些门课程新生尚未选修,则此门课程及学分记录无法保存。

原因:非关键字属性CREDIT仅函数依赖于CNO,也就是CREDIT部分依赖组合关键字(SNO,CNO)而不是完全依赖。

解决方法:分成两个关系模式 SC1(SNO,CNO,GRADE),C2(CNO,CREDIT)。新关系包括两个关系模式,它们之间通过SC1中的外关键字CNO相联系,需要时再进行自然联接,恢复了原来的关系

第三范式(3NF):如果关系模式R(U,F)中的所有非主属性对任何候选关键字都不存在传递信赖,则称关系R是属于第三范式的。

例:如S1(SNO,SNAME,DNO,DNAME,LOCATION) 各属性分别代表学号,

姓名,所在系,系名称,系地址。

关键字SNO决定各个属性。由于是单个关键字,没有部分依赖的问题,肯定是2NF。但这关系肯定有大量的冗余,有关学生所在的几个属性DNO,DNAME,LOCATION将重复存储,插入,删除和修改时也将产生类似以上例的情况。

原因:关系中存在传递依赖造成的。即SNO -> DNO。 而DNO -> SNO却不存在,DNO -> LOCATION, 因此关键辽 SNO 对 LOCATION 函数决定是通过传递依赖 SNO -> LOCATION 实现的。也就是说,SNO不直接决定非主属性LOCATION。

解决目地:每个关系模式中不能留有传递依赖。

解决方法:分为两个关系 S(SNO,SNAME,DNO),D(DNO,DNAME,LOCATION)

注意:关系S中不能没有外关键字DNO。否则两个关系之间失去联系。

BCNF:如果关系模式R(U,F)的所有属性(包括主属性和非主属性)都不传递依赖于R的任何候选关键字,那么称关系R是属于BCNF的。或是关系模式R,如果每个决定因素都包含关键字(而不是被关键字所包含),则RCNF的关系模式。

例:配件管理关系模式 WPE(WNO,PNO,ENO,QNT)分别表仓库号,配件号,职工号,数量。有以下条件

a.一个仓库有多个职工。

b.一个职工仅在一个仓库工作。

c.每个仓库里一种型号的配件由专人负责,但一个人可以管理几种配件。

d.同一种型号的配件可以分放在几个仓库中。

分析:由以上得 PNO 不能确定QNT,由组合属性(WNO,PNO)来决定,存在函数依赖(WNO,PNO) -> ENO。由于每个仓库里的一种配件由专人负责,而一个人可以管理几种配件,所以有组合属性(WNO,PNO)才能确定负责人,有(WNO,PNO)-> ENO。因为 一个职工仅在一个仓库工作,有ENO -> WNO。由于每个仓库里的一种配件由专人负责,而一个职工仅在一个仓库工作,有 (ENO,PNO)-> QNT。

找一下候选关键字,因为(WNO,PNO) -> QNT,(WNO,PNO)-> ENO ,因此 (WNO,PNO)可以决定整个元组,是一个候选关键字。根据ENO->WNO,(ENO,PNO)->QNT,故(ENO,PNO)也能决定整个元组,为另一个候选关键字。属性ENO,WNO,PNO 均为主属性,只有一个非主属性QNT。它对任何一个候选关键字都是完全函数依赖的,并且是直接依赖,所以该关系模式是3NF。

分析一下主属性。因为ENO->WNO,主属性ENO是WNO的决定因素,但是它本身不是关键字,只是组合关键字的一部分。这就造成主属性WNO对另外一个候选关键字(ENO,PNO)的部 分依赖,因为(ENO,PNO)-> ENO但反过来不成立,而P->WNO,故(ENO,PNO)-> WNO 也是传递依赖。

虽然没有非主属性对候选关键辽的传递依赖,但存在主属性对候选关键字的传递依赖,同样也会带来麻烦。如一个新职工分配到仓库工作,但暂时处于实习阶段,没有独立负责对某些配件的管理任务。由于缺少关键字的一部分PNO而无法插入到该关系中去。又如某个人改成不管配件了去负责安全,则在删除配件的同时该职工也会被删除。

解决办法:分成管理EP(ENO,PNO,QNT),关键字是(ENO,PNO)工作EW(ENO,WNO)其关键字是ENO

缺点:分解后函数依赖的保持性较差。如此例中,由于分解,函数依赖(WNO,PNO)-> ENO 丢失了, 因而对原来的语义有所破坏。没有体现出每个仓库里一种部件由专人负责。有可能出现 一部件由两个人或两个以上的人来同时管理。因此,分解之后的关系模式降低了部分完整性约束。

一个关系分解成多个关系,要使得分解有意义,起码的要求是分解后不丢失原来的信息。这些信息不仅包括数据本身,而且包括由函数依赖所表示的数据之间的相互制约。进行分解的目标是达到更高一级的规范化程度,但是分解的同时必须考虑两个问题:无损联接性和保持函数依赖。有时往往不可能做到既有无损联接性,又完全保持函数依赖。需要根据需要进行权衡。

1NF直到BCNF的四种范式之间有如下关系:

BCNF包含了3NF包含2NF包含1NF

范式总结

目地:规范化目的是使结构更合理,消除存储异常,使数据冗余尽量小,便于插入、删除和更新

原则:遵从概念单一化 "一事一地"原则,即一个关系模式描述一个实体或实体间的一种联系。规范的实质就是概念的单一化。

方法:将关系模式投影分解成两个或两个以上的关系模式。

要求:分解后的关系模式集合应当与原关系模式"等价",即经过自然联接可以恢复原关系而不丢失信息,并保持属性间合理的联系。

注意:一个关系模式结这分解可以得到不同关系模式集合,也就是说分解方法不是唯一的。最小冗余的要求必须以分解后的数据库能够表达原来数据库所有信息为前提来实现。其根本目标是节省存储空间,避免数据不一致性,提高对关系的操作效率,同时满足应用需求。实际上,并不一定要求全部模式都达到BCNF不可。有时故意保留部分冗余可能更方便数据查询。尤其对于那些更新频度不高,查询频度极高的数据库系统更是如此。

创建数据库

以SQL SERVER 2000为例子,在该数据库管理系统里面创建数据库。在SQL SERVER 2000里面有两个常用的客户端软件-----企业管理器和查询分析器。这两个客户端软件共同对SQL SERVER 2000数据库进行操作。

首先打开企业管理器:

然后鼠标放数据库选项上面点右键选择----新建数据库

在名称里写上数据库的名字

然后选择数据文件选项—看文件属性边框里内容选项----选择文件自动增长

在下面的两个边框里可以设置一些参数-----文件增长和最大文件大小

然后选日志文件选项 操作同上

如下图:

右键选择----新建数据库(B)…

打开创建数据库的界面:

输入名字。如:student

然后选择------数据文件选项 修改文件属性

同样的选择-------事物日志

点击确定就创建好数据库了。如下图所示:

使用查询分析器创建数据库的操作:

打开查询分析器:

上面的SQL代码如下:

-- =============================================

-- Basic Create Database Template

-- =============================================

--IF EXISTS (SELECT *

-- FROM master..sysdatabases

--- WHERE name = N'<database_name, sysname, test_db>')

-- DROP DATABASE <database_name, sysname, test_db>

--GO

--

--CREATE DATABASE <database_name, sysname, test_db>

--GO

---上面是创建数据库的模板

IF EXISTS (SELECT *

FROM master..sysdatabases

WHERE name = 'javadb')

DROP DATABASE javadb

GO

CREATE DATABASE javadb

GO

创建表

首先用企业管理器创建:

如图所示:

然后点击保存 ---会让你输入表名,然后输入表名 点确定就OK了。

下面是用查询分析器创建表:

代码如下

-- =============================================

-- Create table basic template

-- =============================================

--IF EXISTS(SELECT name

-- FROM sysobjects

-- WHERE name = N'<table_name, sysname, test_table>'

-- AND type = 'U')

-- DROP TABLE <table_name, sysname, test_table>

--GO

--CREATE TABLE <table_name, sysname, test_table> (

--<column_1, sysname, c1> <datatype_for_column_1, , int> NULL,

--<column_2, sysname, c2> <datatype_for_column_2, , int> NOT NULL)

--GO

---上面是摸板

use student ---使用刚才创建的数据库

IF EXISTS(SELECT name

-- FROM sysobjects

WHERE name = 'banji'

AND type = 'U')

DROP TABLE banji

GO

CREATE TABLE banji (

id int not NULL,

name char(30) NOT NULL)

GO

内容总结

? 数据库的基本概念:数据,元数据,数据库,数据库管理系统,数据库系统。

? 关系数据库的特点:

? 采用数据模型表示复杂的数据结构

? 数据冗余度小

? 有较高的数据独立性

? 为用户提供方便的用户接口

? 数据的描述

? 概念设计中的数据描述: 实体,实体集,属性,实体标识符

? 概念设计中的数据描述:字段,记录,文件,关键码

? 物理介质的数据描述: 位,字节,字,块,桶,卷

? 数据联系的描述:1∶1联系, 1∶N联系, M∶N联系

? 数据模型:数据结构,数据操作,数据完整性约束。

? 概念数据模型:实体联系模型(ER)

? 数据库的体系结构分成三级:外部级、概念级和内部级

? 数据库范式: 第一范式(1NF), 第二范式(2NF), 第三范式(3NF)

? 使用sqlserver 企业管理器创建数据库,创建表

独立实践

? 使用ER图设计在线书店的数据库结构。

? 分析在一个学生信息管理系统中,学员,班主任,系,系主任,课程间的管理,并画图表示。

? 使用sqlserver企业管理器创建数据库onlinebook和存放书籍的表Books

根据数据的SCOTT.EMP和SCOTT.DEPT表,完成如下实践

1. 查询出每种工作的平均工资

2. 查询出那种工作的工资最高

3. 查询出工资最低的经理的名字

4. 查询出部门编号为30的部门里面那种工作的平均工资最高

5. 查询出名字中带K的经理

6. 统计出各个部门的各个工作岗位的平均工资

第十七章: 数据库(二)

学习目标:

? 查询(从基本到高级)

? 模糊查询

? 排序

? 集合操作-并

? 集合操作-交

? 集合操作-差

? 常用函数

? 游标

? 分组查询

? 连接查询

? 左外连接

? 右外连接

? 五大约束

? 索引

? 数据库的备份与恢复

? Sql Server数据库设计

查询(从基本到高级)

查询语句是SQL语言里面使用频率最高的语句。这主要是为程序开发人员所使用。我们对数据的操作概括起来就四个字,增,删,改,查。对于删和改操作,首先要先进行查询操作。那么下面将介绍所有查询的语句,来给大家参考。

首先要先创建一张表,然后对该表进行查询。SQL代码如下:

--使用student数据库

use student

go

create table employee

(

id int primary key, --员工编号

name char(30) not null, --员工名字

mid int not null ---领导编号

)

insert into employee values(1009,'abc1',1003)

insert into employee values(1005,'andy',1005)

insert into employee values(1002,'abc2',1005)

insert into employee values(1003,'abc3',1005)

insert into employee values(1110,'欧%阳44',1005)

insert into employee values(1115,'欧阳5',1005)

insert into employee values(1105,'欧阳55',1005)

insert into employee values(1106,'欧阳',1005)

insert into employee values(1116,'欧阳疯',1008)

insert into employee values(1008,'babc7',1008)

insert into employee values(1118,'欧阳_疯',1008)

insert into employee values(1119,'欧阳疯反对撒反对是大撒反',1008)

模糊查询

通配符%等价于DOS中的通配符*,表示零个或多个

select name from employee where name like '欧阳%'

--该语句查询是只要名字是以欧阳开头就行,后面跟零个或多个字符

查询的结果如图:

通配符_等价于DOS中的通配符?表示一个或零个字符

select name from employee

where name like '欧阳_'

注意,‘_’代表一个字符,在SQL SERVER里一个字符占一个字节

汉字占两个字符。但是在这里‘_’既代表了一个英文字符,又代表了一个汉字字符

查询的结果如图:

当要查找的字符串中包括通配符字符本身(%或_)时,比如要查找包括%的某列,则LIKE子句应写为:LIKE ‘%\%%’ ESCAPE ‘\’ ,其中\为转义字符,

其后的%(也就是第二个%)不再具有通配符的意义,而转义为普通字符%。

如:

select * from employee

where name like '%*_%' ESCAPE '*'

‘*’成了转义字符

查询结果如图

更名运算和元组变量

更名的三种形式:

select name as '名字' --or select name ‘名字’ –or select ‘名字’=name

from employee where name like '%\%%' escape '\'

查询结果如图

当用形式“关系名.属性名”书写表达式时,“关系名”就是隐含定义的元组变量。

当给关系更名(即起了别名)后,别名成为元组变量,此时只能用形式“别名.属性名”

书写表达式,而不能再用“关系名.属性名”!原因就是SQL查询语句是先执行from语句,后执行select语句。元组变量在比较同一关系的两个元组时非常有用。

如下例:

select e.name ---如果改成 employee.name就出错了

from employee e

where e.name like '欧阳%'

现在如果我想查出所有领导的名字怎么查呢?

第一, 我们可以用一个嵌套的查询

select name

from employee

where id in (select mid from employee)

—in 代表一个范围

第二, 我们可以用虚拟表查询

--distinct 关键字用来过滤掉重复的数据

select distinct e1.name

from employee e1,employee e2

where e1.id=e2.mid

查询的结果如图

排序

有时候要把查询的结果,按照某个字段进行排序。排序的SQL语句如下:

select * from employee

order by name –默认的是升序排列,该句等价于 order by name asc

查询结果按名字进行升序排列。如图

如果只有select * from employee 那么会按照数据的物理结构的顺序列出来,物理顺序是由该表的主键索引所确定。

select * from employee

结果如图

如果按名字将序排列:

select * from employee order by name desc

结果如图

集合操作-并

首先再创建一张表(演员表,只是为了测试两张表查询,跟数据库的设计没有关系)

create table yanyuan --创建张演员表

(

yid int , --演员的ID

sa money --演员的出场费

)

insert into yanyuan values(1005,500.0)

insert into yanyuan values(1008,500.0)

insert into yanyuan values(1009,500.0)

insert into yanyuan values(1106,500.0)

insert into yanyuan values(1009,500.0)

insert into yanyuan values(3336,500.0)

insert into yanyuan values(4446,500.0)

现在我们要把员工和演员的ID号全查出来

select id from employee

union

select yid from yanyuan

查询结果如下:

两个表的ID就全部查出来了。重复的去掉,没有的加在后面

集合操作-交

有时候我们需要查询即是员工,又是演员的ID号,这时候就用到集合操作-交。SQL语句如下:

select id from employee

intersect

select yid from yanyuan

由于在SQL SERVER 2000里面不支持intersect关键字,所以只能用下列的语句来代替上面的执行:

select id from employee

where id in (select yid from yanyuan)

查询结果如下:

集合操作-差

而有时候我们需要查下不是演员的员工。这时候就用到集合操作-差。SQL语句如下:

select id from employee

except

select yid from yanyuan

同样,对于不支持except关键字的DBMS,可以用下面的SQL语句替代:

select id

from employee

where id not in (select yid from yanyuan )

也可以用exists函数来完成上面的这个操作:

select id from employee

where not exists(select * from yanyuan where id=yid)

查询结果如下:

补充:对于ORACLE来说可以用关键字MINUS来完成以上操作。SQL代码如下:

select id from employee

MINUS

select yid from yanyuan

常用函数

得到当前系统的时间函数 getdate()

select getdate()

结果如图:

统计员工表中员工的总个数count()

select '员工个数'=count(*)

from employee

结果如下:

如果我们想查下员工表(employee)中领导的个数:

select '领导个数'=count(distinct mid)

--distinct 去掉重复的记录

from employee

查询结果如下:

当查询条件是该值大于或者小于一个集合里面某个值时,要用到some()

如:现在要查出大于某个演员ID的员工ID

select id from employee

where id > some(select yid from yanyuan)

其实该语句等价于

select id from employee

where id > (select min(yid) from yanyuan)

结果如下:

数据类型转换函数CAST和CONVERT

declare @val decimal (5, 2)

set @val = 193.57

select cast(@val as varbinary(20)),cast(cast(@val as varbinary(20))

as varbinary(10,5))

--或用 CONVERT

Select convert(varbinary(20), @val),convert(DECIMAL(10,5),

convert(varbinary(20), @val))

统计演员表(yanyuan)中平均工资avg()

先使用游标修改演员(yanyuan)表里工资那个字段里的数据:

DECLARE yanyuan_cursor CURSOR --声明游标

FOR select yid,sa from yanyuan

FOR UPDATE of sa

DECLARE @gz int

SET @gz=100

OPEN yanyuan_cursor --打开游标

FETCH NEXT FROM yanyuan_cursor --提取游标

WHILE (@@fetch_status <> -1)

BEGIN

IF (@@fetch_status <> -2)

BEGIN

UPDATE yanyuan

SET sa = sa+@gz,

@gz=@gz+@gz*0.5

WHERE CURRENT OF yanyuan_cursor

END

FETCH NEXT FROM yanyuan_cursor --INTO @name

END

CLOSE yanyuan_cursor --关闭游标

DEALLOCATE yanyuan_cursor --释放游标

GO

然后再查下平均工资多少:

select '平均工资'=avg(sa)

from yanyuan

查询结果如下:

分组查询

现在要查出员工表(employee)里,领导和该领导下员工的个数:

select '领导编号'=mid,count(id) as '员工个数'

from employee

group by mid

查询结果如下:

现在如果要查出领导编号为1005的员工个数:

select '领导编号'=mid,count(id) as '员工个数'

from employee

group by mid

having mid=1005

查询结果如下:

现在如果要降序排列领导编号大于1003的员工个数的信息

select '领导编号'=mid,count(id) as '员工个数'

from employee

group by mid

having mid>1003

order by count(id) desc

查询的结果如下:

注意:执行group by语句后,select,having,order by等语句后面只能加group by 后面的列或者聚合函数!查询中同时有WHERE和HAVING时,先由WHERE过滤元组,后由HAVING过滤分组!

当执行多表查询时,查询的结果可以“封装”一张虚拟表。如下代码:

select xnb_name,y.yid

from yanyuan y,

(select id,name from employee) as xnb(xnb_id,xnb_name)

where

y.yid=xnb.xnb_id

查询结果如下:

注意:当多表查询时,为了防止笛卡儿集的发生,where条件的个数至少应该是表的个数减一个!

连接查询

连接查询相当于嵌套查询。请看下例:

select *

from employee join yanyuan

on employee.id=yanyuan.yid

查询结果如下:

左外连接:保留左边不匹配的行,这些行中与右表中的列相对应的单元格被设置成NULL,结果集的行数与左表同

select *

from employee e left outer join yanyuan y

on e.id=y.yid

结果如下图:

右外连接:保留右边不匹配的行,这些行中与左表中的列相对应的单元格被设置成NULL,结果集的行数与右表同

select *

from employee e right outer join yanyuan y

on e.id=y.yid

结果如下图:

五大约束

NOT NULL 约束

NOT NULL指定不接受 NULL 值的列。如果用SQL语句创建表那么可以给某列加该约束,如:

create table temp

(

id int not null,

name char(20)

)

CHECK约束

CHECK 约束对可以放入列中的值进行限制,以强制执行域的完整性。 CHECK 约束指定应用于列中输入的所有值的布尔(取值为 true 或 false)搜索条件,拒绝所有不取值为 true 的值。可以为每列指定多个 check 约束。下例显示名为 chk_age 约束的创建,该约束确保只对此关键字输入指定范围内的数字,以进一步强制执行主键的域。如:

create table tempa

(

id int not null,

name char(50),

age int,

constraint chk_age check(age between 3 and 150)

)

UNIQUE约束

UNIQUE 约束在列集内强制执行值的唯一性。 对于 UNIQUE 约束中的列,表中不允许有两行包含相同的非空值。主键也强制执行唯一性,但主键不允许空值。UNIQUE 约束优先于唯一索引。

alter table temp

add constraint un unique(name) --un是该约束的名字

PRIMARY KEY约束

PRIMARY KEY 约束标识列或列集,这些列或列集的值唯一标识表中的行。

在一个表中,不能有两行包含相同的主键值。不能在主键内的任何列中输入 NULL 值。在数据库中 NULL 是特殊值,代表不同于空白和 0 值的未知值。建议使用一个小的整数列作为主键。每个表都应有一个主键。

一个表中可以有一个以上的列组合,这些组合能唯一标识表中的行,每个组合就是一个候选键。数据库管理员从候选键中选择一个作为主键。例如,在 tempb 表中,id 和 name 都可以是候选键,但是只将 id 选作主键。

create table tempb

(

id int primary key,

name char(30),

weight decimal(6,2),

)

FOREIGN KEY 约束

FOREIGN KEY 约束标识表之间的关系。

一个表的外键指向另一个表的候选键。当外键值没有候选键时,外键可防止操作保留带外键值的行。在下例中,tempc 表建立一个外键引用前面定义的 tempb 表。下面只不过是一个简单示例。

create table tempc

(

cid int,

bid int

FOREIGN KEY REFERENCES tempb(id)

ON DELETE NO ACTION,

age int

)

如果一个外键值没有候选键,则不能插入带该值(NULL 除外)的行。如果尝试删除现有外键指向的行,ON DELETE 子句将控制所采取的操作。ON DELETE 子句有两个选项:

NO ACTION 指定删除因错误而失败。

CASCADE 指定还将删除包含指向已删除行的外键的所有行。

如果尝试更新现有外键指向的候选键值,ON UPDATE 子句将定义所采取的操作。它也支持 NO ACTION 和 CASCADE 选项。

列约束和表约束

约束可以是列约束或表约束:

列约束被指定为列定义的一部分,并且仅适用于那个列(前面的示例中的约束就是列约束)。

表约束的声明与列的定义无关,可以适用于表中一个以上的列。

当一个约束中必须包含一个以上的列时,必须使用表约束。

例如,如果一个表的主键内有两个或两个以上的列,则必须使用表约束将这两列加入主键内。假设有一个表记录工厂内的一台计算机上所发生的事件。假定有几类事件可以同时发生,但不能有两个同时发生的事件属于同一类型。这一点可以通过将 type 列和 time 列加入双列主键内来强制执行。

create table tempd

(

id int,

time datetime,

part char(50),

beizhu char(1024),

constraint tempd_key PRIMARY KEY (id, time)

)

索引

可以利用索引快速访问数据库表中的特定信息。索引是对数据库表中一个或多个列(例如,employee 表的姓氏 (name) 列)的值进行排序的结构,类似于一本书的目录,可以通过目录快速的找到用户所需要的数据。如果想按特定职员的姓来查找他或她,则与在表中搜索所有的行相比,索引有助于更快地获取信息。

索引提供指针以指向存储在表中指定列的数据值,然后根据指定的排序次序排列这些指针。数据库使用索引的方式与使用书的目录很相似:通过搜索索引找到特定的值,然后跟随指针到达包含该值的行。

在数据库关系图中,可以为选定的表创建、编辑或删除索引/键属性页中的每个索引类型。当保存附加在此索引上的表或包含此表的数据库关系图时,索引同时被保存。

通常情况下,只有当经常查询索引列中的数据时,才需要在表上创建索引。索引将占用磁盘空间,并且降低添加、删除和更新行的速度。不过在多数情况下,索引所带来的数据检索速度的优势大大超过它的不足之处。然而,如果应用程序非常频繁地更新数据,或磁盘空间有限,那么最好限制索引的数量。在创建索引前,必须确定要使用的列和要创建的索引类型。

索引列

可基于数据库表中的单列或多列创建索引。当某些行中的某一列具有相同的值时,多列索引能区分开这些行。

如果经常在同时搜索两列或多列或按两列或多列排序时,索引也很有帮助。例如,如果经常在同一查询中为姓和名两列设置准则,那么在这两列上创建多列索引将很有意义。

确定索引的有效性:

检查查询中的 WHERE 和 JOIN 子句。在任一子句中包括的每一列都是索引可以选择的对象。

考虑表中已创建的索引数量。最好不要在一个表中创建大量的索引。

检查表中已创建的索引定义。最好避免包含共享列的重叠索引。

检查列中唯一数据值的数量,并与表中的行数进行比较。比较的结果就是该列的可选择性,这有助于确定该列是否适合建立索引,如果适合,确定索引的类型是什么。

索引类型

根据数据库的功能,可在数据库设计器中创建三种类型的索引 — 唯一索引、主键索引和聚集索引。

唯一索引

唯一索引不允许两行具有相同的索引值。如果现有数据中存在重复的键值,则大多数数据库都不允许将新创建的唯一索引与表一起保存。当新数据将使表中的键值重复时,数据库也拒绝接受此数据

主键索引

数据库表通常有一列或列组合,其值用来唯一标识表中的每一行。该列称为表的主键。在数据库关系图中为表定义一个主键将自动创建主键索引,主键索引是唯一索引的特殊类型。主键索引要求主键中的每个值是唯一的。当在查询中使用主键索引时,它还允许快速访问数据。

聚集索引

在聚集索引中,表中各行的物理顺序与键值的逻辑(索引)顺序相同。表只能包含一个聚集索引。如果不是聚集索引,表中各行的物理顺序与键值的逻辑顺序不匹配。聚集索引比非聚集索引有更快的数据访问速度。

下面给temp表创建一个唯一的聚集索引

alter table temp

add constraint suoyi UNIQUE CLUSTERED(id)

也可以用以下命令

create unique clustered index tempc_id

on tempc (cid)

DEFAULT约束

缺省值约束。有时候给表里插入一行记录的时候,某些字段是不想插入的,或者由系统函数来插入。那么这时候可以给该列定义一个缺省值,如下:

alter table temp

add temp_date smalldatetime

constraint temp_def default getdate() with values

数据库的备份与恢复

联机备份恢复

在大多数情况下,可以对数据库进行备份与恢复。如果数据被各种因数遭到破坏,那么使用备份文件可以对数据库进行最大限度的恢复。要对数据库备份,首先要创建一个备份设备。SQL代码如下:

USE master

EXEC sp_addumpdevice 'disk', 'studentback',

'e:\1228\student.dat'

-- Back up the full student database.

BACKUP DATABASE student TO studentback

对数据库恢复的SQL语句如下:

RESTORE DATABASE student

FROM studentback

脱机备份恢复

可以使数据库暂时的脱离DBMS的控制进行备份,称为脱机备份。

操作如图:

脱机后可以直接将数据库文件(.mdf .ndf .ldf)拷贝到其它磁盘以备份;

恢复可以通过以下操作完成:

也可以在查询分析器中完成:

Sql Server数据库设计(ER图事例)

下面是一个简单的信息系统中员工和权限的数据库设计。

打开查询分析器加上下面语句

alter table 员工权限对应表

add constraint 双主键 primary key(员工编号,权限编号)

内容总结

? 数据库查询语句使用:

? 使用通配符实现模糊查询:%表示零个或多个字符或者数字,_表示一个或零个字符。并且要使用like,不能使用=

? order可以对表中的数据根据某些字段进行升序或者降序排序。

? union用于把不同表中具有相同类型的字段联合输出。

? 对数据进行差集合交集操作。

? 使用sql server中的函数进行数据处理。

? 使用group进行分组查询。

? 数据类型转换函数的使用。

? 表连接的操作:内连接与外连接(左外连接,右外连接,全外连接)

? 约束使用

? NOT NULL:被定义列的值不能为空(NULL),主要对字符型字段;

? CHECK:被定义列的值只能是check中定义的值

? UNIQUE:被定义列的值不能有重复值,但可以有NULL值,但只能有一个NULL值。

? PRIMARY KEY:与UNIQUE类似,但不能有NULL值。

? FOREIGN KEY: 被定义列的值必须是另一个表中指定列中的值。

? DEFAULT:被定义列的值的默认值。

? 索引

? 对数据库表中一个或多个列(例如,employee 表的姓氏 (name) 列)的值进行排序的结构

? 类型包括:

? 唯一索引、主键索引和聚集索引

? 数据恢复与备份

? 使用企业管理器进行数据的恢复备份

? 在查询分析器中进行数据恢复备份。

独立实践

1. 设计员工表(Employee)和部门表(Department):

? Employee中必须要有主键

? 年龄必须在18-60

? 名字不能为NULL

? 部门必须是部门表中的值

? 工作地默认为”sz”

? Department中必须要有主键

? 部门名称不能为NULL,且不能重复

2. 使用查询语句把Employee表中name字段中没有重复名字的员工名字输出

3. 设计一个简单的超市信息系统的数据库。在程序块里写一个事务分成两个单元(一个存储点)存储点之前给emp表插入一行数据,存储点之后给emp表的某一行修改下age ,然后查询该行的age ,如果age > 40 回滚到存储点提交事 务,如果条件不满足,提交整个事务

4. 给%TYPE %ROWTYPE RECORD TABLE四种类型分别举出相应的例子

5. 定义一个函数,输入一个大于0的整数n, 返回1到n的整数和

6. 利用游标从dept表打印出所有的部门名字

第十八章:数据库(三)

学习目标:

? 存储过程

? 触发器

? 函数

? 规则

? 事务

? while语句

? case 语句

存储过程

存储过程是数据库管理系统里的一个很重要的对象。它把多个SQL语句放在一个存储过程里面,起到封装功能的作用。类似在面向对象中,封装对象的一个功能一样。几乎任何可写成批处理的 Transact-SQL 代码都可用于创建存储过程。

存储过程的设计规则包括:

? CREATE PROCEDURE 定义本身可包括除下列 CREATE 语句以外的任何数量和类型的 SQL 语句,存储过程中的任意地方都不能使用下列语句:

CREATE DEFAULT CREATE TRIGGER

CREATE PROCEDURE CREATE VIEW

CREATE RULE

? 可在存储过程中创建其它数据库对象。可以引用在同一存储过程中创建的对象,前提是在创建对象后再引用对象。

? 可以在存储过程内引用临时表。

? 如果在存储过程内创建本地临时表,则该临时表仅为该存储过程而存在;退出该存储过程后,临时表即会消失。

? 如果执行调用其它存储过程的存储过程,那么被调用存储过程可以访问由第一个存储过程创建的、包括临时表在内的所有对象。

? 如果执行在远程 Microsoft? SQL Server? 2000 实例上进行更改的远程存储过程,则不能回滚这些更改。远程存储过程不参与事务处理。

? 存储过程中参数的最大数目为 2100。

? 存储过程中局部变量的最大数目仅受可用内存的限制。

? 储过程的最大大小可达 128 MB。

创建存储过程

首先要先创建一张表。创建表的SQL代码如下:

--使用我们以前创建的数据库

use student

go

--在 student数据库里创建表

if exists(select name from sysobjects

where name='stu'and type='u')

drop table stu

go

create table stu

(

s_id int primary key,

s_name char(20),

age int not null default 25 ,

b_id int

)

go

然后给该表插入些数据。代码如下:

--给表插入语句

insert into stu values(1001,'andy',25,101)

insert into stu values(1002,'jacky',16,101)

insert into stu values(1003,'lucy',20,101)

insert into stu values(1004,'gigi',28,102)

insert into stu values(1005,'lray',24,102)

创建不带参数的存储过程:

--1创建不带参数的存储过程

if exists(select name from sysobjects

where name='hh'and type='p')

drop procedure hh

go

create procedure hh

as

select count(age) 'count'

from stu

where age>20

group by b_id

go

--使用存储过程

exec hh

创建带输入参数的存储过程

--2创建带输入参数的存储过程

if exists(select name from sysobjects

where name='hc'and type='p')

drop procedure hc

go

create procedure hc @age int

as

select count(age) 'count'

from stu

where age>@age

group by b_id

go

--使用存储过程

exec hc 20

创建带输入,输出参数的存储过程

--3创建带输入,输出参数的存储过程

if exists(select name from sysobjects

where name='h3'and type='p')

drop procedure h3

go

create procedure h3 @p1 int,@p2 char(30) output

as

select @p2=s_name

from stu

where s_id=@p1

go

--调用该存储过程

declare @out char(30)

exec h3 1002,@out output

select @out

go

触发器

触发器是在用户进行某项操作的时候,会触发触发器的执行。它类似于JAVA中图形界面编程中的事件处理一样,是触发执行。和存储过程的主要区别在于:存储过程类似JAVA的对象,进行功能的封装(方法)。在调用的时候才会执行。而触发器只能在别的操作执行的时候才会触发触发器的执行。

创建触发器

--创建触发器1

if exists(select name from sysobjects

where name='trigger1'and type='tr')

drop trigger trigger1

go

create trigger trigger1

on stu

for insert

as

begin

if exists(select * from stu where age<16 or age>30)

begin

print 'b ok'

rollback

end

else

begin

print 'ok'

commit

end

end

go

--该操作会触发触发器的执行

insert into stu values(2223,'adaf',12,103)

创建触发器2

--创建出发器2

if exists(select name from sysobjects

where name='trigger2'and type='tr')

drop trigger trigger2

go

create trigger trigger2

on stu

for update

as

IF (select max(age) from stu) > 30

BEGIN

RAISERROR ('不能把年龄修改为30岁以上的,或者输入年龄大于30岁以上的', 16, 1)

ROLLBACK TRANSACTION

END

--该更新语句会触发触发器的执行

update stu

set age=116 where s_id=1001

函数

函数的概念与JAVA中类的函数类似,用于完成某个数学计算。类似存储过程。函数主要用于返回一个计算结果。而存储过程主要是封装功能:把批处理语句进行封装。

--创建函数

if exists(select name from sysobjects

where name='fun1')

drop function dbo.fun1

go

create function dbo.fun1(@p1 int,@p2 int)

returns int

as

begin

return @p1+@p2

end

--使用函数

go

select dbo.fun1(5,6)

规则

规则是一个向后兼容的功能,用于执行一些与 CHECK 约束相同的功能。CHECK 约束是用来限制列值的首选标准方法。CHECK 约束比规则更简明,一个列只能应用一个规则,但是却可以应用多个 CHECK 约束。CHECK 约束作为 CREATE TABLE 语句的一部分进行指定,而规则以单独的对象创建,然后绑定到列上。也可以把规则绑定到用户自定义数据类型上。

创建规则并把规则绑定到表的某一列上

if exists(select name from sysobjects

where name='rule1'and type='r')

drop rule rule1

go

create rule rule1

as

@v>150 and @v<200

--使用规则,绑定到表的列上

sp_bindrule rule1 ,'stu.b_id'

--插入一条记录

select * from stu

insert into stu values (1141,'sad',169,169)

--更新记录

update stu

set b_id=13

--解除对表的绑定

sp_unbindrule 'stu.b_id'

注意:把规则绑定到表的某列上时,只是从创建规则起开始执行该规则,进行对该表的这个字段进行规则约束。对于表里以前存在的数据而言不进行规则的约束!

创建规则把规则绑定到用户自定义数据类型上

--使用规则,绑定到用户自定义数据类型上

--首先定义一个数据类型

exec sp_addtype mail ,'varchar(60)','not null'

--创建一个规则

if exists(select name from sysobjects

where name='rule2'and type='r')

drop rule rule2

go

create rule rule2

as

@value like '%_@_%.__%'

--绑定规则到用户自定义数据类型上

sp_bindrule rule2,'mail'

--创建一张表

create table message

(

m_id int identity(1,1) primary key,

m_name char(16) not null,

m_mail mail

)

--给表插入一条数据

insert into message values('aa','goudan513@sohu.com')

--查看刚写的信息

select * from message

--如果插入不符合规则的数据,则插入失败

insert into message values('aa','goudan513sohu.com')

事务

事务类似于JAVA的线程的同步,作为一个单元执行。它有四大特性:原子性,隔离性,一致性,持久性。在SQL SERVER 2000里面还支持存储点的用法。大家都知道,事务是做为一个单元运行,要么全部执行,要么全部不执行。但是有时候我们可以保证事务的一部分可能正确执行,并且这些执行可以直接刷新到数据库里面。那么我们就可以在这个事务的中间部分设置一个或者多个存储点。这样在这个事务大单元里就分成了几个小部分。

如果上面的部分执行正确,下面的部分执行错误,那么就没必要回滚整个事务,只需要回滚到存储点的地方就可以了。下面请看一个带有存储点的事务执行过程。

--创建数据库

if exists(select name from master..sysdatabases

where name='student')

drop database student

go

create database student

go

use student

go

--创建表

if exists(select name from sysobjects

where name='stu'and type='u')

drop table stu

go

create table stu

(

s_id int primary key,

s_name char(20),

age int not null default 25 ,

b_id int

)

go

insert into stu values(1001,'andy',39,101)

insert into stu values(1002,'jacky',33,101)

insert into stu values(1003,'sandy',29,102)

insert into stu values(1004,'lray',19,102)

insert into stu values(1005,'jay',26,102)

insert into stu values(1006,'lucy',22,103)

select * from stu

--事务的操作

declare @status1 int

declare @status2 int

begin tran trstu

insert into stu values(6467,'hhh',28,101)

select @status1=@@error

save tran point

update stu

set s_name = '***ddddd'

--插入重复的主键值的话就会出错

insert into stu values(5167,'aa',21,102)

select @status2=@@error

if @status1<>0

rollback tran trstu

if @status2<>0

begin

print @status2

rollback tran point

end

commit tran trstu

while语句

--while循环的操作

while not exists(select * from stu where b_id>200)

begin --begin end是语句块

update stu

set b_id=b_id+5

if(select max(b_id) from stu)>150

break

else

continue

end

-- 查看修改后的记录

select * from stu

case语句

--case 语句的操作

select 'name'='来自'+

case b_id

when 151 then 'JAVA班'

when 152 then 'C++班'

when 153 then '嵌入式班'

else '无名班'

end +'的'+s_name

from stu

内容总结:

? 存储过程是数据库管理系统中把多个SQL语句放在一个工作单元中,起到封装功能的作用,可以通过存储过程名称调用。

? 创建语法:create procedure 过程名

? 触发器是在用户进行某项操作的时候,会触发触发器的执行

? 创建语法:create triggle 触发器名

? 函数:与JAVA中类的函数类似,用于完成某个功能,并且有返回值。

? 创建语法:create function 函数名

? 规则:是用于执行一些与 CHECK 约束相同的功能,并且具有可重用性。

? 创建语法:create rule规则名

? 事务类似于JAVA的线程的同步,作为一个单元执行。它有四大特性:原子性,隔离性,一致性,持久性,简称为:ACID

独立实践

1、 做一个员工表,对该表进行操作。要求用到存储过程给表进行插入数据,触发器检测插入的数据正确不正确比如性别。要在触发器和存储过程里面用到事务的操作。

2、 使用触发器创建一个只读表

3、 使用触发器在向员工表插入员工时,自动更新部门表中部门的员工数。

4、 创建函数,根据员工的编号,返回员工的名字。

5、 从控制台打印下列信息(用签套的循环,外层循环控制行,内层循环控制*号的个数以及出现的样式)

*

***

*****

*******

*****

***

*

第十九章: JDBC基础

学习目标

? JDBC的概念

? 连接数据库的几种方式

? JAVA编程语言和JDBC

? JDBC编程的步骤

? 通过ODBC建立连接

? 通过SQLSERVER 提供的驱动程序获得连接

? 通过ORACLE提供的驱动程序获得连接

? 通过数据源获得连接

? 通过连接池获得连接

? 总结数据库连接的各种方式

JDBC的概念

JDBC(Java Database Connectivity)是Sun提供的一套数据库编程接口API,由Java语言编写的类、接口组成。其体系结构如下图:

(JDBC体系结构图)

用JDBC写的程序能够自动地将SQL语句传送给相应的数据库管理系统。不但如此,使用Java编写的应用程序可以在任何支持Java的平台上运行,不必在不同的平台上编写不同的应用。Java和JDBC的结合可以让开发人员在开发数据库应用程序时真正实现“Write Once,Run Everywhere!”

连接数据库的几种方式

第一种就是ODBC(Open Database Connection 开放式数据库连接)桥连接:ODBC桥连接是通过操作系统里面的数据源连接到各种不同的数据库。绝大多数数据库都支持操作系统里面的数据源。也提供了相应的驱动程序。包括:SQLSERVER2000,ORACLE9i等等。在JAVA刚开始的时候,SUN公司提供了用于ODBC桥连接的驱动程序JDBC-ODBC桥驱动程序,它把JDBC的调用转换为ODBC操作。这个桥使得所有支持ODBC的DBMS都可以和Java应用程序交互,但仅适用于windows平台,适用于jdbc初学者。

第二种驱动程序也称为部分Java驱动程序(native-API partly-Java Driver),因为它们直接将JDBC API翻译成具体数据库的API。也就是本地库Java驱动程序,将JDBC调用转换为对数据库的客户端API的调用。

第三种驱动程序是网络驱动程序(net-protocol all-java driver(JDBC Proxy)),它将JDBC API转换成独立于数据库的协议。JDBC驱动程序并没有直接和数据库进行通讯;它和一个中间件服务器通讯,然后这个中间件服务器和数据库进行通讯。这种额外的中间层次提供了灵活性:可以用相同的代码访问不同的数据库,因为中间件服务器隐藏了Java应用程序的细节。

第四种驱动程序是纯Java驱动程序(native-protocal all-Java driver),它直接与数据库进行通讯。很多程序员认为这是最好的驱动程序,因为它通常提供了最佳的性能,并允许开发者利用特定数据库的功能。当然,这种紧密偶合会影响灵活性,特别是如果您需要改变应用程序中的底层数据库时。这种驱动程序通常用于applet和其它高度分布的应用程序。适用于企业应用。

如下图所示:

java基础 - 1相关推荐

  1. Java基础入门语法和安装

    1. Java概述 1.1 Java语言背景介绍(了解) 语言:人与人交流沟通的表达方式 计算机语言:人与计算机之间进行信息交流沟通的一种特殊语言 Java语言是美国Sun公司(Stanford Un ...

  2. Java笔记整理-02.Java基础语法

    1,标识符 由英文字母.数字._(下划线)和$组成,长度不限.其中英文字母包含大写字母(A-Z)和小写字母(a-z),数字包含0到9. 标识符的第一个字符不能是数字(即标识符不能以数字开头). 标识符 ...

  3. java基础(十三)-----详解内部类——Java高级开发必须懂的

    java基础(十三)-----详解内部类--Java高级开发必须懂的 目录 为什么要使用内部类 内部类基础 静态内部类 成员内部类 成员内部类的对象创建 继承成员内部类 局部内部类 推荐博客 匿名内部 ...

  4. Java基础概念性的知识总结

    属于个人的所学的知识总结,不是全面的 1.JDK.JRE和JVM三者的区别 01.JDK:(Java Development ToolKit)Java开发工具包,是整个Java的核心.包括了Java的 ...

  5. 我的面试标准:第一能干活,第二Java基础要好,第三最好熟悉些分布式框架!...

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:hsm_computer www.cnblogs.com/J ...

  6. 叮!您收到一份超值Java基础入门资料!

    摘要:Java语言有什么特点?如何最大效率的学习?深浅拷贝到底有何区别?阿里巴巴高级开发工程师为大家带来Java系统解读,带你掌握Java技术要领,突破重点难点,入门面向对象编程,以详细示例带领大家J ...

  7. java重要基础知识点_必看 | 新人必看的Java基础知识点大梳理

    原标题:必看 | 新人必看的Java基础知识点大梳理 各位正在认真苦学Java的准大神,在这烈日炎炎的夏季里,老九君准备给大家带来一个超级大的"冰镇西瓜,"给大家清凉一下,压压惊. ...

  8. Java基础-Date类常用方法介绍

    Java基础-Date类常用方法介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.毫秒值概念 我们在查阅Date类的API文档时,会发现这样的一句话:"The cl ...

  9. [Spring 深度解析]第1章 Java基础

    第1章 ◄Java基础► 在学习Spring之前我们需要对Java基础语法有一定的了解,Java中最重要的两个知识点是注解和反射.注解和反射在Spring框架中应用的最广泛.掌握注解和反射,有助于后面 ...

  10. 清华学长免费分享Java基础核心知识大总结(1)

    自学Java,如果觉得看<Java编程思想>或者<Core Java>等之类的"圣经"觉得内容太多,一下子吃不透的话,不妨看看这本<Java基础核心总 ...

最新文章

  1. 1075 PAT Judge
  2. 使用AWSTATS自动分析Nginx日志
  3. Xcode升级后插件失效的原理与修复办法
  4. php重置下标有什么用,怎么在PHP中删除空数组并重置数组键名
  5. sklearn中的正则化
  6. POJ2560-雀斑(Freckles)【图论,并查集,最小生成树,KURUSKAL】
  7. gitlab定期备份_如何在一分钟内让GitLab为您做定期工作
  8. 如何开始第一个开源项目?
  9. webpack--安装,使用
  10. zpf框架的business使用方法
  11. xsl判断节点存在_[剑指offer]25删除链表中重复的节点
  12. dump java 分析工具,java内存分析工具 jmap,jhat及dump分析
  13. java把秒时长转换为分钟_java - 将秒值转换为小时分钟秒?
  14. igs时间和utc_UTC时间与北京时间的差多久?
  15. 图解机器学习神器:Scikit-Learn
  16. django.db.utils.OperationalError: (1050, Table 'xxx' already exists)
  17. js获取摄像头权限实现拍照功能
  18. js正则表达式过滤表情,输入法表情无法匹配,则反向判断
  19. 达梦数据库亮相第七届中国国际国防电子展览会
  20. DHT11 数字温湿度传感器实验

热门文章

  1. Axure VS Mockplus VS Balsamiq - 原型图设计工具对比
  2. AUV控制中的反步法
  3. 基于Django的商城开发项目笔记(一)
  4. python视频格式转换命令_python怎么实现文件格式的转换 批处理使用ffmpeg为mp4批量加入字幕...
  5. Android App支付:支付宝SDK接入详细指南(附官方支付demo)
  6. 《安富莱嵌入式周报》第254期:2022.02.21--2022.02.27
  7. 老友记台词中英文对照Part4
  8. Python中的shape[0]、shape[1]和shape[-1]含义
  9. 315 433MHZ无线遥控接收解码源程序 Keil源程序 含AD格式电路图
  10. R语言一键制作数据统计三线表(一)