对象导论---JAVA编程思想

第一章 对象导论


序言

“我们之所以将自然界分解,组织成各种概念,并按其含义分类,主要是因为我们是整个口语交流社会共同遵守的协定的参与者,这个协定以语言的形式固定下来…除非赞成这个协定中规定的有关语言信息的组织和分类,否则我们根本无法交谈。”出自于Benjamin Lee Whorf(1897~1941)之口。

计算机革命起源于机器,因此,编程语言的产生也始于对机器的模仿。

但是,计算机并非只是机器那么简单。计算机是头脑延伸的工具(头脑的自行车),同时也是一种不同类型的表达媒体。因此,这种工具看起来越来越不像机器,而更像我们头脑的一部分,以及一种如写作、绘画、雕刻、电影等一样的表达形式。

面向对象程序设计(Object-oriented Programming, OOP)便是这种以计算机作为表达媒体的大趋势中的组成部分。

许多人在没有了解面向对象程序设计的全貌之间,感觉无法轻松自在地从事此类编程。因此,本章介绍的是面向对象的设计。如果是还没有接触过面向对象语言的同学,建议先略过本章。越过本章并不影响对JAVA的学习,不过此后还需回头来看本章。


1.1 抽象过程

所有编程语言都提供抽象机制。可以认为,人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。

所谓的“类型”是指“所抽象的是什么?”汇编语言是对底层机器的轻微抽象。“命令式”语言(FORTRAN、BASIC、C等)都是对汇编语言的抽象。这些语言在汇编语言的基础上有了大幅的改进,但是它们所作的主要抽象仍要求在解决问题时要基于计算机的结构,而不是基于所要解决的问题的结构来考虑。

程序员必须建立起在机器模型(位于“解空间”内,这是你对问题建模的地方,例如计算机)和实际待解决问题的模型(位于“问题空间”内,这是问题存在的地方,例如一项业务)之间的关联

建立这种映射是费力的,而且这不属于编程语言所固定的功能,这使得程序难以编写,并且维护代价高昂,同时也产生了作为副产物的整个* “编程方法”行业*

另一种对机器建模的方式就是只针对待解问题建模。早期的编程语言,如LISP和APL,都是选择考虑世界的某些特定视图(分别对应于“所有问题最终都是列表”或者“所有问题都是算法形式的”)。PROLOG则将所有问题都转换成决策链。此外还产生了基于约束条件编程的语言和专门通过对图形符号操作来实现编程的语言(后者被证明限制性过强)。这些方式对于它们所要解决的特定类型问题都是不错的解决方案,当时一旦超过其特定领域,它们就力不从心了

面向对象方式通过向程序员提供表示问题空间中的元素工具而更进了一步(就是表示问题的形式更容易了)。这种表示方式非常通用,使得程序员不受限于任何特定类型的问题。我们将问题空间中的元素(所面对的待解决问题)及其解空间(解决问题的地方)中的表示称为“对象”

这种思想的实质是:程序可以通过添加新类型的对象使自身适用于某个特定问题。因此,当我们在阅读描述解决方案的代码的同时,也是在阅读问题的表述。相对以前我们所使用的语言(某些编程语言的设计者认为面向对象编程本身不足以轻松地解决所有编程问题,所以他们提倡将不同的方式结合到多聚合编程语言中),这是一种更灵活和更有力的语言抽象。所以,OOP允许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题

即使如此,它仍然与计算机有联系:每个对象看起来都像一台微型计算机—它具有状态,还具有操作,用户可以要求对象执行这些操作。如果要对现实世界中的对象做类比,那么说它们都具有特性和行为似乎不错。

Alan Kay曾经总结了第一个成功的面向对象语言、同时也是JAVA所基于的语言之一的Smalltalk(Smalltalk被公认为历史上第二个面向对象的程序设计语言和第一个真正的集成开发环境 (IDE),Smalltalk对其它众多的程序设计语言的产生起到了极大的推动作用,主要有:Objective-C,Actor, Java 和Ruby等)的五个基本特性,这些特性表现了一种纯粹的面向对象程序方式:

  • 万物皆为对象。将对象视为奇特的变量,它可以存储数据。理论上讲,你可以抽取待求解问题的任何概念化构件(狗、建筑物、服务等),将其表示为程序中的对象。
  • 程序是对象的集合,它们通过发送消息来告知彼此所要做的。要想请求一个对象,就必须对该对象发送一条消息。更具体地说,可以把消息想象为对某个特定对象的方法的调用请求。
  • 每个对象都有自己的由其他对象所构成的存储。换句话说,可以通过现有对象创建新类型的对象。因此,可以在程序中构件复杂的体系,同时将其复杂性隐藏在对象的简单性背后。
  • 每个对象都拥有其类型。按照通用的说法,“每个对象都是某个类(class)的一个实例(instance)”,这里“类”就是“类型”的同义词。每个类最重要的区别就是“可以发送什么样的消息给它”。
  • 某一特定类型的所有对象都可以接收同样的消息。这是一句意味深长的表述,你在稍后便会看到。因为“圆形”类型的对象同时也是“几何形”类型的对象,所以一个“圆形”对象必定能够接受发送给“几何形”对象的消息。这意味着可以编写与“几何形”交互并自动处理所有几何形性质相关的事务代码。这种可替代性是OOP中最强有力的概念之一。

