一、对象导论:从面向过程到面向对象的转变

抽象过程

面向对象程序设计五个基本特性:

  1. 万物皆为对象
    面对具体某个问题时,首先考虑该问题涉及哪些事物或元素,并将其视为一种奇特的变量:不仅具有存储数据的功能,还具有一些特定行为。
  2. 每个对象都有由其他对象构成的存储
    简单的说,就是对象 a 中存在对象 b,这样的对象嵌套现象。因此,可以在程序中构建复杂的体系结构。
  3. 程序是对象的集合,它们通过发送消息来告知彼此应该做什么
    一个程序可以理解为一个问题的具体解决方案,方案中涉及到多个对象以及它们需要做的事情,因此发送消息即为在程序中调用某个对象的某个行为,以此来告知该对象要做什么。
  4. 每个对象都有其类型
    考虑多个对象,并且这些对象除了存储的数据不同外,都相同的情况,可以将这些对象归为一类对象,即某个类(class),区别于其他类的特征是:可以发送什么消息给他
  5. 某一特定类型的所有对象都可以接收同样的消息
    同一个类的对象,都具有该类的所有行为特征(方法),因此该类的所有对象,都具有该类所声明的所有行为(方法)。

在面对具体问题时,首先思考该问题涉及到哪些事物(元素),如何通过事物之间的行为达到解决问题的结果。

每个对象都有一个接口

这里的接口指的是每个类的对象所暴露在外面的接口,程序通过调用这些暴露在外的接口来请求对象做某个操作。

public class Light {
	public void on(){...}
	public void off(){...}
	public void brighten(){...}
	public void dim(){...}
}
Light lt = new Listh();
lt.on();//调用对象暴露在外的接口:on

每个对象都提供服务

将对象视作服务的提供者,有助于我们更好的以面向对象的角度思考解决问题的方案。

当你着手从事某个程序的开发时,不妨先思考一下,是否可以将问题中的事物抽象出来,并且抽象出来的事物(对象)是否存在某种行为能够解决当前问题或者当前问题的子问题。

另一个好处是可以提高对象的内聚性,通俗的说,就是通过定义多个不同的并且具有不同功能的独立对象来组合解决问题,而要避免将过多的功能硬塞在一个对象中。

在检查打印模式的模块中,你可以这样设计一个对象,让它了解所有的打印格式和打印技术。你可能会发现,这些功能对于一个对象来说太多了,你需要的是三个甚至更多的对象,其中,一个对象可以是所有可能的支票排版的目录,它可以被用来查询有关如何打印一张支票;另一个对象可以是一个通用的打印接口,它知道有关不同类型的打印机的信息;第三个对象通过调用另外两个对象的服务来完成打印任务。

上述中的每个对象各司其职,只做一项任务而不是试图包揽所有任务,这样做的好处是创建能够在别处复用的新对象(支票目录排版对象)

被隐藏的具体实现

在实际开发中,以一个大型项目为例,通常需要多个开发人员协作共同完成,因此,可能需要程序员 A 开发功能 A,程序员 B 开发功能 B,功能 B 中需要功能 A 的支持,此时,理想状态是程序员 A 完成了功能 A,程序员 B 在使用功能 A 的时候,不需要关心功能 A 是怎么实现的,而只需要知道怎么使用功能 A。

这便是访问控制,我们可以将程序开发人员分为两类:

  1. 类创建者
  2. 客户端程序员(开发中其他类的使用者)

客户端程序员在使用其他类时,不能够修改那些类内部结构,同样也看不到内部结构。例如人们在使用计算机时,并不需要了解计算机的内部结构,只需要知道如何使用即可,而一旦破坏其内部结构,必将导致无法使用。

访问控制不仅仅是为了让客户端程序员无法触及他们不该触及的部分,也同样给予类创建者一定的便利,即允许修改类内部结构而不影响到客户端程序员对类的使用

Java中提供了类的内部设定边界:publicprivateprotectedpublic是表示对外开放的,private表示只供类创建者和内部方法使用。protectedprivate一样,区别只在于它可以被继承的类使用。

