由于最早学的编程语言都是面向过程的,导致自己先入为主,对面向过程的印象特别深刻。而面向对象,虽然一直都有学习和用到,也知道怎么新建对象,函数,怎么去使用。然而,始终觉得理解的差了那么一点。
市面上的各类教程,大多是教你如何在某个语言中,定义一个类,以及对应的方法,然后再为这个类新建一个对象。之后再教你如何定义子类继承父类,等等。
所举的例子,大多也就是类似这种:
定义动物类型,动物会吃,会睡,吃和睡是方法,而示例中给出的方法实现,一般也就是简单的 print("吃"); print("睡");然后定义子类如鸟,鸟又有新的方法比如会飞,会下蛋;
这些简单的例子虽然清楚的描述了如何使用具体的语言去实现一个类,但是你却很难把他们和实际工作中所要解决的问题联系起来。
比如:为什么要定义动物类型,为什么定义了吃和睡这两种方法,而不是定义生和死?
为什么要从动物类派生出鸟这个子类,它又有什么用处?给出会飞和会下蛋的方法是否合理,是不是还有鸟不会飞或者不会下蛋的?
看这类教程,无法得到答案,并且理解更加浮于表面。导致看的越多,对于面向对象的意义以及背后隐藏的思想,越为迷糊。
于是静心坐下来看这么一本经典的《Java编程思想》,从深入了解对象开始。
抽象过程
所有的编程语言都提供抽象机制。汇编语言是对底层机器语言的轻微抽象。许多所谓命令式语言(FORTRAN、BASIC、C)是对汇编语言的抽象。
它们所做的主要抽象,要求在解决问题时,基于计算机的结构,而不是基于所要解决的问题的结构来考虑。
程序员需要在 机器模型 和 实际待解问题的模型 之间 建立关联。
另一种方式,是只对待解问题建模。如LISP, APL, PROLOG等。但是它们都有特定的应用领域。
面向对象方式是,向程序员提供了表示问题空间中的元素的工具。问题空间中的元素及其在解空间中的表示,称为“对象”。(还需要一些无法类比为问题空间元素的对象。)
这种思想的实质是:可以通过添加新类型的对象,使自身适用于某个特定问题。因此,当在阅读描述解决方案的代码的同时,也是在阅读问题的表述。
OOP允许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题。
每个对象看起来都有点像一台微型计算机——它具有状态,还有操作。和现实世界中的对象作类比,可以说他们都具有特性和行为。
第一个成功的面向对象语言,同时也是Java所基于的语言之一的Smalltalk的五个基本特性:
- 万物皆为对象
- 程序是对象的集合,它们通过发送消息来告知彼此所要做的
- 每个对象都有自己的由其他对象所构成的存储
- 每个对象都拥有类型
- 某一特定类型的所有对象都可以接收同样的信息
一个更加简洁的描述:对象具有状态、行为和标识。
每个对象都拥有内部数据——给出了该对象的状态
方法——它们产生行为
可以唯一地与其他对象区分开来——每一个对象在内存中都有一个唯一的地址
每个对象都有一个接口
所有的对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。
因为类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。
二者的差异在于,程序员通过定义类来适应问题,而不再被迫只能使用现有的用来表示机器中的存储单元的数据类型。
面向对象方法并不是仅局限于构建仿真程序。任何程序都是你所设计的系统的一种仿真。
面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。
怎样获得有用的对象?
必须有某种方式产生对对象的请求,使对象完成各种任务。每个对象都只能满足某些请求,这些请求由对象的接口(interface)所定义,决定接口的便是类型。
类型名称 | Light |
---|---|
接口 | on() off() brighten() dim() |
Light lt = new Light();
lt.on();
接口确定了对某一特定对象所能发出的请求。在程序中必须有满足这些请求的代码。这些代码与隐藏的数据一起构成了实现。
每个对象都提供服务
当正在试图开发或理解一个程序设计时,最好的方法之一就是,将对象想象为“服务提供者”。
你的目标就是,去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。
问一下自己:“如果我可以将问题从表象中抽取出来,那么什么样的对象可以马上解决我的问题呢?”
分析之后,也许某些对象已经存在了,但是对于不存在的对象,看起来像什么样子?能够提供哪些服务?需要哪些对象才能履行他们的义务?
持续这样做之后,就会发现:
- 我肯定某个对象已经存在了。
- 某个对象看起来很简单,可以坐下来写代码了。
这样,就将问题分解为了对象集合。
将对象看作是服务提供者的另一个好处:有助于提高对象的内聚性。
将对象作为服务提供者看待,是一件伟大的简化工具:在设计过程中有用,当其他人试图理解你的代码或重用某个对象时,如果看出了这个对象所能提供的服务的价值,这会使调整对象以适应其设计的过程变得简单。
被隐藏的具体实现
将程序开发人员按照角色分为类创建者(那些创建新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消费者)是很有意义的。
客户端程序员的目标,是收集各种用来实现快速应用开发的类。
类创建者的目标是构建类,这种类只向客户端程序员暴露必备的部分,而隐藏其他部分。
Java用三个关键字在类的内部设定边界:public、private、protected。
public:紧随其后的元素对任何人都是可用的。
private:除类型创建者和类型的内部方法之外的任何人都不能访问的元素。
protected:与private作用相当,区别是,继承的类可以访问protected成员,但是不能访问private成员。
Java还有一种默认访问权限,当没有用到前面提到的访问指定词时,将发挥作用。这种权限是包访问权限。类可以访问在同一个包中的其他类的成员,但是在包之外,这些成员如同指定了private一样。
复用具体实现
最简单地复用某个类的方式,就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中。我们称其为“创建一个成员对象”。
新的类可以由任意数量、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。
组合:使用现有的类合成新的类。
聚合:组合是动态发生的。
组合带来了极大的灵活性。可以在不干扰现有客户端代码的情况下,修改这些成员。也可以在运行时修改这些成员对象,以实现动态修改程序的行为。继承并不具备这样的灵活性,因为编译器必须对通过继承而创建的类施加编译时的限制。
在建立新类时,应该首先考虑组合,因为它更加简单灵活。一旦有了一些经验之后,便能够看出必须使用继承的场合了。
继承
对象这种观念,使得你可以通过概念将数据和功能封装到一起。这些概念用关键字class来表示,它们形成了编程语言中的基本单位。
可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。
例如几何形,类型层次结构同时体现了几何形状之间的相似性和差异性。
以同样的术语,将解决方案转换成问题是大有裨益的,因为不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型。因此,可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。
导出类与基类具有相同的类型。前面的例子,一个圆形也就是一个几何形。通过继承而产生的类型等价性,是理解面向对象程序设计方法内涵的重要门槛。
有两种方法可以使基类与导出类产生差异:
- 直接在导出类中添加新方法。但是,应该仔细考虑是否存在基类也需要这些额外方法的可能性。
- 更重要的一种使导出类和基类之间产生差异的方法,是改变现有基类的方法的行为,这被称之为覆盖那个方法。
“是一个”与“像是一个“关系
继承应该只覆盖基类的方法吗?
如果这样做,就意味着导出类和基类是完全相同的类型,因为他们具有完全相同的接口。
我们经常将这种情况下的基类与导出类之间的关系成为is-a(是一个)关系。
有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。此时基类无法访问新添加的方法。
这种情况我们可以描述为is-like-a(像是一个)关系。
新类型具有旧类型的接口,但是它还包含其他方法,所以不能说它们完全相同。
伴随多态的可互换对象
这部分没太明白,后续再做笔记
单根继承结构
在单根继承结构中所有的对象都具有一个共用接口,所以它们归根到底都是相同的基本类型。
单根继承结构保证所有对象都具备某些功能。
单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器正是Java相对C++的重要改进之一。由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作显得尤其重要,并且给编程带来了更大的灵活性。
容器
如果不知道在解决某个特定问题时需要多少个对象,或者它们将存活多久,那么久不可能知道如何存储这些对象。这类信息只有在运行时才能获得。
这个通常被称为容器的新对象,在任何需要时都可扩充自己,以容纳你置于其中的所有东西。因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。
Java在其标准类库中包含有大量的容器。在Java中,有满足不同需要的各种类型的容器,例如List,Map,Set,以及诸如队列、树、堆栈等更多的构件。
需要对容器有所选择,这有两个原因:
- 第一,不同容器提供了不同类型的接口和外部行为。
- 第二,不同的容器对于某些操作具有不同的效率。
参数化类型
创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能性,这种解决方案被称为参数化类型机制。
参数化类型就是一个编译器可以自动定制作用于特定类型上的类。
参数化类型,在Java中它称为范型。一堆尖括号,中间包含类型信息,通过这些特征就可以识别对范型的使用。
例如,创建一个存储Shape的ArrayList:
ArrayList<Shape> shapes = new ArrayList<Shape>();
对象的创建和生命期
怎样才能知道何时销毁这些对象呢?当处理完某个对象之后,系统其他部分可能还在处理它。
在被称为堆(heap)的内存池中动态地创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配空间。
动态方式有这样一个一般性的逻辑假设:对象趋于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。
Java完全采用了动态内存分配方式。
生命周期。对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。如果是在堆上创建对象,编译器就会对它的生命周期一无所知。Java提供了“垃圾回收”的机制,自动发现对象何时不再被使用,并继而销毁它。垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄漏问题。
异常处理:处理错误
Java一开始就内置了异常处理,而且强制你必须使用它。它是唯一可接受的错误报告方式。这种有保障的一致性有时会使得错误处理非常容易。
异常处理不是面向对象的特征——尽管在面向对象语言中,异常常常被表示称为一个对象。
并发编程
共享资源。如果有多个并行任务都要访问统一资源,那么就会出问题。为了解决这个问题,整个过程是:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。
Java的并发是内置于语言中的。
Java与Internet
Java解决了在万维网上的程序设计问题。
Web是什么
- 客户/服务器计算技术。
客户/服务器系统的核心思想是:系统具有一个中央信息存储池,用来存储某种数据,通常存在于数据库中,可以根据需要将它分发给某些人员或者机器集群。信息存储池、用于分发信息的软件以及信息与软件所驻留的机器或机群被总称为服务器。驻留在用户机器上的软件与服务器进行通信,以获取信息、处理信息,然后将他们先是在被称为客户机的用户机器上。
基本概念并不复杂。问题是单一的服务器,要同时为多个客户服务。因此设计者把数据“均衡”分布于数据表中,以取得最优的使用效果。此外,必须保证一个客户插入的新数据不会覆盖另一个客户插入的新数据,也不会在将其添加到数据库的过程中丢失(这被称为事务处理)。还有一个重要的性能问题,可能任意时刻都有成百上千的客户向服务器发出请求,为了将延迟最小火,程序员努力减轻处理任务的负载,通常是分散给客户端机器处理,但有时也会使用所谓的中间件将负载分散给在服务器端的其他机器。
分发信息这个简单思想的复杂性实际上是有很多不同层次的,它至关重要。
- Web就是一台巨型服务器。
所有的服务器和客户机都同时共存于同一个网络中。
浏览器只是一个观察器,它甚至不能执行最简单的计算任务。引入在客户端浏览器中运行程序的能力,这被称为“客户端编程”。
客户端编程
Web最初的“服务器-浏览器”设计师为了能够提供交互性的内容,其交互性完全由服务器提供。
基本的HTML包含有简单的数据收集机制,收集来的数据,利用按钮的提交动作,通过所有的Web服务器都提供的通用网关接口(CGI)传递。
构建于CGI程序之上的网站可能会迅速变得过于复杂而难以维护,同时产生响应时间过长的问题。CGI程序的响应时间依赖于所必须发送的数据量的大小,以及服务器和Internet的负载。
比如,你肯定经历过对Web输入表单进行数据验证的过程:按下提交按钮,数据发送到服务器,服务器启动CGI脚本检查,发现错误,将错误组装为一个html页面发送给你,之后你必须汇推一个页面,然后再试。
问题的解决方案就是客户端编程。意味着Web浏览器能用来执行任何它可以完成的工作,使得返回给用户的结果更加迅捷。
客户端编程的问题是:它与通常意义上的编程十分不同。Web浏览器就像一个功能受限的操作系统。
- 插件
插件对于客户端编程的价值在于:允许专家级的程序员不需经过浏览器生产厂商的许可,就可以开发某种语言扩展,并将它们添加到服务器中。
2.脚本语言
插件引发了浏览器脚本语言的开发。缺点是代码会暴露给任何人去浏览。
在Web浏览器内部使用的脚本语言实际上总是被用来解决特定类型的问题,主要是用来创建更丰富、更具有交互性的图形化用户界面。脚本语言可以解决客户端编程中所遇到的80%的问题。
- Java
那么剩下的20%呢?Java是处理他们最流行的解决方案。
Java是通过applet以及使用Java Web Start来进行客户端编程的。
applet是只在Web浏览器中运行的小程序,是作为网页的一部分而自动下载的。当applet被激活时,它便开始执行一个程序。它提供一种分发软件的方法,一旦用户需要客户端软件时,就自动从服务器把客户端软件分发给用户。
- 备选方案
这里提到了Flex技术。由于本书出版于2007年,某些当时的新技术可能有些过时,或许后来又出现了更好的技术。笔记仅根据本书而作,不做更多的扩展。
- .NET和C#
同样,这部分内容具有时效性。不多说明。
- Internet与Intranet
Web是最常用的解决客户/服务器问题的方案。对于公司内部典型的客户/服务器问题,也一样可以使用这项技术。
如果程序运行与Intranet上,那么可能会受到不同的限制。就和Internet的情况不一样了。
面对各种解决客户端编程问题的方案时,首先需要进行性价比分析,认真考虑问题的各种限制,然后思考哪种解决方案可以成为最短的捷径。
服务器端编程
这部分主要在企业Java编程思想中进行论述。
总结
过程型语言:数据定义和函数调用。你需要通读函数调用和低层概念,以在脑海里建立一个模型。这些程序使用的表示术语更加面向计算机而不是你要解决的问题。
编写良好的Java程序通常比过程型程序要简单的多,而且也易于理解的多。
OOP和Java也许并不适合所有的人。重要的是正确评估自己的需求。