Booch对对象提出一个更加简洁的描述:对象具有状态、行为和标识。这意味着每一个对象都可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生行为),并且每一个对象都可以唯一地与其他对象区分开来,具体说来,就是每一个对象在内存中都有一个唯一的地址(这确实显得有一点过于受限,因为对象可以存在于不同的机器和地址空间中,它们还可以被存储在硬盘上,在这些情况下,对象的表示就必须由内存地址之外的东西来确定了)。


1.2 每个对象都有一个接口

亚里士多德大概是第一个深入研究类型(type)的哲学家,他曾提出过鱼类和鸟类这样的概念。所有的对象都是唯一的,也是与之具有相同特性和行为的类中一例。这种思想被直接应用于第一个面向对象语言Simula-67,它在程序中使用基本关键字class来引入新的类型。

在程序执行期间具有不同的状态而其他方面都相似的对象会被分组到对象的类中,这就是关键字class的由来。每个类的成员或者元素都具有某种共性。

创建抽象数据类型(类)是面向对象程序设计的基本概念之一。抽象数据类型的运行方式与内置类型几乎完全一致:你可以创建某一类型的变量(对象或者说实例),然后操作这些变量(称其为发送消息或者请求;发送消息,对象就知道要做什么)。

尽管我们在面向对象程序设计中实际上进行的是创建新的数据类型,但事实上所有的面向对象程序设计语言都使用class这个关键词来表示数据类型。当看到类型一词时,可将其作为类来考虑,反之亦然(有些人对此会区别对待,他们认为:类型决定接口(定义一组操作的集合),而类是该接口的一个特定实现)。

类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。例如所有浮点型数字具有相同特性和行为集合。二者的差异在于,程序员可以通过定义类来适应问题,而不再被迫只能使用现有的用来表示机器中的存储单元的数据类型。

事实上,面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射

但是,怎样才能获取有用的对象?每个对象都只能满足某些请求,这些请求由对象的接口所定义,决定接口的便是类型。

以灯泡为例:
这里写图片描述

Light lt = new Light();
lt.on();

接口确定了对某一个特定对象所发出的请求。但是,在程序中必须有满足这些请求的代码。这些代码与隐藏的数据一起构成了实现从过程型编程的观点来看,这并不太复杂

在类型中,每一个可能的请求都有一个方法与之相关联,当向对象发送请求是,与之相关联的方法就会被调用(例如类型Light的一个对象lt执行了on()操作,控制对象lt的开启行为)。


1.3 每个对象都提供服务

当正在试图开发或理解一个程序设计是,最好的方法之一就是将对象想象为“服务提供者”。程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象

着手从事这件事的一种方式就是问一下自己:“如果我可以将问题从表象中抽取出来,那么什么样的对象可以马上解决我的问题呢?”

将对象看做服务提供者还有一个附带的好处:它有助于提高对象的内聚性。在良好的面向对象设计中,每个对象都可以很好的完成一项任务,但是它并不试图做更多的事

将对象作为服务提供者看待是一件伟大的简化工具。这不仅在设计过程中非常有用,而且当其他人试图理解你的代码或重用某个对象时,如果它们看出了这个对象所能提供的服务的价值,它会使调整对象以适应其设计的过程变得简单的多。


1.4 被隐藏的具体实现

将程序开发人员按照角色分为 类创建者客户端程序员

  • 客户端程序员的目标是手机各种用来实现快速应用开发的类。
  • 类创建者的目标是构建类,这种类只向客户端程序员暴露必须的部分,而隐藏其他部分。被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心的或不知情的客户端程序员锁毁坏,因此将实现隐藏起来可以减少程序bug。

正是因为存在类创建者与客户端程序员的这种关系,访问控制符就有了存在的原因:

  • 让客户端程序员无法触及他们不应该触及的部分。
  • 允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。或者说,客户端程序员不必担心自己编写的代码需要随类库的改变而出现程序错误。

JAVA用三个关键字在类的内部设定边界:public、private、protected。分别是公共的、私有的、内部的。此后会说明到。

