- “为什么面向对象的编程会在软件开发领域造成如此震憾的影响?”
- Java和因特网
- 一切都是对象
- 控制程序流程
- 初始化和清除
- 总结:
- 隐藏实施过程
- 接口与实现
- 类访问
- 总结:
- 类再生
- 合成与继承的结合
- 到底选择合成还是继承
- protected
- 累积开发
- 上溯造型
- final关键字
- 初始化和类装载
- 多形性
- 构建器和多形性
- 通过继承进行设计
- 总结:
- 对象的容纳
- 集合
- 枚举器(反复器)
- 集合类型
- 排序
- 通用集合库
- 新集合
- 总结:
- 违例差错控制
- 标准java违例
- 创建自己的违例
- 违例的限制
- 用finally清除
- 构建器
- 违例匹配
- 总结:
- Java10 系统
- 增添属性和有用的接口
- 本身的缺陷:RandomAccessFile
- File类
- IO流的典型应用
- StreamTokenizer
- Java 1.1的IO流
- 压缩
- 对象序列化
- 总结
- 第11章 运行期类型鉴定
- RTTI语法
- 反射:运行期类信息
- 总结:
- 传递和返回对象
- 制作本地副本
- 克隆的控制
- 只读类
- 总结:
- 创建窗口和程序片
- 制作按钮
- 捕获事件
- 文本字段
- 文本区域
- 标签
- 复选框
- 单选钮
- 列表框
- 布局的控制
- action的替代品
- 程序片的局限
- 视窗化应用
- 新型AWT
- Java 1.1用户接口API
- 桌面颜色
- 可视编程和Beans
- Swing入门(注释⑦)
- 总节:
- 多线程
- 共享有限的资源
- 堵塞
- 优先级
- 套接字
- 远程方法
- 范式的概念
- 观察器范式
- 模拟垃圾回收站
- 项目
- 复杂性理论
- 总结
- 附录A使用非JAVA代码
- 微软的解决方案
- J/Direct
- @dll.import引导命令
- 总结本内容都来自:
“为什么面向对象的编程会在软件开发领域造成如此震憾的影响?”
- 优点:
对管理人员,它实现了更快和更廉价的开发与维护过程。
对分析与设计人员,建模处理变得更加简单,能生成清晰、易于维护的设计方案。
对程序员,对象模型显得如此高雅和浅显。
- 缺点:
抽象
所有编程语言的最终目的都是提供一种“抽象”方法。(就是把机械语言抽象成人类可以抽象成理解的)
- 1.所有东西都是对象
可将对象想象成一种新型变量;它保存着数据,但可要求它对自身进行操作。理论上讲,可从要解决的问题身上提出所有概念性的组件,然后在程序中将其表达为一个对象。
2.程序是一大堆对象的组合;
通过消息传递,各对象知道自己该做些什么。为了向对象发出请求,需向那28个对象“发送一条消息”。更具体地讲,可将消息想象为一个调用请求,它调用的是从属于目标对象的一个
子例程或函数。- 每个对象都有自己的存储空间,可容纳其他对象。
或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
- 4.每个对象都是一种类型
根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
- 5.同一类所有对象都接收相同的消息
这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消
息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括
“圆”。这一特性称为对象的“可替换性”,是OOP 最重要的概念之一。
接口
class关键字
- 有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”是那个接口的一种特殊实现方
实现方案的隐藏(封装)
- 为方便后面的讨论,让我们先对这一领域的从业人员作一下分类。从根本上说,大致有两方面的人员涉足面
向对象的编程:“类创建者”(创建新数据类型的人)以及“客户程序员”(在自己的应用程序中采用现成
数据类型的人;注释④)。对客户程序员来讲,最主要的目标就是收集一个充斥着各种类的编程“工具
箱”,以便快速开发符合自己要求的应用。而对类创建者来说,他们的目标则是从头构建一个类,只向客户
程序员开放有必要开放的东西(接口),其他所有细节都隐藏起来。为什么要这样做?隐藏之后,客户程序
员就不能接触和改变那些细节,所以原创者不用担心自己的作品会受到非法修改,可确保它们不会对其他人
造成影响。
“接口”(Interface)规定了可对一个特定的对象发出哪些请求。
然而,必须在某个地方存在着一些代码,以便满足这些请求。这些代码与那些隐藏起来的数据便叫作“隐藏的实现”。
封装的原因:
第一个原因是防止程序员接触他们不该接触的东西——通常是内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,毋需明白这些信息。我们
向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。
第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。
public,private,protected 以及暗示性的friendly.
- Java 采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边界:就是上面的这4种,若未明确指定其他关键字,则默认为后者。
- “public”(公共)意味着后续的定义任何人均可使用。
- “private”(私有)意味着除您自己、类型的创建者以及那个类型的内部函数成员,其他任何人都不能访问后续的定义信息。private 在您与客户程序员之间竖起了一堵墙。若有人试图访问私有30成员,就会得到一个编译期错误。
- “friendly”(友好的)涉及“包装”或“封装”(Package)的概念——即Java 用来构建库的方法。
若某样东西是“友好的”,意味着它只能在这个包装的范围内使用
“protected”(受保护的)与“private”相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。
方案的重复使用
创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。
继承:重复使用接口
前景:我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现大致相同的功能,那会是一件非常令人灰心的事情。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。
但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子
类)也会反映出这种变化。在 Java 语言中,继承是通过 extends 关键字实现的使用继承时,相当于创建了一个新类。
改善基础类
尽管extends 关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定如此。为区分我们的新类,第二个办法是改变基础类一个现有函数的行为。我们将其称作“改善”那个函数。
等价与类似关系
针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若答案是肯定的,则衍生类型就是与基础类完全相同的类型,因为都拥有完全相同的接口。
多形对象的互换使用
通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。
void doStuff(Shape s) {
s.erase();
// 删除和描述,shape是几何形状
s.draw();
}
如果我们在其他一些程序里使用 doStuff()函数:
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
动态绑定
在doStuff()的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当的。
抽象的基础类和接口
设计程序时,我们经常都希望基础类只为自己的衍生类提供一个接口。也就是说,我们不想其他任何人实际创建基础类的一个对象,只对上溯造型成它,以便使用它们的接口。
为达到这个目的,需要把那个类变成33“抽象”的——使用abstract 关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这种工具可有效强制实行一种特殊的设计。
对象的创建和存在时间
最重要的问题之一是对象的创建及破坏方式。
Java 确实提供了一个垃圾收集器(Smalltalk 也有这样的设计;尽管 Delphi 默认为没有垃圾收集器,但可选择安装;而 C++亦可使用一些由其他公司开发的垃圾收集产品)。
集合与继承器
针对一个特定问题的解决,如果事先不知道需要多少个对象,或者它们的持续时间有多长,那么也不知道如何保存那些对象。
单根结构
在面向对象的程序设计中,由于C++的引入而显得尤为突出的一个问题是:所有类最终是否都应从单独一个基础类继承。在Java 中(与其他几乎所有OOP 语言一样),对这个问题的答案都是肯定的,而且这个终级基础类的名字很简单,就是一个“Object”。这种“单根结构”具有许多方面的优点。
集合库与方便使用的集合
由于集合是我们经常都要用到的一种工具,所以一个集合库是十分必要的,它应该可以方便地重复使用。
下溯造型与模板/通用性
为了使这些集合能够重复使用,或者“再生”,Java 提供了一种通用类型,以前曾把它叫作“Object”。单根结构意味着、所有东西归根结底都是一个对象”!所以容纳了Object 的一个集合实际可以容纳任何东西。这使我们对它的重复使用变得非常简便。
清除时的困境:由谁负责清除?
每个对象都要求资源才能“生存”,其中最令人注目的资源是内存。
注意这一点只对内存堆里创建的对象成立(用 new 命令创建的)。但在另一方面,对这儿描述的问题以及其他所有常见的编程问题来说,都要求对象在内存堆里创建。违规控制:解决错误
对大多数错误控制方案来说,最主要的一个问题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。如果程序员不够警惕——若比较匆忙,这几乎是肯定会发生的——程序所依赖的错误控制方案便会失效。
“违例控制”将错误控制方案内置到程序设计语言中,有时甚至内建到操作系统内。这里的“违例”(Exception)属于一个特殊的对象,它会从产生错误的地方“扔”或“掷”出来。多线程
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。
有些时候,中断对那些实时性很强的任务来说是很有必要的。但还存在其他许多问题,它们只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求。在一个程序中,这些独立运行的片断叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程处理一个常见的例子就是用户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后才开始响应。Java 也提供了有限的资源锁定方案。
它能锁定任何对象占用的内存(内存实际是多种共享资源的一种),所以同一时间只能有一个线程使用特定的内存空间。为达到这个目的,需要使用synchronized 关键字。其他类型的资源必须由程序员明确锁定,这通常要求程序员创建一个对象,用它代表一把锁,所有线程在访问那个资源时都必须检查这把锁。
永久性
创建一个对象后,只要我们需要,它就会一直存在下去。但在程序结束运行时,对象的“生存期”也会宣告结束。
Java和因特网
Java 除了可解决传统的程序设计问题以外,还能解决World Wide Web(万维网)上的编程问题。
什么是Web?
我们在这里有必要作一些深入的探讨,但在这之前,必须理解客户机/服务器系统的概念,这是充斥着许多令人迷惑的问题的又一个计算领域。
客户机/服务器计算
- 客户机/服务器计算
客户机/服务器系统的基本思想是我们能在一个统一的地方集中存放信息资源。一般将数据集中保存在某个数据库中,根据其他人或者机器的请求将信息投递给对方。
事务的说法:这样看来,客户机/服务器的基本概念并不复杂。这里要注意的一个主要问题是单个服务器需要同时向多个客户提供服务。在这一机制中,通常少不了一套数据库管理系统,使设计人员能将数据布局封装到表格中,以获得最优的使用。除此以外,系统经常允许客户将新信息插入一个服务器。这意味着必须确保客户的新数据不会与其他客户的新数据冲突,或者说需要保证那些数据在加入数据库的时候不会丢失(用数据库的术语来说,这叫作“事务处理”)。
web是一个巨大的服务器
Web 实际就是一套规模巨大的客户机/服务器系统。
web就是在客户端编程,那后端就是面对服务器编程哟。客户端编程
Web 最初采用的“服务器-浏览器”方案可提供交互式内容,但这种交互能力完全由服务器提供,为服务器和因特网带来了不小的负担。
“通用网关接口”(CGI)
用户提交的信息通过所有Web 服务器均能支持的“通用网关接口”(CGI)回传到服务器。
- 插件
朝客户端编程迈进的时候,最重要的一个问题就是插件的设计。利用插件,程序员可以方便地为浏览器添加新功能,用户只需下载一些代码,把它们“插入”浏览器的适当位置即可。
- 脚本编程语言
插件造成了脚本编制语言的爆炸性增长。通过这种脚本语言,可将用于自己客户端程序的源码直接插入 HTML页,而对那种语言进行解释的插件会在 HTML 页显示的时候自动激活。
脚本语言一般都倾向于尽量简化,易于理解。
- Java
如果说一种脚本编制语言能解决80%的客户端程序设计问题,那么剩下的20%又该怎么办呢?它们属于一些 高难度的问题吗?目前最流行的方案就是 Java。
它不仅是一种功能强大、高度安全、可以跨平台使用以及国际通用的程序设计语言,也是一种具有旺盛生命力的语言。
- ActiveX
在某种程度上,Java 的一个有力竞争对手应该是微软的 ActiveX,尽管它采用的是完全不同的一套实现机制。
- 安全
自动下载和通过因特网运行程序听起来就象是一个病毒制造者的梦想。在客户端的编程中,ActiveX 带来了最让人头痛的安全问题。
因特网和内联网
Web 是解决客户机/服务器问题的一种常用方案,所以最好能用相同的技术解决此类问题的一些“子集”,特别是公司内部的传统客户机/服务器问题。
服务器端编程
我们的整个讨论都忽略了服务器端编程的问题。如果向服务器发出一个请求,会发生什么事情?大多数时候的请求都是很简单的一个“把这个文件发给我”。浏览器随后会按适当的形式解释这个文件:作为HTML 页、一幅图、一个Java 程序片、一个脚本程序等等。
其中包括基于Java 的 Web 服务器,它允许我们用Java 进行所有服务器端编程,写出的程序就叫作“小服务程序”(Servlet)。
一个独立的领域:应用程序
与Java 有关的大多数争论都是与程序片有关的。Java 实际是一种常规用途的程序设计语言,可解决任何类型的问题,至少理论上如此。
分析和设计
面向对象的范式是思考程序设计时一种新的、而且全然不同的方式,许多人最开始都会在如何构造一个项目上皱起了眉头。
不要迷失
在整个开发过程中,最重要的事情就是:不要将自己迷失!但事实上这种事情很容易发生。
- 时刻提醒自己注意以下几个问题:
(1) 对象是什么?(怎样将自己的项目分割成一系列单独的组件?)
(2) 它们的接口是什么?(需要将什么消息发给每一个对象?)
阶段0:拟出一个计划
第一步是决定在后面的过程中采取哪些步骤。
这听起来似乎很简单(事实上,我们这儿说的一切都似乎很简单),但很常见的一种情况是:有些人甚至没有进入阶段 1,便忙忙慌慌地开始编写代码。如果你的计划本来就是“直接开始开始编码”,那样做当然也无可非议(若对自己要解决的问题已有很透彻的理解,便可考虑那样做)。但最低程度也应同意自己该有个计划。
阶段1:要制定什么?
在上一代程序设计中(即“过程化或程序化设计”),这个阶段称为“建立需求分析和系统规格”。
需求分析的意思是“建立一系列规则,根据它判断任务什么时候完成,以及客户怎样才能满意”。
系统规格则表示“这里是一些具体的说明,让你知道程序需要做什么(而不是怎样做)才能满足要求”。
阶段2:如何构建
在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上是什么样子,以及相互间是如何沟通的。此时可考虑采用一种特殊的图表工具:“统一建模语言”(UML)。
阶段3:开始创建
读这本书的可能是程序员,现在进入的正是你可能最感兴趣的阶段。
由于手头上有一个计划——无论它有多么简要,而且在正式编码前掌握了正确的设计结构,所以会发现接下去的工作比一开始就埋头写程序要简单得多。
阶段4:校订
事实上,整个开发周期还没有结束,现在进入的是传统意义上称为“维护”的一个阶段。
“维护”是一个比较暧昧的称呼,可用它表示从“保持它按设想的轨道运行”、“加入客户从前忘了声明的功能”或者更传统的“除掉暴露出来的一切臭虫”等等意思。
什么时候才叫“达到理想的状态”呢?这并不仅仅意味着程序必须按要求的那样工作,并能适应各种指定的“使用条件”,它也意味着代码的内部结构应当尽善尽美。至少,我们应能感觉出整个结构都能良好地协调运作。没有笨拙的语法,没有臃肿的对象,也没有一些华而不实的东西。除此以外,必须保证程序结构有很强的生命力。由于多方面的原因,以后对程序的改动是必不可少。但必须确定改动能够方便和清楚地进行。这里没有花巧可言。
计划的回报
java还是c++
Java 特别象 C++;由此很自然地会得出一个结论:C++似乎会被Java 取代。
无论如何,C++仍有一些特性是Java 没有的。而且尽管已有大量保证,声称Java 有一天会达到或超过C++的速度。
同时,考虑到 Java 还拥有我迄今为止尚未在其他任何一种语言里见到的最“健壮”的类型检查及错误控制系统,所以Java 确实能大大提高我们的编程效率。这一点是勿庸置疑的!
一切都是对象
“尽管以C++为基础,但 Java 是一种更纯粹的面向对象程序设计语言”。
无论C++还是Java 都属于杂合语言。但在 Java 中,设计者觉得这种杂合并不象在 C++里那么重要。杂合语言允许采用多种编程风格;之所以说 C++是一种杂合语言,是因为它支持与 C 语言的向后兼容能力。由于C++是 C 的一个超集,所以包含的许多特性都是后者不具备的,这些特性使 C++在某些地方显得过于复杂。
用句柄操纵对象
每种编程语言都有自己的数据处理方式。
或处理过一些间接表示的对象吗(C 或C++里的指针)?
所有这些在 Java 里都得到了简化,任何东西都可看作对象。
所有对象都必须创建
创建句柄时,我们希望它同一个新对象连接。
因此,一种更安全的做法是:创建一个句柄时,记住无论如何都进行初始化:
String s = "asdf";
通常用new 关键字达到这一目的。new 的意思是:“把我变成这些对象的一种新类型”。
String s=new String("刘")
对象保存到什么地方
程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的是内存的分配。有六个地方都可以保存数据:
- (1) 寄存器。
这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。
- (2) 堆栈。
驻留于常规 RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。
- (3) 堆。
一种常规用途的内存池(也在 RAM 区域),其中保存了Java 对象。和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。
- (4) 静态存储。
这儿的“静态”(Static)是指“位于固定位置”(尽管也在 RAM 里)。
- (5) 常数存储。
常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。
- (6) 非RAM 存储。
若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”。
特殊情况:主要类型
有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。
Java 决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是 Java 程序具有很强移植能力的原因之一。
主类型 大小 最小值 最大值 封装器类型
boolean 1 位 - - Boolean
char 16 位 Unicode 0 Unicode 2 的 16 次方-1 Character
byte 8 位 -128 +127 Byte(注释①)
short 16 位 -2 的15 次方 +2 的 15 次方-1 Short(注释①)
int 32 位 -2 的 31 次方 +2 的31 次方-1 Integer
long 64 位 -2 的63 次方 +2 的 63 次方-1 Long
float 32 位 IEEE754 IEEE754 Float
double 64 位 IEEE754 IEEE754 Double
注意:①:到 Java 1.1 才有,1.0 版没有。
数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。主数据类型也拥有自己的“封装器”(wrapper)类。这意味着假如想让堆内一个非主要对象表示那个主类
型,就要使用对应的封装器。
例如:
char c = 'x';
Character C = new Character('c');
也可以直接使用:
Character C = new Character('x');
高精度数字
Java 1.1 增加了两个类,用于进行高精度的计算:BigInteger 和 BigDecimal。尽管它们大致可以划分为“封装器”类型,但两者都没有对应的“主类型”。
也就是说,能对int 或 float 做的事情,对BigInteger 和BigDecimal 一样可以做。只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢
失任何信息。
BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的币值计算。
java的数组
几乎所有程序设计语言都支持数组。
- Java 的一项主要设计目标就是安全性。
创建对象数组时,实际创建的是一个句柄数组。而且每个句柄都会自动初始化成一个特殊值,并带有自己的关键字:null(空)。一旦 Java 看到null,就知道该句柄并未指向一个对象。正式使用前,必须为每个句柄都分配一个对象。若试图使用依然为null 的一个句柄,就会在运行期报告问题。因此,典型的数组错误在Java 里就得到了避免。
绝对不要清除对象
在大多数程序设计语言中,变量的“存在时间”(Lifetime)一直是程序员需要着重考虑的问题。
作用域
- 大多数程序设计语言都提供了“作用域”(Scope)的概念。对于在作用域里定义的名字,作用域同时决定了它的“可见性”以及“存在时间”。
在C,C++和 Java 里,作用域是由花括号的位置决定的。
例如:
{
int x = 12;
/* only x available */
{
int q = 96;
/* both x & q available */
}
/* only x available */
/* q “out of scope” */
}
作为在作用域里定义的一个变量,它只有在那个作用域结束之前才可使用。
对象的作用域
-
新建数据类型:类
如果说一切东西都是对象,那么用什么决定一个“类”(Class)的外观与行为呢?换句话说,是什么建立起了一个对象的“类型”(Type)呢?大家可能猜想有一个名为“type”的关键字。但从历史看来,大多数面向对象的语言都用关键字“class”表达这样一个意思:“我准备告诉你对象一种新类型的外观”。class 关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这个关键字的后面,应该跟随新数据类型的名称。
字段和方法
定义一个类时(我们在 Java 里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函(通常叫“方法”)。
- 如果是指向对象的一个句柄,则必须初始化那个句柄,用一种名为“构建器”,的特殊函数将其与一个实际对象连接起来。
每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例:
class DataOnly {
int i;
float f;
boolean b;
}
这个类并没有做任何实质性的事情,但我们可创建一个对象:
DataOnly d = new DataOnly();
可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上对象句柄的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“对象句柄.成员”。
d.i = 47;
d.f = 1.1f;
d.b = false;
一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的数据。
对于这个问题,只需保持“连接句点”即可。
myPlane.leftTank.capacity = 100;
除容纳数据之外,DataOnly 类再也不能做更多的事情,因为它没有成员函数(方法)。
主成员的默认值
若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。
主类型 默认值
Boolean false
Char '\u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
方法、自变量和返回值
迄今为止,我们一直用“函数”(Function)这个词指代一个已命名的子例程。
但在 Java 里,更常用的一个词却是“方法”(Method),代表“完成某事的途径”。
Java 的“方法”决定了一个对象能够接收的消息。它最基本的形式:
返回类型 方法名( /* 自变量列表*/ ) {
/* 方法主体 */
}
返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体的方法进行标识和引用。自变量列表列出了想传递给方法的信息类型和名称。
Java 的方法只能作为类的一部分创建。自变量列表
自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同Java 内其他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个对象的名字。
注意:
对于前面提及的“特殊”数据类型 boolean,char,byte,short,int,long,,float 以及double 来
说是一个例外。但在传递对象时,通常都是指传递指向对象的句柄。构建Java程序
正式构建自己的第一个 Java 程序前,还有几个问题需要注意。
名字的可见性
在所有程序设计语言里,一个不可避免的问题是对名字或名称的控制。
使用其他组件
一旦要在自己的程序里使用一个预先定义好的类,编译器就必须知道如何找到它。
statsic关键字
通常,我们创建类时会指出那个类的对象的外观与行为。
除非用new 创建那个类的一个对象,否则实际上并未得到任何东西。只有执行了 new 后,才会正式生成数据存储空间,并可使用相应的方法。但在两种特殊的情形下,上述方法并不堪用。一种情形是只想用一个存储区域来保存一个特定的数据——无论要创建多少个对象,甚至根本不对象。
- 另一种情形是我们需要一个特殊的方法,它没有与这个类的任何对象关联。也就是说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求,可使用static(静态)关键字。
尽管是“静态”的,但只要应用于一个数据成员,就会明确改变数据的创建方式(一个类一个成员,以及每个对象一个非静态成员)。若应用于一个方法,就没有那么戏剧化了。对方法来说,static 一项重要的用途就是帮助我们在不必创建对象的前提下调用那个方法。正如以后会看到的那样,这一点是至关重要的——特别是在定义程序运行入口方法 main()的时候。和其他任何方法一样,static 方法也能创建自己类型的命名对象。所以经常把 static 方法作为一个“领头羊”使用,用它生成一系列自己类型的“实例”。
我们的第一个Java程序
Java 标准库的 System 对象的多种方法。
注释: //
// Property.java
import java.util.*;//包名
public class Property {
public static void main(String[] args) {
System.out.println(new Date());
Properties p = System.getProperties();
p.list(System.out);
System.out.println("--- Memory Usage:");
Runtime rt = Runtime.getRuntime();
System.out.println("Total Memory = "
+ rt.totalMemory()
+ " Free Memory = "
+ rt.freeMemory());
}
}
关键字“public”意味着方法可由外部世界调用
- main()的自变量是包含了String 对象的一个数组。args 不会在本程序中用到,但需要在这个地方列出,因为它们保存了在命令调用的自变量。
- System.getProperties()是System 类的一个 static 方法。由于它是“静态”的,所以不必创建任何对象便可调用该方法。无论是否存在该类的一个对象,static 方法随时都可使用。
- 调用getProperties()时,它会将系统属性作为 Properties类的一个对象生成(注意Properties 是“属性”的意思)。随后的的句柄保存在一个名为p 的 Properties句柄里。
- 大家可看到Properties 对象有一个名为list()的方法,它将自己的全部内容都发给一个我们作为自变量传递的PrintStream 对象。
- main()的第四和第六行是典型的打印语句。注意为了打印多个 String 值,用加号(+)分隔它们即可。然而,也要在这里注意一些奇怪的事情。
在 String 对象中使用时,加号并不代表真正的“相加”。处理字串时,我们通常不必考虑“+”的任何特殊含义。但是,Java 的 String 类要受一种名为“运算符过载”的机制的制约。也就是说,只有在随同String 对象使用时,加号才会产生与其他任何地方不同的表现。对于字串,它的意思是“连接这两个字串”。
注释和嵌入文档
Java里有两种类型的注释
第一种是传统的、C语言风格的注释,是从C++继承而来的。
语法:
/* 这是
* 一段注释,
* 它跨越了多个行
*/
/* 这是一段注释,
它跨越了多个行 */
注释文档
用于提取注释的工具叫作javadoc。它采用了部分来自Java编译器的技术,查找我们置入程序的特殊注释标记。它不仅提取由这些标记指示的信息,也将毗邻注释的类名或方法名提取出来。这样一来,我们就可用最轻的工作量,生成十分专业的程序文档。 javadoc输出的是一个HTML文件,可用自己的Web浏览器查看。该工具允许我们创建和管理单个源文件,并生动生成有用的文档。由于有了jvadoc,所以我们能够用标准的方法创建文档。而且由于它非常方便,所以我们能轻松获得所有Java库的文档。
语法:
/** 一个类注释 */
public class docTest {
/** 一个变量注释 */
public int i;
/** 一个方法注释 */
public void f() {}
}
注意javadoc只能为public(公共)和protected(受保护)成员处理注释文档。
嵌入HTML
-
@see:引用其他类
格式:
@see 类名
@see 完整类名
@see 完整类名#方法名
类文档标记
- 随同嵌入HTML和@see引用,类文档还可以包括用于版本信息以及作者姓名的标记。
- @version
2. @author变量文档标记
变量文档只能包括嵌入的HTML以及@see引用。方法文档标记
除嵌入HTML和@see引用之外,方法还允许使用针对参数、返回值以及违例的文档标记。
1. @param
格式:@param 参数名 说明
2. @return
格式:@return 说明 其中,“说明”是指返回值的含义。它可延续到后面的行内。
3. @exception
4. @deprecated文档示例
```java //: Property.java import java.util.; /* The first Thinking in Java example program.
- Lists system information on current machine.
- @author Bruce Eckel
- @author http://www.BruceEckel.com
- @version 1.0 /
public class Property {
/* Sole entry point to class & application- @param args array of string arguments
- @return No return value
- @exception exceptions No exceptions thrown
*/ public static void main(String[] args) {
System.out.println(new Date());
Properties p = System.getProperties(); p.list(System.out);
System.out.println(“—- Memory Usage:”); Runtime rt = Runtime.getRuntime();
System.out.println(“Total Memory = “ + rt.totalMemory() + “ Free Memory = “ + rt.freeMemory()); } 59} ///:~ ```编码样式
一个非正式的Java编程标准是大写一个类名的首字母。若类名由几个单词构成,那么把它们紧靠到一起(也就是说,不要用下划线来分隔名字)。总结:
通过本章的学习,大家已接触了足够多的Java编程知识,已知道如何自行编写一个简单的程序。练习:
控制程序流程
使用Java运算符
运算符以一个或多个自变量为基础,可生成一个新值。
加号(+)、减号和负号(-)、乘号(*)、除号(/)以及等号(=)的用法与其他所有编程语言都是类似的。
几乎所有运算符都只能操作“主类型”(Primitives)。唯一的例外是“=”、“==”和“!=”,它们能操作所有对象(也是对象易令人混淆的一个地方)。除此以外,String类支持“+”和“+=”。优先级
运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。赋值
赋值是用等号运算符(=)进行的。方法调用中的别名处理
算术运算符
Java的基本算术运算符与其他大多数程序设计语言是相同的。其中包括加号(+)、减号(-)、除号(/)、乘号(*)以及模数(%,从整数除法中获得余数)。
-
自动递增和递减
和C类似,Java提供了丰富的快捷运算方式。
两种很不错的快捷运算方式是递增和递减运算符(常称作“自动递增”和“自动递减”运算符)。
其中,递减运算符是“--”,意为“减少一个单位”;
递增运算符是“++”,意为“增加一个单位”。
例子:
//: AutoInc.java
// Demonstrates the ++ and -- operators
public class AutoInc {
public static void main(String[] args) {
int i = 1;
prt("i : " + i); prt("++i : " + ++i); // Pre-increment
prt("i++ : " + i++); // Post-increment
prt("i : " + i);
prt("--i : " + --i); // Pre-decrement
prt("i-- : " + i--); // Post-decrement
prt("i : " + i);
}
static void prt(String s) {
System.out.println(s);
}
}
该程序的输出如下:
i : 1
++i : 2
i++ : 2
i : 3
--i : 2
i-- : 2
i : 1
关系运算符
关系运算符生成的是一个“布尔”(Boolean)结果。
它们评价的是运算对象值之间的关系。若关系是真实的,关系表达式会生成true(真);若关系不真实,则生成false(假)。关系运算符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有内建的数据类型,但其他比较不适用于boolean类型。 - 检查对象是否相等
逻辑运算符
逻辑运算符AND(&&)、OR(||)以及NOT(!)能生成一个布尔值(true或false)——以自变量的逻辑关系为基础。//: Bool.java
// Relational and logical operators
import java.util.*;
public class Bool {
public static void main(String[] args) {
Random rand = new Random();
int i = rand.nextInt() % 100;
int j = rand.nextInt() % 100;
prt("i = " + i);
prt("j = " + j);
prt("i > j is " + (i > j));
prt("i < j is " + (i < j));
prt("i >= j is " + (i >= j));
prt("i <= j is " + (i <= j));
prt("i == j is " + (i == j));
prt("i != j is " + (i != j));
// Treating an int as a boolean is
// not legal Java
//! prt("i && j is " + (i && j));
//! prt("i || j is " + (i || j));
//! prt("!i is " + !i);
prt("(i < 10) && (j < 10) is " + ((i < 10) && (j < 10)) );
prt("(i < 10) || (j < 10) is " + ((i < 10) || (j < 10)) );
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
- 检查对象是否相等
短路
按位运算符
按位运算符允许我们操作一个整数主数据类型中的单个“比特”,即二进制位。按位运算符会对两个自变量中对应的位执行布尔代数,并最终生成一个结果。
移位运算符
三元if -else 运算符
这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,因为它最终也会生成一个值。
- 格式:
若“布尔表达式”的结果为true,就计算“值0”,而且它的结果成为最终由运算符产生的值。布尔表达式 ? 值0:值1
例子:static int ternary(int i) {
return i < 10 ? i * 100 : i * 10;
}
逗号运算符
在C和C++里,逗号不仅作为函数自变量列表的分隔符使用,也作为进行后续计算的一个运算符使用。在Java里需要用到逗号的唯一场所就是for循环,本章稍后会对此详加解释。字符运算+
这个运算符在Java里有一项特殊用途:连接不同的字串。运算符常规操作规则
使用运算符的一个缺点是括号的运用经常容易搞错。造型运算符
“造型”(Cast)的作用是“与一个模型匹配”。
1. 字面值
2.转型Java没有“sizeof”
在C和C++中,sizeof()运算符能满足我们的一项特殊需要:获知为数据项目分配的字符数量。在C和C++中,size()最常见的一种应用就是“移植”。复习计算顺序
在我举办的一次培训班中,有人抱怨运算符的优先顺序太难记了。一名学生推荐用一句话来帮助记忆:“Ulcer Addicts Really Like C A lot”,即“溃疡患者特别喜欢(维生素)C”。运算符总结:
下面这个例子向大家展示了如何随同特定的运算符使用主数据类型。从根本上说,它是同一个例子反反复复地执行,只是使用了不同的主数据类型。下面这个例子向大家展示了如何随同特定的运算符使用主数据类型。从根本上说,它是同一个例子反反复复地执行,只是使用了不同的主数据类型。在char,byte和short中,我们可看到算术运算符的“转型”效果。对这些类型的任何一个进行算术运算,都会获得一个int结果。
执行控制
Java使用了C的全部控制语句,所以假期您以前用C或C++编程,其中大多数都应是非常熟悉的。
真和假
if -else
if-else语句或许是控制程序流程最基本的形式
if(布尔表达式) 语句 或者 if(布尔表达式) 语句 else 语句
条件必须产生一个布尔结果。
- return
static int test2(int testval) {
if(testval > target)
return -1;
if(testval < target)
return +1;
return 0; // match
}
不必加上else,因为方法在遇到return后便不再继续。
反复
while,do-while和for控制着循环,有时将其划分为“反复语句”。除非用于控制反复的布尔表达式得到“假”的结果,否则语句会重复执行下去。while循环的格式如下:
在循环刚开始时,会计算一次“布尔表达式”的值 ```java //: WhileTest.java // Demonstrates the while loop public class WhileTest { public static void main(String[] args) { double r = 0; while(r < 0.99d) {while(布尔表达式)
语句
} }r = Math.random();
System.out.println(r);
}
- return
它用到了Math库里的static(静态)方法random()。该方法的作用是产生0和1之间(包括0,但不包括1)的一个double值。while的条件表达式意思是说:“一直循环下去,直到数字等于或大于0.99”。由于它的随机性,每运行一次这个程序,都会获得大小不同的数字列表。<br />
<a name="ozJ7c"></a>
##### do-while
格式:
```java
do
语句
while(布尔表达式)
while和do-while唯一的区别就是do-while肯定会至少执行一次;也就是说,至少会将其中的语句“过一遍”——即便表达式第一次便计算为false。而在while循环结构中,若条件第一次就为false,那么其中的语句根本不会执行。在实际应用中,while比do-while更常用一些。
for
for循环在第一次反复之前要进行初始化。
格式:
for(初始表达式; 布尔表达式; 步进)
语句
无论初始表达式,布尔表达式,还是步进,都可以置空。每次反复前,都要测试一下布尔表达式。若获得的结果是false,就会继续执行紧跟在for语句后面的那行代码。在每次循环的末尾,会计算一次步进。 for循环通常用于执行“计数”任务:
//: ListCharacters.java
// Demonstrates "for" loop by listing
// all the ASCII characters.
public class ListCharacters {
public static void main(String[] args) {
for( char c = 0; c < 128; c++)
if (c != 26 )// ANSI Clear screen
System.out.println(
"value: " + (int)c + " character: " + c);
}
} ///:~
- 臭名昭著的“goto”
label1:开关
“开关”(Switch)有时也被划分为一种“选择语句”。根据一个整数表达式的值,switch语句可从一系列代码选出一段执行
格式:switch(整数选择因子) {
case 整数值1 : 语句; break;
case 整数值2 : 语句; break;
case 整数值3 : 语句; break;
case 整数值4 : 语句; break;
case 整数值5 : 语句; break; //..
default:语句;
}
-
总结:
本章总结了大多数程序设计语言都具有的基本特性:计算、运算符优先顺序、类型转换以及选择和循环等等。现在,我们作好了相应的准备,可继续向面向对象的程序设计领域迈进。在下一章里,我们将讨论对象的初始化与清除问题,再后面则讲述隐藏的基本实现方法。
初始化和清除
开场白:
“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。” “初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。 C++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。用构造器自动初始化
对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。
例子://: SimpleConstructor.java // Demonstration of a simple constructor
package c04;
class Rock {
Rock() { // This is the constructor
System.out.println("Creating Rock");
}
}
public class SimpleConstructor {
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new Rock();
}
}
现在,一旦创建一个对象: new Rock(); 就会分配相应的存储空间,并调用构建器。
例子:class Rock {
Rock(int i) {
96 System.out.println( "Creating Rock number " + i);
}
}
public class SimpleConstructor {
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new Rock(i);
}
}
利用构建器的自变量,我们可为一个对象的初始化设定相应的参数。
方法过载
在任何程序设计语言中,一项重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的名字。方法名代表的是一种具体的行动。通过用名字描述自己的系统,可使自己的程序更易人们理解和修改。它非常象写散文——目的是与读者沟通。
①:在Sun公司出版的一些Java资料中,用简陋但很说明问题的词语称呼这类构建器——“无参数构建器”(no-arg constructors)。但“默认构建器”这个称呼已使用了许多年,所以我选择了它。
我们也有可能希望通过多种途径调用info()方法。区分过载方法
若方法有同样的名字,Java怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个过载的方法都必须采取独一无二的自变量类型列表。
主类型的过载
主(数据)类型能从一个“较小”的类型自动转变成一个“较大”的类型。
返回值过载
我们很易对下面这些问题感到迷惑:为什么只有类名和方法自变量列出?为什么不根据返回值对方法加以区分?比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的:
void f() {}
int f() {}默认构造器
this关键字
如果有两个同类型的对象,分别叫作a和b,那么您也许不知道如何为这两个对象同时调用一个f()方法:
class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);
- 在构建器里调用构建器
2. static的含义清除:收尾和垃圾收集
程序员都知道“初始化”的重要性,但通常忘记清除的重要性。毕竟,谁需要来清除一个int呢?但是对于库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java可用垃圾收集器回收由不再使用的对象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没有使用new。垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,可为我们的类定义它。finalize()用途何在
此时,大家可能已相信了自己应该将finalize()作为一种常规用途的清除方法使用。它有什么好处呢? 要记住的第三个重点是:
-
必须执行清除
为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。
成员初始化
Java尽自己的全力保证所有变量都能在使用前得到正确的初始化。
void f() {
int i;
i++;
}
规定初始化
如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部定义变量的同时也为其赋值(注意在C++里不能这样做,尽管C++的新手们总“想”这样做)。
构造器初始化
可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。
- 初始化顺序
- 静态数据的初始化
- 明确进行的静态初始化
- 非静态实例的初始化
数组初始化
在C中初始化数组极易出错,而且相当麻烦。C++通过“集合初始化”使其更安全(注释⑥)。Java则没有象C++那样的“集合”概念,因为Java中的所有东西都是对象。但它确实有自己的数组,通过数组初始化来提供支持。多维数组
在Java里可以方便地创建多维数组:
用于打印的代码里使用了length,所以它不必依赖固定的数组大小。 第一个例子展示了基本数据类型的一个多维数组。我们可用花括号定出数组内每个矢量的边界://: MultiDimArray.java // Creating multidimensional arrays.
import java.util.*;
public class MultiDimArray {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
}:
for(int i = 0; i < a1.length; i++)
for(int j = 0; j < a1[i].length; j++)
prt("a1[" + i + "][" + j +
"] = " + a1[i][j]);
// 3-D array with fixed length:
int[][][] a2 = new int[2][2][4];
for(int i = 0; i < a2.length; i++)
for(int j = 0; j < a2[i].length; j++)
for(int k = 0; k < a2[i][j].length;
k++)
prt("a2[" + i + "][" +
j + "][" + k +
"] = " + a2[i][j][k]);
// 3-D array with varied-length vectors:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(5)];
}
for(int i = 0; i < a3.length; i++)
for(int j = 0; j < a3[i].length; j++)
for(int k = 0; k < a3[i][j].length;
k++)
prt("a3[" + i + "][" +
j + "][" + k + "] = " + a3[i][j][k]);
// Array of non-primitive objects:
Integer[][] a4 = { { new Integer(1), new Integer(2)}, {
new Integer(3),
new Integer(4)}, {
new Integer(5),
new Integer(6)}, };
for(int i = 0; i < a4.length; i++)
for(int j = 0; j < a4[i].length; j++)
prt("a4[" + i + "][" + j +
"] = " + a4[i][j]);
Integer[][] a5;
a5 = new Integer[3][];
for(int i = 0; i < a5.length; i++) {
a5[i] = new Integer[3];
for(int j = 0; j < a5[i].length; j++)
a5[i][j] = new Integer(i*j);
}
for(int i = 0; i < a5.length; i++)
for(int j = 0; j < a5[i].length; j++)
prt("a5[" + i + "][" + j +
"] = " + a5[i][j]);
}
static void prt(String s) {
System.out.println(s);
}
}
int[][] a1 = { { 1, 2, 3, }, { 4, 5, 6, }, };
每个方括号对都将我们移至数组的下一级。 第二个例子展示了用new分配的一个三维数组。在这里,整个数组都是立即分配的: int[][][] a2 = new int[2][2][4]; 但第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(5)];
}
对于第一个new创建的数组,它的第一个元素的长度是随机的,其他元素的长度则没有定义。for循环内的第二个new则会填写元素,但保持第三个索引的未定状态——直到碰到第三个new。 根据输出结果,大家可以看到:假若没有明确指定初始化值,数组值就会自动初始化成零。 可用类似的表式处理非基本类型对象的数组。这从第四个例子可以看出,它向我们演示了用花括号收集多个new表达式的能力:
Integer[][] a4 = { {
new Integer(1), new Integer(2)},
{ new Integer(3), new Integer(4)},
{ new Integer(5), new Integer(6)},
};
第五个例子展示了如何逐渐构建非基本类型的对象数组:
i*j只是在Integer里置了一个有趣的值。Integer[][] a5;
a5 = new Integer[3][];
for(int i = 0; i < a5.length; i++) {
a5[i] = new Integer[3];
for(int j = 0; j < a5[i].length; j++)
a5[i][j] = new Integer(i*j);
}
总结:
作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。 在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构建器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。 由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构建器造成的影响。隐藏实施过程
“进行面向对象的设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔开。”包:库单元
我们用import关键字导入一个完整的库时,就会获得“包”(Package)。
①:Java并没有强制一定要使用解释器。一些固有代码的Java编译器可生成单独的可执行文件。创建独一无二的包名
大家或许已注意到这样一个事实:由于一个包永远不会真的“封装”到单独一个文件里面,它可由多个.class文件构成,所以局面可能稍微有些混乱。为避免这个问题,最合理的一种做法就是将某个特定包使用的所有.class文件都置入单个目录里。也就是说,我们要利用操作系统的分级文件结构避免出现混乱局面。这正是Java所采取的方法。
②:ftp://ftp.internic.net
1. 自动编译 、
2. 冲突自定义工具库
掌握前述的知识后,接下来就可以开始创建自己的工具库,以便减少或者完全消除重复的代码。
1. CLASSPATH的陷阱利用导入改变行为
Java已取消的一种特性是C的“条件编译”,它允许我们改变参数,获得不同的行为,同时不改变其他任何代码。Java之所以抛弃了这一特性,可能是由于该特性经常在C里用于解决跨平台问题:代码的不同部分根据具体的平台进行编译,否则不能在特定的平台上运行。由于Java的设计思想是成为一种自动跨平台的语言,所以这种特性是没有必要的。包的停用
大家应注意这样一个问题:每次创建一个包后,都在为包取名时间接地指定了一个目录结构。java访问指示符
针对类内每个成员的每个定义,Java访问指示符poublic,protected以及private都置于它们的最前面——无论它们是一个数据成员,还是一个方法。每个访问指示符都只控制着对那个特定定义的访问。友好的
如果根本不指定访问指示符,就象本章之前的所有例子那样,这时会出现什么情况呢?默认的访问没有关键字,但它通常称为“友好”(Friendly)访问。
(1) 使成员成为“public”(公共的)。这样所有人从任何地方都可以访问它。
(2) 变成一个“友好”成员,方法是舍弃所有访问指示符,并将其类置于相同的包内。这样一来,其他类就可以访问成员。
(3) 正如以后引入“继承”概念后大家会知道的那样,一个继承的类既可以访问一个protected成员,也可以访问一个public成员(但不可访问private成员)。只有在两个类位于相同的包内时,它才可以访问友好成员。但现在不必关心这方面的问题。
(4) 提供“访问器/变化器”方法(亦称为“获取/设置”方法),以便读取和修改值。这是OOP环境中最正规的一种方法,也是Java Beans的基础public:接口访问
使用public关键字时,它意味着紧随在public后面的成员声明适用于所有人,特别是适用于使用库的客户程序员。
1.默认包private: 不能接触
private关键字意味着除非那个特定的类,而且从那个类的方法里,否则没有人能访问那个成员。同一个包内的其他成员不能访问private成员,这使其显得似乎将类与我们自己都隔离起来。protected:“友好的一种”
protected(受到保护的)访问指示符要求大家提前有所认识。首先应注意这样一个事实:为继续学习本书一直到继承那一章之前的内容,并不一定需要先理解本小节的内容。但为了保持内容的完整,这儿仍然要对此进行简要说明,并提供相关的例子。接口与实现
我们通常认为访问控制是“隐藏实施细节”的一种方式。将数据和方法封装到类内后,可生成一种数据类型,它具有自己的特征与行为。但由于两方面重要的原因,访问为那个数据类型加上了自己的边界。第一个原因是规定客户程序员哪些能够使用,哪些不能。我们可在结构里构建自己的内部机制,不用担心客户程序员将其当作接口的一部分,从而自由地使用或者“滥用”。类访问
在Java中,亦可用访问指示符判断出一个库内的哪些类可由那个库的用户使用。若想一个类能由客户程序员调用,可在类主体的起始花括号前面某处放置一个public关键字。它控制着客户程序员是否能够创建属于这个类的一个对象。
(1) 每个编译单元(文件)都只能有一个public类。每个编译单元有一个公共接口的概念是由那个公共类表达出来的。根据自己的需要,它可拥有任意多个提供支撑的“友好”类。但若在一个编译单元里使用了多个public类,编译器就会向我们提示一条出错消息。 (2) public类的名字必须与包含了编译单元的那个文件的名字完全相符,甚至包括它的大小写形式。所以对于Widget来说,文件的名字必须是Widget.java,而不应是widget.java或者WIDGET.java。同样地,如果出现不符,就会报告一个编译期错误。 (3) 可能(但并常见)有一个编译单元根本没有任何公共类。此时,可按自己的意愿任意指定文件名。总结:
对于任何关系,最重要的一点都是规定好所有方面都必须遵守的界限或规则。创建一个库时,相当于建立了同那个库的用户(即“客户程序员”)的一种关系——那些用户属于另外的程序员,可能用我们的库自行构建一个应用程序,或者用我们的库构建一个更大的库。类再生
开场白:
“Java引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。” 在象C那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与Java的其他地方一样,这个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。 但这样做必须保证不会干扰原有的代码。在这一章里,我们将介绍两个达到这一目标的方法。第一个最简单:在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。 第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。 对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。在本章,我们将深入学习这些代码再生或者重复使用的机制。合成的语法:
就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对象句柄即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。
WaterSource内定义的一个方法是比较特别的:toString()。大家不久就会知道,每种非基本类型的对象都有一个toString()方法。若编译器本来希望一个String,但却获得某个这样的对象,就会调用这个方法。所以在下面这个表达式中: System.out.println(“source = “ + source) ; 编译器会发现我们试图向一个WaterSource添加一个String对象(”source =”)。这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。继承的语法
继承与Java(以及其他OOP语言)非常紧密地结合在一起。我们早在第1章就为大家引入了继承的概念,并在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。除此以外,创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类Object中继承。初始化基础类
由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。
1. 含有自变量的构建器
2. 捕获基本构建器的违例合成与继承的结合
许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作:确保正确的清除
Java不具备象C++的“破坏器”那样的概念。在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必要的时候自动回收内存。
1. 垃圾收集的顺序名字的隐藏
只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。到底选择合成还是继承
无论合成还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如何选择。protected
现在我们已理解了继承的概念,protected这个关键字最后终于有了意义。在理想情况下,private成员随时都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍生类的成员。protected关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。也就是说,Java中的protected会成为进入“友好”状态。累积开发
继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。这样一来,与修改现有代码的主体相比,改正错误所需的时间和精力就可以少很多。上溯造型
继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样总结该关系:“新类属于现有类的一种类型”。何谓“上溯造型”?
之所以叫作这个名字,除了有一定的历史原因外,也是由于在传统意义上,类继承图的画法是根位于最顶部,再逐渐向下扩展(当然,可根据自己的习惯用任何方法描绘这种图)。因素,Wind.java的继承图就象
1. 再论合成与继承final关键字
由于语境(应用环境)不同,final关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇有些区别,所以也许会造成final关键字的误用。final数据
许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面: (1) 编译期常数,它永远不会改变 (2) 在运行期初始化的一个值,我们不希望它发生变化 对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在Java中,这些形式的常数必须属于基本数据类型
2. 空白final
3. final自变量final方法:
之所以要使用final方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。final类
如果说整个类都是final(在它的定义前冠以final关键字),就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(子类处理)。final的注意事项
设计一个类时,往往需要考虑是否将一个方法设为final。可能会觉得使用自己的类时执行效率非常重要,没有人想覆盖自己的方法。这种想法在某些时候是正确的。初始化和类装载
在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程序。在这些语言中,必须对初始化过程进行慎重的控制,保证static数据的初始化不会带来麻烦。比如在一个static数据获得初始化之前,就有另一个static数据希望它是一个有效值,那么在C++中就会造成问题。继承初始化
我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。
总结:
无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础类。对于下一章要讲述的多形性问题,这一点是至关重要的。多形性
开场白:
“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。” “多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。 通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。 在这一章中,大家要由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多形性有关的代码。上溯造型
在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。为什么要上溯造型
这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument的每种类型写一个全新的tune()。深入理解
对于Music.java的困难性,可通过运行程序加以体会。输出是Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。产生正确的行为
知道Java里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。”
上溯造型可用下面这个语句简单地表现出来: Shape s = new Circle();扩展性
现在,让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改true()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变,覆盖和过载
现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过载处理,使其不报告出错。但这种行为可能并不是我们所希望的。抽象类和方法
在我们所有乐器(Instrument)例子中,基础类Instrument内的方法都肯定是“伪”方法。若去调用这些方法,就会出现错误。接口
“interface”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static和final。接口只提供一种形式,并不提供实施的细节。 接口这样描述自己:“对于实现我的所有类,看起来都应该象我现在这个样子”。因此,采用了一个特定接口的所有代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以我们常把接口用于建立类和类之间的一个“协议”。有些面向对象的程序设计语言采用了一个名为“protocol”(协议)的关键字,它做的便是与接口相同的事情。
Java的多重继承
接口只是比抽象类“更纯”的一种形式。它的用途并不止那些。由于接口根本没有具体的实施细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点是至关重要的,因为我们经常都需要表达这样一个意思:“x从属于a,也从属于b,也从属于c”。在C++中,将多个类合并到一起的行动称作“多重继承”,而且操作较为不便,因为每个类都可能有一套自己的实施细节。在Java中,我们可采取同样的行动,但只有其中一个类拥有具体的实施细节。所以在合并多个接口的时候,C++的问题不会在Java中重演
通过继承扩展接口
利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。常数分组
由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum非常相似的效果。
注意根据Java命名规则,拥有固定标识符的static final基本数据类型(亦即编译期常数)都全部采用大写字母(用下划线分隔单个标识符里的多个单词)。初始化接口中字段
接口中定义的字段会自动具有static和final属性。它们不能是“空白final”,但可初始化成非常数表达式。内部类
在Java 1.1中,可将一个类定义置入另一个类定义中。这就叫作“内部类”。内部类对我们非常有用,因为利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”。然而,我们必须认识到内部类与以前讲述的“合成”方法存在着根本的区别。内部类和上溯造型
迄今为止,内部类看起来仍然没什么特别的地方。毕竟,用它实现隐藏显得有些大题小做。Java已经有一个非常优秀的隐藏机制——只允许类成为“友好的”(只在一个包内可见),而不是把它创建成一个内部类。 然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。方法和作用域中内部类
(1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
(2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。链接到外部类
迄今为止,我们见到的内部类好象仅仅是一种名字隐藏以及代码组织方案。尽管这些功能非常有用,但似乎并不特别引人注目。然而,我们还忽略了另一个重要的事实。创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能访问那个封装对象的成员——毋需取得任何资格
②:这与C++“嵌套类”的设计颇有不同,后者只是一种单纯的名字隐藏机制。在C++中,没有指向一个封装对象的链接,也不存在默认的访问权限。static内部
为正确理解static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。然而,假如我们说一个内部类是static的,这种说法却是不成立的。
static内部类意味着:
(1) 为创建一个static内部类的对象,我们不需要一个外部类对象。
(2) 不能从static内部类的一个对象中访问一个外部类对象。引用外部类对象
若想生成外部类对象的句柄,就要用一个点号以及一个this来命名外部类。举个例子来说,在Sequence.SSelector类中,它的所有方法都能产生外部类Sequence的存储句柄,方法是采用Sequence.this的形式。结果获得的句柄会自动具备正确的类型(这会在编译期间检查并核实,所以不会出现运行期的开销)。从内部类继承
由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。内部类可以覆盖吗?
若创建一个内部类,然后从封装类继承,并重新定义内部类,那么会出现什么情况呢?也就是说,我们有可能覆盖一个内部类吗?这看起来似乎是一个非常有用的概念,但“覆盖”一个内部类——好象它是外部类的另一个方法——这一概念实际不能做任何事情内部类标识符
由于每个类都会生成一个.class文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生了一个名为Class对象的元类),所以大家或许会猜到内部类也必须生成相应的.class文件,用来容纳与它们的Class对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$,再跟随内部类的名字。为什么要用内部类:控制框架
到目前为止,大家已接触了对内部类的运作进行描述的大量语法与概念。但这些并不能真正说明内部类存在的原因。为什么Sun要如此麻烦地在Java 1.1里添加这样的一种基本语言特性呢?答案就在于我们在这里要学习的“控制框架”。构建器和多形性
同往常一样,构建器与其他种类的方法是有区别的。在涉及到多形性的问题后,这种方法依然成立。构建器的调用顺序
构建器调用的顺序已在第4章进行了简要说明,但那是在继承和多形性问题引入之前说的话。继承和finalize()
通过“合成”方法创建新类时,永远不必担心对那个类的成员对象的收尾工作。构造器内部的多性方法的行为
构建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会认为这应该在构建器内部发生。通过继承进行设计
纯继承与扩展
学习继承时,为了创建继承分级结构,看来最明显的方法是采取一种“纯粹”的手段。下溯造型与运行期类型标识
由于我们在上溯造型(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信息——亦即在分级结构中向下移动——我们必须使用 “下溯造型”技术。总结:
“多形性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基础类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。 通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多形性的一个例子。多形性是一种不可独立应用的特性(就象一个switch语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法过载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多形性。 为使用多形性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。对象的容纳
开场白:
“如果一个程序只含有数量固定的对象,而且已知它们的存在时间,那么这个程序可以说是相当简单的。” 通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。若非程序正式运行,否则我们根本不知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的句柄来容纳自己的每一个对象,就象下面这样: MyObject myHandle; 因为根本不知道自己实际需要多少这样的东西。 为解决这个非常关键的问题,Java提供了容纳对象(或者对象的句柄)的多种方式。其中内建的类型是数组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java的工具(实用程序)库提供了一些“集合类”(亦称作“容器类”,但该术语已由AWT使用,所以这里仍采用“集合”这一称呼)。利用这些集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。数组
数组和第一类对象
无论使用的数组属于什么类型,数组标识符实际都是指向真实对象的一个句柄。
1. 基本数据类型集合数组的放回
假定我们现在想写一个方法,同时不希望它仅仅返回一样东西,而是想返回一系列东西。此时,象C和C++这样的语言会使问题复杂化,因为我们不能返回一个数组,只能返回指向数组的一个指针。这样就非常麻烦,因为很难控制数组的“存在时间”,它很容易造成内存“漏洞”的出现。集合
现在总结一下我们前面学过的东西:为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。在本章剩下的部分,大家将接触到一些更常规的情况。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,Java提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。8.2.1 缺点:类型未知
也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用 214下述场合:
使用Java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。
(1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。 (2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。枚举器(反复器)
在任何集合类中,必须通过某种方法在其中置入对象,再用另一种方法从中取得对象。毕竟,容纳各种各样的对象正是集合的首要任务。在Vector中,addElement()便是我们插入对象采用的方法,而elementAt()是提取对象的唯一方法。Vector非常灵活,我们可在任何时候选择任何东西,并可使用不同的索引选择多个元素。
(1) 用一个名为elements()的方法要求集合为我们提供一个Enumeration。我们首次调用它的nextElement()时,这个Enumeration会返回序列中的第一个元素。
(2) 用nextElement()获得下一个对象。
(3) 用hasMoreElements()检查序列中是否还有更多的对象。集合类型
标准Java 1.0和1.1库配套提供了非常少的一系列集合类。但对于自己的大多数编程要求,它们基本上都能胜任。正如大家到本章末尾会看到的,Java 1.2提供的是一套重新设计过的大型集合库。Vector
Vector的用法很简单,这已在前面的例子中得到了证明。尽管我们大多数时候只需用addElement()插入对象,用elementAt()一次提取一个对象,并用elements()获得对序列的一个“枚举”。
1. 崩溃JavaBitSet
BitSet实际是由“二进制位”构成的一个Vector。如果希望高效率地保存大量“开-关”信息,就应使用BitSet。它只有从尺寸的角度看才有意义;如果希望的高效率的访问,那么它的速度会比使用一些固有类型的数组慢一些。Stack
Stack有时也可以称为“后入先出”(LIFO)集合。换言之,我们在堆栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有Java集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。Hashtable
Vector允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?堆栈就是这样的一个例子:它的选择标准是“最后压入堆栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个Vector,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。再论枚举器
我们现在可以开始演示Enumeration(枚举)的真正威力:将穿越一个序列的操作与那个序列的基础结构分隔开。排序
Java 1.0和1.1库都缺少的一样东西是算术运算,甚至没有最简单的排序运算方法。因此,我们最好创建一个Vector,利用经典的Quicksort(快速排序)方法对其自身进行排序。通用集合库
通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。新集合
对我来说,集合类属于最强大的一种工具,特别适合在原创编程中使用。
使用Collections
下面这张表格总结了用一个集合能做的所有事情(亦可对Set和List做同样的事情,尽管List还提供了一些额外的功能)。Map不是从Collection继承的,所以要单独对待。
boolean add(Object) *保证集合内包含了自变量。如果它没有添加自变量,就返回false(假) boolean addAll(Collection) *添加自变量内的所有元素。如果没有添加元素,则返回true(真) void clear() *删除集合内的所有元素 boolean contains(Object) 若集合包含自变量,就返回“真” boolean containsAll(Collection) 若集合包含了自变量内的所有元素,就返回“真” boolean isEmpty() 若集合内没有元素,就返回“真” Iterator iterator() 返回一个反复器,以用它遍历集合的各元素 boolean remove(Object) *如自变量在集合里,就删除那个元素的一个实例。如果已进行了删除,就返回“真” boolean removeAll(Collection) *删除自变量里的所有元素。如果已进行了任何删除,就返回“真” boolean retainAll(Collection) *只保留包含在一个自变量里的元素(一个理论的“交集”)。如果已进行了任何改变,就返回“真” int size() 返回集合内的元素数量 Object[] toArray() 返回包含了集合内所有元素的一个数组使用Lists
List(接口) 顺序是List最重要的特性;它可保证元素按照规定的顺序排列。List为Collection添加了大量方法,以便我们在List中部插入和删除元素(只推荐对LinkedList这样做)。List也会生成一个ListIterator(列表反复器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对LinkedList这样做) ArrayList* 由一个数组后推得到的List。作为一个常规用途的对象容器使用,用于替换原先的Vector。允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。一般只应该用ListIterator对一个ArrayList进行向前和向后遍历,不要用它删除和插入元素;与LinkedList相比,它的效率要低许多 LinkedList 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用ArrayList。也提供了addFirst(),addLast(),getFirst(),getLast(),removeFirst()以及removeLast()(未在任何接口或基础类中定义),以便将其作为一个规格、队列以及一个双向队列使用使用Sets
Set拥有与Collection完全相同的接口,所以和两种不同的List不同,它没有什么额外的功能。相反,Set完全就是一个Collection,只是具有不同的行为(这是实例和多形性最理想的应用:用于表达不同的行为)。在这里,一个Set只允许每个对象存在一个实例(正如大家以后会看到的那样,一个对象的“值”的构成是相当复杂的)。
Set(接口) 添加到Set的每个元素都必须是独一无二的;否则Set就不会添加重复的元素。添加到Set里的对象必须定义equals(),从而建立对象的唯一性。Set拥有与Collection完全相同的接口。一个Set不能保证自己可按任何特定的顺序维持自己的元素 HashSet* 用于除非常小的以外的所有Set。对象也必须定义hashCode() ArraySet 由一个数组后推得到的Set。面向非常小的Set设计,特别是那些需要频繁创建和删除的。对于小Set,与HashSet相比,ArraySet创建和反复所需付出的代价都要小得多。但随着Set的增大,它的性能也会大打折扣。不需要HashCode() 243TreeSet 由一个“红黑树”后推得到的顺序Set(注释⑦)。这样一来,我们就可以从一个Set里提到一个顺序集合使用Maps
Map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值 HashMap* 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构建器对这一性能进行调整,以便设置散列表的“能力”和“装载因子” ArrayMap 由一个ArrayList后推得到的Map。对反复的顺序提供了精确的控制。面向非常小的Map设计,特别是那些需要经常创建和删除的。对于非常小的Map,创建和反复所付出的代价要比HashMap低得多。但在Map变大以后,性能也会相应地大幅度降低 TreeMap 在一个“红-黑”树的基础上实现。查看键或者“键-值”对时,它们会按固定的顺序排列(取决于Comparable或Comparator,稍后即会讲到)。TreeMap最大的好处就是我们得到的是已排好序的结果。TreeMap是含有subMap()方法的唯一一种Map,利用它可以返回树的一部分决定实施方案
从早些时候的那幅示意图可以看出,实际上只有三个集合组件:Map,List和Set。而且每个接口只有两种或三种实施方案。若需使用由一个特定的接口提供的功能,如何才能决定到底采取哪一种方案呢? 为理解这个问题,必须认识到每种不同的实施方案都有自己的特点、优点和缺点。比如在那张示意图中,可以看到Hashtable,Vector和Stack的“特点”是它们都属于“传统”类,所以不会干扰原有的代码。但在另一方面,应尽量避免为新的(Java 1.2)代码使用它们。
1. 决定使用何种List 为体会各种List实施方案间的差异,最简便的方法就是进行一次性能测验。下述代码的作用是建立一个内部基础类,将其作为一个测试床使用。然后为每次测验都创建一个匿名内部类。每个这样的内部类都由一个test()方法调用。利用这种方法,可以方便添加和删除测试项目。
内部类Tester是一个抽象类,用于为特定的测试提供一个基础类。它包含了一个要在测试开始时打印的字串、一个用于计算测试次数或元素数量的size参数、用于初始化字段的一个构建器以及一个抽象方法test()。test()做的是最实际的测试工作。各种类型的测试都集中到一个地方:tests数组。我们用继承于Tester的不同匿名内部类来初始化该数组。为添加或删除一个测试项目,只需在数组里简单地添加或移去一个内部类定义即可,其他所有工作都是自动进行的。
3. 决定使用何种Map未支持的操作
利用static(静态)数组Arrays.toList(),也许能将一个数组转换成List排序和搜索
Java 1.2添加了自己的一套实用工具,可用来对数组或列表进行排列和搜索。这些工具都属于两个新类的“静态”方法。这两个类分别是用于排序和搜索数组的Arrays,以及用于排序和搜索列表的Collections。
1. 数组
2. 可比较与比较器
3. 列表实用工具
enumeration(Collection) 为自变量产生原始风格的Enumeration(枚举) max(Collection),min(Collection) 在自变量中用集合内对象的自然比较方法产生最大或最小元素 max(Collection,Comparator),min(Collection,Comparator) 在集合内用比较器产生最大或最小元素 nCopies(int n, Object o) 返回长度为n的一个不可变列表,它的所有句柄均指向o subList(List,int min,int max) 返回由指定参数列表后推得到的一个新列表。可将这个列表想象成一个“窗口”,它自索引为min的地方开始,正好结束于max的前面
1. 使Collection或Map不可修改
2. Collection或Map的同步总结:
(1) 数组包含了对象的数字化索引。它容纳的是一种已知类型的对象,所以在查找一个对象时,不必对结果进行造型处理。数组可以是多维的,而且能够容纳基本数据类型。但是,一旦把它创建好以后,大小便不能变化了。
(2) Vector(矢量)也包含了对象的数字索引——可将数组和Vector想象成随机访问集合。当我们加入更多的元素时,Vector能够自动改变自身的大小。但Vector只能容纳对象的句柄,所以它不可包含基本数据类型;而且将一个对象句柄从集合中取出来的时候,必须对结果进行造型处理。
(3) Hashtable(散列表)属于Dictionary(字典)的一种类型,是一种将对象(而不是数字)同其他对象关联到一起的方式。散列表也支持对对象的随机访问,事实上,它的整个设计方案都在突出访问的“高速度”。
(4) Stack(堆栈)是一种“后入先出”(LIFO)的队列。违例差错控制
开场白:
Java的基本原理就是“形式错误的代码不会运行”。 与C++类似,捕获错误最理想的是在编译期间,最好在试图运行程序以前。然而,并非所有错误都能在编译期间侦测到。有些问题必须在运行期间解决,让错误的缔结者通过一些手续向接收者传递一些适当的信息,使其知道该如何正确地处理遇到的问题。 在C++和其他早期语言中,可通过几种手续来达到这个目的。而且它们通常是作为一种规定建立起来的,而非作为程序设计语言的一部分。典型地,我们需要返回一个值或设置一个标志(位),接收者会检查这些值或标志,判断具体发生了什么事情。然而,随着时间的流逝,终于发现这种做法会助长那些使用一个库的程序员的麻痹情绪。他们往往会这样想:“是的,错误可能会在其他人的代码中出现,但不会在我的代码中”。这样的后果便是他们一般不检查是否出现了错误(有时出错条件确实显得太愚蠢,不值得检验;注释①)。另一方面,若每次调用一个方法时都进行全面、细致的错误检查,那么代码的可读性也可能大幅度降低。由于程序员可能仍然在用这些语言维护自己的系统,所以他们应该对此有着深刻的体会:若按这种方式控制错误,那么在创建大型、健壮、易于维护的程序时,肯定会遇到不小的阻挠。 ①:C程序员研究一下printf()的返回值便知端详。 解决的方法是在错误控制中排除所有偶然性,强制格式的正确。这种方法实际已有很长的历史,因为早在60年代便在操作系统里采用了“违例控制”手段;甚至可以追溯到BASIC语言的on error goto语句。但C++的违例控制建立在Ada的基础上,而Java又主要建立在C++的基础上(尽管它看起来更象Object Pascal)。 “违例”(Exception)这个词表达的是一种“例外”情况,亦即正常情况之外的一种“异常”。在问题发生的时候,我们可能不知具体该如何解决,但肯定知道已不能不顾一切地继续下去。此时,必须坚决地停下来,并由某人、某地指出发生了什么事情,以及该采取何种对策。但为了真正解决问题,当地可能并没有足够多的信息。因此,我们需要将其移交给更级的负责人,令其作出正确的决定(类似一个命令链)。 违例机制的另一项好处就是能够简化错误控制代码。我们再也不用检查一个特定的错误,然后在程序的多处地方对其进行控制。此外,也不需要在方法调用的时候检查错误(因为保证有人能捕获这里的错误)。我们只需要在一个地方处理问题:“违例控制模块”或者“违例控制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。 由于违例控制是由Java编译器强行实施的,所以毋需深入学习违例控制,便可正确使用本书编写的大量例子。本章向大家介绍了用于正确控制违例所需的代码,以及在某个方法遇到麻烦的时候,该如何生成自己的违例。基本违例
“违例条件”表示在出现什么问题的时候应中止方法或作用域的继续。为了将违例条件与普通问题区分开,违例条件是非常重要的一个因素。在普通问题的情况下,我们在当地已拥有足够的信息,可在某种程度上解决碰到的问题。而在违例条件的情况下,却无法继续下去,因为当地没有提供解决问题所需的足够多的信息。此时,我们能做的唯一事情就是跳出当地环境,将那个问题委托给一个更高级的负责人。这便是出现违例时出现的情况。违例自变量
和Java的其他任何对象一样,需要用new在内存堆里创建违例,并需调用一个构建器。违例的捕获
若某个方法产生一个违例,必须保证该违例能被捕获,并获得正确对待。对于Java的违例控制机制,它的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。try块
若位于一个方法内部,并“掷”出一个违例(或在这个方法内部调用的另一个方法产生了违例),那个方法就会在违例产生过程中退出。
try {
// 可能产生违例的代码
}违例控制器
当然,生成的违例必须在某个地方中止。这个“地方”便是违例控制器或者违例控制模块。
- 非静态实例的初始化
-
违例规范
在Java中,对那些要调用方法的客户程序员,我们要通知他们可能从自己的方法里“掷”出违例。
捕获所有违例
重新“掷”出违例
在某些情况下,我们想重新掷出刚才产生过的违例,特别是在用Exception捕获所有可能的违例时。由于我们已拥有当前违例的句柄,所以只需简单地重新掷出那个句柄即可。
标准java违例
Java包含了一个名为Throwable的类,它对可以作为违例“掷”出的所有东西进行了描述。
RuntimeException的特殊情况
if(t == null) throw new NullPointerException(); 看起来似乎在传递进入一个方法的每个句柄中都必须检查null(因为不知道调用者是否已传递了一个有效的句柄),这无疑是相当可怕的。但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。若对一个空句柄发出了调用,Java会自动产生一个NullPointerException违例。所以上述代码在任何情况下都是多余的。
创建自己的违例
并不一定非要使用Java违例。这一点必须掌握,因为经常都需要创建自己的违例,以便指出自己的库可能生成的一个特殊错误——但创建Java分级结构的时候,这个错误是无法预知的。
违例的限制
用finally清除
无论一个违例是否在try块中发生,我们经常都想执行一些特定的代码。
用finally做什么
在没有“垃圾收集”以及“自动调用破坏器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。
缺点:丢失的违例
构建器
为违例编写代码时,我们经常要解决的一个问题是:“一旦产生违例,会正确地进行清除吗?”大多数时候都会非常安全,但在构建器中却是一个大问题。构建器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构建器内部“掷”出一个违例,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构建器时,我们必须特别加以留意。
违例匹配
“掷”出一个违例后,违例控制系统会按当初编写的顺序搜索“最接近”的控制器。一旦找到相符的控制器,就认为违例已得到控制,不再进行更多的搜索工作。
违例准则
用违例做下面这些事情:
(1) 解决问题并再次调用造成违例的方法。
(2) 平息事态的发展,并在不重新尝试方法的前提下继续。
(3) 计算另一些结果,而不是希望方法产生的结果。
(4) 在当前环境中尽可能解决问题,以及将相同的违例重新“掷”出一个更高级的环境。
(5) 在当前环境中尽可能解决问题,以及将不同的违例重新“掷”出一个更高级的环境。
(6) 中止程序执行。
(7) 简化编码。若违例方案使事情变得更加复杂,那就会令人非常烦恼,不如不用。
(8) 使自己的库和程序变得更加安全。这既是一种“短期投资”(便于调试),也是一种“长期投资”(改善应用程序的健壮性)总结:
通过先进的错误纠正与恢复机制,我们可以有效地增强代码的健壮程度。对我们编写的每个程序来说,错误恢复都属于一个基本的考虑目标。它在Java中显得尤为重要,因为该语言的一个目标就是创建不同的程序组件,以便其他用户(客户程序员)使用。为构建一套健壮的系统,每个组件都必须非常健壮。 在Java里,违例控制的目的是使用尽可能精简的代码创建大型、可靠的应用程序,同时排除程序里那些不能控制的错误。
Java10 系统
开场白:
“对语言设计人员来说,创建好的输入/输出系统是一项特别困难的任务。” 由于存在大量不同的设计方案,所以该任务的困难性是很容易证明的。其中最大的挑战似乎是如何覆盖所有可能的因素。不仅有三种不同的种类的IO需要考虑(文件、控制台、网络连接),而且需要通过大量不同的方式与它们通信(顺序、随机访问、二进制、字符、按行、按字等等)。 Java库的设计者通过创建大量类来攻克这个难题。事实上,Java的IO系统采用了如此多的类,以致刚开始会产生不知从何处入手的感觉(具有讽刺意味的是,Java的IO设计初衷实际要求避免过多的类)。从Java 1.0升级到Java 1.1后,IO库的设计也发生了显著的变化。此时并非简单地用新库替换旧库,Sun的设计人员对原来的库进行了大手笔的扩展,添加了大量新的内容。因此,我们有时不得不混合使用新库与旧库,产生令人无奈的复杂代码。 本章将帮助大家理解标准Java库内的各种IO类,并学习如何使用它们。本章的第一部分将介绍“旧”的Java 1.0 IO流库,因为现在有大量代码仍在使用那个库。本章剩下的部分将为大家引入Java 1.1 IO库的一些新特性。注意若用Java 1.1编译器来编译本章第一部分介绍的部分代码,可能会得到一条“不建议使用该特性”(Deprecated feature)警告消息。代码仍然能够使用;编译器只是建议我们换用本章后面要讲述的一些新特性。但我们这样做是有价值的,因为可以更清楚地认识老方法与新方法之间的一些差异,从而加深我们的理解(并可顺利阅读为Java 1.0写的代码)。输入和输出
可将Java库的IO类分割为输入与输出两个部分,这一点在用Web浏览器阅读联机Java类文档时便可知道。通过继承,从InputStream(输入流)衍生的所有类都拥有名为read()的基本方法,用于读取单个字节或者字节数组。类似地,从OutputStream衍生的所有类都拥有基本方法write(),用于写入单个字节或者字节数组。然而,我们通常不会用到这些方法;它们之所以存在,是因为更复杂的类可以利用它们,以便提供一个更有用的接口。因此,我们很少用单个类创建自己的系统对象。一般情况下,我们都是将多个对象重叠在一起,提供自己期望的功能。我们之所以感到Java的流库(Stream Library)异常复杂,正是由于为了创建单独一个结果流,却需要创建多个对象的缘故。
InputStream的类型
InputStream的作用是标志那些从不同起源地产生输入的类。这些起源地包括(每个都有一个相关的InputStream子类): (1) 字节数组 (2) String对象 (3) 文件 (4) “管道”,它的工作原理与现实生活中的管道类似:将一些东西置入一端,它们在另一端出来。 (5) 一系列其他流,以便我们将其统一收集到单独一个流内。 (6) 其他起源地,如Internet连接等(将在本书后面的部分讲述)。
OutputStream的类型
这一类别包括的类决定了我们的输入往何处去:一个字节数组(但没有String;假定我们可用字节数组创建一个);一个文件;或者一个“管道”。
增添属性和有用的接口
利用层次化对象动态和透明地添加单个对象的能力的做法叫作“装饰器”(Decorator)方案——“方案”属于本书第16章的主题(注释①)。装饰器方案规定封装于初始化对象中的所有对象都拥有相同的接口,以便利用装饰器的“透明”性质——我们将相同的消息发给一个对象,无论它是否已被“装饰”。这正是在Java IO库里存在“过滤器”(Filter)类的原因:抽象的“过滤器”类是所有装饰器的基础类(装饰器必须拥有与它装饰的那个对象相同的接口,但装饰器亦可对接口作出扩展,这种情况见诸于几个特殊的“过滤器”类中)。
通过FilterInputStream从InputStream里读入数据
FilterInputStream类要完成两件全然不同的事情。其中,DataInputStream允许我们读取不同的基本类型数据以及String对象(所有方法都以“read”开头,比如readByte(),readFloat()等等)。伴随对应的DataOutputStream,我们可通过数据“流”将基本类型的数据从一个地方搬到另一个地方。这些“地方”是由表10.1总结的那些类决定的。若读取块内的数据,并自己进行解析,就不需要用到DataInputStream。但在其他许多情况下,我们一般都想用它对自己读入的数据进行自动格式化。
通过FilterOutputStream向OutputStream里写入数据
与DataInputStream对应的是DataOutputStream,后者对各个基本数据类型以及String对象进行格式化,并将其置入一个数据“流”中,以便任何机器上的DataInputStream都能正常地读取它们。所有方法都以“wirte”开头,例如writeByte(),writeFloat()等等。
本身的缺陷:RandomAccessFile
RandomAccessFile用于包含了已知长度记录的文件,以便我们能用seek()从一条记录移至另一条;然后读取或修改那些记录。各记录的长度并不一定相同;只要知道它们有多大以及置于文件何处即可。
File类
File类有一个欺骗性的名字——通常会认为它对付的是一个文件,但实情并非如此。它既代表一个特定文件的名字,也代表目录内一系列文件的名字。若代表一个文件集,便可用list()方法查询这个集,返回的是一个字串数组。之所以要返回一个数组,而非某个灵活的集合类,是因为元素的数量是固定的。而且若想得到一个不同的目录列表,只需创建一个不同的File对象即可。事实上,“FilePath”(文件路径)似乎是一个更好的名字。本节将向大家完整地例示如何使用这个类,其中包括相关的FilenameFilter(文件名过滤器)接口。
目录列表器
现在假设我们想观看一个目录列表。可用两种方式列出File对象
1. 匿名内部类
2. 顺序目录列表检查与创建目录
File类并不仅仅是对现有目录路径、文件或者文件组的一个表示。亦可用一个File对象新建一个目录,甚至创建一个完整的目录路径——假如它尚不存在的话。
IO流的典型应用
尽管库内存在大量IO流类,可通过多种不同的方式组合到一起,但实际上只有几种方式才会经常用到。然而,必须小心在意才能得到正确的组合
输入流
当然,我们经常想做的一件事情是将格式化的输出打印到控制台,但那已在第5章创建的com.bruceeckel.tools中得到了简化。
1. 缓冲的输入文件
2. 从内存输入
3. 格式化内存输入
4. 行的编号与文件输出输出流
两类主要的输出流是按它们写入数据的方式划分的:一种按人的习惯写入,另一种为了以后由一个DataInputStream而写入。RandomAccessFile是独立的,尽管它的数据格式兼容于DataInputStream和DataOutputStream。
5. 保存与恢复数据
6. 读写随机访问文件快捷文件处理
由于以前采用的一些典型形式都涉及到文件处理,所以大家也许会怀疑为什么要进行那么多的代码输入——这正是装饰器方案一个缺点。
7. 快速文件输入
8. 快速输出格式化文件
9. 快速输出数据文件从标准输入中读取数据
以Unix首先倡导的“标准输入”、“标准输出”以及“标准错误输出”概念为基础,Java提供了相应的System.in,System.out以及System.err。贯这一整本书,大家都会接触到如何用System.out进行标准输出,它已预封装成一个PrintStream对象。System.err同样是一个PrintStream,但System.in是一个原始的InputStream,未进行任何封装处理。这意味着尽管能直接使用System.out和System.err,但必须事先封装System.in,否则不能从中读取数据。
管道数据流
本章已简要介绍了PipedInputStream(管道输入流)和PipedOutputStream(管道输出流)。尽管描述不十分详细,但并不是说它们作用不大。
StreamTokenizer
尽管StreamTokenizer并不是从InputStream或OutputStream衍生的,但它只随同InputStream工作,所以十分恰当地包括在库的IO部分中。
StringTokenizer
Java 1.1的IO流
到这个时候,大家或许会陷入一种困境之中,怀疑是否存在IO流的另一种设计方案,并可能要求更大的代码量。
(1) 在老式层次结构里加入了新类,所以Sun公司明显不会放弃老式数据流。
(2) 在许多情况下,我们需要与新结构中的类联合使用老结构中的类。为达到这个目的,需要使用一些“桥”类:InputStreamReader将一个InputStream转换成Reader,OutputStreamWriter将一个OutputStream转换成Writer。数据的发起与接收
Java 1.0的几乎所有IO流类都有对应的Java 1.1类,用于提供内建的Unicode管理。
修改数据流的行为
在Java 1.0中,数据流通过FilterInputStream和FilterOutputStream的“装饰器”(Decorator)子类适应特定的需求。
未改变的类
显然,Java库的设计人员觉得以前的一些类毫无问题,所以没有对它们作任何修改,可象以前那样继续使用它们:
一个例子
为体验新类的效果,下面让我们看看如何修改IOStreamDemo.java示例的相应区域,以便使用Reader和Writer类:
重导向标准IO
Java 1.1在System类中添加了特殊的方法,允许我们重新定向标准输入、输出以及错误IO流。
压缩
Java 1.1也添加一个类,用以支持对压缩格式的数据流的读写。它们封装到现成的IO类中,以提供压缩功能。
用GZIP进行简单压缩
GZIP接口非常简单,所以如果只有单个数据流需要压缩(而不是一系列不同的数据),那么它就可能是最适当选择。
用Zip进行多文件保存
Java归档(jar)实用程序
Zip格式亦在Java 1.1的JAR(Java ARchive)文件格式中得到了采用。
对象序列化
Java 1.1增添了一种有趣的特性,名为“对象序列化”(Object Serialization)。
寻找类
读者或许会奇怪为什么需要一个对象从它的序列化状态中恢复。举个例子来说,假定我们序列化一个对象,并通过网络将其作为文件传送给另一台机器。
序列化的控制
正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特殊的安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那一部分需要重新创建。
1. transient(临时)关键字
2. Externalizable的替代方法
3. 版本问题利用“持久性”
一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在具体实现以前,有些问题是必须解决的。
总结
Java IO流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网(参见第15章)进行读写。可以创建新的输入和输出对象类型(通过从InputStream和OutputStream继承)。向一个本来预期为收到字串的方法传递一个对象时,由于Java已限制了“自动类型转换”,所以会自动调用toString()方法。而我们可以重新定义这个toString(),扩展一个数据流能接纳的对象种类。 在IO数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全可以指定一旦有人试图覆盖该文件就“掷”出一个违例——有的编程系统允许我们自行指定想打开一个输出文件,但唯一的前提是它尚不存在。但在Java中,似乎必须用一个File对象来判断某个文件是否存在,因为假如将其作为FileOutputStream或者FileWriter打开,那么肯定会被覆盖。若同时指定文件和目录路径,File类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”! IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的IO包都提供了这方面的支持(这一点没有在Java 1.1里得以纠正,它完全错失了改变库设计方案的机会,反而增添了更特殊的一些情况,使复杂程度进一步提高)。Java 1.1转到那些尚未替换的IO库,而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。 然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个时候,为此多付出的代码行应该不至于使你觉得太生气。
第11章 运行期类型鉴定
运行期类型鉴定(RTTI)的概念初看非常简单——手上只有基础类型的一个句柄时,利用它判断一个对象的正确类型。 然而,对RTTI的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问题正式摆上了桌面。 本章将讨论如何利用Java在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI,它假定我们已在编译和运行期拥有所有类型;另一种是Java1.1特有的“反射”机制,利用它可在运行期独立查找类信息。首先讨论“传统”的RTTI,再讨论反射问题。
对RTTI的需要
Class对象
为理解RTTI在Java里如何工作,首先必须了解类型信息在运行期是如何表示的。
1. 类标记造型前的检查
-
RTTI语法
Java用Class对象实现自己的RTTI功能——即便我们要做的只是象造型那样的一些工作。Class类也提供了其他大量方式,以方便我们使用RTTI。
反射:运行期类信息
如果不知道一个对象的准确类型,RTTI会帮助我们调查。但却有一个限制:类型必须是在编译期间已知的,否则就不能用RTTI调查它,进而无法展开下一步的工作。换言之,编译器必须明确知道RTTI要处理的所有类。
一个类方法提取器
很少需要直接使用反射工具;之所以在语言中提供它们,仅仅是为了支持其他Java特性,比如对象序列化(第10章介绍)、Java Beans以及RMI(本章后面介绍)。
总结:
利用RTTI可根据一个匿名的基础类句柄调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有些时候多形性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列switch语句。他们可能用RTTI做到这一点,从而在代码开发和维护中损失多形性技术的重要价值。Java的要求是让我们尽可能地采用多形性,只有在极特别的情况下才使用RTTI。
传递和返回对象
到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个句柄。 在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在C++中则是变得非常复杂)。Java亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。 若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java有指针吗?”有些人认为指针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java有如此好的口碑,所以应该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java是有指针的!事实上,Java中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说,Java有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“句柄”,但你可以把它想像成“安全指针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢来,要习惯一些沉闷的工作。
传递句柄
别名问题
“别名”意味着多个句柄都试图指向同一个对象,就象前面的例子展示的那样。
制作本地副本
稍微总结一下:Java中的所有自变量或参数传递都是通过传递句柄进行的。
按值传递
克隆对象
若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。
使类具有克隆能力
尽管克隆方法是在所有类最基本的Object中定义的,但克隆仍然不会在每个类里自动进行。
1. 使用protected时的技巧
2. 实现Cloneable接口成功的克隆
理解了实现clone()方法背后的所有细节后,便可创建出能方便复制的类,以便提供了一个本地副本
Object.clone()的效果
调用Object.clone()时,实际发生的是什么事情呢?当我们在自己的类里覆盖clone()时,什么东西对于super.clone()来说是最关键的呢?根类中的clone()方法负责建立正确的存储容量,并通过“按位复制”将二进制位从原始对象中复制到新对象的存储空间。
克隆合成对象
用Vector进行深层复制
通过序列化进行深层复制
若研究一下第10章介绍的那个Java 1.1对象序列化示例,可能发现若在一个对象序列化以后再撤消对它的序列化,或者说进行装配,那么实际经历的正是一个“克隆”的过程。
使克隆具有更大的深度
若新建一个类,它的基础类会默认为Object,并默认为不具备克隆能力(就象在下一节会看到的那样)。只要不明确地添加克隆能力,这种能力便不会自动产生。但我们可以在任何层添加它,然后便可从那个层开始向下具有克隆能力。
为什么有这个奇怪的设计
克隆的控制
为消除克隆能力,大家也许认为只需将clone()方法简单地设为private(私有)即可,但这样是行不通的,因为不能采用一个基础类方法,并使其在衍生类中更“私有”。
副本构建器
克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。
只读类
尽管在一些特定的场合,由clone()产生的本地副本能够获得我们希望的结果,但程序员(方法的作者)不得不亲自禁止别名处理的副作用。
创建只读类
“一成不变”的弊端
不变字串
- 隐式常数
2. 覆盖”+”和StringBufferString和StringBuffer类
这里总结一下同时适用于String和StringBuffer的方法,以便对它们相互间的沟通方式有一个印象。
首先总结String类的各种方法: 方法 自变量,覆盖 用途字串的特殊性
现在,大家已知道String类并非仅仅是Java提供的另一个类总结:
由于Java中的所有东西都是句柄,而且由于每个对象都是在内存堆中创建的——只有不再需要的时候,才会当作垃圾收集掉,所以对象的操作方式发生了变化,特别是在传递和返回对象的时候。创建窗口和程序片
在Java 1.0中,图形用户接口(GUI)库最初的设计目标是让程序员构建一个通用的GUI,使其在所有平台上都能正常显示。为何要用AWT?
(1)Java 1.1的新型AWT是一个更好的编程模型,并向更好的库设计迈出了可喜的一步。而Java Beans则是那个库的框架。
(2)“GUI构建器”(可视编程环境)将适用于所有开发系统。在我们用图形化工具将组件置入窗体的时候,Java Beans和新的AWT使GUI构建器能帮我们自动完成代码。其它组件技术如ActiveX等也将以相同的形式支持。基本程序片
库通常按照它们的功能来进行组合。
init() 程序片第一次被创建,初次运行初始化程序片时调用
start() 每当程序片进入Web浏览器中,并且允许程序片启动它的常规操作时调用(特殊的程序片被stop()关闭);
同样在init()后调用 paint() 基础类Component的一部分(继承结构中上溯三级)。
作为update()的一部分调用,以便对程序片的画布进行特殊的描绘 stop() 每次程序片从Web浏览器的视线中离开时调用,使程序片能关闭代价高昂的操作;同样在调用destroy()前调用
destroy() 程序片不再需要,将它从页面中卸载时调用,以执行资源的最后清除工作程序片的测试
我们可在不必建立网络连接的前提下进行一次简单的测试,方法是启动我们的Web浏览器,然后打开包含了程序片标签的HTML文件(Sun公司的JDK同样包括一个称为“程序片观察器”的工具,它能挑出html文件的