复用具体实现

具有一定功能的类被创建后,它就代表一个有用的代码单元,考虑如何将其复用,即如何使用它,有两种情况:

  1. 组合
    将这个类作为新类的一个成员对象,例如创建了对象引擎,再次创建对象汽车,将引擎作为汽车的一个成员对象,从而汽车拥有了引擎的功能。
  2. 聚合,动态得发生组合

继承

当我们创建一个类后,即使另一个新类与它很相似,也还是需要重新创建一个类。而相似部分依旧需要重新实现,考虑是否存在一种机制,将多个相似类的共同特征集合在一起,作为基类,其他类作为基类的导出类,能够继承这些在基类中的共同特征。

以垃圾回收机制为例,回收的垃圾都具有一个性质:被遗弃不可用的东西,而垃圾中存在能被磁化的金属,塑料等具有特殊性质的垃圾,由此,可以通过继承来构建一个类型层次结构,从垃圾继承而来的金属垃圾,即拥有垃圾的特性,还有被磁化的特性。

再举个例子,游戏仿真系统中的多边形,都具有尺寸,位置,颜色等信息,同时还能被绘色,移动,擦除等操作。在多边形基础上,可以继承出三角形,四边形,五边形等特殊的多边形,每种继承出来的多边形除了拥有多边形的基础特性,还拥有各自的特殊性质,例如不同多边形计算面积的方式不一样等。

通常我们将多边形,垃圾这样的能衍生出其他类的称作基类或者父类。继承出来的有特殊性质的类称作子类。所以,如何让基类与子类产生差异:

  1. 直接在子类中新增方法。
  2. 覆盖父类中的方法,在子类中重写。

思考:继承体系中的关系是 “is a” 还是 “like a”?
①上述例子中的多边形与三角形的关系,即三角形是一个多边形,像这样父类与子类是完全相同的类型,可以称作 “is a” 的关系
②假设房子已经具备让你控制冷气设备的接口,当制冷机坏了,你替换了一台空调上去,这个时候,房子只具备制冷功能,而空调具备制冷和制热功能,因此,房子的制冷系统不能控制制热功能,因此,空调只能被称为像一个制冷机,而不能说是一个制冷机。

伴随多态的可互换对象

当我们创建了基类,并且通过基类衍生出不同的子类时,通常将一个类型当作特定子类来看待,但是有些时候我们会想把它当作基类来做一些所有子类都能做的事情。

例如:下图中BirdController处理的仅仅是泛化的Bird对象,而忽略该对象的具体类型。思考这其中是如何发生的呢?
Bird类继承体系

在这之前,先了解一下函数调用的不同机制:①前期绑定,②后期绑定;
①在非面向对象的语言中,编译器在编译的过程中,对调用的具体函数解析到其代码的绝对地址。
②在 OOP 中,编译器不能确定某个函数调用的具体代码位置,只有在发生调用时,才能确定函数要执行的代码。

再来看几何形状的例子:

//Shape是几何形状的基类
void doSomething(Shape shape){
	shape.erase();//所有几何形状都具有的行为
	....
	shape.draw();
}
Circle circle = new Circle();
Line line = new Line();
doSomething(circle);
doSomething(line);

doSomething接收的是一个几何类型,圆形,三角形等都是几何类型,因此,如果只看doSomething方法,我们不知道它调用几何类型的方法的具体代码是在圆形类型中还是三角形类型中,只有当我们真正调用时,给了它一个具体的对象,doSomething方法才能知道调用的函数代码的位置。

这么做是完全正确并且合乎逻辑的,这叫做对象的向上转型,向上的意思就是将它作为父类来看待,例如,将圆形当做几何类型来看,是正确的。

单根继承结构

Java中所有的类最终都继承自Object类。

这样做的好处是,从全局看,所有的对象都是Object类型,因此在系统层的一些操作会变得非常容易实现,例如,垃圾回收机制,在参数传递上不需要考虑具体类型。

容器