JAVA还有一种默认的访问权限,即当没有使用前面提到的三个关键字的任何一个。这种权限通常被称为包访问权限


1.5 复用具体实现

产生一个具有可复用性的代码并不容易,需要丰富的经验和敏锐的洞察力。代码复用是面向对象程序设计语言所提供的最了不起的优点之一。

最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新类中。

我们将现有的类组合成新的类,这种概念被称为组合(composition)。举个例子,就是一个类中创建几个对象。

如果组合是动态发生的,那么通常被称为聚合(aggregation)。举个例子,新类的构造函数需要传入一个对象,或者说可以 get/set 变换其中的对象。

组合带来了极大的灵活性。新类的成员对象通常都被声明为 private,使得使用新类的客户端程序员不能访问它们。

我们还知道,继承也拥有一定的复用性。但是继承如果过多,会导致设计很复杂。所以我们在考虑是使用继承还是组合时,首先考虑组合,因为它更加简单而灵活


1.6 继承

对象这种概念,本身就是十分方便的工具,使得你可以通过概念将数据和功能封装到一起,因此可以对问题空间的观念给出恰当的表示

遗憾的是,这样做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。

如果我们能够以现有的类为基础,然后稍作添加和修改那就好多了。通过继承便可以达到这样的效果。不过也有例外,当基类发生改变,子类也会反应出相应的变动

我们知道继承之后,导出类并不拥有基类的所有成员。它拥有一个更重要的意义,就是复制了基类的接口。所以,可以发送给基类对象的消息同时也可以发送给导出类对象。意味着导出类与基类具有相同的类型

当然,如果只是简单的继承而并不做其他任何事情的话。貌似没有什么意义,于是我们通常有两种方式让继承显得有意义。

  • 直接在导出类中添加新方法。
  • 覆盖基类中的方法

1.61 “是一个”与“像是一个”关系

替代原则是指可以用一个导出类对象来完全替代一个基类对象。

通俗的说“is-a”关系就是继承关系。

“is-like-a”关系就是接口关系。


1.7 伴随多态的可互换对象

处理类型的层次结构时,经常想把一个对象不当作它所属的特定类型来对待,而是将其当作其基类的对象来对待

如下图:
这里写图片描述
我们通过 BirController 这个类来控制 Bird 对象的操作。我们即使忽略 Bird 对象的具体类型,也会产生正确的行为。例如,bird是一个Goose(企鹅),当我们通过BirdController.reLocate()时,也可以得到一个正确的 企鹅移动的结果 。

面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用。

一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定

在OOP中,程序知道运行时才能够有确定代码的地址。所以,当消息发送到一个泛化对象时,必须采用其他的机制。为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念。后期绑定是在当对象发送消息时,被调用的代码直到运行时才能确定代码地址

为了执行后期绑定,JAVA使用了一小段特殊的代码来替代绝对地址调用(这个表示自己也不知道)。

在某些语言中,必须明确声明希望某个方法具备后期绑定属性所带来的灵活性(C++是使用virtual关键字来实现的)。在这类语言中,方法的默认情况下不是动态绑定的。

JAVA中,动态绑定则是默认行为,不需要添加额外的关键字来实现多态。

把将导出类看做是它的基类的过程称为向上转型(upcasting)


1.8 单根继承结构

在OOP中,自C++面世以来就已变得非常瞩目的一个问题就是,是否所有的累最终都继承单一的基类。在JAVA中(事实上还包括除C++以外的所有OOP语言),答案是yes,这个终极基类的名字就是Object

单根继承结构保证所有对象都具备某些功能(我总觉得我自己没说到上,为什么堆上创建变得容易了?)。

单根继承结构使垃圾回收器的实现变得容易得多(这一想象,我们运行程序的时候还有一个垃圾回收器线程,控制着Object对象的释放)。这样JAVA给编程带来了更大的灵活性。因为我们不需要考虑某些对象释放异常等等。这与C++是大为不同的地方


1.9 容器

就是具有存储功能的类,例如List(用于存储序列)、Map(用来存储对象直接的关联)、Set(每种对象只持有一个),以及诸如队列、树、堆栈等更多的构件。

不同的类提供了不同的接口行为。这些类的实现与数据结构一书息息相关


1.9.1 参数化类型

在JAVA SE5出现之前,我们的容器存储的对象只有Object类型。这种模式可能是非常好,它能够存储任何东西。当我们将一个对象存入容器中,这个对象将会向上转型。此后,我们取出时也需要对这个对象向下转型

我们考虑两个问题,第一个问题是:向上转型会出现问题吗?

答案显然是不会。我们知道任何东西都继承Object。

第二个问题:向下转型会出现问题吗?

答案是会出现问题,如果向下转型为错误类型,就会得到一个异常

在JAVA SE5出现之后,提出了一个泛型的概念。自此,我们将使用泛型来指定容器存储的数据类型


1.10 对象的创建和生命周期

创建对象的两种方式:

  • C++认为效率控制是重要的议题,所以给程序员提供了选择的权利。为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈或静态存储区内来实现。这种方式将存储空间和释放置于优先考虑的位置,同时也牺牲了灵活性。(可能大多数人不理解为什么失去了灵活性?看下面你应该就能懂为什么了!)
  • 第二种方式是在被称为的内存池中动态的创建对象。这种方式中,知道运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。这些答案只能在程序运行时相关代码被执行到的那一刻才能确定(这很重要)。

因为在堆中创建空间远比在堆栈创建空间慢,这也是为什么JAVA比C++效率慢的原因。

动态方式有这样一个一般性的逻辑假设:对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击(说明动态方式相对稳定?)。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。

JAVA完全采用了动态内存分配方式。使用 new 关键字来构建对象的动态实例。

对象的生命周期:

对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。

然而,如果是在堆上创建对象,编译器就会对它的生命周期一无所知。

在像C++这样的语言中,必须通过编程方式来确定何时销毁对象,这可能会因为不能正确处理而导致内存泄漏(这在C++程序中是常见的问题,学过C++的同学应该懂 new 一个对象,通过new生成的对象存放在堆中)。

JAVA提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。

JAVA的垃圾回收器被设计用来处理内存释放问题。垃圾回收期“知道”对象何时不再被使用,并自动释放对象占用的内存。这一点同所有对象都是继承自单根基类Object以及只能以一种方式创建对象(在堆上创建)这两个特性结合起来,使得用JAVA编程的过程较之用C++编程要简单的多,所要做出的决策和要克服的障碍也要少的多(这也是导致效率比C++慢的原因)。


1.11 异常处理:处理错误

异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中

异常是一种对象,它从出错地地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。

异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的代码执行。这往往使得代码编写变得简单,因为不需要被迫定期检查错误。

异常提供了一种从错误状况进行可靠恢复的途径。现在不再是只能退出程序,你可以经常进行校正,并回复程序的执行,这些都有助于编写出更健壮的程序。


1.12 并发编程

在计算机百年城中有一个基本概念,就是在同一时刻处理多个任务的思想。

使用这类编程时,是因为我们想把问题切分成多个可独立运行的部分,从而提高程序的响应能力。

并发有一个隐患:资源共享。如果有多个并行任务都要访问同一项资源,那么就会出现问题。例如,两个进程不恩呢该同时向一台打印机发送消息。为了解决这个问题,必须将打印机在被使用期间加锁,待正在任务的进程执行完成后释放锁


1.13 JAVA 与 Internet

JAVA不仅仅在传统的单机程序设计问题非常有用之外,同样重要的是,它还解决了在万维网(WWW)上的程序设计问题(不管是以前的CGI还是到现在的JAVA EE)。


1.13.1 Web是什么

2个点可以概括:

  • 客户/服务器计算技术
  • Web就是一台巨型服务器

由这两点就可以说明,一切计算都交由服务器处理(其实现在的Web是将部分计算由客户端来做,而且最近还出现了Node.js),并且由此可知,它是安全的。


1.13.2 客户端编程

这一个篇幅我可能无法做出客观的解释,因为我也没接触过applet编程。听说是因为flash火了之后,applet被淘汰了。还有的好像说是因为一些商业性的原因导致的。不过本人见过java的applet,发现applet太难使用了。还需要安装七七八八的东西才可以用。


1.13.3 服务器端编程

我们这里强调一个服务器端的编程,那就是现在流行的JSP和servlet所编写的服务器程序。虽然还有更高级的东西,比如SSH、SSM、Spring Boot这类听着都很高端的技术,但是,他们都是需要JSP和servlet基础才可以看得懂的并且使用的技术。因为基础不好会导致开发时遇到问题解决不了,这也是为什么笔者回来重新看基础的原因。


1.14 总结

面向对象程序设计带给人们的喜悦之一就是:对于设计良好的程序,通过阅读它就可以很容易地理解其代码。通常,其代码也会少很多,因为许多问题都可以通过重用现有类库代码而得到解决

本章所有内容都是重点,因为这与之后的开发息息相关。OOP程序设计时,你必须对OOP的概念有所了解,甚至有时候了解OOP的概念还不够,因为你编写程序时会遇到非常多你自己无法预测到的难题。

猜你喜欢

转载自blog.csdn.net/qq_37340753/article/details/81062525