java 对象的概念

《Java编程思想》的一些记录。

抽象

所有编程语言都提供抽象机制。汇编语言是对底层机器的轻微抽象。接着出现的 “命令式” 语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。

面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。他允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。

程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”) 之间建立起一种关联。面向对象的程序设计将问题空间中的元素以及它们在解决方案空间的表示称作 “对象”(Object)。

对象有五大基本特征
1. 万物皆对象。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其 “发出请求” 时执行本身的操作。理论上讲,你总是可以从 要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一 个对象。

2. 程序是一组对象,通过消息传递来告知彼此该做什么。要请求调用一个对象的方法,你需要向该对象发送消息。

3. 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现 有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。

4. 每个对象都有一种类型。根据语法,每个对象都是某个 “类” 的一个 “实 例”。其中,“类”(Class)是 “类型”(Type)的同义词。一个类最重要 的特征就是 “能将什么消息发给它?”。

5. 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法, 大家不久便能理解。由于类型为 “圆”(Circle)的一个对象也属于类型 为 “形状”(Shape)的一个对象,所以一个圆完全能接收发送给 “形状” 的消息。这意味着可让程序代码统一指挥 “形状”,令其自动控制所有 符合 “形状” 描述的对象,其中自然包括 “圆”。这一特性称为对象的 “可替换性”,是 OOP 最重要的概念之一。

更简洁的描述为:一个对象具有自己的状态,行为和标识。 这意味着对象有自己的内部数据 (提供状态)、方法 (产生行为),并彼此区分(每个对象 在内存中都有唯一的地址)。

接口

创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在 “问题空间”(问题实际存在的地方)的元素与 “方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的 “一对一” 的映射关系。

那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求, 令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的 “接口” (Interface)定义的,对象的 “类型” 或 “类” 则规定了它的接口形式。“类型” 与 “接口” 的对应关系是面向对象程序设计的基础。

封装

我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发 生。

Java 有三个显式关键字来设置类中的访问权限:public(公开),private(私有) 和 protected(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。

复用

代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到 “组合” 和 “聚合” 的概念:

• 组合(Composition)经常用来表示 “拥有” 关系(has-a relationship)。例如,“汽 车拥有引擎”。

• 聚合(Aggregation)动态的组合。

聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。

继承

“继承” 给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达 “问题空间” 的概念,而不用受制于必须使用底层机器语言。

通过使用 class 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么 做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还 是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行 “克隆”,再根据情 况进行添加和修改,情况就显得理想多了。“继承” 正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的 “克隆” 类(正式名称叫作继承类或者子类)也会反映出这种变化。

这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为, 但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以 不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。

在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。

有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求, 所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。

尽管继承有时意味着你要在接口中添加新方法(尤其是在以 extends 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。

多态

多态就是同一个接口,使用不同的实例而执行不同操作。

我们在处理类的层次结构时,通常把一个对象看成是它所属的基类,而不是把它当成具体类。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个 “形状” 的例子中,“方法(method)操纵的是通用 “形状”,而不关心它们是 “圆”、“正方形”、 “三角形” 还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此 “方法” 向其中的任何代表 “形状” 的对象发送消息都不必担心对象如何处理信息。

这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。例如,你可以通过通用的 “形状” 基类派生出新的 “五角形” 形状的子类,而不需要修改通用 “形状” 基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。

这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类(“圆” 当作 “形状”,“自行车” 当作 “车”,“鸬鹚” 当作 “鸟” 等等),编译器(compiler)在编译时期就无法准确地知道什么 “形状” 被擦除,哪一种 “车” 在行驶,或者是哪种 “鸟” 在飞行。这就是关键所在:当程序接收这种消息时,程 序员并不想知道哪段代码会被执行。“绘图” 的方法可以平等地应用到每种可能的 “形 状” 上,形状会依据自身的具体类型执行恰当的代码。

如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子 类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 BirdController 对象和通用 Bird 对象中,BirdController 不 知道 Bird 的确切类型却还能一起工作。从 BirdController 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 Bird 对象的确切类型或行为。那么,在调用 move() 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?

 这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的早期绑定,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用, 该调用会被解析为将执行的代码的绝对地址。 通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执 行类型检查,但是它不知道要执行的确切代码。

为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值