java学习笔记--对象导论
-
抽象过程
-
接口
-
服务
-
实现
-
复用
-
继承
-
多态
-
容器
-
对象创建
-
异常处理
-
并发编程
-
Java web
1.抽象过程
面向对象方式通过向程序员提供表示问题空间中的元素的工具而更进了一步。将问题空间中的元素及其在解空间中的表示称为“对象”。面向对象思想的实质是:程序可以通过添加新类型的对象使自身适用于某个特定问题。因此,当你在阅读描述解决方案的代码的同时,也是在阅读问题的表述。OOP允许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题。
特性:
1).万物皆为对象
2).程序是对象的集合,它们通过发送消息来告知彼此所要做的。
3).每个对象都有自己的由其他对象所构成的存储。
4).每个对象都拥有其类型
5).某一特定类型的所有对象都可以接收同样的消息
2.接口
所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。类描述了具有相同特性(数据元素)和行为(功能)的对象的集合。
面向对象程序设计中实际上进行的是创建新的数据类型,故,一个类就是一个数据类型,例如:所有浮点型数字具有相同的特性和行为集合。
程序员通过定义类来适应问题,而不再被迫只能使用现有的用来表示机器中的存储单元的数据类型(int,float,string,double,...)。可以根据需求,通过添加新的数据类型来扩展编程语言。
一旦类被创建,就可以随心所欲地创建类的任意个对象,然后去操作它们,就像他们存在于你的待求解问题中的元素一样。
面向对象程序设计的挑战之一:问题空间的元素和解空间的对象之间创建一对一的映射。
如何获取有用的对象:
必须有某种方式产生对对象的请求,使对象完成各种任务,如完成一笔交易,在屏幕上画图,打开开关等等。每个对象都只能满足某些请求,这些请求由对象的接口定义,决定接口的便是类型(一个类便是一个数据类型)。
接口确定了对某一特定对象所能发出的请求。但是,在程序中必须有满足这些请求的代码。这些代码与隐藏的数据一起构成了实现。
这个例子中,类的名称是Light,特定的Light对象的名称是lt,可以向Light对象发出的请求是:打开,关闭,调高亮度,调低亮度。
创建Light对象过程:
定义这个对象的“引用”(lt),然后调用new方法来创建该类型的新对象。为了向对象发送消息,需要声明对象的名称,并以圆点符号连接一个消息请求。从预定义类的用户观点来看,这些差不多就是用对象来进行设计的全部。
3.每个对象都提供服务
试图开发或理解一个程序设计时,最好的方法之一就是将对象想象为”服务提供者“。程序本身向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目的就是创建能够提供理想的服务来解决问题的一系列对象。
将对象看作是服务提供者还有一个附带的好处:它有助于提高对象的内聚性。高内聚是软件设计的基本质量要求之一:这意味这一个软件构件(例如一个对象,当然它也有可能是指一个方法或一个对象库)的各个方面组合得很好。设计对象时常面临的一个问题:将过多的功能都塞在一个对象中。
将对象作为服务提供者看待是一件伟大的简化工具,这不仅在设计过程中非常有用,而且其他人试图理解你的代码或重用某个对象时,如果他们看出了这个对象所能提供的服务的价值,它会使调整对象以适应其设计的过程变得简单得多。
4.实现
将程序开发人员按照角色分为类创建者(那些创建新数据类型得程序员)和客户端程序员(那些在应用中使用数据类型的类消费者)。客户端程序员的目标是收集各种用来实现快速应用开发的类。类创建者的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其他部分。
隐藏的原因:
如果加以隐藏,那么客户端程序员将不能够访问它,这意味着类创建者可以任意修改被隐藏的部分,而不用担心对其他人造成影响。被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心或不知内情的客户端程序员所毁坏,因此将实现隐藏起来可以减少程序bug。
在任何相互关系中,具有关系所涉及的各方都遵守的边界是十分重要的事情。当创建一个类库时,就建立了与客户端程序员之间的关系,他们同样也是程序员,但是他们是使用你的类库来构建应用,或者构建更大的类库的程序员。如果所有的类成员对任何人都是可用的,那么客户端程序员就可以对类做任何事情,而不受任何约束。即使你希望客户端程序员不要直接操作你的类中的某些成员,但是如果没有任何访问控制,将无法阻止此事发生。所有东西都将赤裸裸地暴露于世人面前。
访问控制存在的原因:
1).让客户端程序员无法触及他们不应该触及的部分--这些部分对数据类型的内部操作来说是必需的,但并不是用户待解决特定问题所需的接口的一部分。这对客户端程序员来说其实是一项服务,因为他们可以很容易地看出哪些东西对他们来说很重要,而哪些东西可以忽略。
2).允许库设计者可以改变内部的工作方式而不用担心会影响到客户端程序员。例如:可以为了减轻开发任务而以某种简单的方式实现了某个特定类,但稍后你会发现必须修改它才能使其运行得更快。如果接口和实现可以清晰地分离并得以保护,那你就可以轻而易举地完成这项工作。
Java用三个关键字在类的内部设定边界:public,private,protected。这些访问指定词(access specifier)决定了紧跟其后被定义的东西可以被谁使用。public表示紧跟其后的元素对任何人都是可用的,而private关键字表示除了类创建者和类内部方法以外的任何人都不能访问的元素。protected关键字与作用相当,差别仅在于继承的类可以访问protected成员,但是不能访问private成员。java还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,他将发挥作用。这种权限通常称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构建)中的其他类的成员,但是在包之外,这些成员如果指定了private一样。
5.复用具体实现
一旦类被创建并被测试完,那么它就应该代表一个有用的代码单元。但这种复用性并不容易达到我们所希望的那种程度,产生一个可复用的对象设计需要丰富的经验和敏锐的洞察力。但一旦有了这样的设计,它就可供复用。代码复用时面向对象程序设计语言所提供的最了不起的优点之一。
最简单的复用:直接使用该类的一个对象,也可以将该对象置于某个新的类中。称其为“创建一个成员对象”。
组合的概念:新的类可以由任意数量、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成,因为是在使用现有的类合成新的类。如果组合是动态的,通常称为聚合。组合的关系:"has a"(拥有)
组合具有极大的灵活性。新类的成员对象通常都被声明为private,使得使用新类的客户端程序员不能访问它们。这也使得你可以在不干扰现有客户端代码的情况下,修改这些成员。也可以在运行时修改这些成员对象,以实现动态修改程序的行为。
6.继承
编译器必须对通过继承而创建的类施加编译时的限制。若处处使用继承,将会导致难以使用并过分复杂的设计。实际上,在建立新类时,应该首先考虑组合,因为它更加简单灵活。采用组合,设计会变得更加清晰。
对象这种概念,本身就是十分方便的工具,使得可以通过概念将数据和功能封装到一起。因此可以对问题空间的概念给出恰当的表示,而不用受制于必须使用底层机器语言。这些概念用关键字class来表示,它们形成了编程语言中的基本单位。
存在的问题:
在创建一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。如果能够以现有的类为基础,复制它,然后通过添加和修改这个副本来创建新类那就要好多了。通过继承便可达到这样的效果,但也有例外,当源类(基类,超类或父类)发生变动时,被修改的“副本”(被称为导出类、继承类或子类)也会反映出这些变动。
类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。两个类型可以有相同的特性和行为,但是一个类可能比另一个类含有更多的特性,并且可以处理更多的消息(或以不同的方式来处理消息)。
继承使用基类型和导出类型的概念表示了这种类型之间的相似性。
一个基类型包含其所有导出类所共享的特性和行为。可以创建一个基类性来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。
当继承现有类时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private成员被隐藏起来了,并且不可访问),更重要的是,它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对象。由于通过发送给类的消息的类型可知类的类型,所以也就意味着导出类与基类具有相同的类型。通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛。
由于基类和导出类具有相同的基础接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有某些代码去执行。如果只是简单的继承一个类,并不做其他任何事,那么在基类接口中的方法将会直接继承到导出类中。这意味着导出类的对象不仅与基类拥有相同的类型,而且还拥有相同的行为,这样做毫无意义。
两种方法使基类和导出类产生差异:
1).直接在导出类中添加新方法。这些新方法并不是基类接口中的一部分。这意味着基类不能直接满足你的所有需求,因此必须添加更多的方法。这种简单的继承方式,有时对问题确实是很好的解决方法。但是需要考虑基类是否也需要这些额外方法的可能性。
2).改变现有基类方法的行为,这称之为覆盖(overriding)
要想覆盖某个方法,可以直接在导出类中创建方法的新定义即可。可以说:“此时,我正在使用相同的接口方法,但我想在新类型中做些不同的事”
is-a(是一个) 和is-like-a(像是一个)
is-a(是一个):继承只覆盖基类的方法,不添加在基类中没有的方法,结果可以用导出类对象完全替代基类对象,也叫纯粹替代。
is-like-a(像是一个):在导出类中添加新的接口元素,扩展了接口。这个新的类仍然可以替代基类,但这种替代不完美,因为基类无法访问新添加的方法。新类型具有旧类型的接口,但它还包含其他方法,所以不能说它们完全相同。
替代原则包括纯粹替代和添加新接口方法替代
7.多态
前期绑定:编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。
后期绑定:OOP中,程序直到运行时才能确定代码的地址,当消息发送到一个泛化对象时。必须采用其他的机制,这个机制就是后期绑定。当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查,但是并不知道将被执行的确切代码。
为了执行后期绑定,java使用一小段特殊的代码来代替绝对地址的调用。这段代码使用在对象中存储的信息来计算方法体的地址。根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道这条消息应该做些什么。
在处理类型的层次结构时,经常把一个对象不当作它所属的特定类型来对待,而是将其当作基类的对象来对待。这使得人们可以编写出不依赖特定类型的代码。
这样的代码不受添加新类型的影响,而且添加新类型时扩展一个面向对象程序以便处理新情况的最常用的方式。通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。
这样扩展的一个问题是:如果某个方法要让泛化几何形状绘制自己,那么编译器在编译时是不可能知道应该执行哪一段代码的。关键:当发送消息时,程序员并不想知道哪一段代码将被执行;而对象会根据自己的具体类型来执行恰当的代码,这个实现的方法就是后期绑定。
什么是多态:
doSomething方法只和基类交互,这段代码和具体类型信息是分离的(decoupled)。如果通过继承机制新添加一个新类型,如Hexagon(六边形),所编写的代码对Shape的新类型的处理与对已有类型的处理会同样出色。正因为如此,称这个程序是可扩展的。这个方法可以与任何shape对话,因为它是独立于任何它要绘制和擦除的对象的具体类型的。如果程序中其他部分用到了doSomething()方法,对doSomething()的调用会自动地正确处理,而不管对象的确切类型。
向上转型:将导出类看作是它的基类的过程称为向上转型(upcasting)。一个面向对象程序肯定会在某处包含向上转型,这正是将自己从必须知道确切类型中解放出来的关键。
当java编译器在编译doSomething()的代码时,并不能确切知道doSomething()要处理的确切类型。所以通常期望它的编译结果是调用基类Shape的erase()和draw()版本,而不是具体的Circle、Square或Line的相应版本。正是因为堕胎才使得事情总是能够被正确处理。
8.单根继承
java使用单根继承,也就是一个类只能继承一个类,但基类是否继承了其他类则不在考虑范围内。这和C++允许继承多个类有显著区别,这样做的好处是:
1).单根继承保证所有对象都具备某些功能。所有对象都可以很容易地在堆上创建,而参数传递也得到极大的简化。
2).单根继承使垃圾回收器的实现变得容易很多,而垃圾回收机制正是Java相对c++的重要改进之一。由于所有对象都保证具有基类信息,因此不会因无法确定对象的类型而陷入僵局。
9.容器
如果不知道在解决某个特定问题时需要多少个对象,或者它们讲存活多久,那么就不可能知道如何存储这些对象。如何知道需要多少空间来创建这些对象?答案是不知道,那么如何解决这些问题?
对于OOP,解决方案是:创建另一种对象类型。这些新的对象类型具有对其他对象的引用。这个对象类型可以是大多数语言都有的数组类型,但在java中称为容器(也叫集合),容器的特点是:在任何需要时都可以扩充自己以容纳你置于其中的所有东西。这样就可以不需要知道将来会有多少个对象会置于容器中,只需要创建一个容器就可以了。
Java中的容器包括:List(用于存储序列),Map(也被称为关联数组,用来建立对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列,树,堆栈等更多的构件。
为什么设计这么多容器,如果单一容器就可以解决所有问题,那就不需要设计不容种类的序列了?
1).不同容器提供了不同类型的接口和外部行为。它们之中的某种容器提供的解决方案可能比其他容器要灵活得多。
2).不同的容器对于某些操作具有不同的效率。例子:ArrayList和LinkedList都是具有相同的接口和外部行为的简单的序列,但是它们对某些操作所花费的代价却有很大区别。在ArrayList中,随机访问元素是一个花费固定时间的操作;但是,对LinkedList来说,随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素,话费的时间越长。另一方面,如果想在序列中间插入一个元素,LinkedList的开销却比ArrayList要小。
参数化类型
单根继承结构意味着所有东西都是Object类型,所以可以存储Object的容器可以存储任何东西,这使得容器很容易被复用。使用这样的容器,只需要在其中置入对象引用,,稍后还可以将它们取回。但是由于容器只存储Object,所以当将对象引用置入容器时,它必须被向上转型为Object,因此它会丢失其身份。当把它取回时,就获取了一个对Object对象的引用,而不是对置入时的那个类型的对象的引用。带来的问题是,怎样才能把它变回先前置入容器中时的具有实用接口的对象呢?
解决方法就是向下转型,向下转型为更具体的类型。向上转型是安全的,例如Circle是一种Shape类型,但无法确定某个Object是Circle还是Shape,所以除非确切知道所要处理的对象的类型,否则向下转型是不安全的。
要让向下转型安全,解决方案就是以某种方式记住这些对象究竟是什么类型。
这种方式就是参数化类型机制:
参数化类型就是一个编译器可以自动定制作用于特定类型上的类。例如:通过使用参数化类型,编译器可以定制一个只接纳和取出Shape对象的容器(说白了就是只存储相同父类的不同子类)
参数化类型在java中又称为泛型,用一对尖括号中间包含类型信息。如下图所示:
10.对象创建和生命期
使用对象最关键的问题之一:生成和销毁方式
java完全采用动态内存分配方式。每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。
对象生命周期:
对于允许在堆栈上创建对象的语言,编辑器可以确定对象存活的时间,并可以自动销毁它。然而,如果是在堆上创建对象,编译器就会对它的生命周期一无所知。C++中,必须通过编程方式来确定何时销毁对象,这可能会因为不能正确处理而导致内存泄露。Java提供了被称为“垃圾回收”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。
11.异常处理
对于编程语言,错误处理始终是最困难的问题之一。设计一个良好的错误处理机制非常困难,所以许多语言直接略去这个问题,将其交给程序库设计者处理,而这些设计者也只是提出一些不彻底的方法,这些方法可用于许多很容易就可以绕过此问题的场合,而且其解决方式通常也只是忽略此问题。大多数错误处理机制的主要问题在于,它们都依赖于程序员自身的警惕性,这种警惕性来源于一种共同的约定,而不是编程语言所强制的。
异常处理讲错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的执行代码。异常不能被忽略,所以它保证一定会在某处得到处理。
异常提供了一种从错误状况进行可靠恢复的途径。这样,就不再是只能退出程序,还可以经常进行校正,并恢复程序的执行,这些都有助于编写出更健壮的程序。
java一开始就内置了异常处理,而且强制必须使用它。它是唯一可接受的错误报告方式。
12.并发编程
同一时刻处理多个任务。要求程序能够停下正在做的工作,转而处理某个其他问题,然后再返回主进程。最初,可行的方式是使用硬件中断服务程序,但这样做难度比较大,且程序不能移植。
这种中断正在处理的工作,转而去做其他的工作,然后再返回主线程的方式,并不是很好的解决方法。现在只想把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力,从而提高程序的相应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为"并发"。例子:用户界面,通过使用任务,用户可以在按下按钮后快速得到一个响应,而不用被迫等待到程序完成当前任务为止。
通常线程只是一种为单一处理器分配执行时间的手段。但是如果操作系统支持多处理器,那么每个任务都可以被指派给不同的处理器,并且它们是在真正地被并行执行。
并行处理的隐患:
共享资源,如果有多个并行任务需要访问同一项资源,就会出问题,解决方案:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其它任务可以使用这项资源。
13.Java web