1.抽象
面向过程的语言,它们所作的主要抽象仍要求在解决问题时要基于计算机结构,而不是基于所要解决的问题的结构来考虑。程序员还需建立起机器模型(如计算机)与实际待解决问题的模型之间的关联,耗时费力,难以维护。
而面向对象,只针对待解问题建模。把需求想象为一个空间,而对象则是里面的物件。
我们将问题空间中的元素及其在解空间中的表示称为“对象”。这种思想的实质是,程序可以通过添加新类型的对象使自身适用于某个特定问题。因此,当你在阅读描述解决方案的代码的同时,也是在阅读问题的表述。
Alan Klay总结了面向对象程序设计的五个特性,这些特性也表现了面向对象程序设计的方式:
1)万物皆为对象。理论上讲,你可以抽取待求解问题的任何概念化构件(狗,建筑物,服务等),将其表示为程序中的对象。
2)程序是对象的集合,它们通过发送消息来告之彼此所要做的。要想请求一个对象,就必须对该对象发送一条消息。更具体地说,可以把消息想象为对某个特定对象的方法的调用请求。
3)每个对象都有自己的由其他对象所构成的存储。换句话说,可以通过创建包含现有对象的包的方式来创建新类型的对象。(继承?)
4)每个对象都拥有其类型。每个对象都是某个类(class)的一个实例(instance)。
5)某一特定类型的所有对象都可以接收同样的消息。因为“圆形“类型的对象同时也是”几何形“类型的对象,所以一个”圆形“对象必定能够接受发送给”几何形“对象的消息。这意味着可以编写与”几何形“交互并自动处理所有与几何形性质相关的实物的代码。(这种可替代性是多态?)
2.每个对象都有一个接口
面向对象方法并不是仅局限于构建仿真程序。无论你是否赞成以下观点,即任何程序都是你所设计的系统的一种仿真,面向对象技术的应用确实可以将大量的问题很容易地降解为一个简单的解决方案。(这一段尚未明白)
事实上,面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。
必须有某种方式产生对对象的请求,使对象完成各种任务,这样才能获得有用的对象。这些请求由对象的接口(interface)所定义,决定接口的便是类型。
向某个对象”发送消息“(产生请求),这个对象便知道此消息的目的,然后执行对应的程序代码。
3.每个对象都提供服务
当试图开发或理解一个程序设计时,最好的方法之一就是将对象想象为”服务提供者“。程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。
将对象看作是服务提供者还有一个附带的好处:它有助于提高对象的内聚性。
4.被隐藏的具体实现
访问控制的第一个存在原因就是让客户端程序员无法触及他们不应该触及的部分----这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。
访问控制的第二个存在原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。
5.复用具体实现
新的类可以由任意数量,任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。因为是在使用现有的类合成新的类,所以这种概念被称为组合(Composition),如果组合是动态发生的,那么它通常被称为聚合(aggregation)。组合经常被视为"has-a"(拥有)关系。
实际上,在建立新类时,应该首先考虑组合,因为它更加简单灵活。如果采用这种方式,设计会变得更加清晰。
6.继承
当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private成员被隐藏了起来,并且不可访问),而且最重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对象。
有两种方法可以使基类与导出类产生差异。第一种方法非常直接:直接在导出类中添加新方法。但是,应该仔细考虑是否存在基类也需要这些额外方法的可能性。
第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖(overriding)那个方法。
如果继承只覆盖基类的方法(而并不添加在基类中没有的新方法),就意味着导出类和基类是完全相同的类型,因为它们具有完全相同的接口。结果可以用一个导出类对象完全替代一个基类对象。这可以被视为纯粹替代,通常称之为替代原则。我们经常将这种情况下的基类与导出类的关系称为is-a(是一个)关系。
如果在导出类中添加新的接口元素,这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法,这种情况我们可以描述为is-like-a(像是一个)关系。
7.伴随多态的可互换对象
在试图将导出类型的对象当作其泛化基类型对象来看待时(把圆形看作是几何形,把自行车看作是交通工具等等),仍然存在一个问题。如果某个方法要让泛化几何形状绘制自己,让泛化交通工具行驶,或者让泛化的鸟类移动,那么编译器在编译时是不可能知道应该执行哪一段代码的。
这就是关键所在:当发送这样的消息时,程序员并不想知道哪一段代码将被执行;绘图方法可以被等同地应用于圆形,正方形,三角形,而对象会依据自身的具体类型来执行恰当的代码。
如果不需要知道哪一段代码会被执行,那么当添加新的子类型时,不需要更改调用它的方法,它就能够执行不同的代码。(个人理解:使用父类对象引用接口方法,动态绑定会自己调用最适合的子类方法)。因此,编译器无法精确地了解哪一段代码将会被执行。
这个问题的答案,也是面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用(编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址)。在OOP中,程序直到运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制。
为了解决这个问题,面向对象程序设计语言使用了后期绑定(动态绑定)的概念。当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查(无法提供此类保证的语言被称为是弱类型的),但是并不知道将被执行的确切代码。
为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址(反射?)。
多态小Demo:
public class PolymorphismDemo { public static void doSomething(Person person){ person.talk(); } public static void main(String[] args) { Person person=new Person(); Fans dk=new Fans(); Spiderman peter=new Spiderman(); PolymorphismDemo.doSomething(dk); PolymorphismDemo.doSomething(peter); PolymorphismDemo.doSomething(person); }}class Person{ public void talk(){ System.out.println("nm"); }}class Fans extends Person{ @Override public void talk() { System.out.println("嘤嘤嘤"); }}class Spiderman extends Person{ @Override public void talk() { System.out.println("jiujiujiu"); }}
把将导出类看做是它的基类的过程称为向上转型(upcasting)。一个面向对象程序肯定会在某处包含向上转型,因为这正是将自己从必须知道确切类型中解放出来的关键。
8.单根继承结构
单根继承结构保证所有对象都具备某些功能。因此你知道,在你的系统中你可以在每个对象上执行某些基本操作。所有对象都可以很容易地在堆上创建,而参数传递也得到了极大的简化。
单根继承结构使垃圾回收器的实现变得容易得多。由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。
9.容器
通常来说,如果不知道在解决某个特定问题时需要多少个对象,或者它们将存活多久,那么就不可能知道为何存储这些对象。如何才能知道需要多少空间来创建这些对象呢?答案是你不可能知道,因为这类信息只有在运行时才能获得。(这一段尚未明白)。
对于面向对象设计中的大多数问题而言,这个问题似乎过于轻率:创建另一种对象类型。这种新的对象类型持有对其他对象的引用。这个通常被称为容器(也称为集合)。
在任何需要时都可扩充自己以容纳你置于其中的所有东西。因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。
从设计的观点来看,真正需要的只是一个可以被操作,从而解决问题的序列。如果单一类型的容器可以满足所有需要,那么就没有理由设计不同种类的序列了。然而还是需要对容器有所选择,这有两个原因:
第一,不同容器提供了不同类型的接口和行为。
第二,不同的容器对于某些操作具有不同的效率。
向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。那么创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这样不是更有意义吗?这种解决方案被称为参数化类型机制。
参数化类型就是一个编译器可以自动定制作用于特定类型上的类。
10.对象的创建和生命期
怎样才能知道何时销毁这些对象呢?当处理完某个对象之后,系统某个其他部分可能还在处理它。
1)对象的数据位于何处?怎样控制对象的生命周期?C++为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈(它们有时被称为自动变量(automatic variable)或限域变量(scoped variable))或静态存储区来实现。这种方式将存储空间分配和释放置于优先考虑的位置,但也牺牲了灵活性。
2)第二种方式是在被称为堆(heap)的内存池中动态地创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。这些问题的答案只能在程序运行时相关代码被执行到的那一刻才能确定。如果需要一个新对象,可以在需要的时刻直接在堆中创建。
因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能远远大于堆栈中创建存储空间的时间。在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指令即可,分别对应将栈顶指针向下移动和向上移动。创建堆存储空间的时间依赖于存储机制的设计。
动态方式有这样一个一般性的逻辑假设:对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。(这一段尚未明白)
Java完全采用了动态内存分配方式。每当想要创建新对象时,就要用new关键字来构建此对象的动态实例。
如果是在堆上创建对象,编译器就会对它的生命周期一无所知。Java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。
垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄露问题。
11.异常处理:处理错误
异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。
异常处理就像是与程序正常执行路径并行的,在错误发生时执行的另一条路径。
12.并发编程
对于大量的其他问题,我们只是向把问题切分成多个可独立运行的部分(任务),从而提供程序的相应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。
并发看起来相当简单,但有一个隐患:共享资源。因此,整个过程是:某个任务锁定某项资源,完成其任务,然后就释放资源锁,使其他任务可以使用这项资源。
13.Java与Internet(暂时跳过)