1、对象的概念
编程语⾔就是创建应⽤程序的思想结构。
⾯向对象编程(Object-Oriented Programming OOP)是⼀种编程思维⽅式和编码架构。
等你具备⼀定编程基础后,请务必再回头看。只有这样你才能深刻理解⾯向对象编程的重要性及设计⽅式。
1、抽象
从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这⾥的“类型”意思是:抽象的内容是什么?
汇编语⾔是对底层机器的轻微抽象。接着出现的“命令式”语⾔(如 FORTRAN,BASIC 和 C)是对汇编语⾔的抽象。
程序员必须要在机器模型(“解决⽅案空间”)和实际解决的问题模型(“问题空间”)之间建⽴起⼀种关联。
为机器建模的另⼀个⽅法是为要解决的问题制作模型。
对⼀些早期语⾔来说,如 LISP 和 APL,它们的做法是“从不同的⾓度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有 问题都归纳为决策链。对于这些语⾔,我们认为它们⼀部分是“基于约束”的编程,另⼀部分则是专为 处理图形符号设计的(后者被证明限制性太强)。每种⽅法都有⾃⼰特殊的⽤途,适合解决某⼀类的问题。只要超出了它们⼒所能及的范围,就会显得⾮常笨拙。
⾯向对象的程序设计在此基础上跨出了⼀⼤步,程序员可利⽤⼀些⼯具表达“问题空间”内的元素。由于这种表达⾮常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决⽅案空间的表⽰称作“对象”(Object)。 当然,还有⼀些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进⾏灵活的调整,以便与特定的问题配合。总之,OOP 允许我们根据问题来描述问题,⽽不是根据运⾏解决⽅案的计算机。
通过这些特征,我们可理解“纯粹”的⾯向对象程序设计⽅法是什么样的:
1. 万物皆对象。你可以将对象想象成⼀种特殊的变量。它存储数据,但可以在你对其“发出请求”时执⾏本⾝的操作。理论上讲,你总
是可以从要解决的问题⾝上抽象出概念性的组件,然后在程序中将其表⽰为⼀个对象。
2. 程序是⼀组对象,通过消息传递来告知彼此该做什么。要请求调⽤⼀个对象的⽅法,你需要向该对象发送消息。
3. 每个对象都有⾃⼰的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念⾮常简
单,但在程序中却可达到任意⾼的复杂程度。
4. 每个对象都有⼀种类型。根据语法,每个对象都是某个“类”的⼀个“实例”。其中,“类”(Class)是“类型”(Type)的同义
词。⼀个类最重要的特征就是“能将什么消息发给它?”。
5. 同⼀类所有对象都能接收相同的消息。这实际是别有含义的⼀种说法,⼤家不久便能理解。由于类型为“圆”(Circle)的⼀个对象
也属于类型为“形状”(Shape)的⼀个对象,所以⼀个圆完全能接收发送给"形状”的消息。这意味着可让程序代码统⼀指挥“形状”,令其⾃动控制所有符合“形状”描述的对象,其中⾃然包括“圆”。这⼀特性称为对象的“可替换性”,是OOP最重要的概念之⼀。
⼀个对象具有⾃⼰的状态,⾏为和标识。这意味着对象有⾃⼰的内部数据(提供状态)、⽅法 (产⽣⾏为),并彼此区分(每个对象在内存中都有唯⼀的地址)。
2、接⼝
所有对象都是唯⼀的,但同时也是具有相同的特性和⾏为的对象所归属的类的⼀部分。
创建好⼀个类后,可根据情况⽣成许多对象。随后,可将那些对象作为要解决问题中存在的元素进⾏处理。事实上,当我们进⾏⾯向对象的程序设计时,⾯临的最⼤⼀项挑战性就是:如何在“问题空间”(问题实际存在的地⽅)的元素与“⽅案空间”(对实际问题进⾏建模的地⽅,如计算机)的元素之间建⽴理想的“⼀对⼀”的映射关系。
那么如何利⽤对象完成真正有⽤的⼯作呢?必须有⼀种办法能向对象发出请求,令其解决⼀些实际的问题。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接⼝”(Interface)定义的,对象的“类型”或“类”则规定了它的接⼝形式。“类型”与“接
⼝”的对应关系是⾯向对象程序设计的基础。
Light lt =new Light();
<();
3、服务提供
在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本⾝将为⽤户提供服务,并且它能通过调⽤其他对象提供的服务来实现这⼀点。我们的最终⽬标是开发或调⽤⼯具库中已有的⼀些对象,提供理想的服务来解决问题。
我们该选择哪个对象来解决问题呢?
对于还没有的对象,我们该设计成什么样呢?
这些对象需要提供哪些服务,以及还需要调⽤其他哪些对象?
我们可以将这些问题⼀⼀分解,抽象成⼀组服务。软件设计的基本原则是⾼内聚:每个组件的内部作⽤明确,功能紧密相关。在良好的⾯向对象设计中,每个对象功能单⼀且⾼效。这样的程序设计可以提⾼我们代码的复⽤性,同时也⽅便别⼈阅读和理解我们的代码。只有让⼈知道你提供什么服务,别⼈才能更好地将其应⽤到其他模块或程序中。
4、封装
可以把编程的侧重领域划分为研发和应⽤。应⽤程序员调⽤研发程序员构建的基础⼯具类来做快速开发。
研发程序员开发⼀个⼯具类,该⼯具类仅向应⽤程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该⼯具类被错误的使⽤和更改,从⽽减少程序出错的可能。彼此职责划分清晰,相互协作。当应⽤程序员调⽤研发程序员开发的⼯具类时,双⽅建⽴了关系。应⽤程序员通过使⽤现成的⼯具类组装应⽤程序或者构建更⼤的⼯具库。
如果⼯具类的创建者将类的内部所有信息都公开给调⽤者,那么有些使⽤规则就不容易被遵守。因为前者⽆法保证后者是否会按照正确的规则来使⽤,甚⾄是改变该⼯具类。只有设定访问控制,才能从根本上阻⽌这种情况的发⽣。汇编语言要什么基础
因此,使⽤访问控制的原因有以下两点:
1. 让应⽤程序员不要触摸他们不应该触摸的部分。(请注意,这也是⼀个哲学决策。部分编程语⾔认为如果程序员有需要,则应该让他
们访问细节部分。)
2. 使类库的创建者(研发程序员)在不影响后者使⽤的情况下完善更新⼯具库。例如,我们开发了⼀个功能简单的⼯具类,后来发现可
以通过优化代码来提⾼执⾏速度。假如⼯具类的接⼝和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。
Java 有三个显式关键字来设置类中的访问权限:public(公开),private(私有)和protected(受保护)。这些访问修饰符决定了谁能使⽤它们修饰的⽅法、变量或类。
1. public(公开) 表⽰任何⼈都可以访问和使⽤该元素;
2. private(私有) 除了类本⾝和类内部的⽅法,外界⽆法直接访问该元素。private 是类和调⽤者之间的屏障。任何试图访问私有成员
的⾏为都会报编译时错误;
3. protected(受保护) 类似于 private,区别是⼦类(下⼀节就会引⼊继承的概念)可以访问 protected 的成员,但不能访问
private 成员;
4. default(默认) 如果你不使⽤前⾯的三者,默认就是 default 访问权限。default 被称为包访问,因为该权限下的资源可以被**同⼀
包(库组件)**中其他类的成员访问。
5、复⽤
⼀个类经创建和测试后,理应是可复⽤的。
代码和设计⽅案的复⽤性是⾯向对象程序设计的优点之⼀。我们可以通过重复使⽤某个类的对象来达
到这种复⽤性。同时,我们也可以将⼀个类的对象作为另⼀个类的成员变量使⽤。新的类可以是由任意数量和任意类型的其他对象构成。这⾥涉及到“组合”和“聚合”的概念:
组合(Composition) 经常⽤来表⽰“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。组合关系中,整件拥有部件的⽣命周期,所以整件删除时,部件⼀定会跟着删除。实⼼箭头表⽰
聚合(Aggregation) 动态的组合。聚合关系中,整件不会拥有部件的⽣命周期,所以整件删除时,部件不会被删除。空⼼箭头表⽰
新建的类中,成员对象会使⽤ private 访问权限,这样应⽤程序员则⽆法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。也可以在“运⾏时"改变成员对象从⽽动态地改变程序的⾏为,这进⼀步增⼤了灵活性。
在创建新类时⾸先要考虑“组合”,因为它更简单灵活,⽽且设计更加清晰。
6、继承
“继承”给⾯向对象编程带来极⼤的便利。它在概念上允许我们将各式各样的数据和功能封装到⼀起,这样便可恰当表达“问题空间”的概念,⽽不⽤受制于必须使⽤底层机器语⾔。
在创建了⼀个类之后,即使另⼀个新类与其具有相似的功能,你还是得重新创建⼀个新类。但我们若能利⽤现成的数据类型,对其进⾏“克隆”,再根据情况进⾏添加和修改,情况就显得理想多了。“继承”正是针对这个⽬标⽽设计的。
在继承过程中,若原始类(正式名称叫作基类、超类或⽗类)发⽣了变化,修改过的“克隆”类(正式名称叫作继承类或者⼦类)也会反映出这种变化。
两种类型可以具有共同的特征和⾏为,但是⼀种类型可能包含⽐另⼀种类型更多的特征,并且还可以处理更多的消息(或者以不同的⽅式处理它们)。继承通过基类和派⽣类的概念来表达这种相似性。基类包含派⽣⾃它的类型之间共享的所有特征和⾏为。创建基类以表⽰思想的核⼼。从基类中派⽣出其他类型来表⽰实现该核⼼的不同⽅式。
继承的类型等价性是理解⾯向对象编程含义的基本门槛之⼀。因为基类和派⽣类都具有相同的基本接⼝,所以伴随此接⼝的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执⾏代码。**如果继承⼀个类⽽不做其他任何事,则来⾃基类接⼝的⽅法直接进⼊派⽣类。**这意味着派⽣类和基类不仅具有相同的类型,⽽且具有相同的⾏为。
区分新的派⽣类与原始的基类:
在派⽣类中添加新⽅法。这些新⽅法不是基类接⼝的⼀部分。要仔细考虑是否在基类中也要有这些额外的⽅法。这种设计的发现与迭代过程在⾯向对象程序设计中会经常发⽣。
改变现有基类⽅法的⾏为。这被称为覆盖 (overriding),要想覆盖⼀个⽅法,只需要在派⽣类中重新定义这个⽅法即可。(更重要)6.1、“是⼀个” 与 “像是⼀个”
是⼀个(is-a)关系:继承只覆盖基类的⽅法(不添加基类中没有的⽅法)
像是⼀个(is-like-a)关系:在派⽣类添加了新的接⼝元素,从⽽扩展接⼝。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类⽆法访问新添加的⽅法。不能说新旧类型完全相同。
7、多态
在处理类的层次结构时,通常把⼀个对象看成是它所属的基类,⽽不是把它当成具体类。通过这种⽅式,我们可以编写出不局限于特定类型的代码。
这样的代码不会受添加的新类型影响,并且添加新类型是扩展⾯向对象程序以处理新情况的常⽤⽅法。
这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执⾏。如果不需要知道执⾏了哪部分代码,那我们就能添加⼀个新的不同执⾏⽅式的⼦类⽽不需要更改调⽤它的⽅法。
那么编译器在不确定该执⾏哪部分代码时是怎么做的呢?
早期绑定:编译器⽣成对特定函数名的调⽤,该调⽤会被解析为将执⾏的代码的绝对地址。
后期绑定:程序直到运⾏时才能确定代码的地址。当向对象发送信息时,被调⽤的代码直到运⾏时才确定。编译器确保⽅法存在,并对参数和返回值执⾏类型检查,但是它不知道要执⾏的确切代码。
为了执⾏后期绑定,Java 使⽤⼀个特殊的代码位来代替绝对调⽤。这段代码使⽤对象中存储的信息来计算⽅法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的⾏为根据特定代码位的内容⽽不同。
在 Java 中,动态绑定是默认⾏为,不需要额外的关键字来实现多态性。
void doSomething(Shape shape){
// ...
shape.draw();
}
Circle circle =new Circle();
Triangle triangle =new Triangle();
Line line =new Line();
doSomething(circle);
doSomething(triangle);
doSomething(line);
这种把⼦类当成其基类来处理的过程叫做“向上转型”(upcasting)。在⾯向对象的编程⾥,经常利⽤这种⽅法来给程序解耦。
发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执⾏是正确的,这就是对象的“多态性”(Polymorphism)。
⾯向对象的程序设计语⾔是通过“动态绑定”的⽅式来实现对象的多态性的。
7.1、单继承结构
是否所有的类都应该默认从⼀个基类继承呢?
单继承的结构使得垃圾收集器的实现更为容易。由于运⾏期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如异常处理。同时,这也让我们的编程具有更⼤的灵活性。
8、集合
通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储⽅式。那么我们如何知悉程序在运⾏时需要分配的内存空间呢?
在⾯向对象的设计中,问题的解决⽅案有些过于轻率:创建⼀个新类型的对象来引⽤、容纳其他的对象。
“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要⾃动扩容,我们不⽤关⼼过程是如何实现的。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论