实际开发中,可能需要存储一定量的数据,并且我们并不知道这些数据会有多少,只有在运行时才能够确定,好的 OOP 语言都有一组容器,来解决这个问题。

C++ 的容器是标准 C++ 类库的一部分,Java 的标准类库中也存在大量的容器。例如List(存储序列),Map(建立一对一的关联),Set(每种类型只持有一个)。

如果某个单一容器能满足所有需要,那么为什么设计这么多不同的容器呢?
①不同的容器提供了不同类型的接口和行为,例如堆栈相比队列所具备的行为是不一样的,对于处理特定问题更加灵活。
②不同容器对不同的操作效率不同,例如 ArrayList 底层是数组,随机访问元素效率很高,而 LinkedList 底层是链表,随机取元素效率较低。

参数化类型
在所有类都继承自Object的前提下,考虑容器的复用性,我们可以将容器的存储对象设定为Object,这样一来容器就可以存储任何对象了。

思考:实际开发中,向容器中存储对象后,该对象向上转型Object类型,当我们需要从容器中获取对象时,我们如何知道Object类型的对象的具体类型呢?

从父类类型转换为特定子类类型被称为向下转型,向上转型是安全的,而向下转型是不安全的,因为我们事先可能不知道对象的具体类型。

因此 Java SE5 之后,为解决这个问题,引入了泛型,也就是将容器从一开始就指定存储对象的类型。

ArrayList<Shape> shapes = new ArrayList<Shape>();

对象的创建和生命周期

使用对象时,最关键的部分是对象的生成和销毁。每个对象在创建时都需要消耗资源,尤其是内存,我们需要在必要时创建对象,在合适的时候销毁对象。

简单的情况下,当我们使用完对象,就销毁它,但是当我们无法知道对象何时是不被使用时,就无从下手了,例如,我们创建了airPlane对象,主要功能是飞机起飞,但是当其他程序在监控飞机的状态时,则不能简单的直接在起飞后将飞机对象销毁。

因此,如何创建对象?如何销毁对象呢?

首先,对象的创建有两种方式:
堆栈中创建,例如 C++ 为了追求效率,在堆栈中或静态存储区域创建对象,这种情况下,编译器能够知道对象存活的时间,并销毁它,并且在堆栈中创建一个对象效率很高,只需一条汇编指令即可。
在被称为内存池中动态的创建对象,这种方式下,只有到程序运行时才知道创建多少对象,生命周期如何。由于存储空间是动态分配的,所以需要大量的时间用于内存分配。创建存储空间的时间依赖于存储机制的设计。

Java 完全采用了动态内存分配方式,当要创建对象时,使用new关键字来构建实例。

再来考虑一下如何销毁对象,也就是对象的生命周期,当对象被创建后,若存储在堆栈中,则编译器可以确定对象存活的时间,并自动销毁,而存储在堆上,编译器对对象的生命周期一无所知,因此 Java 提供了垃圾回收机制,来支持堆中动态创建的对象的自动销毁功能。

垃圾回收机制相比较 C++,可以有效避免内存泄漏的问题。

异常处理

我们将程序出错的状态看做一个对象,当出错时,将它从出错点抛出,并且设计相应的异常处理器来对抛出的异常进行捕获并做相应的操作。

异常处理和程序运行相当于是并行的,不会干扰程序的正常运行,而抛出的异常对象不能被忽略,因此它通常可以在某处被捕获得到正确的处理,并恢复程序的正常运行。

异常处理的机制使得能够写出更具健壮性的程序。

并发编程

计算机中,有时候需要终止当前程序,转而处理其他程序,这需要编写中断程序实现,基于 CPU 强大的计算能力,通过不停切换处理的任务,宏观上可以近似的看成同一时间执行同的任务。

对于处理时间性极强的任务,中断是必须的,而对于大量任务,我们只想将它切分成多个可独立执行的部分,从而提高程序响应能力。

在支持多处理器的操作系统中,语言级别上的多线程可以不需要考虑将任务分配给哪个处理器处理,而只需要将任务分成多个线程,如何运行交给操作系统即可。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值