常用设计模式及Java程序


http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 308 第二部分 常用设计模式 及 Java 程序设计 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 309 第一章 设计模式基础 教学目标: i掌握设计模式的基本概念 i掌握模式在设计中的应用 i掌握程序设计的基本要求 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 310 一:什么是设计模式 在面向对象的软件设计中,总是希望避免重复设计或尽可能少做重复设计。有经验的面向对象 设计者的确能做出良好的设计,而新手则面对众多选择无从下手,总是求助于以前使用过的非面向 对象技术。有经验的设计者显然知道一些新手所不知道的东西,这又是什么呢? 内行的设计者知道:不是解决任何问题都要从头做起。他们更愿意复用以前使用过的解决方案。 当找到一个好的解决方案,他们会一遍又一遍地使用。这些经验是他们成为内行的部分原因。它们 帮助设计者将新的设计建立在以往工作的基础上,复用以往成功的设计方案。一个熟悉这些模式的 设计者不需要再去发现它们,而能够立即将它们应用于设计问题中。 设计模式使人们可以更加简单方便地复用成功的设计和体系结构。将已证实的技术表述成设计 模式也会使新系统开发者更加容易理解其设计思路。设计模式帮助你做出有利于系统复用的选择, 避免设计损害了系统复用性。通过提供一个显式类和对象作用关系以及它们之间潜在联系的说明规 范,设计模式甚至能够提高已有系统的文档管理和系统维护的有效性。简而言之,设计模式可以帮 助设计者更快更好地完成系统设计。 1:什么是设计模式 Christopher Alexander 说过:“每一个模式描述了一个在我们周围不断重复发生的问题,以 及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。尽管A l e x a n d e r所指的是城市和建筑模式,但他的思想也同样适用于面向对象设计模式,只是在面向 对象的解决方案里,我们用对象和接口代替了墙壁和门窗。两类模式的核心都在于提供了相关问题 的解决方案。 一般而言,一个模式有四个基本要素: 模式名称(pattern name) 一个助记名,它用一两个词来描述模式的问题、解决方案和效果。 问题(problem) 描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,它可能描述了特定的 设计问题,如怎样用对象表示算法等。也可能描述了导致不灵活设计的类或对象结构。有时候,问 题部分会包括使用模式必须满足的一系列先决条件。 解决方案(solution) 描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模 板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计 问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。 效果(consequences) 描述了模式应用的效果及使用模式应权衡的问题。尽管我们描述设计决策时,并不总提到模式 效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重要意义。软件效果大多关注对 时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模 式效果包括它对系统的灵活性、扩充性或可移植性的影响,显式地列出这些效果对理解和评价这些 模式很有帮助。出发点的不同会产生对什么是模式和什么不是模式的理解不同。一个人的模式对另 一个人来说可能只是基本构造部件。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 311 二:设计模式怎样解决设计问题 设计模式采用多种方法解决面向对象设计者经常碰到的问题。这里给出几个问题以及使用设计 模式解决它们的方法。 1: 寻找合适的对象 面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过 程 通 常称为方法或操作。 对象在收到客户的请求(或消息)后,执行相应的操作。 客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。由于这些限 制,对象的内部状态是被封装的,它不能被直接访问,它的表示对于对象外部是不可见的。 面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依 赖关系、灵活性、性能、演化、复用等等,它们都影响着系统的分解,并且这些因素通常还是互相 冲突的。 面向对象设计方法学支持许多设计方法。你可以写出一个问题描述,挑出名词和动词,进而创 建相应的类和操作;或者,你可以关注于系统的协作和职责关系;或者,你可以对现实世界建模, 再将分析时发现的对象转化至设计中。至于哪一种方法最好,并无定论。 设计的许多对象来源于现实世界的分析模型。但是,设计结果所得到的类通常在现实世界中并 不存在,有些是像数组之类的低层类,而另一些则层次较高。设计中的抽象对于产生灵活的设计是 至关重要的。 设计模式帮你确定并不明显的抽象和描述这些抽象的对象。例如,描述过程或算法的对象现实 中并不存在,但它们却是设计的关键部分。 2 决定对象的粒度 对象在大小和数目上变化极大。它们能表示下自硬件或上自整个应用的任何事物。那么我们怎 样决定一个对象应该是什么呢?设计模式很好地讲述了这个问题,具体的我们会在以后的设计模式 的学习中讲到。 3 指定对象接口 对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构( s i g n a t u r e )。对象操作所定义的所有操作型构的集合被称为该对象的接口( i n t e r f a c e )。 对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给 该对象。 类型(type) 是用来标识特定接口的一个名字。如果一个对象接受“ Wi n d o w”接口所定义 的所有操作请求,那么我们就说该对象具有“ Wi n d o w”类型。一个对象可以有许多类型,并且 不同的对象可以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分则可用其 他类型刻画。两个类型相同的对象只需要共享它们的部分接口。接口可以包含其他接口作为子集。 当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型( s u b t y p e ), 另一个类型称之为它的超类型( s u p e r t y p e )。我们常说子类型继承了它的超类型的接口。 在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如果 不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。对象接口与其功能实 现是分离的,不同对象可以对请求做不同的实现,也就是说,两个有相同接口的对象可以有完全不 同的实现。 当给对象发送请求时,所引起的具体操作既与请求本身有关又与接受对象有关。支持相同请求 的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时刻的 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 312 连接就称之为动态绑定(dynamic binding)。 动态绑定是指发送的请求直到运行时刻才受你的具体的实现的约束。因而,在知道任何有正确 接口的对象都将接受此请求时,你可以写一个一般的程序,它期待着那些具有该特定接口的对象。 进一步讲,动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换性就称为多态( p o l y m o r p h i s m ),它是面向对象系统中的核心概念之一。多态允许客户对象仅要求其他对象 支持特定接口,除此之外对其假设几近于无。多态简化了客户的定义,使得对象间彼此独立,并可 以在运行时刻动态改变它们相互的关系。 设计模式通过确定接口的主要组成成分及经接口发送的数据类型,来帮助你定义接口。设计模 式也许还会告诉你接口中不应包括哪些东西。设计模式也指定了接口之间的关系。 4 描述对象的实现 至此,我们很少提及到实际上怎么去定义一个对象。对象的实现是由它的类决定的,类指定了 对象的内部数据和表示,也定义了对象所能完成的操作。 对象通过实例化类来创建,此对象被称为该类的实例。当实例化类时,要给对象的内部数据(由 实例变量组成)分配存储空间,并将操作与这些数据联系起来。对象的许多类似实例是由实例化同一 个类来创建的。 4.1. 类继承与接口继承的比较 理解对象的类( c l a s s )与对象的类型( t y p e )之间的差别非常重要。一个对象的类定 义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。但是对象的类型只与它的接 口有关,接口即对象能响应的请求的集合。一个对象可以有多个类型,不同类的对象可以有相同的 类型。 当然,对象的类和类型是有紧密关系的。因为类定义了对象所能执行的操作,也定义了对象的 类型。当我们说一个对象是一个类的实例时,即指该对象支持类所定义的接口。 理解类继承和接口继承(或子类型化)之间的差别也十分重要。类继承根据一个对象的实现定义 了另一个对象的实现。简而言之,它是代码和表示的共享机制。然而,接口继承(或子类型化)描述 了一个对象什么时候能被用来替代另一个对象。 4.2. 对接口编程,而不是对实现编程 类继承是一个通过复用父类功能而扩展应用功能的基本机制。它允许你根据旧对象快速定义新 对象。它允许你从已存在的类中继承所需要的绝大部分功能,从而几乎无需任何代价就可以获得新 的实现。 然而,实现的复用只是成功的一半,继承所拥有的定义具有相同接口的对象族的能力也是很重 要的(通常可以从抽象类来继承)。为什么?因为多态依赖于这种能力。 当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅添加 或重定义操作,而没有隐藏父类的操作。这时,所有的子类都能响应抽象类接口中的请求,从而子 类的类型都是抽象类的子类型。 只根据抽象类中定义的接口来操纵对象有以下两个好处: 1) 客户无须知道他们使用对象的特定类型,只须对象有客户所期望的接口。 2) 客户无须知道他们使用的对象是用什么类来实现的,他们只须知道定义接口的抽象类。 这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原则: 针对接口编程,而不是针对实现编程。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 313 5 运用复用机制 理解对象、接口、类和继承之类的概念对大多数人来说并不难,问题的关键在于如何运用它们 写出灵活的、可复用的软件。设计模式将告诉你怎样去做。 5.1. 继承和组合的比较 面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。正如 我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用 通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的 内部细节对子类可见。 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。 对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言 直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作 时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。 但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改 变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示 了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密 的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。 当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新 的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用 性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。 对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接 口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这 还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运 行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实 现上存在较少的依赖关系。 对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被 集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大 物。另一方面,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象 间的关系而不是被定义在某个类中。 这导出了我们的面向对象设计的第二个原则: 优先使用对象组合,而不是类继承。 理想情况下,你不应为获得复用而去创建新的构件。你应该能够只使用对象组合技术,通过组 装已有的构件就能获得你需要的功能。但是事实很少如此,因为可用构件的集合实际上并不足够丰 富。使用继承的复用使得创建新的构件要比组装旧的构件来得容易。这样,继承和对象组合常一起 使用。 然而,经验表明:设计者往往过度使用了继承这种复用技术。但依赖于对象组合技术的设计却 有更好的复用性(或更简单)。你将会看到设计模式中一再使用对象组合技术。 5.2. 委托 委托(d e l e g a t i o n)是一种组合方法,它使组合具有与继承同样的复用能力。在委托方 式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者(d e l e g a t e)。 这类似于子类将请求交给它的父类处理。使用继承时,被继承的操作总能引用接受请求的对象,C + http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 314 +中通过t h i s成员变量, S m a l l t a l k中则通过s e l f。委托方式为了得到同样的效果, 接受请求的对象将自己传给被委托者(代理人),使被委托的操作可以引用接受请求的对象。 举例来说,我们可以在窗口类中保存一个矩形类的实例变量来代理矩形类的特定操作,这样窗 口类可以复用矩形类的操作,而不必像继承时那样定义成矩形类的子类。也就是说,一个窗口拥有 一个矩形,而不是一个窗口就是一个矩形。窗口现在必须显式的将请求转发给它的矩形实例,而不 是像以前它必须继承矩形的操作。 委托的主要优点在于它便于运行时刻组合对象操作以及改变这些操作的组合方式。假定矩形对 象和圆对象有相同的类型,我们只需简单的用圆对象替换矩形对象,则得到的窗口就是圆形的。 委托与那些通过对象组合以取得软件灵活性的技术一样,具有如下不足之处:动态的、高度参 数化的软件比静态软件更难于理解。还有运行低效问题,不过从长远来看人的低效才是更主要的。 只有当委托使设计比较简单而不是更复杂时,它才是好的选择。要给出一个能确切告诉你什么时候 可以使用委托的规则是很困难的。因为委托可以得到的效率是与上下文有关的,并且还依赖于你的 经验。委托最适用于符合特定程式的情形,即标准模式的情形。 委托是对象组合的特例。它告诉你对象组合作为一个代码复用机制可以替代继承。 5.3. 继承和参数化类型的比较 另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是 类属( generic ) ( Ada、Eiffel )或模板(templates) (C++)。它允许你在定义一个类型时并不指 定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。例如,一个列表类能 够以它所包含元素的类型来进行参数化。如果你想声明一个Integer列表,只需将Integer类型作为 列表参数化类型的参数值;声明一个String列表,只需提供String类型作为参数值。语言的实现将 会为各种元素类型创建相应的列表类模板的定制版本。 参数化类型给我们提供除了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。 许多设计可以使用这三种技术中的任何一种来实现。实现一个以元素比较操作为可变元的排序例程, 可有如下方法: 1) 通过子类实现该操作( Template Method的一个应用)。 2 ) 实现为传给排序例程的对象的职责( S t r a t e g y ( 5 . 9 ) )。 3) 作为C + +模板或A d a类属的参数,以指定元素比较操作的名称。 这些技术存在着极大的不同之处。对象组合技术允许你在运行时刻改变被组合的行为,但是它 存在间接性,比较低效。继承允许你提供操作的缺省实现,并通过子类重定义这些操作。参数化类 型允许你改变类所用到的类型。但是继承和参数化类型都不能在运行时刻改变。哪一种方法最佳, 取决于你设计和实现的约束条件。 5.4 设计应支持变化 获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求你的系统设计要 能够相应地改进。 为了设计适应这种变化、且具有健壮性的系统,你必须考虑系统在它的生命周期内会发生怎样 的变化。一个不考虑系统变化的设计在将来就有可能需要重新设计。这些变化可能是类的重新定义 和实现,修改客户和重新测试。重新设计会影响软件系统的许多方面,并且未曾料到的变化总是代 价巨大的。 设计模式可以确保系统能以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允 许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某一种特殊变化将更健壮。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 315 6:设计模式的应用 使用设计模式有助于增强软件的灵活性。这种灵活性所具有的重要程度取决于你将要建造的软 件系统。让我们看一看设计模式在开发如下三类主要软件中所起的作用:应用程序、工具箱和框架。 6.1: 应用程序 如果你将要建造像文档编辑器或电子制表软件这样的应用程序(Application Program),那么它 的内部复用性、可维护性和可扩充性是要优先考虑的。内部复用性确保你不会做多余的设计和实现。 设计模式通过减少依赖性来提高内部复用性。松散耦合也增强了一类对象与其他多个对象协作的可 能性。例如,通过孤立和封装每一个操作,以消除对特定操作的依赖,可使在不同上下文中复用一 个操作变得更简单。消除对算法和表示的依赖可达到同样的效果。 当设计模式被用来对系统分层和限制对平台的依赖性时,它们还会使一个应用更具可维护性。 通过显示怎样扩展类层次结构和怎样使用对象复用,它们可增强系统的易扩充性。同时,耦合程度 的降低也会增强可扩充性。如果一个类不过多地依赖其他类,扩充这个孤立的类还是很容易的。 6.2: 工具箱 一个应用经常会使用来自一个或多个被称为工具箱( Toolkit ) 的 预 定义 类 库 中的类。工具箱是 一组相关的、可复用的类的集合,这些类提供了通用的功能。工具箱的一个典型例子就是列表、关 联表单、堆栈等类的集合, C + + 的I/O流库是另一个例子。工具箱并不强制应用采用某个特定的设 计,它们只是为你的应用提供功能上的帮助。工具箱强调的是代码复用,它们是面向对象环境下的 “子程序库”。 工具箱的设计比应用设计要难得多,因为它要求对许多应用是可用的和有效的。再者,工具箱 的设计者并不知道什么应用使用该工具箱及它们有什么特殊需求。这样,避免假设和依赖就变得很 重要,否则会限制工具箱的灵活性,进而影响它的适用性和效率。 6.3: 框架 框架( Framework)是构成一类特定软件可复用设计的一组相互协作的类。例如,一个框架能帮 助建立适合不同领域的图形编辑器,像艺术绘画、音乐作曲和机械C A D 。 另 一个 框架也许能帮助你 建立针对不同程序设计语言和目标机器的编译器。而再一个也许能帮助你建立财务建模应用。你可 以定义框架抽象类的应用相关的子类,从而将一个框架定制为特定应用。 框架规定了你的应用的体系结构。它定义了整体结构,类和对象的分割,各部分的主要责任, 类和对象怎么协作,以及控制流程。框架预定义了这些设计参数,以便于应用设计者或实现者能集 中精力于应用本身的特定细节。框架记录了其应用领域的共同的设计决策。因而框架更强调设计复 用,尽管框架常包括具体的立即可用的子类。 这个层次的复用导致了应用和它所基于的软件之间的反向控制(inversion of control)。当你 使用工具箱(或传统的子程序库)时,你需要写应用软件的主体并且调用你想复用的代码。而当你使 用框架时,你应该复用应用的主体,写主体调用的代码。你不得不以特定的名字和调用约定来写操 作地实现,但这会减少你需要做出的设计决策。 你不仅可以更快地建立应用,而且应用还具有相似的结构。它们很容易维护,且用户看来也更 一致。另一方面,你也失去了一些表现创造性的自由,因为许多设计决策无须你来作出。 如果说应用程序难以设计,那么工具箱就更难了,而框架则是最难的。框架设计者必须冒险决 定一个要适应于该领域的所有应用的体系结构。任何对框架设计的实质性修改都会大大降低框架所 带来的好处,因为框架对应用的最主要贡献在于它所定义的体系结构。因此设计的框架必须尽可能 地灵活、可扩充。 更进一步讲,因为应用的设计如此依赖于框架,所以应用对框架接口的变化是极其敏感的。当 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 316 框架演化时,应用不得不随之演化。这使得松散耦合更加重要,否则框架的一个细微变化都将引起 强烈反应。 刚才讨论的主要设计问题对框架设计而言最具重要性。一个使用设计模式的框架比不用设计模 式的框架更可能获得高层次的设计复用和代码复用。成熟的框架通常使用了多种设计模式。设计模 式有助于获得无须重新设计就可适用于多种应用的框架体系结构。 当框架和它所使用的设计模式一起写入文档时,我们可以得到另外一个好处。了解设计模式的 人能较快地洞悉框架。甚至不了解设计模式的人也可以从产生框架文档的结构中受益。加强文档工 作对于所有软件而言都是重要的,但对于框架其重要性显得尤为突出。学会使用框架常常是一个必 须克服很多困难的过程。设计模式虽然无法彻底克服这些困难,但它通过对框架设计的主要元素做 更显式的说明可以降低框架学习的难度。 因为模式和框架有些类似,人们常常对它们有怎样的区别和它们是否有区别感到疑惑。它们最 主要的不同在于如下三个方面: 1) 设计模式比框架更抽象 框架能够用代码表示,而设计模式只有其实例才能表示为代码。框架的威力在于它们能够 使用程序设计语言写出来,它们不仅能被学习,也能被直接执行和复用。 2) 设计模式是比框架更小的体系结构元素 一个典型的框架包括了多个设计模式,而反之决非如此。 3) 框架比设计模式更加特例化 框架总是针对一个特定的应用领域。一个图形编辑器框架可能被用于一个工厂模拟,但它 不会被错认为是一个模拟框架。 框架变得越来越普遍和重要。它们是面向对象系统获得最大复用的方式。较大的面向对象应用 将会由多层彼此合作的框架组成。应用的大部分设计和代码将来自于它所使用的框架或受其影响。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 317 第二章 Java 程序设计中最基本的设计模式 教学目标: i掌握单例模式 i掌握工厂模式 i掌握值对象模式 i掌握 DAO 模式 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 318 一:单例模式 1:环境: 几乎在每个应用程序中,都需要有一个从中进行全局访问和维护某种类型数据的区域。 在面向 对象的 (OO) 系统中也有这种情况,在此类系统中,在任何给定时间只应运行一个类或某个类的一 组预定义数量的实例。 例如,当使用某个类来维护增量计数器时,此简单的计数器类需要跟踪在多 个应用程序领域中使用的整数值。 此类需要能够增加该计数器并返回当前的值。 对于这种情况, 所需的类行为应该仅使用一个类实例来维护该整数,而不是使用其它类实例来维护该整数。 最初,人们可能会试图将计数器类实例只作为静态全局变量来创建。 这是一种通用的 方法,但实际上只解决一部分问题;它解决了全局可访问性问题,但没有采取任何措施 来确保在任何给定的时间只运行一个类实例。 应该由类本身来负责只使用一个类实例, 而不是由类用户来负责。 应该始终不要让类用户来监视和控制运行的类实例的数量。 2:问题: 采用什么方法来控制创建类实例,然 后 确保 在 任何给定的时间只创建一个类实例。 这会确切地 给我们提供所需的行为,并使客户端不必了解任何类细节。 3:解决方案: Singleton 模式主要作用是保证在 Java 应用程序中,一 个类 Class 只有一个实例存在。 在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。还有, singleton 能 够被状态化; 这样,多个单态类在一起就可以作为一个状态仓库一样向外提供服务,比 如,你要论坛中的帖子计数器,每次浏览一次需要计数,单态类能否保持住这个计数, 并且能 synchronize 的安全自动加 1,如果你要把这个数字永久保存到数据库,你可以在 不修改单态接口的情况下方便的做到。 另外方面,Singleton 也能够被无状态化。提供工具性质的功能,Singleton 模式就为 我们提供了这样实现的可能。使用 Singleton 的好处还在于可以节省内存,因为它限制 了实例的个数,有利于 Java 垃圾回收(garbage collection)。我们常常看到工厂模式中类 装入器(class loader)中也用 Singleton 模式实现的,因为被装入的类实际也属于资源。 Singleton 的逻辑模型如下: 我们看到的是一个简单的类图表,显 示 有一个 Singleton 对象的私有静态属性以及返回此相同 属性的公共方法 Instance()。 这实际上是 Singleton 的核心。 还有其他一些属性和方法,用于 说明在该类上允许执行的其他操作。 为了便于此次讨论,让我们将重点放在实例属性和方法上。 客户端仅通过实例方法来访问任何 Singleton 实例。 此处没有定义创建实例的方式。 我们还希望 能够控制如何以及何时创建实例。 在 OO 开发中,通常可以在类的构造函数中最好地处理特殊对象 的创建行为。 这种情况也不例外。 我们可以做的是,定义我们何时以及如何构造类实例,然后禁 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 319 止任何客户端直接调用该构造函数。 这是在 Singleton 构造中始终使用的方法。 让我们看一下 Design Patterns 中的原始示例。 通常,将下面所示的 C++ Singleton 示例实现代码示例视为 Singleton 的默认实现。 本示例已移植到很多其他编程语言中,通常它在任何地方的形式与此几乎 相同。 4:使用示例: 一般 Singleton 模式通常有两种形式: 第一种形式: public class Singleton { private Singleton(){} //在自己内部定义自己一个实例,是不是很奇怪? //注意这是 private 只供内部调用 private static Singleton instance = new Singleton(); //这里提供了一个供外部访问本 class 的静态方法,可以直接访问 public static Singleton getInstance() { return instance; } } 第二种形式: public class Singleton { private static Singleton instance = null; public static synchronized Singleton getInstance() { //这个方法比上面有所改进,不用每次都进行生成对象,只是第一次 //使用时生成实例,提高了效率! if (instance==null) instance=new Singleton(); return instance; } } 使用 Singleton.getInstance()可以访问单态类。 上面第二中形式是 lazy initialization,也就是说第一次调用时初始 Singleton,以后就不用 再生成了。 注意到 lazy initialization 形式中的 synchronized,这个 synchronized 很重要,如果没有 synchronized , 那 么 使用 getInstance() 是有可能得到多个 Singleton 实 例 。 关 于 lazy initialization 的 Singleton 有很多涉及 double-checked locking (DCL)的讨论,有兴趣者进一步 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 320 研究。 一般认为第一种形式要更加安全些。 使用 Singleton 注意事项: 有时在某些情况下,使用 Singleton 并不能达到 Singleton 的目的,如有多个 Singleton 对象 同时被不同的类装入器装载;在 EJB 这样的分布式系统中使用也要注意这种情况,因为 EJB 是跨服 务器,跨 JVM 的。 我们以 SUN 公司的宠物店源码(Pet Store 1.3.1)的 ServiceLocator 为例稍微分析一下: 在 Pet Store 中 ServiceLocator 有两种,一个是 EJB 目录下;一个是 WEB 目录下,我们检查这 两个 ServiceLocator 会发现内容差不多,都是提供 EJB 的查询定位服务,可是为什么要分开呢?仔 细研究对这两种 ServiceLocator 才发现区别:在 WEB 中的 ServiceLocator 的采取 Singleton 模式, ServiceLocator 属于资源定位,理所当然应该使用 Singleton 模式。但是在 EJB 中,Singleton 模 式已经失去作用,所以 ServiceLocator 才分成两种,一 种 面向 WEB 服务的,一 种 是面向 EJB 服务的。 Singleton 模式看起来简单,使用方法也很方便,但是真正用好,是非常不容易,需要对 Java 的类 线程内存等概念有相当的了解。 二:工厂模式 1:环境 框架使用抽象类定义和维护对象之间的关系。这些对象的创建通常也由框架负责。 考虑这样一个应用框架,它可以向用户显示多个文档。在这个框架中,两个主要的抽象是类 Application 和 Document。这两个类都是抽象的,客户必须通过它们的子类来做与具体应用相关的 实现。例如,为创建一个绘图应用,我们定义类 DrawingApplication 和 DrawingDocument。 Application 类负责管理 Document 并根据需要创建它们—例如,当用户从菜单中选择 Open 或 New 的时候。 因为被实例化的特定 Document 子类是与特定应用相关的,所以 Application 类不可能预测到哪 个 Document 子类将被实例化—Application 类仅知道一个新的文档何时应被创建,而不知道哪一种 Document 将被创建。这就产生了一个尴尬的局面:框架必须实例化类,但是它只知道不能被实例化 的抽象类。 2:问题 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例 化延迟到其子类。 3:解决方案 工厂模式是一种创建性模式,它定义了一个创建对象的接口,但是却让子类来决定具体实例化哪 一个类.当一个类无法预料要创建哪种类的对象或是一个类需要由子类来指定创建的对象时我们就 需要用到工厂模式了.简单说来工厂模式可以根据不同的条件产生不同的实例,当然这些不同的实例 通常是属于相同的类型。 工厂模式把创建这些实例的具体过程封装起来了,简化了客户端的应用, 也改善了程序的扩展性,使得将来可以做最小的改动就可以加入新的待创建的类. 通常我们将工厂 模式作为一种标准的创建对象的方法,当发现需要更多的灵活性的时候,就开始考虑向其它创建型模 式转化。 工厂模式是我们最常用的模式了,工厂模式在 Java 程序系统可以说是随处可见,结构如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 321 为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的 new,我们经常要根据类 Class 生成实例对象,如 A a=new A() 工厂模式也是用来创建实例对象的,所以以后 new 时就要多 个心眼,是否可以考虑实用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的 可扩展性和尽量少的修改量。 我们以类 Sample 为例, 如果我们要创建 Sample 的实例对象: Sample sample=new Sample(); 可是,实际情况是,通常我们都要在创建 sample 实例时做点初始化的工作,比如赋值 查询数据 库等。 首先,我们想到的是,可以使用 Sample 的构造函数,这样生成实例就写成: Sample sample=new Sample(参数); 但是,如果创建 sample 实例时所做的初始化工作不是象赋值这样简单的事,可能是很长一段代 码,如果也写入构造函数中,那你的代码很难看了(就需要 Refactor 重整)。 为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段 代码,说明要做的工作很多,将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是 很危险的,这也是有背于 Java 面向对象的原则,面向对象的封装(Encapsulation)和分派 (Delegation)告诉我们,尽 量 将长 的 代 码分 派 “ 切割” 成 每 段 ,将 每段 再 “ 封装”起来(减少段和段 之间偶合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动 百的事情。 在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例 所需要的大量初始化工作从 Sample 的构造函数中分离出去。 这时我们就需要 Factory 工厂模式来生成对象了,不能再用上面简单 new Sample(参数)。还有, 如果 Sample 有个继承如 MySample, 按照面向接口编程,我们需要将 Sample 抽象成一个接口.现在 Sample 是接口,有两个子类 MySample 和 HisSample .我们要实例化他们时,如下: Sample mysample=new MySample(); Sample hissample=new HisSample(); 随着项目的深入,Sample 可能还会"生出很多儿子出来", 那么我们要对这些儿子一个个实例化, 更糟糕的是,可能还要对以前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避 免的. 但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了. http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 322 工厂模式中有: 工厂方法(Factory Method) 抽象工厂(Abstract Factory),这两个模式区别在 于需要创建对象的复杂程度上。 3.1:工厂方法 你会建立一个专门生产 Sample 实例的工厂: public class Factory{ public static Sample creator(int which){ //getClass 产生 Sample 一般可使用动态类装载装入类。 if (which==1) return new SampleA(); else if (which==2) return new SampleB(); } } 那么在你的程序中,如果要实例化 Sample 时.就使用 Sample sampleA=Factory.creator(1); 这样,在整个就不涉及到 Sample 的具体子类,达到封装效果,也就减少错误修改的机会,这个原 理可以用很通俗的话来比喻:就是具体事情做得越多,越容易范错误.这每个做过具体工作的人都深 有体会,相反,官做得越高,说出的话越抽象越笼统,范错误可能性就越少. 使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的 Sample,产品接口下有 Sample 接口的实现类,如 SampleA,其次要有一个 factory 类,用来生成产品 Sample,进一步稍微复 杂一点,就是在工厂类上进行拓展,工厂类也有继承它的实现类 concreteFactory 了。 3.2:抽象工厂 如果我们创建对象的方法变得复杂了,如上面工厂方法中是创建一个对象 Sample,如果我们还有 新的产品接口 Sample2. 这里假设:Sample 有两个 concrete 类 SampleA 和 SamleB,而 Sample2 也有两个 concrete 类 Sample2A 和 SampleB2 那么,我们就将上例中 Factory 变成抽象类,将共同部分封装在抽象类中,不同部分使用子类实 现,下面就是将上例中的 Factory 拓展成抽象工厂: public abstract class Factory{ public abstract Sample creator(); public abstract Sample2 creator(String name); } public class SimpleFactory extends Factory{ public Sample creator(){ ......... return new SampleA } public Sample2 creator(String name){ http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 323 ......... return new Sample2A } } public class BombFactory extends Factory{ public Sample creator(){ ...... return new SampleB } public Sample2 creator(String name){ ...... return new Sample2B } } 从上面看到两个工厂各自生产出一套 Sample 和 Sample2,也许你会疑问,为什么我不可以使用 两个工厂方法来分别生产 Sample 和 Sample2? 抽象工厂还有另外一个关键要点,是因为 SimpleFactory 内,生产 Sample 和生产 Sample2 的 方法之间有一定联系,所以才要将这两个方法捆绑在一个类中,这个工厂类有其本身特征,也许制 造过程是统一的,比如:制造工艺比较简单,所以名称叫 SimpleFactory。 在实际应用中,工厂方法用得比较多一些,而且是和动态类装入器组合在一起应用。 4:使用示例 我们以 Jive 的 ForumFactory 为例,这个例子在前面的 Singleton 模式中我们讨论过,现在再 讨论其工厂模式: public abstract class ForumFactory { private static Object initLock = new Object(); private static String className = "com.jivesoftware.forum.database.DbForumFactory"; private static ForumFactory factory = null; public static ForumFactory getInstance(Authorization authorization) { //If no valid authorization passed in, return null. if (authorization == null) { return null; } //以下使用了 Singleton 单态模式 if (factory == null) { synchronized(initLock) { if (factory == null) { ...... try { //动态转载类 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 324 Class c = Class.forName(className); factory = (ForumFactory)c.newInstance(); } catch (Exception e) { return null; } } } } //Now, 返回 proxy.用来限制授权对 forum 的访问 return new ForumFactoryProxy(authorization, factory, factory.getPermissions(authorization)); } //真正创建 forum 的方法由继承 forumfactory 的子类去完成. public abstract Forum createForum(String name, String description) throws UnauthorizedException, ForumAlreadyExistsException; .... } 因为现在的 Jive 是通过数据库系统存放论坛帖子等内容数据,如果希望更改为通过文件系统实 现,这个工厂方法 ForumFactory 就提供了提供动态接口: private static String className = "com.jivesoftware.forum.database.DbForumFactory"; 你 可以使用自己开发的创建 forum 的 方 法代替 com.jivesoftware.forum.database.DbForumFactory 就可以. 在上面的一段代码中一共用了三种模式,除了工厂模式外,还有 Singleton 单态模式,以及 proxy 模式,proxy 模式主要用来授权用户对 forum 的访问,因为访问 forum 有两种人:一个是注册用户 一 个是游客 guest,那么那么相应的权限就不一样,而且这个权限是贯穿整个系统的,因此建立一个 proxy,类似网关的概念,可以很好的达到这个效果。 看看 Java 宠物店中的 CatalogDAOFactory: public class CatalogDAOFactory { /** * 本方法制定一个特别的子类来实现 DAO 模式。 * 具体子类定义是在 JEE 的部署描述器中。 */ public static CatalogDAO getDAO() throws CatalogDAOSysException { CatalogDAO catDao = null; try { InitialContext ic = new InitialContext(); //动态装入 CATALOG_DAO_CLASS http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 325 //可以定义自己的 CATALOG_DAO_CLASS,从而在无需变更太多代码 //的前提下,完成系统的巨大变更。 String className =(String) ic.lookup(JNDINames.CATALOG_DAO_CLASS); catDao = (CatalogDAO) Class.forName(className).newInstance(); } catch (NamingException ne) { throw new CatalogDAOSysException(" CatalogDAOFactory.getDAO: NamingException while getting DAO type : \n" + ne.getMessage()); } catch (Exception se) { throw new CatalogDAOSysException(" CatalogDAOFactory.getDAO: Exception while getting DAO type : \n" + se.getMessage()); } return catDao; } } CatalogDAOFactory 是典型的工厂方法,catDao 是通过动态类装入器 className 获得 CatalogDAOFactory 具体实现子类,这个实现子类在 Java 宠物店是用来操作 catalog 数据库,用户 可以根据数据库的类型不同,定制自己的具体实现子类,将自己的子类名给与 CATALOG_DAO_CLASS 变量就可以。 由此可见,工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,只要我们更换一下 具体的工厂方法,系统其他地方无需一点变换,就有可能将系统功能进行改头换面的变化。 三:值对象模式 1:环境 在基于客户需要与 ejb 大量地交换数据的情况,具体来说,在 JEE 平台中,应用系统通常将服 务器端的程序组件实现为会话 bean 和实体 bean,而这些组件的部分方法则需要将数据返回给客户; 这种情况下,通常一个用户会重复调用相关方法多次,直到它得到相关信息,应该注意的是,多数 情况这些方法调用的目的都是为了取得单一的信息,例如用户名或者用户地址等。 显而易见,在 JEE 平台上,这种调用基本上都是来自远程的。也就是说,用户多次调用相应的 方法会给 Web 带来极大的负担,即使用户和 EJB 容器加载相同的 JVM、OS 和计算机上运行 EJB 程 序,由于方法调用被缺省地认为是远程任务,所以这种问题依然存在。 2:问题 应用程序客户端需要与企业 Bean 之间交换数据,程序层间交换数据 3:解决方案 值对象(value object)模式通过减少分布式通信的消息而促进数据的交换,通常这里所指的通信是 在 Web 层和 EJB 层之间。在一个远程调用中,一个单一值对象可以被用来取出一系列相关数据并提 供给客户。 由于以上所提到的问题,在远程方法的调用次数增加的时候,相关的应用程序性能将会有很大 的下降,因此利用多次方法调用而取得单一的信息是非常低效的;在这种情况,JEE 的研究人员建 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 326 议使用值对象来包含所有的程序数据,即每次方法调用可以发送和接收这个值对象;当用户向 EJB 发出对于程序数据的请求时,EJB 会创建这个值对象,将它的各个域赋以相关的数值,并将整个对 象传送给用户。 当 EJB 使用值对象的时候,用户可以通过仅仅一次方法调用来取得整个对象,而不是使用 多次方法调用以得到对象中每个域的数值;由于值对象是通过值传递而交送给用户的,所以所 有对于该值对象的调用或取值都是本地调用,而不是远程方法调用。不过需要注意的是,这个 值对象必须具有对应于每个属性的访问方法,或者将所有属性都设为公共的。 下图以类图的形式表明了业务对象和值对象之间的关系。 业务对象创建了值对象。而用户通过访问业务对象,既得到了所需的信息,也对相关数据 做出了一定的修改;为了能够使得用户可以修改业务对象各个域的取值,这个对象必须提供一 定的变值方法,而出于对 Web 负担的考虑,业务对象所提供的方法最好以值对象为参数。相应 地,这些方法可以去调用值对象所提供的方法,来设置值对象的各个成员变量的取值;同时在 值对象的方法中,我们也可以植入数据验证和完整性检查的逻辑,这样在用户从业务对象的方 法得到值对象时,可以直接调用值对象的成员方法进行本地数据访问,当然这种本地数据访问 不会影响到业务对象。 4:使用示例 4.1 实现值对象模式 我们列举一个例子,假设某名为 project 的业务对象被模拟或者实现为一个实体 bean。当客户 端调用值对象的 getProjectData()方法时,该 project 实体 bean 需要通过该值对象向客户端发送 数据。如下例: public class ProjectVO implements java.io.Serializable { private String projectId; private String projectName; private String managerId; private String customerId; private Date startDate; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 327 private Date endDate; private boolean started; private boolean completed; private boolean accepted; private Date acceptedDate; private String projectDescription; private String projectStatus; // Value object constructors... //以下就是具体的每一个的 get 和 set 方法 } 四:DAO 模式 1:环境 目前大部分的 JEE 应用程序都需要在一定程度上使用可持久性的数据,而实现持久性数据的方 法因应用程序不同而异,并且访问不同存储格式数据的应用程序接口(API)也有着显著的差别;有的 时候,应用程序还会访问存储在不同操作平台上的数据,这使得问题更为复杂,通常,应用程序会 使用共享的分布式组件,如实体 bean 来表达持久性数据。应用程序可以使用 bean 管理的持久性实 体 bean,而在实体 bean 中植人数据访问逻辑,或者使用容器管理的持久性实体 bean,从而使容器 管理所有的事务和持久性细节;而如果应用程序对于数据访问的需求十分简单的话,也可以采用会 话 bean 或 Servlet 直接访问持久性存储来读取和修改数据。 一些应用程序可以使用 JDBC 应用程序接口来访问关系数据库中的数据,JDBC 负责一般的持久 性数据访问和管理,在 JEE 应用程序中,JDBC 中可以嵌入 SQL 语句,用以访问关系型数据库,当然 根据数据库类型的不同,SQL 语句的词法和语法也会有所不同;需要说明的是,当数据存储格式不 同的时候,数据访问逻辑的区别就更加明显了,例如关系型数据库、面向对象数据库和磁盘文件, 各自数据的访问逻辑各有千秋,这样一来就造成了程序代码和数据访问代码之间的依赖关系;当程 序组件,即实体 bean、会话 bean 或 servlet、JSP 等需要访问数据源时,它们会使用正确的应用程 序接口来得到连接并管理数据源,但这样也会造成这些组件与数据源物理实现之间的依赖关系,从 而使得应用程序很难从一个数据存储实体移植到另一个数据存储实体中去;当数据源的物理实现变 化的时候,应用程序也必须相应地加以改变。 2:问题 根据数据源不同,数据访问也不同。根据存储的类型(关系数据库、面向对象数据库、纯文件 等等)和供应商实现不同,持久性存储(如数据库)的访问差别也很大。如何对存储层以外的模块 屏蔽这些复杂性,以提供统一的调用存储实现。 3:解决方案 数据访问对象(data access object,DAO)模式将数据访问逻辑抽象为特殊的资源,也就是说将 系统资源的接口从其底层访问机制中隔离出来;通过将数据访问的调用打包,数据访问对象可以促 进对于不同数据库类型和模式的数据访问。 这种模式出现的背景在于数据访问的逻辑极大程度上取决于数据存储的格式,比如说关系型数 据库、面向对象数据库、磁盘文件等。 基于以上所讨论的问题,开发人员开始采用数据访问对象的方法。数据访问对象实际上就是包 含对于所有数据访问逻辑的对象,并管理着对于数据源的连接,根据数据源的不同,数据访问对象 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 328 实现了不同的访问机制,这里所说的数据源可以是持久性存储介质,如关系型数据库,也可以是外 部服务,如 B2B 的数据交换;不仅是用户,而且包括应用系统中的其他组件,也可以使用数据访问 对象所提供的数据访问接口,数据访问对象将数据源的物理实现细节与其用户完全分离开来,并且 在底层数据源变化的时候,数据访问对象向用户提供的接口是不会变化的;这种方法使应用系统使 用数据访问对象时可以适应多种数据存储介质,总之,数据访问对象就是系统组件和数据源中间的 适配器。 下图中的类图表示了数据访问对象设计模式的参与对象和之间的调用关系 下图是 DAO 模式的时序图 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 329 对于序列图中的组件加以解释如下: (1)业务对象(Business Object) 表示数据的用户,它需要对于数据的访问,一个业务对象可以用会话 bean、实体 bean 或 是其他 Java 程序来实现。 (2)数据访问对象(Data Access Object) 数据访问对象是这种模式中的主题,它提供了底层数据访问的对象,并将其提供给业务对 象以使得后者能够透明地访问数据源;同时业务对象也将数据的加载和存储操作移交给数据访 问对象处理。 (3)数据源(Data source) 这里指的是数据源的物理实现,这个数据源可以是一个数据库,包括关系型数据库、面向 对象数据库或文件系统。 (4)值对象(Transfer Object) 这里的值对象指的是数据载体。数据访问对象可以使用值对象来向用户返回数据,而数据 访问对象同样可以从用户那里得到值对象来对数据源中的数据进行更新。 下面给出几种实现数据访问对象设计模式的方法。 (1)自动数据访问对象代码的生成 既然每一个业务对象都对应于一个数据访问对象,那么开发人员就可以建立业务对象、 数据访问对象和底层实现的关系;一旦这种关系建立起来,开发人员就可以为所有的数据访 问对象编写特殊的代码生成工具。 生成数据访问对象的信息通常存储在一个开发人员定义的描述文件中,如果对于数据访 问对象的要求过于复杂,开发人员可以考虑使用第三方工具来为关系型数据库提供对象对关 系的映射。这些工具通常是一些 GUI 程序,可以用来将业务对象映射为持久性的存储对象, http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 330 并定义中间运作的数据访问对象,在映射完成的时候,这些工具可以自动地生成代码,并提 供一些相应的功能,如缓存结果、缓存查询、与应用服务器整合、与第三方产品整合等。 (2)数据访问对象代理(Factory for Data Access Objects) 当底层的数据存储不会轻易改变的时候,开发人员可以采取这种方法来实现相应的, 数据访问对象,下图是这种方法的类图。 当底层的数据存储可能会变化的时候,开发人员可以采用抽象代理的方法来实现数 据访问对象;抽象代理的方法会创建一些虚拟的数据访问对象代理和各种类型的实际数据 访问对象代理,每种对象对应一种持久性存储介质的实现,一旦组件得到这些代理,就可 以利用来创建需要使用的数据访问对象。 下 图给出了这种情况的类图。该类图表示了一个基础的数据访问对象代理,它是一 个抽象类,被其他一些实际的数据访问对象代理继承以支持特定的数据访问函数;用户可 以得到一个实际的数据访问对象,并利用它来创建需要的数据访问对象而访问相关的数据, 每一个实际的数据访问对象都负责建立对于数据源的连接,并得到和管理所支持的业务数 据。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 331 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 332 上图是这种情况下的序列图。 这种设计模式的优势: 透明性好 业务对象可以在不知道数据源实现细节的情况下访问数据。由于一切数据访问细节被数据 访问对象所隐藏,所以这种访问过程是透明的。 可移植性好 在应用系统中添加数据访问对象,可以使得前者能够很方便地移植到另外一种数据库实现 上。业务对象与数据实现是隔离的,所以在移植过程中,仅仅对数据访问对象进行一些变化即 可。 减少业务对象的代码复杂度 由于数据访问对象可以管理所有的数据访问复杂细节,这也就简化了业务模块和其他数据 客户的代码。同时也提高了应用系统的整体可读性和开发率。 集中处理所有数据访问 由于所有的数据访问操作都移交给数据访问对象,这样应用系统其他部分就与数据访问实 现隔离开来,而全部相关操作都与数据访问对象集中处理,这样也使得相关操作更加容易被维 护和管理。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 333 这种设计模式的缺陷: 对于容器管理的持久性不能利用 如果 EJB 容器采取容器管理的方式,那么所有对于持久性数据存储的管理都由容器负责。 这样的话应用系统就无需实现数据访问对象了,因为应用服务将透明地提供这一功能。 添加了额外的层面 数据访问对象在数据用户和数据源之间添加了一个层面,也就增加了一些额外的设计和实 现的负担。当然,我们认为它是物有所值的。 总之,在开发人员选择不同模式的时候,应该注意,一定的模式对应于一定的应用层次。 比如说,与视图和显示相关的模式就是在 Web 层应用的。而一些与业务逻辑控制相关的模式则 是与 EJB 层次相关的。另外一些关于读取数据和分派操作的模式则适用于不同的层次之间。 4:应用示例 4.1:抽象的 DAOFactory 类 public abstract class DAOFactory { // List of DAO types supported by the factory public static final int CLOUDSCAPE = 1; public static final int ORACLE = 2; public static final int SYBASE = 3; ... // There will be a method for each DAO that can be // created. The concrete factories will have to // implement these methods. public abstract CustomerDAO getCustomerDAO(); public abstract AccountDAO getAccountDAO(); public abstract OrderDAO getOrderDAO(); ... public static DAOFactory getDAOFactory( int whichFactory) { switch (whichFactory) { case CLOUDSCAPE: return new CloudscapeDAOFactory(); case ORACLE : return new OracleDAOFactory(); case SYBASE : return new SybaseDAOFactory(); ... default : return null; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 334 } } } 4.2:具体的 DAOFactory 的实现 public class CloudscapeDAOFactory extends DAOFactory { public static final String DRIVER= "COM.cloudscape.core.RmiJdbcDriver"; public static final String DBURL= "jdbc:cloudscape:rmi://localhost:1099/CoreJEEDB"; // method to create Cloudscape connections public static Connection createConnection() { // Use DRIVER and DBURL to create a connection // Recommend connection pool implementation/usage } public CustomerDAO getCustomerDAO() { // CloudscapeCustomerDAO implements CustomerDAO return new CloudscapeCustomerDAO(); } public AccountDAO getAccountDAO() { // CloudscapeAccountDAO implements AccountDAO return new CloudscapeAccountDAO(); } public OrderDAO getOrderDAO() { // CloudscapeOrderDAO implements OrderDAO return new CloudscapeOrderDAO(); } ... } 4.3:Customer 的基本 DAO 接口 public interface CustomerDAO { public int insertCustomer(...); public boolean deleteCustomer(...); public Customer findCustomer(...); public boolean updateCustomer(...); public RowSet selectCustomersRS(...); public Collection selectCustomersVO(...); ... } 4.4:Customer 的 Cloudscape DAO 实现 import java.sql.*; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 335 public class CloudscapeCustomerDAO implements CustomerDAO { public CloudscapeCustomerDAO() { // initialization } // The following methods can use // CloudscapeDAOFactory.createConnection() // to get a connection as required public int insertCustomer(...) { // Implement insert customer here. // Return newly created customer number // or a -1 on error } public boolean deleteCustomer(...) { // Implement delete customer here // Return true on success, false on failure } public Customer findCustomer(...) { // Implement find a customer here using supplied // argument values as search criteria // Return a value object if found, // return null on error or if not found } public boolean updateCustomer(...) { // implement update record here using data // from the customerData value object // Return true on success, false on failure or // error } public RowSet selectCustomersRS(...) { // implement search customers here using the // supplied criteria. // Return a RowSet. } public Collection selectCustomersVO(...) { // implement search customers here using the http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 336 // supplied criteria. // Alternatively, implement to return a Collection // of value objects. } ... } 4.5:Customer 值对象 public class Customer implements java.io.Serializable { // member variables int CustomerNumber; String name; String streetAddress; String city; ... // getter and setter methods... ... } 4.6:使用 DAO 和 DAO 工厂——客户端代码 ... // create the required DAO Factory DAOFactory cloudscapeFactory = DAOFactory.getDAOFactory(DAOFactory.DAOCLOUDSCAPE); // Create a DAO CustomerDAO custDAO = cloudscapeFactory.getCustomerDAO(); // create a new customer int newCustNo = custDAO.insertCustomer(...); // Find a customer object. Get the value object. Customer cust = custDAO.findCustomer(...); // modify the values in the value object. cust.setAddress(...); cust.setEmail(...); // update the customer object using the DAO custDAO.updateCustomer(cust); // delete a customer object custDAO.deleteCustomer(...); // select all customers in the same city http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 337 Customer criteria=new Customer(); criteria.setCity(“纽约”); Collection customersList = custDAO.selectCustomersVO(criteria); // returns customersList - collection of Customer // value objects. iterate through this collection to // get values. ... http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 338 第三章 Java 程序设计和模式应用 教学目标: i常见面向对象的设计原则 i常见设计模式的使用 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 339 一:常见面向对象的设计原则 前面我们已经讲到了两个面向对象的设计原则: 一是针对接口编程,而非针对实现编程 二是优先使用对象组合,而非类继承 下面我们来看看其它的常见的面向对象的设计原则。 1:开放-封闭法则(OCP) 软件组成实体应该是可扩展的,但是是不可修改的 开放-封闭法则认为我们应该试图去设计出永远也不需要改变的模块。 我们可以添加新代码来扩展系统的行为。我们不能对已有的代码进行修改。 符合 OCP 的模块需满足两个标准: (1)可扩展,即“对扩展是开放的”(Open For Extension)-模块的行为可以被扩展, 以需要满足新的需求。 (2)不可更改,即“对更改是封闭的”(Closed for Modification)-模块的源代码是不 允许进行改动的。 我们能如何去做呢? (1)抽象(Abstraction) (2)多态(Polymorphism) (3)继承(Inheritance) (4)接口(Interface) 一个软件系统的所有模块不可能都满足 OCP,但是我们应该努力最小化这些不满足 OCP 的模块 数量。 开放-封闭法则是 OO 设计的真正核心。 符合该法则便意味着最高等级的复用性(reusability)和可维护性(maintainability)。 OCP 示例 考虑下面某类的方法: 以上函数的工作是在制订的部件数组中计算各个部件价格的总和。 若 Part 是一个基类或接口且使用了多态,则 该 类 可 很 容易地来适应新类型的部件,而不必对其 进行修改。 其将符合 OCP 但是在计算总价格时,若财务部颁布主板和内存应使用额外费用,则将如何去做。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 340 下列的代码是如何来做的呢? 这符合 OCP 吗? 当每次财务部提出新的计价策略,我们都不得不要修改 totalPrice()方法!这并非“对更改是 封闭的”。显然,策略的变更便意味着我们不得不要在一些地方修改代码的,因此我们该如何去做呢? 为了使用我们第一个版本的 totalPrice(),我们可以将计价策略合并到 Part 的 getPrice()方 法中。 这里是 Part 和 ConcretePart 类的示例: 但是现在每当计价策略发生改变,我们就必须修改 Part 的每个子类! 一个更好的思路是采用一个 PricePolicy 类,通过对其进行继承以提供不同的计价策略: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 341 看起来我们所做的就是将问题推迟到另一个类中。但是使用该解决方案,我们可通过改变 Part 对象,在运行期间动态地来设定计价的策略。 另一个解决方案是使每个 ConcretePart 从数据库或属性文件中获取其当前的价格。 单选法则 单选法则(the Single Choice Principle)是 OCP 的一个推论。 单选法则: 无论在什么时候,一个软件系统必须支持一组备选项,理想情况下,在系统中只能有一个 类能够知道整个的备选项集合。 2:Liskov 替换法则(LSP) 使用指向基类(超类)的引用的函数,必须能够在不知道具体派生类(子类)对象类型的情况 下使用它们。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 342 显而易见,Liskov 替换法则(LSP)是根据我所熟知的“多态”而得出的。 例如: 方法 drawShape 应该可与 Sharp 超类的任何子类一起工作(或者,若 Sharp 为 Java 接口,则该 方法可与任何实现了 Sharp 接口的类一起工作) 但是当我们在实现子类时必须要谨慎对待,以确保我们不会无意中违背了 LSP。 若一个函数未能满足 LSP,那么可能是因为它显式地引用了超类的一些或所有子类。这样的函 数也违背了 OCP,因为当我们创建一个新的子类时,会不得不进行代码的修改。 LSP 示例 考虑下面 Rectangle 类: 现在,Square 类会如何呢?显然,一个正方形是一个四边形,因此 Square 类应该从 Rectangle 类派生而来,对否?让我们看一看! 观察可得: 正方形不需要将高和宽都作为属性,但是总之它将继承自 Rectangle。因此,每一个 Square 对 象会浪费一点内存,但这并不是一个主要问题。 继承而来的 setWidth()和 setHeight()方法对于 Square 而言并非真正地适合,因为一个正方形 的高和宽是相同。因此我们将需要重写 setWidth()和 setHeight()方法。不得不重写这些简单的方 法有可能是一种不恰当的继承使用方式。 Square 类如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 343 看起来都还不错。但是让我们检验一下! http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 344 测试程序输出: 看上去好像我们违背了 LSP! 这里的问题出在哪里呢?编写 testLsp()方法的程序员做了一个合理的假设,即改变 Rectangle 的宽而保持它的高不变。 在将一个 Square 对象传递给这样一个方法时产生了问题,显然是违背了 LSP Square 和 Rectangle 类是相互一致和合法的。尽管程序员对基类作了合理的假设,但其所编写 的方法仍然会导致设计模型的失败。 不能孤立地去看待解决方案,必须根据设计用户所做的合理假设来看待它们。 一个数学意义上的正方形可能是一个四边形,但是一个 Square 对象不是一个 Rectangle 对象, 因为一个 Square 对象的行为与一个 Rectangle 对象的行为是不一致的! 从行为上来说,一个 Square 不是一个 Rectangle!一个 Square 对象与一个 Rectangle 对象之 间不具有多态的特征。 总结 Liskov 替换法则(LSP)清楚地表明了 ISA 关系全部都是与行为有关的。 为了保持 LSP(并与开放-封闭法则一起),所有子类必须符合使用基类的 client 所期望的行 为。 一个子类型不得具有比基类型(base type)更多的限制,可能这对于基类型来说是合法的,但 是可能会因为违背子类型的其中一个额外限制,从而违背了 LSP! LSP 保证一个子类总是能够被用在其基类可以出现的地方! 二:常见设计模式的应用 下面我们就前面学到的面向对象设计原则和常见设计模式,来学习如何使用他们,构成较好的 面向对象的 Java 程序设计。 1:面向接口编程 面向接口编程是第一大原则。 在 Java 程序设计里面,非 常 讲 究 层 的 划 分和模块的划分。通常我们按照三层来划分 Java 程序, 分别是 UI 层、逻辑层、数据存储层,他们之间都要通过接口来通讯。 在每一个层里面,又有很多个小模块,一个小模块对外也应该是一个整体,那么一个模块对外 也应该提供接口,其他地方需要使用到这个模块的功能,都应该通过此接口来进行调用。这也就是 常说的”接口是被其隔离部分的外观”。 具体的图示如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 345 在一个层内部的各个模块交互也要通过接口,比如: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 346 具体各个部分的接口应该如何去定义,具体的内容是什么,我们不去深究,那是需要具体问题 具体分析的,我们这里只是来学习设计的方法。 大家看到,不管是一层还是一个模块或者一个组件,都是一个被接口隔离的整体,那么下面我 们就不去区分他们,统一认为都是接口隔离部分即可,也即是如下图: 2:接口定义中的参数和返回值 接口里面一定会定义接口隔离体部分需要暴露的方法,对于方法就有参数和返回值的定义,到 底我们需要在接口里面怎么来描述需要传递的参数呢? 值对象模式给了我们很好的解决方案。事实上,值对象已经成为了不同层或是不同模块之间数 据交换的标准方法,它体现的是数据的封装,也利于对象的复用。 那么上面的设计图就演变成了: 3:编程中如何得到接口 我们知道,在 Java 程序中,使用一个类要先通过 new 的操作,得到一个类的实例,然后通过这 个类实例去调用类的属性和方法。 可是接口呢?我们怎么得到它的实例呢?要知道接口是不能直接 new 操作的。前面我们也学过, 获取接口的方式其实就是获取接口实现类的实例,也既是: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 347 Interface inf = new 实现类(); 但是在面向接口编程的世界里,如果这样做,意味着客户端必须知道你具体的实现类,这破坏 了接口的隔离性。使用接口就是不让外部知道隔离体内部的实现的。怎么办呢? 前面学过的工厂模式给出了这个问题的解决方案,使用 Factory 来”生产”接口,对外仅仅提 供工厂和接口。 那么上面的设计图就演变成了: 4:内部实现中如何进行数据存储操作 好了到目前为止,我们已经学会了如何设计一个模块对外公开的部分,接下来的问题就是,在 模块内部实现的时候,应该怎么做呢?关于这个问题,也有很多的设计模式来处理,但是我们这个 课程不讲那么细,具体的内容请参看有关设计模式的资料。 在本课程里面我们重点讲一下模块实现与外部的交互,前面已经学会了如何设计一个模块对外 公开的部分,那么,在内部实现的时候,遇到需要数据存储的时候,我们应该怎么操作呢?对于这 个问题,DAO 模式给出了较好的解决方案。 那么上面的设计图就演变成了: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 348 5:总结 总结上面所学到的知识,我们知道了如何去设计一个功能块的对外的部分,那就是采用接口进 行隔离,然后同时暴露值对象和工厂类,具体的设计实现就完全被封装起来了。如果是需要数据存 储的功能,又会通过 DAO 模式去与数据存储层交互。 至于具体的实现内部,也会有很多的设计模式和解决方案,留待以后的学习。 下图是到目前为止,我们设计一个功能块的结构图: 功能块 内部实现,不对外公开 工厂:对 外 生 产 接口 接口:访问功 能 块 的唯 一 入口 值对象: 封装交 换数据 DAO:数据存储 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 349 第三部分 案例分析 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 350 第一个案例:地址薄 在这个案例里面,我 们将应用前面讲到的设计模式和构建的系统架构,让大家初步领略 Java 程 序设计的实际应用。 地址薄这个程序是基于 Swing 的应用程序,虽然很小,但是很能体现问题,而且在这个程序里 面涉及到了 Java 众多的基础知识,值得好好去体会。 由于我们是用来做教学示例,为了要体现主要的内容,并没有仔细去完善,包括界面和很多细 节处理上,但是用来学习是简洁明了的,更多的细节希望同学们可以去继续完善它。 1:功能需求 地址薄的功能需求很简单(主要是考虑到教学案例的简洁和明确性)。 (1)要求能够新增记录 (2)要求能够对已有的记录进行修改 (3)要求能够删除无用的记录 (4)要求能够按照一些条件来查询记录 应用环境要求: (1)本系统是一个单机的应用,所以考虑使用 Swing (2)由于是纯 Java 的程序示例,所以只能考虑使用文件来作为数据存储的介质 (3)要求程序要有很好的可扩展性,可以在以后很方便的添加其他的实现方式,比如使用数据 库来存储数据;或者是把界面替换成为 jsp 等等。在替换这些方式的时候,要求是不需要去改动逻 辑部分。 用例图如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 351 2:系统设计 2.1 系统架构 根据前面学到的知识,我们首先把这个系统分成三层,图示如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 352 大家看到在上面的设计图中,我们把层间接口也设计了出来,当然,具体的接口的内容,需要 到后面才设计,现在只是定下来层与层之间有接口,名称是什么。 2.2:包结构 在分析完系统构架过后,我们一起来看看如何安排包的结构。事实上,包结构是系统构架在代 码级上的体现,所以我们通过察看一个系统的包结构就能大致知道系统的设计,一定要重视它。 我们把设计的重点放在逻辑层和数据处理层,UI 层不做过多的设计。原因是 Java 的界面一直 是它的弱项,而且目前应用的也不是太多。 对于逻辑层我们的结构主要综合利用前面学习到的接口、工厂、单例、值对象的知识,在加上 对接口的内部实现,来合理安排包结构。 逻辑层包结构图如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 353 对于数据处理层我们的结构主要综合利用前面学习到的接口、工厂、单例、值对象和 DAO 的知 识,在加上对接口的内部实现,来合理安排包结构。 数据处理层包结构图如下: 2.3:类设计 根据需求和前面的设计,分别设计逻辑层和数据处理层的类图如下: 逻辑层的类图如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 354 数据处理层的类图如下: 3:系统实现 UI 层采用 swing 来实现,所以对每一种操作对应一个 panel,放到 panel 包里面。数据存储使 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 355 用文件来实现,除了使用 IO 外,会大量使用集合框架的知识。 具体的实现代码请参看电子代码。 4:系统总结 虽然地址薄是一个小得不能再小的示例,但是“麻雀虽小,五脏俱全”。它可以训练到我们的: Java 语法、控制语句、接口、类等基本知识,还可以训练到我们的:IO 包的使用、集合框架的使用 等常用的类包,更可以在 Java 程序的设计上对我们做一些基本的训练。所以请同学们认真体会这个 例子,早日进入编写 Java 程序的大门。 以下为辅助案例,不做重点讲解,有余力的同学可以去认真看一下,看的过程中要注意,这些 程序的设计均不是非常好,虽然实现了功能。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 356 第二个案例:数字时钟 功能: 1.该程序完成各种丰富的数字时钟的显示功能。例如: 2.它可以通过参数调整调整前景和背景的颜色。 3.下面是参考源代码约 800 行。 (程序代码详见课堂电子版。) 第三个案例:一个实现类似 ICQ 网络聊天功能的程序 功能: 1.该程序能够实现类似 ICQ 的网络聊天的功能。 2.它需要利用网络编程实现实时在线聊天功能。 3.需要利用数据库编程,实现客户资料的保存,好友信息的保存; 4,需要同时实现客户端和服务端的程序。 (程序代码详见课堂电子版。) 第四个案例:一个 JDBC 连接池的例子 功能: 1. 该程序来自某个 JEE 容器中的 jdbc 连接池的实现源代码,看懂该代码,理解连接池的实现技术。 2. 重点理解如下的内容: a) 如何实现连接池技术? b) 如何分配、释放连接? c) 如何进行同步操作? d) 当连接数达到上限后,它的处理策略? (程序代码详见课堂电子版。) http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 357 体验式拟订分组讨论题目 第一组讨论题: 1. 如何理解 JAVA 语 言 虚 拟 机 的 概念?APPLET 和 APPLICATION 语言的异同是什么?JAVA 是那 种类型的语言? 2. JAVA 语 言 的 标识符、关键字有那些,起什么作用? 3. JAVA 的基本数据类型和 C、C++相比异同点是什么? 4. JAVA 的 语 法 和 C 语 言 的 语 法 有 什么不同? 5. 对 JAVA 语 言 的 整 体 感 受 是 什么? 第二组讨论题: 1. JAVA 数 组 中的限制条件是什么?JAVA 中已经取消了指针的概念,你如何看待这样的 2. C++的面向对象和 JAVA 的面向对象有什么异同? 3. 如何理解接口概念? 4. 如何理解 JAVA 中的抽象类和内部类? 5. Final 和 Static 两个关键字的含义是什么? 第三组讨论题: 1. 什么样的编程环境下异常处理是必不可少的? 2. 日常生活中你是如何处理异常的? 3. JAVA 中 异 常处理的方法是什么? 4. 异常处理和错误处理有什么异同? 5. 你的对文件的操作有那些,JAVA 中对文件操作的 I/O 函数是如何实现你的操作的?是否还有写 操作 JAVA 没有办法时下? 第四组讨论题: 1. 理解高级 I/O 中的高级的含义? 2. 为啥要在 GUI 中引进事件处理机制? 3. JAVA 中的布局管理器有什么功能? 4. JAVA 的 AWT 能满足你对界面的要求吗? 5. JAVA 中的组件和容器有什么特点? 第五组讨论题: 1. 你能举出现实生活中线程的例子吗?举出你所知道所有的?线程处理常有的步骤有那些? 2. 多进程和多线程的异同点在什么地方? 3. 网络编程为啥要用网络协议和接口? 4. SOCK 的特点有那些? 5. APPLET 是 JAVA 的 最 好 表现形式吗?它的最大的优点和缺点各是什么? http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 358 附录 1 对比 C++和 Java 作为一名 C++程序员,我们早已掌握了面向对象程序设计的基本概念,而且 Java 的语法无疑是 非常熟悉的。事实上,Java 本来就是从 C++衍生出来的。” 然而,C++和 Java 之间仍存在一些显著的差异。可以这样说,这些差异代表着技术的极大进步。 一旦我们弄清楚了这些差异,就会理解为什么说 Java 是一种优秀的程序设计语言。本附录将引导大 家认识用于区分 Java 和 C++的一些重要特征。 (1) 最大的障碍在于速度:解释过的 Java 要比 C 的执行速度慢上约 20 倍。无 论 什么都不能阻止 Java 语言进行编译。写作本书的时候,刚刚出现了一些准实时编译器,它们能显著加快速度。当然, 我们完全有理由认为会出现适用于更多流行平台的纯固有编译器,但假若没有那些编译器,由于速 度的限制,必须有些问题是 Java 不能解决的。 (2) 和 C++一样,Java 也提供了两种类型的注释。 (3) 所有东西都必须置入一个类。不存在全局函数或者全局数据。如果想获得与全局函数等价的 功能,可考虑将 static 方法和 static 数据置入一个类里。注意没有象结构、枚举或者联合这一类的东 西,一切只有“类”(Class)! (4) 所有方法都是在类的主体定义的。所以用 C++的眼光看,似乎所有函数都已嵌入,但实情 并非如何(嵌入的问题在后面讲述)。 (5) 在 Java 中,类定义采取几乎和 C++一样的形式。但没有标志结束的分号。没有 class foo 这 种形式的类声明,只有类定义。 class aType(){ void aMethod() {/* 方法主体 */} } (6) Java 中没有作用域范围运算符“::”。Java 利用点号做所有的事情,但可以不用考虑它,因为 只能在一个类里定义元素。即使那些方法定义,也必须在一个类的内部,所以根本没有必要指定作 用域的范围。我们注意到的一项差异是对 static 方法的调用:使用 ClassName.methodName()。除此 以外,package(包)的名字是用点号建立的,并能用 import 关键字实现 C++的“#include”的一部 分功能。例如下面这个语句: import java.awt.*; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 359 (#include 并不直接映射成 import,但在使用时有类似的感觉。) (7) 与 C++类似,Java 含有一系列“主类型”(Primitive type),以实现更有效率的访问。在 Java 中,这些类型包括 boolean,char,byte,short,int,long,float 以及 double。所有主类型的大小都 是固有的,且与具体的机器无关(考虑到移植的问题)。这肯定会对性能造成一定的影响,具体取决 于不同的机器。对类型的检查和要求在 Java 里变得更苛刻。例如: ■条件表达式只能是 boolean(布尔)类型,不可使用整数。 ■必须使用象 X+Y 这样的一个表达式的结果;不能仅仅用“X+Y”来实现“副作用”。 (8) char(字符)类型使用国际通用的 16 位 Unicode 字符集,所以能自动表达大多数国家的字符。 (9) 静态引用的字串会自动转换成 String 对象。和 C 及 C++不同,没有独立的静态字符数组字 串可供使用。 (10) Java 增添了三个右移位运算符“>>>”,具有与“逻辑”右移位运算符类似的功用,可在最 末尾插入零值。“>>”则会在移位的同时插入符号位(即“算术”移位)。 (11) 尽管表面上类似,但与 C++相比,Java 数组采用的是一个颇为不同的结构,并具有独特的 行为。有一个只读的 length 成员,通过它可知道数组有多大。而且一旦超过数组边界,运行期检查 会自动丢弃一个异常。所有数组都是在内存“堆”里创建的,我们可将一个数组分配给另一个(只 是简单地复制数组句柄)。数组标识符属于第一级对象,它的所有方法通常都适用于其他所有对象。 (12) 对于所有不属于主类型的对象,都只能通过 new 命令创建。和 C++不同,Java 没有相应的 命令可以“在堆栈上”创建不属于主类型的对象。所有主类型都只能在堆栈上创建,同时不使用 new 命令。所有主要的类都有自己的“封装(器)”类,所以能够通过 new 创建等价的、以内存“堆”为 基础的对象(主类型数组是一个例外:它们可象 C++那样通过集合初始化进行分配,或者使用 new)。 (13) Java 中不必进行提前声明。若想在定义前使用一个类或方法,只需直接使用它即可——编 译器会保证使用恰当的定义。所以和在 C++中不同,我们不会碰到任何涉及提前引用的问题。 (14) Java 没有预处理机。若想使用另一个库里的类,只需使用 import 命令,并指定库名即可。 不存在类似于预处理机的宏。 (15) Java 用包代替了命名空间。由于将所有东西都置入一个类,而且由于采用了一种名为“封 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 360 装”的机制,它能针对类名进行类似于命名空间分解的操作,所以命名的问题不再进入我们的考虑 之列。数据包也会在单独一个库名下收集库的组件。我们只需简单地“import”(导入)一个包,剩 下的工作会由编译器自动完成。 (16) 被定义成类成员的对象句柄会自动初始化成 null。对基本类数据成员的初始化在 Java 里得 到了可靠的保障。若不明确地进行初始化,它们就会得到一个默认值(零或等价的值)。可对它们进 行明确的初始化(显式初始化):要么在类内定义它们,要么在构建器中定义。采用的语法比 C++ 的语法更容易理解,而 且 对于 static 和非 static 成员来说都是固定不变的。我 们不必从外部定义 static 成员的存储方式,这和 C++是不同的。 (17) 在 Java 里,没有象 C 和 C++那样的指针。用 new 创建一个对象的时候,会获得一个引用 (本书一直将其称作“句柄”)。例如: String s = new String("howdy"); 然而,C++引用在创建时必须进行初始化,而且不可重定义到一个不同的位置。但 Java 引用并 不一定局限于创建时的位置。它们可根据情况任意定义,这便消除了对指针的部分需求。在 C 和 C++ 里大量采用指针的另一个原因是为了能指向任意一个内存位置(这同时会使它们变得不安全,也是 Java 不提供这一支持的原因)。指针通常被看作在基本变量数组中四处移动的一种有效手段。Java 允许我们以更安全的形式达到相同的目标。解决指针问题的终极方法是“固有方法”(已在附录 A 讨论)。将指针传递给方法时,通常不会带来太大的问题,因为此时没有全局函数,只有类。而且我 们可传递对对象的引用。Java 语言最开始声称自己“完全不采用指针!”但随着许多程序员都质问没 有指针如何工作?于是后来又声明“采用受到限制的指针”。大家可自行判断它是否“真”的是一个 指针。但不管在何种情况下,都不存在指针“算术”。 (18) Java 提供了与 C++类似的“构建器”(Constructor)。如果不自己定义一个,就会获得一个 默认构建器。而如果定义了一个非默认的构建器,就不会为我们自动定义默认构建器。这和 C++是 一样的。注意没有复制构建器,因为所有自变量都是按引用传递的。 (19) Java 中没有“破坏器”(Destructor)。变量不存在“作用域”的问题。一个对象的“存在时 间”是由对象的存在时间决定的,并非由垃圾收集器决定。有个 finalize()方法是每一个类的成员, 它在某种程度上类似于 C++的“破坏器”。但 finalize()是由垃圾收集器调用的,而且只负责释放“资 源”(如打开的文件、套接字、端口、URL 等等)。如需在一个特定的地点做某样事情,必须创建一 个特殊的方法,并调用它,不能依赖 finalize()。而在另一方面,C++中的所有对象都会(或者说“应 该”)破坏,但并非 Java 中的所有对象都会被当作“垃圾”收集掉。由于 Java 不支持破坏器的概念, 所以在必要的时候,必须谨慎地创建一个清除方法。而且针对类内的基础类以及成员对象,需要明 确调用所有清除方法。 (20) Java 具有方法“过载”机制,它的工作原理与 C++函数的过载几乎是完全相同的。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 361 (21) Java 不支持默认自变量。 (22) Java 中没有 goto。它采取的无条件跳转机制是“break 标签”或者“continue 标准”,用于 跳出当前的多重嵌套循环。 (23) Java 采用了一种单根式的分级结构,因此所有对象都是从根类 Object 统一继承的。而在 C++ 中,我 们 可在任何地方启动一个新的继承树,所以最后往往看到包含了大量树的“一片森林”。在 Java 中,我们无论如何都只有一个分级结构。尽管这表面上看似乎造成了限制,但由于我们知道每个对 象肯定至少有一个 Object 接口,所以往往能获得更强大的能力。C++目前似乎是唯一没有强制单根 结构的唯一一种 OO 语言。 (24) Java 没有模板或者参数化类型的其他形式。它提供了一系列集合:Vector(向量),Stack(堆 栈)以及 Hashtable(散列表),用于容纳 Object 引用。利用这些集合,我们的一系列要求可得到满 足。但这些集合并非是为实现象 C++“标准模板库”(STL)那样的快速调用而设计的。Java 1.2 中 的新集合显得更加完整,但仍不具备正宗模板那样的高效率使用手段。 (25) “垃圾收集”意味着在 Java 中出现内存漏洞的情况会少得多,但也并非完全不可能(若调 用一个用于分配存储空间的固有方法,垃圾收集器就不能对其进行跟踪监视)。然而,内存漏洞和资 源漏洞多是由于编写不当的 finalize()造成的,或是由于在已分配的一个块尾释放一种资源造成的 (“破坏器”在此时显得特别方便)。垃圾收集器是在 C++基础上的一种极大进步,使许多编程问题 消弥于无形之中。但对少数几个垃圾收集器力有不逮的问题,它却是不大适合的。但垃圾收集器的 大量优点也使这一处缺点显得微不足道。 (26) Java 内建了对多线程的支持。利用一个特殊的 Thread 类,我们可通过继承创建一个新线程 (放弃了 run()方法)。若将 synchronized(同步)关键字作为方法的一个类型限制符使用,相互排斥 现象会在对象这一级发生。在任何给定的时间,只有一个线程能使用一个对象的 synchronized 方法。 在另一方面,一个 synchronized 方法进入以后,它首先会“锁定”对象,防止其他任何 synchronized 方法再使用那个对象。只有退出了这个方法,才会将对象“解锁”。在线程之间,我们仍然要负责实 现更复杂的同步机制,方法是创建自己的“监视器”类。递归的 synchronized 方法可以正常运作。 若线程的优先等级相同,则时间的“分片”不能得到保证。 (27) 我们不是象 C++那样控制声明代码块,而是将访问限定符(public,private 和 protected) 置入每个类成员的定义里。若未规定一个“显式”(明确的)限 定 符 ,就 会 默 认 为“ 友 好的”(friendly)。 这意味着同一个包里的其他元素也可以访问它(相当于它们都成为 C++的“friends”——朋友),但 不可由包外的任何元素访问。类——以及类内的每个方法——都有一个访问限定符,决定它是否能 在文件的外部“可见”。private 关键字通常很少在 Java 中使用,因为与排斥同一个包内其他类的访 问相比,“友好的”访问通常更加有用。然而,在多线程的环境中,对 private 的恰当运用是非常重 要的。Java 的 protected 关键字意味着“可由继承者访问,亦可由包内其他元素访问”。注意 Java 没 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 362 有与 C++的 protected 关键字等价的元素,后者意味着“只能由继承者访问”(以前可用“private protected”实现这个目的,但这一对关键字的组合已被取消了)。 (28) 嵌套的类。在 C++中,对类进行嵌套有助于隐藏名称,并便于代码的组织(但 C++的“命 名空间”已使名称的隐藏显得多余)。Java 的“封装”或“打包”概念等价于 C++的命名空间,所以 不再是一个问题。Java 1.1 引入了“内部类”的概念,它秘密保持指向外部类的一个句柄——创建内 部类对象的时候需要用到。这意味着内部类对象也许能访问外部类对象的成员,毋需任何条件—— 就好象那些成员直接隶属于内部类对象一样。这样便为回调问题提供了一个更优秀的方案——C++ 是用指向成员的指针解决的。 (29) 由于存在前面介绍的那种内部类,所以 Java 里没有指向成员的指针。 (30) Java 不存在“嵌入”(inline)方法。Java 编译器也许会自行决定嵌入一个方法,但我们对 此没有更多的控制权力。在 Java 中,可为一个方法使用 final 关键字,从而“建议”进行嵌入操作。 然而,嵌入函数对于 C++的编译器来说也只是一种建议。 (31) Java 中的继承具有与 C++相同的效果,但采用的语法不同。Java 用 extends 关键字标志从一 个基础类的继承,并用 super 关键字指出准备在基础类中调用的方法,它与我们当前所在的方法具有 相同的名字(然而,Java 中的 super 关键字只允许我们访问父类的方法——亦即分级结构的上一级)。 通过在 C++中设定基础类的作用域,我们可访问位于分级结构较深处的方法。亦可用 super 关键字 调用基础类构建器。正如早先指出的那样,所有类最终都会从 Object 里自动继承。和 C++不同,不 存在明确的构建器初始化列表。但编译器会强迫我们在构建器主体的开头进行全部的基础类初始化, 而且不允许我们在主体的后面部分进行这一工作。通过组合运用自动初始化以及来自未初始化对象 句柄的异常,成员的初始化可得到有效的保证。 public class Foo extends Bar { public Foo(String msg) { super(msg); // Calls base constructor } public baz(int i) { // Override super.baz(i); // Calls base method } } (32) Java 中的继承不会改变基础类成员的保护级别。我们不能在 Java 中指定 public,private 或 者 protected 继承,这一点与 C++是相同的。此外,在衍生类中的优先方法不能减少对基础类方法的 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 363 访问。例如,假设一个成员在基础类中属于 public,而我们用另一个方法代替了它,那么用于替换 的方法也必须属于 public(编译器会自动检查)。 (33) Java 提供了一个 interface 关键字,它的作用是创建抽象基础类的一个等价物。在其中填充 抽象方法,且没有数据成员。这样一来,对于仅仅设计成一个接口的东西,以及对于用 extends 关键 字在现有功能基础上的扩展,两者之间便产生了一个明显的差异。不值得用 abstract 关键字产生一种 类似的效果,因为我们不能创建属于那个类的一个对象。一 个 abstract(抽象)类可包含抽象方法(尽 管并不要求在它里面包含什么东西),但它也能包含用于具体实现的代码。因此,它被限制成一个单 一的继承。通过与接口联合使用,这一方案避免了对类似于 C++虚拟基础类那样的一些机制的需要。 为创建可进行“例示”(即创建一个实例)的一个 interface(接口)的版本,需使用 implements 关键字。它的语法类似于继承的语法,如下所示: public interface Face { public void smile(); } public class Baz extends Bar implements Face { public void smile( ) { System.out.println("a warm smile"); } } (34) Java 中没有 virtual 关键字,因为所有非 static 方法都肯定会用到动态绑定。在 Java 中,程 序员不必自行决定是否使用动态绑定。C++之所以采用了 virtual,是 由 于我们对性能进行调整的时候, 可通过将其省略,从而获得执行效率的少量提升(或者换句话说:“如果不用,就没必要为它付出代 价”)。virtual 经常会造成一定程度的混淆,而且获得令人不快的结果。final 关键字为性能的调整规 定了一些范围——它向编译器指出这种方法不能被取代,所以它的范围可能被静态约束(而且成为 嵌入状态,所以使用 C++非 virtual 调用的等价方式)。这些优化工作是由编译器完成的。 (35) Java 不提供多重继承机制(MI),至少不象 C++那样做。与 protected 类似,MI 表面上是一 个很不错的主意,但只有真正面对一个特定的设计问题时,才知道自己需要它。由于 Java 使用的是 “单根”分级结构,所以只有在极少的场合才需要用到 MI。interface 关键字会帮助我们自动完成多 个接口的合并工作。 (36) 运行期的类型标识功能与 C++极为相似。例如,为获得与句柄 X 有关的信息,可使用下述 代码: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 364 X.getClass().getName(); 为进行一个“类型安全”的紧缩造型,可使用: derived d = (derived)base; 这与旧式风格的 C 造型是一样的。编译器会自动调用动态造型机制,不要求使用额外的语法。 尽管它并不象 C++的“new casts”那样具有易于定位造型的优点,但 Java 会检查使用情况,并丢弃 那些“异常”,所以它不会象 C++那样允许坏造型的存在。 (37) Java 采取了不同的异常控制机制,因为此时已经不存在构建器。可添加一个 finally 从句, 强制执行特定的语句,以便进行必要的清除工作。Java 中的所有异常都是从基础类 Throwable 里继 承而来的,所以可确保我们得到的是一个通用接口。 public void f(Obj b) throws IOException { myresource mr = b.createResource(); try { mr.UseResource(); } catch (MyException e) { // handle my exception } catch (Throwable e) { // handle all other exceptions } finally { mr.dispose(); // special cleanup } } (38) Java 的异常规范比 C++的出色得多。丢弃一个错误的异常后,不是象 C++那样在运行期间 调用一个函数,Java 异常规范是在编译期间检查并执行的。除此以外,被取代的方法必须遵守那一 方法的基础类版本的异常规范:它们可丢弃指定的异常或者从那些异常衍生出来的其他异常。这样 一来,我们最终得到的是更为“健壮”的异常控制代码。 (39) Java 具有方法过载的能力,但不允许运算符过载。String 类不能用+和+=运算符连接不同的 字串,而且 String 表达式使用自动的类型转换,但那是一种特殊的内建情况。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 365 (40) 通过事先的约定,C++中经常出现的 const 问题在 Java 里已得到了控制。我们只能传递指 向对象的句柄,本地副本永远不会为我们自动生成。若希望使用类似 C++按值传递那样的技术,可 调用 clone(),生成自变量的一个本地副本(尽管 clone()的设计依然尚显粗糙——参见第 12 章)。根 本不存在被自动调用的副本构建器。为创建一个编译期的常数值,可象下面这样编码: static final int SIZE = 255 static final int BSIZE = 8 * SIZE (41) 由于安全方面的原因,“应用程序”的编程与“程序片”的编程之间存在着显著的差异。一 个最明显的问题是程序片不允许我们进行磁盘的写操作,因为这样做会造成从远程站点下载的、不 明来历的程序可能胡乱改写我们的磁盘。随着 Java 1.1 对数字签名技术的引用,这一情况已有所改 观。根据数字签名,我们可确切知道一个程序片的全部作者,并验证他们是否已获得授权。Java 1.2 会进一步增强程序片的能力。 (42) 由于 Java 在某些场合可能显得限制太多,所以有时不愿用它执行象直接访问硬件这样的重 要任务。Java 解决这个问题的方案是“固有方法”,允许我们调用由其他语言写成的函数(目前只支 持 C 和 C++)。这样一来,我们就肯定能够解决与平台有关的问题(采用一种不可移植的形式,但 那些代码随后会被隔离起来)。程序片不能调用固有方法,只有应用程序才可以。 (43) Java 提供对注释文档的内建支持,所以源码文件也可以包含它们自己的文档。通过一个单 独的程序,这些文档信息可以提取出来,并重新格式化成 HTML。这无疑是文档管理及应用的极大 进步。 (44) Java 包含了一些标准库,用于完成特定的任务。C++则依靠一些非标准的、由其他厂商提 供的库。这些任务包括(或不久就要包括): ■连网 ■数据库连接(通过 JDBC) ■多线程 ■分布式对象(通过 RMI 和 CORBA) ■压缩 ■商贸 由于这些库简单易用,而且非常标准,所以能极大加快应用程序的开发速度。 (45) Java 1.1 包含了 Java Beans 标准,后者可创建在可视编程环境中使用的组件。由于遵守同样 的标准,所以可视组件能够在所有厂商的开发环境中使用。由于我们并不依赖一家厂商的方案进行 可视组件的设计,所以组件的选择余地会加大,并可提高组件的效能。除此之外,Java Beans 的设 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 366 计非常简单,便于程序员理解;而那些由不同的厂商开发的专用组件框架则要求进行更深入的学习。 (46) 若访问 Java 句柄失败,就会丢弃一次异常。这种丢弃测试并不一定要正好在使用一个句柄 之前进行。根据 Java 的设计规范,只是说异常必须以某种形式丢弃。许多 C++运行期系统也能丢弃 那些由于指针错误造成的异常。 (47) Java 通常显得更为健壮,为此采取的手段如下: ■对象句柄初始化成 null(一个关键字) ■句柄肯定会得到检查,并在出错时丢弃异常 ■所有数组访问都会得到检查,及时发现边界违例情况 ■自动垃圾收集,防止出现内存漏洞 ■明确、“ 傻瓜式”的异常控制机制 ■为多线程提供了简单的语言支持 ■ 对网络程序片进行字节码校验 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 367 附录 2 Java 编程规范 1.应用范围 本规范应用于采用 JEE 规范的项目中,所有项目中的 JAVA 代码(含 JSP,SERVLET,JAVABEAN, EJB)均应遵守这个规范。同时,也可作为其它项目的参考。 2.设计类和方法 2.1 创建具有很强内聚力的类 方法的重要性往往比类的重要性更容易理解,方法是指执行一个统一函数的一段代码。类常被 错误的视为是一个仅仅用于存放方法的容器。有些开发人员甚至把这种思路作了进一步的发挥,将 他们的所有方法放入单个类之中。 之所以不能正确的认识类的功能,原因之一是类的实现实际上并不影响程序的执行。当一个工 程被编译时,如果所有方法都放在单个类中或者放在几十个类中,这没有任何关系。虽然类的数量 对代码的执行并无太大的影响,但是当创建便于调试和维护的代码时,类的数量有时会带来很大的 影响。 类应该用来将相关的方法组织在一起。 当类包含一组紧密关联的方法时,该类可以说具有强大的内聚力。当类包含许多互不相关的方 法时,该类便具有较弱的内聚力。应该努力创建内聚力比较强的类。 大多数工程都包含许多并不十分适合与其他方法组合在一起的方法。在这种情况下,可以为这 些不合群的方法创建一个综合性收容类。 创建类时,应知道“模块化”这个术语的含义是什么。类的基本目的是创建相当独立的程序单 元。 2.2 创建松散连接和高度专用的方法 (1) 使所有方法都执行专门的任务 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 368 每个方法都应执行一项特定的任务,它应出色的完成这项任务。应避免创建执行许多不同 任务的方法。 创建专用方法有许多好处。首先调试将变得更加容易。 (2) 尽量使方法成为自成一体的独立方法 当一个方法依赖于其他方法的调用时,称为与其他方法紧密连接的方法。紧密连接的方法 会使调试和修改变得比较困难,因为它牵涉到更多的因素。松散连接的方法优于紧密连接的方 法,但你不可能使每个方法都成为独立的方法。 若要使方法具备较强的独立性,方法之一是尽量减少类变量。 创建方法时,设法将每个方法视为一个黑箱,其他例程不应要求了解该方法的内部工作情 况,该方法也不应要求了解它外面的工程情况。这就是为什么你的方法应依靠参数而不应依靠 全局变量的原因。 创建专用方法时,请考虑下列指导原则: 1) 将复杂进程放入专用方法。如果应用程序使用复杂的数学公式,请考虑将每个公 式放入它自己的方法中。这样使用这些公式的其他方法就不包含用于该公式的实际代码。 这样也可以更容易发现与公式相关的问题。 2) 将数据输入/输出(I/O)放入专用方法。 3) 将专用方法中可能要修改的代码隔离。如果你知道某个进程经常变更,请将这个 多变的代码放入专用方法,以便以后可以更容易的进行修改,并减少无意中给其他进程带 来问题的可能性。 4) 将业务规则封装在专用方法中。业务规则常属于要修改的代码类别,应与应用程 序的其余部分隔开。其他方法不应知道业务规则,只有要调用的方法才使用这些规则。 (3) 设计类和方法时,要达到下列目的: 1) 创建更加容易调试和维护的方法 2) 创建具有强大内聚力的类 3) 创建高度专用的方法 4) 创建松散连接的方法 5) 尽量使方法具有独立性 6) 提高方法的扇入性 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 369 7) 降低方法的扇出性 2.3 编程原则 (1) 为方法和类赋予表义性强的名字 为了使代码更加容易理解,最容易的方法之一是为你的方法赋予表义性强的名字。函数名 DoIt、GetIt 的可读性很难与 CalculateSalesTax、 RetrieveUserID 相比。 由缩写方法名组成的代码很难理解和维护,没有理由再这样做了。 给方法正确的命名,可使程序工程的调试和维护工作大大的改观。请认真对待方法命名的 工作,不要为了减少键入操作量而降低方法的可理解度。 实际应用举例: 1) 给方法命名时应大小写字母混合使用。如果句子全使用大写字母,那么阅读起来就非 常困难,而大小写字母混合使用的句子,阅读起来就很容易。 2) 定义方法名时不要使用缩写。如果你认为应用程序中的某些工程应使用缩写,那么请 将这些情况加上注释,并确保每个人在所有时间内都使用这些缩写。决不要在某些方法中对某 些单词进行缩写,而在别的方法中却不使用缩写。 (2) 为每个方法赋予单个退出点 (3) 创建方法时,始终都应显式地定义它的作用域。 1) 如果你真的想创建一个公用方法,请向代码阅读者说明这一点。 2) 通过为每个方法赋予一个明确定义的作用域,可以减少代码阅读者需要投入的工作量。 应确保你为方法赋予最有意义的作用域。如果一个方法只被同一类中的另一个方法调用,那么 请将它创建成私有方法。如果该方法是从多个类中的多个方法中调用,请将该说明为公用方法。 (4) 用参数在方法之间传递数据 应尽量避免使用类变量。一般来说,变量的作用域越小越好。为了减少类变量,方法之一 是将数据作为参数在不同方法之间传递,而不是让方法共享类变量。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 370 1) 为每个参数指定数据类型。 2) 始终要对数进行检验,决不要假设你得数据没有问题。程序员常犯的一个错误是在编 写方法时假设数据没有问题。在初始编程阶段,当编写调用方法时,这样的假设并无大碍。这 时你完全能够知道什么是参数的许可值,并按要求提供这些值。但如果你不对参数的数据进行 检验,那么下列情况就会给你带来很大麻烦:另外某个人创建了一个调用方法,但此人不知道 允许的值;你在晚些时候添加了新的调用方法,并错误的传递了坏数据。 3. 命名约定 所有变量的定义应该遵循匈牙利命名法,它使用 3 字符前缀来表示数据类型,3 个字符的前缀 必须小写,前缀后面是由表意性强的一个单词或多个单词组成的名字,而且每个单词的首写字母大 写,其它字母小写,这样保证了对变量名能够进行正确的断句。 这样,在一个变量名就可以反映出变量类型和变量所存储的值的意义两方面内容,这使得代码 语句可读性强、更加容易理解。 3.1 包、类及方法命名 标示符类型 命名约定 例子 包 全部小写。 标识符用点号分隔开来。为了使包的名 字更易读,Sun 公司建议包名中的标识符用 点号来分隔。 Sun 公司的标准 java 分配包用标识 符 .java 开头。 全局包的名字用你的机构的 Internet 保留域名开头 。 局部包: interface.screens 全局包: com.rational.www. interface.screens 类,接口 类的名字应该使用名词。 每个单词第一个字母应该大写。 避免使用单词的缩写,除非它的缩写已 经广为人知,如 HTTP。 Class Hello ; Class HelloWorld ; Interface Apple ; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 371 方法 第一个单词一般是动词。 第一个字母是小些,但是中间单词的第 一个字母是大写。 如果方法返回一个成员变量的值,方法名 一般为 get+成员变量名,如若返回的值是 bool 变量,一般以 is 作为前缀。 如果方法修改一个成员变量的值,方法名 一般为:set + 成员变量名。 getName(); setName(); isFirst(); 变量 第一个字母小写,中间单词的第一个字母 大写。 不要用_或&作为第一个字母。 尽量使用短而且具有意义的单词。 单字符的变量名一般只用于生命期非常 短暂的变量。i,j,k,m,n 一般用于 integers; c,d,e 一般用于 characters。 如果变量是集合,则变量名应用复数。 命名组件采用匈牙利命名法,所有前缀 均应遵循同一个组件名称缩写列表。 String myName; int[] students; int i; int n; char c; btNew; (bt 是 Button 的缩写) 常量 所有常量名均全部大写,单词间以‘_’ 隔开。 int MAX_NUM; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 372 4. 使用常量 4.1 使用常量 (1) 常数很容易在数据输入时出错 常数存在的主要问题之一是你很容易在键入数字时出错,从而颠倒了数字的位置。例如,当你 键入数字 10876 时,很容易的键入 10867 或 18076。与处理变量和保留字的方法不同,编译器并不 在乎颠倒了位置和不正确的数字,有时简单的错误造成的问题不会立即表现出来,而当问题表现出 来时,它们会以随机的计算错误的形式出现,这些错误很难准确定位。用常量来取代常数时,编译 器将在编译时检查常量的有效性。如果常量不存在,编译器便将这一情况通知你,并拒绝进行编译, 这可以消除错误键入的数字带来的问题,只要常量拥有正确的值,使用该常量的所有代码也有使用 该正确值。 (2)常数很难不断更新 (3)常量使代码更容易阅读 使用常量后,得到的一个额外好处是可使创建的代码更容易阅读。常数很不直观。也许你对常 数非常了解,但其他人则根本看不明白。通过合理的给常量命名,使用这些常量的代码就变得比较 直观了,更容易阅读。 为常量赋予较宽的作用域,这与使用变量时的情况不同。在一个应用程序中你决不应该两次创 建相同的常量。如果你发现自己复制了一个常量,请将原始的常量说明转至较宽的作用域,直到该 常量可供引用它的所有方法为止。 5. 变量 5.1 定义有焦点的变量 用于多个目的的变量称为无焦点(多焦点)的变量。无焦点变量所代表的意义与程序的执行流 程有关,当程序处于不同位置时,它所表示的意义是不固定的,这样就给程序的可读性和可维护性 带来了麻烦。 5.2 只对常用变量名和长变量名进行缩写 如果需要对变量名进行缩写时,一定要注意整个代码中缩写规则的一致性。例如,如果在代码 的某些区域中使用 Cnt,而在另一些区域中又使用 Count,就会给代码增加不必要的复杂性。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 373 变量名中尽量不要出现缩写。 5.3 使用统一的量词 通过在结尾处放置一个量词,就可创建更加统一的变量,它们更容易理解,也更容易搜索。例 如,请使用 strCustomerFirst 和 strCustomerLast,而不要使用 strFirstCustomer 和 strLastCustomer。 量词列表: 量词后缀 说明 First 一组变量中的第一个 Last 一组变量中的最后一个 Next 一组变量中的下一个变量 Prev 一组变量中的上一个 Cur 一组变量中的当前变量 5.4 使用肯定形式的布尔变量 给布尔变量命名时,始终都要使用变量的肯定形式,以减少其它开发人员在理解布尔变量所代 表的意义时的难度。 5.5 为每个变量选择最佳的数据类型 这样即能减少对内存的需求量,加快代码的执行速度,又会降低出错的可能性。用于变量的数 据类型可能会影响该变量进行计算所产生的结果。在这种情况下,编译器不会产生运行期错误,它 只是迫使该值符合数据类型的要求。这类问题极难查找。 5.6 尽量缩小变量的作用域 如果变量的作用域大于它应有的范围,变量可继续存在,并且在不再需要该变量后的很长时间 内仍然占用资源。 它们的主要问题是,任何类中的任何方法都能对它们进行修改,并且很难跟踪究竟是何处进行 修改的。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 374 占用资源是作用域涉及的一个重要问题。对变量来说,尽量缩小作用域将会对应用程序的可靠 性产生巨大的影响。 6. 代码的格式化 6.1 对代码进行格式化时,要达到的目的 (1) 通过代码分割成功能块和便于理解的代码段,使代码更容易阅读和理解; (2) 使用空行和注释行,将程序中逻辑上不相关的代码块分开。比如:变量声明部分和代码语 句间的分隔;较长的方法中,完成不同功能的代码块间的分隔。要避免出现逻辑上混乱的分隔,如: 某一逻辑功能代码块中间用空行进行了分隔,但是在相邻功能代码块之间却没有分隔,这样会给程 序阅读者造成错觉。 (3) 减少为理解代码结构而需要做的工作; (4) 使代码的阅读者不必进行假设; (5) 使代码结构尽可能做到格式清楚明了。 6.2 编程原则 (1) 要将多个语句放在同一行上 不论是变量声明,还是语句都不要在一行上书写多个。 (2) 缩进后续行 当你将变量设置为某个值时,所有后续行的缩进位置应与第一行的变量值相同; 当你调用一个方法时,后续行缩进到第一个参数的开始处; 当你将变量或属性设置为等于表达式的计算结果时,请从后面分割该语句,以确保该表达 式尽可能放在同一行上。 (3) 在 if 语句后缩进; 在 else 语句后缩进 在 switch 语句后缩进 在 case 语句后缩进 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 375 在 do 句后缩进 已经用行接续符分割的语句的各个行要缩进 对从属于行标注的代码进行缩进。 (4) 在执行统一任务的各个语句组之间插入一个空行。好的代码应由按逻辑顺序排列的进 程或相关语句组构成。 7. 代码的注释 7.1 使用代码注释的目的 (1) 文字说明代码的作用(即为什么要用编写该代码,而不是如何编写); (2) 确指出该代码的编写思路和逻辑方法; (3) 人们注意到代码中的重要转折点; (4) 使代码的阅读者不必在他们的头脑中仿真运行代码的执行方法. 7.2 编程原则 (1) 用文字说明代码的作用: 简单的重复代码做写什么,这样的注释几乎不能给注释增加什么信息.如果你使用好的命名 方法来创建直观明了的代码那么这些类型的注释绝对增加不了什么信息. (2) 如果你想违背好的编程原则,请说明为什么 有的时候你可能需要违背好的编程原则,或者使用了某些不正规的方法,.遇到这种情况时, 请用内部注释来说明你在做什么和为什么要这样做。 技巧性特别高的代码段,一定要加详细的注释,不要让其他开发人员花很长时间来研究一 个高技巧但不易理解的程序段。 (3) 用注释来说明何时可能出错和为什么出错 (4) 在编写代码前进行注释 给代码加注释的方法之一是在编写一个方法前首先写上注释.如果你愿意,可以编写完整句 子的注释或伪代码.一旦你用注释对代码进行了概述,就可以在注释之间编写代码. http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 376 (5) 在要注释的代码前书写注释 注释一定出现在要注释的程序段前,不要在某段程序后书写对这段程序的注释,先看到注 释对程序的理解会有一定帮助。 如果有可能,请在注释行与上面代码间加一空行。 (6) 纯色字符注释行只用于主要注释 注释中要分隔时,请使用一行空注释行来完成,不要使用纯色字符,以保持版面的整洁、 清晰。 (7) 避免形成注释框 用星号围成的注释框,右边的星号看起来很好,但它们给注释增加了任何信息吗?实际上这 会给编写或编辑注释的人增加许多工作。 (8) 增强注释的可读性 注释是供人阅读的,而不是让计算机阅读的。 1) 使用完整的语句。虽然不必将注释分成段落(最好也不要分成段落),但你应尽量将 注释写成完整的句子。 2) 避免使用缩写。缩写常使注释更难阅读,人们常用不同的方法对相同的单词进行缩写, 这会造成许多混乱,如果必须对词汇缩写,必须做到统一。 3) 将整个单词大写,以突出它们的重要性。若要使人们注意注释中的一个或多个单词, 请全部使用大写字母。 (9) 对注释进行缩进,使之与后随的语句对齐。 注释通常位于它们要说明的代码的前面。为了从视觉上突出注释与它的代码之间的关系, 请将注释缩进,使之与代码处于同一个层次上。 (10)为每个方法赋予一个注释标头 每个方法都应有一个注释标头。方法的注释标头可包含多个文字项,比如输入参数、返回 值、原始作者、最后编辑该方法的程序员、上次修改日期、版权信息。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 377 (11)当行尾注释用在上面这种代码段结构中时,它们会使代码更难阅读。 使用多个行尾注释时(比如用于方法顶部的多个变量说明),应使它们互相对齐。这可使 它们稍容易阅读一些。 (12)何时书写注释 1) 请在每个 if 语句的前面加上注释。 2) 在每个 switch 语句的前面加上注释。与 if 语句一样,switch 语句用于评估对程序执 行产生影响的表达式。 3) 在每个循环的前面加上注释。每个循环都有它的作用,许多情况下这个作用不清楚直 观。 7.3 注释那些部分 项目 注释哪些部分 实参/ 参数 参数类型 参数用来做什么 任何约束或前提条件 示例 字段/ 字段/属性 字段描述 注释所有使用的不变量 示例 并行事件 可见性决策 类 类的目的 已知的问题 类的开发/维护历史 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 378 注释出采用的不变量 并行策略 编译单元 每一个类/类内定义的接口,含简单的说明 文件名和/或标识信息 版权信息 接口 目的 它应如何被使用以及如何不被使用 局部变量 用处/目的 成员函数注释 成员函数做什么以及它为什么做这个 哪些参数必须传递给一个成员函数 成员函数返回什么 已知的问题 任何由某个成员函数抛出的异常 可见性决策 成员函数是如何改变对象的 包含任何修改代码的历史 如何在适当情况下调用成员函数的例子适用的前提条 件和后置条件 成员函数内部注释 控制结构 代码做了些什么以及为什么这样做 局部变量 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 379 难或复杂的代码 处理顺序 7.4 示例 7.4.1 块注释 主要用来描述文件,类,方法,算法等。一般用在文档和方法的前面,也可以放在文档的任何 地方。以‘/*’开头,‘*/’结尾。例: …… /* * 注释 */ …… 7.4.2 行注释 主要用在方法内部,对代码,变量,流程等进行说明。与块注释格式相似,但是整个注释占据 一行。例: …… /* 注释 */ …… 7.4.3 尾随注释 与行注释功能相似,放在代码的同行,但是要与代码之间有足够的空间,便于分清。例: int m=4 ; /* 注释 */ 如果一个程序块内有多个尾随注释,每个注释的缩进应该保持一致。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 380 7.4.4 行尾注释 与行注释功能相似,放在每行的最后,或者占据一行。以‘//’开头。 7.4.5 文档注释 与块注释相似,但是可以被 javadoc 处理,生成 HTML 文件。以‘/**’开头,‘*/’结尾。文 档注释不能放在方法或程序块内。例: /** 注释 */ 8. 表达式和语句 8.1 每行应该只有一条语句 8.2 if-else,if-elseif 语句 任何情况下,都应该有“{”,“}”,格式如下: if (condition) { statements; } else if (condition) { statements; } else{ statements; } 8.3 for 语句格式如下: for (initialization; condition; update) { statements; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 381 } 如果语句为空: for (initialization; condition; update) ; 8.4 while 语句格式如下: while (condition) { statements; } 如果语句为空: while (condition); 8.5 do-while 语句格式如下: do { statements; } while (condition); 8.6 switch 语句 每个 switch 里都应包含 default 子语句,格式如下: switch (condition) { case ABC: statements; /* falls through */ case DEF: statements; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 382 break; case XYZ: statements; break; default: statements; break; } 8.7 try-catch 语句格式如下: try { statements; } catch (ExceptionClass e) { statements; } finally { statements; } 9. 错误处理和异常事件 通常的思想是只对错误采用异常处理:逻辑和编程错误,设置错误,被破坏的数据,资源耗尽, 等等。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 383 通常的法则是系统在正常状态下以及无重载和硬件失效状态下,不应产生任何异常。异常处理 时可以采用适当的日志机制来报告异常,包括异常发生的时刻。不要使用异常实现来控制程序流程 结构。 10. 封装、事务 1. 非商务公用组件单独封装 2. 每一个业务流程单独封装 3. 一次方法(组件)的调用应能完成某一项功能或流程,即符合完整性 4. 一次方法(组件)的调用符合 ACID 事务性 5. 多次方法(组件)的调用应包含在一个事务中 11. 可移植性 1. 尽量不要使用已经被标为不赞成使用的类或方法。 2. 如果需要换行的话,尽量用 println 来代替在字符串中使用"\n"。 3. 用 separator()方法代替路径中的”/”或”\” 。 4. 用 pathSeptarator()方法代替路径中的 ” : ” 或 ” ;”
还剩75页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 11 人已下载

下载pdf

pdf贡献者

sere4t4ey

贡献于2011-07-30

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf