对象导论
本内容摘自Java编程思想,笔者选取较重要的简单描述:
1.抽象过程
1.1.所有编程语言都有提供抽象机制。解决问题的复杂性取决于抽象的类型和质量。
1.2.解决问题要基于问题的结构来考虑,而不是基于计算机的结构来解决问题。
1.3.面向对象的方式向程序员提供表示问题空间的元素,使程序员不受限制于任何问题,程序员可以添加新类型的对象来适应某些特定问题。且在阅读描述解决方案的代码时,也在阅读问题本身,程序员可以要求对象执行哪些操作,也符合现实中情况。
1.4.对象5个基本特性:
1.4.1:万物皆对象。(你在我看来只是一个类型的对象)
1.4.2:程序是对象的集合,它们通过发送消息来告知彼此要做的。
1.4.3: 每个对象都有自己的由其他对象所构成的存储。(对象中包含其他对象实例)
1.4.4:每个对象都拥有其类型。(每个对象都是一个类的实例)
1.4.5: 某一特定类型的所有对象都可以接收同样的消息(圆形能接收几何形的消息)
2.每个对象都有一个接口
2.1.所有的对象都是唯一的,也是具有相同特性(属性)和行为的对象(方法)所归属的类的一部分。
2.2.创建抽象数据类型(类class)是面向对象程序设计的基本概念。
2.3.抽象数据类型的运行方式与内置类型方式几乎完全一致:创建某一类型的变量(对象),然后操作这些变量(发送消息或请求,对象就知道做什么)。
2.4.每个类的成员或元素都具有某种共性。(基于业务特点的相同的属性)
2.5.每个对象都属于定义了特性和行为的某个特定的类。
2.6.一个类实际上就是一种数据类型,程序员可以通过定义类来适应问题。深意义上来说就是程序员可添加新的数据类型扩展编程语言。(编程系统接受新的类,且像对待内置类型一样照管它们和进行类型检查。)
2.7.面向对象程序设计的挑战之一 :问题空间的元素和解空间的对象之间的一对一的映射关系。(程序要解决的问题需要的元素和解决问题的对象空间的映射关系!)
2.8.接口确定了某一特定对象所能发出的请求。但程序必须有满足这些请求的代码。这些代码与隐藏数据一起构成了实现。每一个可能的请求都有一个方法与之关联,对象发生请求时,对应的方法就会被调用。(向某个对象发送请求,这个对象便会知道该请求的目的,然后执行对应的程序代码。)
2.9.UML(Unified Modelling Language)形式图,每一个类用方框表示,类名在方框的顶部,数据成员都描述在方框的中间,方法(属于此对象的,用来接收你发给此对象的消息的函数)在方法的底部。(通常只有类名和公共方法表示在UML设计图中)。
3.每个对象都提供服务
3.1.开发或理解一个程序时,最好将对象想象成服务提供者。因为程序本身就是向用户提供服务的,它将通过调用其他对象提供的服务来实现。而你的目标就是去创建能够提供理想的服务来解决问题的一系列对象。
3.2.将对象看做服务提供者有一个附带好处:它有助于提高对象的内聚性。(高内聚是软件设计的基本质量要求之一:这意味着一个软件构建<对象,方法或对象库>的各个方面“组合”的很好。)
3.3.不要把过多的功能都塞给一个对象。应该视情况划分成合理的几个对象。每个对象都有一个它能提供服务的内聚集合,良好的面向对象设计中,每个对象都可以很好的完成一项任务,它不需要做更多的事。
3.4.如3.3所述每个对象专注于自己的服务,这样使别人更容易理解该对象所能提供的服务价值(对象功能),它还能调整对象以适应其设计的过程变得简单。
4.被隐藏的具体实现
4.1.通常我们将程序员分为两种:类创建者和客户端程序员。这样做是有好处的。类创建者的目标是构建类,这种类只向客户端程序员暴露必须的部分,隐藏其他部分。这样做能让隐藏的部分(通常是对象内脆弱的部分)受到保护,防止客户端程序员修改代码引起BUG。
4.2.就4.1所述在任何相互的关系中,这种各方都应遵守的边界是十分重要的。如果说另一个程序员要使用你构建的类,但你的类成员都是公开可使用的,这意味着别人能任意的访问你类的成员,假设你不希望别人修改某项数据,但没有任何访问控制,那就无法阻止其发生。
4.3.访问控制还有一个好处:让类创建者能改变类的结构而不影响客户端程序员。例如:你实现了某个特定的类,但稍后你又发现改写这个类会使其运行得更有效率。如果接口和实现可以清晰地分离并得到保护,那么你就不用担心会影响其他程序员而轻松地完成。
4.4.Java用三个关键字在类的内部设定边界:public , private , protected。这些访问指定词决定了被定义的定西可以被谁使用。public表示对任何人都是可用的。private表示只有类型创建者和类型的内部方法才能访问,如果别人试图访问private成员,就会在编译时得到错误信息!protected与private作用差不多,差别在继承的类可以访问protected成员,但继承的类不能访问private成员。
4.5.Java还有一种默认的访问权限,就是没有使用访问指定词的成员,这些成员的权限为:同一个包(同一个文件夹)中的其他类成员都能访问,但在这个包之外的类成员是不能访问的。(简单的来说同包public,不同包private。)
5.复用具体实现
5.1.一旦类被创建并被测试完,那么它就应该代表一个有用的代码单元。但事实证明,这种复用性很难达到我们希望的程度,所以产生一个可复用的对象设计需要丰富的经验和敏锐的洞察力。
5.2.代码复用是面向对象程序设计语言所提供的最了不起的优点之一。
5.3.最简单的复用某个类的方式就是直接使用该类的对象。
5.4.我们也可以通过将一个类的一个对象置于一个某个新的类中。(也能称为成员变量)。新的类可以有任意数量和任意类型的其他对象以任意的方式实现新的类中想要的功能的组成。(使用现有的类合成新的类),这种概念叫组合,如果组合是动态发生的通常称它为“聚合”。组合经常被视为“has-a”(拥有)关系,比如说:电视需要有屏幕,汽车需要有引擎。
5.5.组合带来了极大的灵活性。而继承是不具备这种灵活性的(5.6详解),因为编辑器必须对通过继承而创建的类施加编译时的限制。
5.6.继承在面向对象程序设计中如此重要。以至于新手会觉得处处都应该使用继承,当你实践后,你会发现这样设计会让程序过分的复杂且难以使用。这时,我推荐你考虑组合,因为它很灵活,且让设计变得更加清晰。(有一定经验后,你便能知道继承使用的场合了)
6.继承
6.1.对象这种观念,是十分方便的工具,让你能通过概念将数据和功能封装到一起。这些概念使用class关键字来表示,它们是编程语言中的基本单位。
6.2.在创建一个类,如果我们将该类为基础并复制它创建出另一个新类,这样新类就继承了原有的类,也是我们说的继承。注意:要当心源类(基类,超类,父类)发生变动时,被修改的“副本”也会发生变动。
6.3.类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型的关系。两个类型可以有相同的特性和行为,但其中一个类型可能比另一个类型含有更多的特性,并能处理更多的消息。继承使用基类型和导出类型的概念表示这种类型之间的相似性。一个基类型包含其所有导出类型所共享的特性和行为。这样我们就能创建一个基类型来表示系统中某些对象的核心概念,从基类中导出其他类型,来表示此核心可以被实现的各种不同方式。
6.4.当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private成员被隐藏起来,且不能访问),而且更重要的是它复制了基类的接口。也就是说:所有可以发送给基类对象的消息同时也可以发送给导出类对象。由于通过发送给类的消息的类型可以知道类的类型,这意味着导出类与基类有相同的类型。通过继承产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛。
6.5.由于基类和导出类具有相同的的基础接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到消息时,必须有某些代码去执行。如果只是简单的继承一个类而不做其他事,那么在基类接口中的方法将会直接继承到导出类中。这意味着导出类的对象不仅与基类拥有相同的类型,而且还拥有相同的行为,这样做这个导出类就没什么意义了。
6.6.如6.5所述基类必须与导出类产生差异。第一种方式非常直接:直接在导出类中添加新方法,这些方法并不是基类接口的一部分。这意味着基类并不能满足你的所有需求,所以需添加更多的方法。这种对继承简单而基本的使用方式,有时对问题来说确实是一种完美的解决方式。但,也应该考虑基类是否也需要这些额外方法的可能性。(这种设计的发现与迭代过程在面向对象程序中会经常发现)。
6.7.续6.6的内容谈第二种方式:使导出类和基类之间产生差异:在导出类中改变现有基类的方法的行为,称为覆盖。要覆盖某一个方法可以直接在导出类中创建该方法新的定义即可。(可以理解为:我正在使用相同的接口方法,但我想在新类型中做点不同的事!)
6.8.“是一个”与“像是一个”关系:
6.8.1.继承可能引发某种争论:继承应该只覆盖基类的方法吗?(不添加再基类中没有的新方法),如果这样做,就意味着导出类和基类是完全相同的类型,因为它们有完全相同的接口。结果可以用一个导出类对象完全替换一个基类对象。这可以被视为纯粹替代,通常也称替代原则。在某种意义上,这是一种处理继承的理想方式。我们将这种情况下的基类与导出类之间的关系称为is-a(是一个)的关系,因为可以说“一个圆形就是一个几何形状”。判断是否继承,就是要确定是否可以用is-a来描述类之间的关系,并使之有实际意义。 6.8.2.有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但这种替代并不完美,因为基类无法访问新添加的方法。这种情况我们可以描述为is-like-a(像是一个)的关系。新类型具有旧类型的接口,但还包含其他方法,所有不能说它们完全相同。
6.8.3.当你看到替代原则,也许会觉得这种方式是唯一可行的。的确这种方式设计的很好,但随着你的经验累积,你会发现在导出类中添加新的方法解决问题也是有裨益的,只要你能仔细审视,两种方法的使用场景是相当明显的。
7.伴随多态的可互换对象
7.1.在处理类型的层次结构时,经常想把一个对象不当做它所归属的特定类型来对待,而是当做基类的对象来对待。这使得人们可以编写出不依赖特定类型的代码。比如:在几何形中有圆形,正方形,三角形,我们操作的都是泛化的形状,它们都具有被绘制,擦除和移动,这些方法都直接对一个几何对象发送消息,它们不担心对象将如何处理消息。
7.2.如7.1所述,这样的代码是不会受添加新类型影响的,而且添加新类型是拓展一个面向对象程序以便处理新情况的最常用方式。例如:我们能从几何形中导出一个新的子类型五角形,而并不需要修改处理泛化几何形状的方法。这种通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。这种能力可以极大地改善我们的设计,也降低了软件维护的代价。
7.3.在试图将导出类型的对象当作其泛化基类型对象来看待时,仍然有个问题:如何某个方法要让泛化的几何形状绘制自己,让泛化的交通工具去行驶,那么编译器在编译时是不知道应该执行哪段代码的;这就是问题的关键:当发出这样的消息时,程序员并不想知道哪段代码将被执行;绘制的方法可以被等同地运用于圆形,正方形,三角形,而对象会依据自身的具体类型来执行恰当的代码。
7.4.为解决上述的问题,面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用的参数和返回值执行类型检查(无法提供此检查的语言被称为弱类型的),但是并不知道将被执行确切代码。
7.5.续7.4所述,为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址的调用。这段代码使用对象中存储的信息来计算方法体的地址。这样根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就知道这条消息要做什么,在Java中这种动态绑定是默认行为,不需要多余的关键字来实现多态。
7.6.把将导出类看做是它的基类的过程称为向上转型(upcasting)。转型这个名称的灵感来源于模型铸造的塑模动作;而向上这个词来源于继承图的典型布局方式:通常基类在顶部,而导出类在其下部散开。因此,转型成为一个基类就是在继承图向上移动,即“向上转型”。
7.7.因为多态会使得事情总是能正确的处理。编译器和运行系统会处理相关的细节,你需要做的就是怎样通过它来设计。当一个对象发送消息时,即使涉及到向上转型,该对象也知道要执行什么样的正确行为。
8.单根继承结构
8.1.在OOP中,是否所有的类都继承自单一的基类?在Java中,答案是Yes,这个终极的基类就是Object。单根继承结构带有很多的益处。
8.2.单根继承结构保证所有的对象都具备某些功能。这样你就知道,在你的系统中你可以在每个对象上执行某些基本操作。所有对象都能很容易的在堆上创建,而参数传递也得到了极大的简化。
8.3.单根继承结构是垃圾回收器的实现变得容易,而垃圾回收器正是Java相较于C++做出的重要改进之一。由于所有的对象都保证具有其类型信息,因此不会因为无法确定对象的类型而陷入僵局。这对于系统级操作(异常处理)显得尤为重要,而且给编程带来巨大的灵活性。(Object可作为任意的类型)
9.容器
9.1.通常来说,如果不知道在解决某个特定的问题需要多少个对象,或者它们将活多久,那么就可能知道如何去创建这个对象。如何才能知道需要多少空间来创建这些对象呢?答案是你不可能知道,因为这类信息只有运行时餐能获得。
9.2.如9.1所述,那么我们如何才能解决这样的问题呢?我们可以创建另一种对象类型,这种对象类型持有对其他类型的引用。也能使用大多数语言中都有的数组类型来实现相同的功能。还有一种被成为容器的新对象(也称集合,但Java的类库总以不同的含义使用集合这个术语,所以我将使用容器来表示),这个容器在任何需要时都能扩充自己以容纳你添置其中的所有东西,因此我们不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后添加自己需要的东西它会帮你处理所有的细节。
9.3.你应该感到高兴,因为OOP语言都有一组容器,它们作为开发包的一部分。在Java标准的类库中包含了大量的容器,它们能满足不同需要。(List用于存储序列,Map称为关系数组,用于建立对象之间的关联,Set每种对象类型只能有一个,还有诸如队列,树,堆栈等更多的构件)。
9.4.从设计的观点看,我们需要的只是一个可以被操作,从而能解决问题的序列。如果单一类型的容器可以满足所有需要,那么就没有理由设计不同种类的序列了。然而为什么还是要对容器有所选择呢?
这有两个原因:
第一,不同的容器提供了不同类型的接口和外部行为。堆栈相比于队列就具备不同的接口和行为,也不同于集合和列表的接口和行为。它们之中的某种容器提供的解决方案可能比其他容器要灵活。
第二,不同容器对于某些操作具有不同的效率。拿两种List比较:ArrayList和LinkedList。它们都具有相同的接口和外部行为的简单序列,但是它们对某些操作的所花费的代价却有着天壤之别。在ArrayList中,随机访问元素是一个花费固定时间的操作;但对于LinkedList来说,随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素,花费的时间越长。而另一个方面,如果想在序列中插入一个元素,LinkedList的开销却比ArrayList要小。这里不做详细的说明。如果我们一开始使用LinkedList来构建程序,而在优化系统性能时使用ArrayList。这样接口所带来的抽象,把容器之间进行转换时对代码产生的影响降到最小限额。
9.5.参数化类型:
9.5.1.在Java SE5 出现之前,容器存储的对象都只具有Java中的通用类型:Object。单根继承结构意味着所有的东西都是Objectd类型的,所以可以存储Object的容器能存储任何东西。这样使得容器很容易被复用。
9.5.2.续9.5.1所述,要使用这样的容器,只需在其中置入对象引用,稍后还可以将它们取回。由于容器只存储Object,所以将对象引用置入容器时,它必须被向上转型为Object,这样做会使它丢失身份。当把他取回时,就获取了一个对Object对象的引用,而不是对置入时的那个类型的对象的引用。那么怎么才能将它变回先前置入容器中时的具有实用接口的对象呢?
9.5.3.续9.5.2所答,我们可以用转型,但这次转型不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。这种转型方式称为向下转型。我们知道,向上转型是安全的,但对向下转型而言,除非你知道要确切处理的类型,不然它几乎是不安全的。
9.5.4.向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。那么创建一个自己知道要存储什么对象类型的容器岂不是更好(它不需要向下转型和消除犯错误的可能)?接下来我们谈一谈这种解决方案:参数化类型机制。参数化类型就是一个编译器可以自动定制作用于特定类型的类。(通过使用参数化类型,编译器可以定制一个只接纳和取出Shape对象的容器)。
9.5.5.Java SE5的重大变化之一就是增加了参数化类型,在Java中也称它为范型。一对尖括号,中间包含类型信息,通过这些特征就能识别对范型的使用。
例如:ArrayList<Shape> shapes = new ArrayList<Shape>();
9.5.6.为了利用范型的优点,很多标准类库构件都进行了修改。接下来我们会看到范型这篇文档中的许多代码都产生着重要的影响。
10.对象的创建和生命期
10.1.在使用对象时,最关键的问题之一便是它们的生成和销毁方式。每个对象为了生成都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉,使其占用的资源被释放和重用。
10.2.对象是在一个呗称为堆的内存池中动态创建地。在这种方式中,直到运行时才能知道需要有多少对象。
10.3.它们的生命周期,以及它们是什么类型?
这些问题的答案是:只能在程序运行时相关代码被执行到那一刻才能确定。如果需要一个新的对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中创建存储空间的时间。在堆栈中创建存储空间和释放存储空间通常个需要一条汇编语句即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。创建堆存储空间的时间依赖于存储机制的设计。
10.4.动态的方式有这样一个一般性的逻辑假设:对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。
10.5.如果在堆上创建对象,编译器就会对他的生命周期一无所知。所以Java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。垃圾回收器非常有用,因为它减少了所必须考虑的议题和必须编写的代码。更为重要的是,垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄漏问题。
10.6.Java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其他方面)。垃圾回收器“知道”对象何时不再被使用,并自动释放对象占用的内存。
11.异常处理:处理异常
11.1.自从编程语言问世以来,错误处理就始终是最困难的问题之一。
11.2.异常处理就像是与程序正常执行路径并行的,在错误发生时执行的另一条路径。因为它是另一个完全分离的执行路径,所以他不会干扰正常执行的代码。这往往使得代码编写变得简单,这样我们就不需要被迫定期去检查错误。
11.3.异常不能被忽略,所以他保证一定会在某处得到处理。
11.4.异常提供了一种从错误状况进行可靠恢复的途径。
12.并发编程
12.1.在计算机中有一个基本概念,就是在同一时刻处理多个任务的思想。许多的程序设计问题都要求,程序能够停下正在处理的工作,转而处理某个其他问题,然后在返回主进程。
12.2.有是中断对于处理时间性强的任务是必须的,但对于大量的其他问题,我们只能把问题切分成多个独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称之为线程,上述的这些概念被称为“并发”。
12.3.通常,线程只是一种为单一处理器分配执行时间的手段。但是如果操作系统支持多处理器,那么每个人物都可以被指派给不同的处理器,并且它们是在真正地并行执行。
12.4.所有这些都使得并发看起来相当的简单,但有一个隐患:共享资源。如果有多个并行任务都要访问同一个资源,那么就会出问题。所以这个共享的资源必须上锁:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。
12.5.Java的并发是内置语言中的,Java SE5已经添加了大量额外的库支持。
13.Java与Internet
13.1.客户/服务器计算技术
客户端/服务器系统的核心思想是:系统具有一个中央信息存储池,用来存储某种数据,它通常存在于数据库中,你可以根据需要将它分发给某些人员或机器集群。客户/服务器概念关键在于信息存储池的位置集中于中央,这使得它可以被修改,并且这些修改将被传播给信息消费者。总之,信息存储池,用于分发信息的软件以及信息与软件所驻留的机器或集群被总称为服务器。驻留在用户机器上的软件与服务器进行通信,以获得信息,处理信息,然后将它们显示在被称为客户机的用户机器上。
客户/服务器的计算技术的基本概念并不复杂。问题在于你只有单一的服务器,却要同时为多个客户服务。通常,这会涉及数据库管理系统,因此设计者把数据“均衡”分布于数据表中,已取得最优的使用效果。此外,系统通常允许客户在服务器中插入新的信息。这意味着必须保证一个客户插入的新数据不会覆盖另一个客户插入的新数据,也不会在将其添加到数据库的过程中丢失(这被称为事物处理)。如果客户端软件发生变化,那么它必须被重新编译,调试并安装到客户端机器上,事实证明这比想象的要更加复杂和费力。如果想要支持多种不同类型的计算机和操作系统,问题将更麻烦。还有一个最重要的性能问题:可能在任意时刻都有成百上千的客户向服务器发出请求,所以任何小的延迟都会产生重大影响。为了将延迟最小化,程序员努力减轻处理服务的负载,通常是分散给客户端机器处理,但有时也会使用中间件将负载分散给在服务器端的其他机器(中间件也被用来提高可维护性)。
分发信息这个简单思想的复杂性实际上是有很多不同层次的,这使得整个问题可能看起来高深莫测。但是它仍然至关重要:算起来客户/服务器计算技术大概占了所有程序设计行为的一半。
13.2.Web就是一台巨型服务器。
13.3.客户端编程
13.3.1.插件
13.3.2.脚本语言
13.3.3.Java
13.3.4.备选方案
13.3.5.(.NET)和C#
13.3.6.Internet和Intranet
13.4.服务器端编程
13.4.1.暂不写作。

浙公网安备 33010602011771号