第一章 对象导论
一、抽象过程
所有编程语言都提供抽象机制,人们所能解决的问题的复杂性直接取决于抽象的类型和质量。
1.1 只针对问题来进行建模
早期:基于约束条件编程的语言和通过图形符号操作来实现的编程语言常常在领域更替后就变得寸步难行。
面向对象
面向对象的方式通过向程序员提供表示问题空间元素的工具。–对象
这种思想的实质就是,程序员可以添加新类型的对象来使自身适用于某个特定的问题
对象具有状态、行为、标识,每个对象都可以拥有内部数据和方法,并且每一个对象都可以唯一的与其他对象区分开,每个对象在内存中都有一个唯一的地址
二、每一个对象都有一个接口
尽管所有的对象都是唯一的,但同时也是具有相同特性(内部数据)和行为(方法)的对象所归属的类的一部分
2.1 银行出纳员问题
第一个面向对象语言:Simula
银行出纳员问题:
可以形成对象对象:出纳,客户账户,交易和货币单位
执行期间:具有不同状态,但是其他方面相似的对象会分组到对象的类中
**创建抽象数据类型(类)是面向对象程序设计的基本概念之一**
可以创建某一类型的变量(对象、实例),然后操作这些变量(告诉对象要做什么)
每个类的成员或者元素都具有某种共性:
每个账户:结余金额(元素)
每个出纳:都有处理存款方法
、、、
因此:出纳,客户、账户、交易都可以表示为一个实体,这个实体就叫对象,每个对象属于某个具有特定行为和元素的类
2.2 小总结
面向程序设计实际进行的就是创建一个新的类型:class
程序员通过创建不同的类,来适应问题,而不再被迫用表示机器存储单元的数据类型。
2.3 如何获取有用的对象?
有某种方式产生对对象的请求,使对象完成各种任务。
某个对象只能满足某些请求,这些请求由对象的接口所定义,决定接口的便是类型。
接口确定了对某一特定对象所能发出的请求。
但是,程序中必须有满足这些请求的代码,这些代码与隐藏的数据一同构成了实现,请求与方法关联,发起一个请求,一个关联的方法就会被调用。
三、每个对象都提供服务
试图开发或者理解一个程序设计,最好的方法之一就是将对象当作服务的提供者。
程序本身将向用户提供服务,他通过调用其他对象提供的服务来实现这一目的。
你的目标就是去创建能够提供理想服务的对象来解决问题。
3.1 将对象看作是服务提供者的好处:有利于提高对象的内聚性
这里的举例是一个打印模块
可以设计一个对象了解所有的格式和打印技术,但对这个对象来说会使他很臃肿。
也可以设计一个对象,是所有的可能的支票排版目录,用来查询有关如何打印一张支票的信息,另一个对象可以是通用的打印接口,他知道所有不同类型的打印机的信息,第三个对象通过调用另外两个对象的服务来完成打印工作。这样每个对象都有一个他所能提供服务内聚的集合。
在良好的面向对象程序设计中,每个对象都可以很好的完成一项任务,但他不试图做更多的事。
四、被隐藏的具体实现
将程序开发人员按照角色分为类创建者(创建新数据类型的程序员)和客户端程序员(哪些在其应用中使用数据类型的类消费者)是大有好处的。
类创建者的目标是构建类:他们构建的类,只向客户端程序员暴露必须的部分,而隐藏其他部分。
客户端程序员:收集各种用来实现快速应用开发的类。
4.1 访问控制存在的原因
第一存在原因:让客户端程序员无法触及他们不应该触及的部分。
第二存在原因:允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员
4.2 关键字
Java用三个关键字来在类的内部设定边界:public,private,protected
public:紧随其后的元素对任何人都是可用的
private:除类型创建者和类型的内部方法之外的任何人都不能访问的元素
protected:与private作用相当,差别在于继承该类的类可以访问protected成员
默认的访问权限:前面没有写任何指定词,同一个包中的其他类成员可以访问,但是包之外就像加了private一样,包内像public
五、复用具体体现
代码复用是面向对象程序设计语言所提供的最了不起的优点之一。
最简单的复用某个类的方式:直接使用该类的一个对象,新的类可以由任意数量,任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。
使用现有的类产生新的类,这种方式成为组合,如果组合是动态发生的,我们称之为聚合
组合带来极大的灵活性,新类的成员对象通常被声明为private,使得新类的客户端程序员不能访问他们,这也使得你可以在不干扰现有客户端代码的情况下,修改这些成员,也可以在运行时修改这些成员独享,以实现动态修改程序的行为。
在建造新类的时候,首先应该考虑的是组合而不是继承,他更加灵活,而且因为过度的使用继承来导致程序过度设计,继承应当合理的使用,而不是盲目地使用
六、继承(有点复杂,反复回味)
对象:本身就是十分方便的工具,使得我们可以通过概念将数据与功能封装在一起。由此可以对问题空间的概念给出恰当的表示,而不用被底层机器语言限制,这种概念由class关键字来表示,形成了编程语言的基本单位。
6.1 为什么需要继承?
如果只有对象的概念,不断地创建新的对象存在的缺点:容易创造出相似的类,比如两个类明明很相似,还需要反复创作新类。不如创作一个类为基础,复制他,通过添加和修改这个副本来创建新类,不过当源类被修改时,副本类也会有相应的变动
类型不仅仅知识描述作用于一个对象集合的约束条件,同时还有与其他类型之间的关系,两个类型之间可以存在相同的特性与行为,但其中一个比另一个有更多的特性,并且还可以处理更多消息。
继承使用基类和导出类的概念表示这种类型之间的相似性。一个基类包括所有导出类型所共享的特性和行为。
6.2 继承的举例
可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他的类型,来表示此核心可以被实现的各种不同的方式
一,垃圾分类的例子:虽然垃圾都成为垃圾,但是分类的不同垃圾有其不同的特性,垃圾只是他们的总称
6.3 类型的等价性
可以通过使用继承来构建一个类型层次结构,以此来表示待求解的某种类型的问题,类型的层次结构同时体现出不同的类的相似性和差异性。
当继承现有类型时,也就创造了新的类型,新的类型不仅包括了现有类型的所有成员,而且更重要的是他复制了基类的接口。其意思也可以理解为,所有发送给基类对象的消息同时也可以发送给导出类对象。
导出类复制了基类的方法
发送给基类的方法==》也可以发送给导出类
由:发送给类的消息的类型可以知道类的类型
==》基类类型 = 导出类类型
由于通过发送给类的消息的类型可知类的类型,这也就意味着导出类与基类具有相同的数据类型。通过继承而产生类型的等价性,是理解面向对象设计方法内涵的重要门槛。
6.4 基类与导出类产生差异
基类与导出类产生差异的方法:两种
一、直接在导出类中添加新方法
二、在导出类中覆盖基类中的方法
6.5 is-a(是一个) 、is-like-a(像是一个)
讨论:继承应该只覆盖基类的方法?
如果这样做,意味着导出类和基类是完全相同的类型,因为他们有完全相同接口,可以用一个导出类对象完全替代一个基类对象,这可以被视为纯粹替代,通常称之为替代原则。某些情况下,这是处理继承的理想方式。这种情况下的基类与导出类之间的关系称为 is-a关系,判断是否继承,就是要确定是否可以用is-a来描述类之间的关系。
有时必须在导出类型中添加新的接口元素,这样就拓展了接口,这个类型仍然可以替换基类,但这种替换是不完全的,因为基类无法访问新添加的方法,这种情况我们称之为is-like-a。
七、伴随多态的可互换对象
在处理类型的层次结构时,经常会把一个对象不当作他所属的特定类型来处理,而是将其当作基类的对象来对待。这种方式使得人们不依赖于特定类型的代码。
通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一
7.1 遇到的问题
但是在试图将导出类性的对象,当作其泛化基类型对象来看待时(圆形当作几何形处理,自行车当作交通工具),因为基类型有多个继承类,编译器不知道在编译时该执行哪一段代码。
7.2 问题该如何解决
上述问题的答案,也是面向对象程序设计的最重要的秘诀:
编译器不可能产生传统意义上的函数调用。
一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定,这么做意味着编译器将产生对一个具体的函数名字的调用,而运行时将这个调用解析到将来要被执行的代码的绝对地址。
但是在oop中,程序知道运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制
面向对象的程序设计语言:后期绑定
当向对象发送消息时,被调用的代码直到运行时才能确定。
编译器确保被调用的方法的存在,并对调用参数和返回值执行类型检查,但并不知道将被执行的确定的代码。
为了执行后期绑定,Java使用一小段特殊的代码来替换绝对地址调用,这段代码使用在对象中存储的信息来计算方法体的地址,根据这一小段代码的内容,每一个对象可以具有不同的行为表现。当对象发送消息时,该对象能够知道这条消息应该做些什么。
7.3 自己写的小案例(图片丢了,还是截书里的吧)
doSomething(Shape shape)方法可以与任何Shape对话,当Line被传入Shape中,由于Line和Circle都是Shape的字类,doSomething(Shape shape)可以发送给Shape的任何消息,Line都可以接收。
7.4 向上转型
向上转型:把导出类看作是它的基类的过程
向上:继承图中的典型布局,基类在顶部,导出类向下散开,所有由此而来
转型:模型铸造的塑膜动作
八、单根继承结构
8.1 问题:是否所有的类最终都继承自单一的基类
C++:no
Java:yes 均继承类Object
8.2 单根继承的优势
C++无法保证所有的对象都属于一个基本数据类型,从向后兼容的角度看,这么做可以更好地适应C模型,但进行完全的面向对象程序设计时,则必须构建自己的体系,使得C++享受不到面向对象语言的遍历,需要自己去设计,从而极大的降低了生产率。
单根继承结构的所有对象都具备某些功能。,可以在每个对象中执行一些基本操作,所有的对象都很容易在堆上创建,参数传递也得到了极大简化。
单根继承也使得垃圾回收器的实现更加容易,而垃圾回收器正是Java与C++的重大改进之一。由于所有的对象都保证具有其类型信息,因此不会因为无法确定对象的类型而陷入僵局,这对系统级别的操作,如异常处理显得尤其重要,并且给编程带来了更大的灵活性。
九、容器
9.1 容器概述
满足不同需要的各种类型的容器:List,Map,Set等
容器被设计得多样化的原因:
不同的容器提供了不同类型的接口和外部行为
不同的容器对于某些操作具有不同得效率
9.2 参数化类型
因为Java的单根继承,容器中存储对象,在存储时的复用性来源于所有的类都继承自object,但是容器不只有存储,还有取出,我们可以知道存储的过程是,某一类型向上转型为object,这是安全的,但是取出时是将某一object类型转换为具体类型,我们称之为向下转型,但向下转型就存在危险了,你无法确定需要将其转换为什么类型,向下转型如果转为错误的类型就会得到运行时错误,所以在取出集合时要利用某种方式记住这些对象是什么类型的。
这时就需要创建出一个容器,他知道自己所保存的对象的类型,这种方式就叫做参数化类型机制,参数化类型就是一个编译器可以自动定制作用域特定类型上的类。
Java 5 增加了参数化类型:泛型
十、对象的创建和生命期
在使用对象时间,最关键的问题之一是他们的生成和销毁方式,每个对象为了生存都需要资源,尤其是内存,所以在我们不需要一个对象的时候,它必须被清理掉,使其占有的资源可以被释放和重用。
10.1 问题一
对象的数据位于何处?怎样控制对象的生命周期?
C++的实现方式,他认为效率应该排在第一位,所以他给程序员提供了选择的权力。为了追求更大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈或静态存储区域内来实现。这种方式将存储空间分配和释放放置于优先考虑的位置。某些情况下很有价值,但在一些情况下显得太不灵活了,因为在编写程序时就要知道对象的确切的数量,生命周期和类型。
其他的实现方式:在称为堆的内存池中动态的创建对象,在这种方式中,直到运行时才知道需要多少对象,他们的生命周期如何,以及他们具体的类型是什么。这些问题只有在程序运行时,相关代码被执行到的时候才会确定,如果需要一个新的对象,可以在需要的时刻,直接在堆中创建。
存储空间在运行时被动态管理
需要大量的时间在堆中分配存储空间,远大于在堆栈中创建存储空间的时间,在堆栈中创建和释放存储空间通常各需要一条汇编指令就可以了,分别将对应的栈顶指针向下移动(我感觉是上)或向上(我感觉是下)移动,(有大佬看到可以帮我解释一下为什么吗?)创建存储空间的时间依赖于存储机制的设计。·
动态方式有一个一般性的逻辑假设:对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击,动态方式所带来的灵活性是解决一般编程问题的要点所在。
Java完全采用动态编程的方式,需要创建新的对象的时候就需要new新的动态实例。
10.2 销毁问题
那么还有一个问题:如果在堆上来创建对象,又将在什么时候将对象销毁呢?在堆栈上创建对象的语言,编译器可以确定对象的存活时间,并可以自动销毁他。
Java垃圾回收器:被设计用来处理内存释放问题,尽管他不包括清理对象的其他方面,垃圾回收器“知道”对象何时不再使用,并自动释放对象占用的内存。
10.3 Java、小Tips
两个特性:
所有对象都继承自单根基类Object
所有对象只能以一种方式创建对象,在堆上创建。
十一、异常处理:处理错误
Java内置了异常处理,而且强制你必须使用他
十二、并发编程
在计算机编程中有一个基本概念,就是同一时刻处理多个任务的思想。许多程序设计问题都要求,程序能够停下正在做的事情,转而处理其他的问题,然后再来处理主程序。
线程只是一种为单一处理器分配执行时间的手段。如果操作系统支持多处理器,那么每个任务都可以指派给不同的处理器,他们是真正的并行执行的。
隐患:共享资源,如果多个并行任务都要访问统一资源,就会出问题,比如公用打印机,一方用的时候就要将打印机锁定,不能同时用,整个过程:某个任务锁定某资源,完成任务,释放锁,使其他任务能使用这项资源
十三、Java与Internet
待办
十四、总结
待办