JavaSE基础知识(五)--面向对象代码实现基础(从待求解问题到代码过度)

本文内容参考自Java8标准

一、面向对象(面向对象代码组织方式概谈):
我不知道我前面的博文是否写的通俗易懂,如果都看过了,那么在这里,我决定深入研习一本书,既然是Java语言,目前,值得研习的,我又熟悉的,只有一本–
个人觉得,Java编程思想[第四版]真正做到了深入浅出,而且它章节的编排顺序非常适合一个初学者学习,当然,是需要有一定计算机基础的初学者!
从第一章的第一句话开始:
我们之所以将自然界分解,组织成各种概念,并按其主要含义分类,主要是因为我们是整个口语交流社会共同遵守的协定的参与者,这个协定以语言的形式固定下来,除非赞成这个协定中规定的有关语言信息的组织和分类,否则我们根本无法交谈。
上面开篇这句话是什么意思呢?通俗的解释就是:我们人将自然界的所有物体按它们的主要含义都归好了类,比如,什么样的物体是牛!如果按种类分,牛里面又分奶牛,牦牛,水牛等,如果按性别分,牛里面又分公牛和母牛。其中,牛、奶牛、牦牛、水牛、这样的分类就是协定。并且,这些协定应该得到所有人的认同(我们一旦认同,实际上就是协定的参与者了)。同时,这些协定以语言也可以说是文字的形式保存了下来,我们用它们进行交流就是在统一的协定下交流,就不会出现当我说前面一头牛在吃草,你一定要跟我争那是马—如果真的是这样,确实无法交谈。
实际上,以上内容还能用更精辟的两个字进行概括:常识!
如果你被质问:你能不能有一点常识?这个常识就是所谓的协定!也就是说,你如果缺乏常识,就是不了解某一方面的协定了!
计算机革命起源于机器,编程语言的产生也始于对机器的模仿–上面这句话个人觉得非常重要,因为,这是一个源头,一九四几年到一九五几年的硬件和目前的硬件是没法比的,所以在一九四几年的时候,编程语言刚开始产生时,只能尽量去模仿机器(也就是无法脱离硬件性能的瓶颈),那个时候的编程语言几乎都是底层的机器指令、晦涩难懂。从一九五几年到一九九几年,经过好几代人的努力,计算机在摩尔定律的引导下,性能突飞猛进,逐渐成为了人们头脑延伸的工具,同时还是一种不同类型的表达媒体(通过思考以及尝试,计算机能做的事情越来越多,比如写作,绘画,雕刻,动画,电影等),因此,它看起来越来越不像机器,而是越来越接近我们头脑的一部分,在这几十年的时间内,面向对象程序设计,便成为了这种以计算机作为表达媒体的大趋势中的一个重要组成部分。
面向对象对面是编程语言发展过程的必然趋势–其实这种趋势也很好理解,因为之前都是机器指令,在用这些指令进行编程实现各种不同功能的时候,往往会发现这些功能实际上都会包含某些相同的指令,但是不得不每次都重复地写一遍,这种事情对于大神级别的程序员根本无法容忍,于是乎,一个大胆的想法就会萌生:是不是每次实现一个功能,都必须从最最最底层的指令开始,还是能从更高层开始,一九五几年的时候,这简直是异想天开,但是在新千年初,俨然变成了一个切实可行的事情。于是,通过不断的尝试,最终从过程式编程演变到现在的面向对象编程。
如果觉得上面的内容难懂,那就看这里,山顶洞人时期[对应一九五几年],如果一个山顶洞人需要一把锤子,可能你会说,山顶洞人可能还不知道什么是锤子,你这就是在跟我抬杠了,我其实就是举例,他会怎么办呢?他可能需要去找一块铁矿石,经过各种加工变成铁,然后再找一根合适的木头,将它们组合在一起,就行了,那么现在,你需要一把锤子,你会怎么办,你会直接去五金店里买一把,这几乎可以认为是拿来即用。过程式编程[计算机指令集编程]就类似于山顶洞人制造锤子的过程,你需要熟悉其中的每一道加工工序[对应于你必须了解每一条指令],这样的效率是极其低下的,而面向对象编程就类似于你去买锤子,拿来即用的过程,你知道你需要什么,直接拿来用,至于它怎么实现的,你不需要知道,也就是说,如果你需要使用一个对象,首先的想法不是去实现这个对象的类类型代码,而是需要去找是否已经有这个对象的类类型代码,然后直接用它来创建这个类型的对象。– 除非你是类类型代码的创建者,你需要知道怎么去构建一个类类型的代码,但是这个类类型,你也只需要构建一次,以后都是直接就能拿来创建对象的。已经完全告别了每次都需要从底层机器指令开始的年代。
幸运的是,目前已经存在的类类型代码已经能满足百分之99的编程需要,就算你要再去创建新的类类型代码,也无需了解底层的硬件机制,而是在Java大量的基础类类型代码之上去选择进行组合而已,说到底,其实与拿来直接使用没有根本性的区别–因为现在你在创建类类型代码的时候,往往考虑的是用什么现成的类类型组合更合适,换一句话说,现在的编程趋势总是在现有的类类型上去产生新的类类型。
还有一个重要的点:之前是使用机器指令,而现在是使用英文字母或单词编程,这是不是与面向对象有关,个人觉得有关,但是关系不紧密,仅仅是为了提高编程效率,普及编程语言而已,因为机器指令实在是晦涩难懂,普及难度极高,所以现在所有的编程语言都会有一套完整的语法,为什么需要有语法, 因为你不能瞎写。这些语法都是一些固定的句式,你按照这些句式来编程,这样特定的语言才能将你的最终代码再转化成机器指令去执行,所以你了解了,机器最终执行的永远都是机器指令,而不是你现在看上去非常熟悉的英文单词。最最最重要的一点,英文单词或者说高级语言编程能让你更切实地感受到面向对象编程思想,比如你能用关键字"class" 和house(翻译成中文是"房子")声明一个房子类类型



你可以试试用java的反编译功能,看一下它反编译后的机器指令是什么,给你十分钟,你能看懂,说明你对机器指令编程是有一定天赋的。
但是class house{ },你可能在0.01秒以内就明白。所以为什么称Java是高级编程语言,这个高级只是因为大多数人能在短时间内学会,以及能看懂而已。从计算机运行效率以及性能来讲,实际上不怎么高级。
如果你理解了上面的总体概述,那我们可以继续,以下内容分了步骤:
⑴、抽象过程
所有的编程语言都提供抽象机制(Java中用变量表示状态,用方法表示行为),可以这么说,用编程语言去解决问题的复杂性完全取决于你抽象出的类型和质量(类型中应该包含那些变量?应该包含哪些方法?),所谓的类型是指:所抽象的是什么(这里需要分清楚,是所抽象的是什么,而不是抽象以后看上去像什么,代码的抽象应该与实际需求紧密结合)。
汇编语言是对底层机器的轻微抽象,接着出现的许多命令式的语言,比如FORTRAN,BASIC,C等,是对汇编语言的抽象,它们在汇编语言的基础上有了很大的改进,但是它们所作的抽象仍要求在解决问题的时候基于计算机硬件的结构(也就是说,不论解决什么问题,都必须先考虑计算机硬件结构是如何运行的,有什么局限性等,然后以它为基础去组织代码),程序员必须建立起机器模型(“解空间”,就是计算机,这是你对问题建模的地方)和待解决问题模型(“问题空间”,比如一项业务,这是问题存在的地方,)之间的关联,建立这种关联是极其费劲的,同时,这也不是编程语言所固有的功能(在这种情况下,没有任何一种编程语言能脱颖而出,因为根本的局限性在于硬件,硬件没突破,什么编程语言都不好使)。所以导致程序难以编写,而且维护的代价及其高昂。
另一种解决方式是只针对特定的待解决问题建模(这也就意味着计算机这个解空间是固定的!),如LISP和APL,认为所有问题最终都是列表或者所有问题最终都是算法形式,PROLOG认为所有问题最终都是决策链,此外还产生了基于约束条件的编程语言和专门通过对图形符号的操作来实现编程的语言(专门通过操作图形符号的编程语言在实践中被证明限制性过强),遗憾的是,这些编程语言对于他们所要解决的特定问题是非常不错的解决方案,但是一旦超出其特定领域,他们就显得力不从心了。
面向对象的方式通过对程序员提供更多的表示问题空间中元素的工具而更进一步,这种表示方式非常通用,使得程序员不会受限于任何特定类型的问题。我们将问题空间元素在解空间的表示称为"对象"(实际上,你可能还会需要一些无法类比为问题空间元素的对象–因为总会需要一些辅助)。这种思想的实质是:程序可以通过添加新类型的对象使其自身适用于某个特定的问题,因此,当你在阅读描述解决方案的代码的同时,也是在阅读问题的描述,相比于以前基于硬件的编程语言,是一种更灵活和更强有力的语言抽象,所以,面向对象语言是根据问题来描述问题,而不再是根据运行解决方案的计算机来描述问题(实际上仍然与计算机有关系),每个对象都具有状态,还具有操作,如果类比现实世界中的对象,那么说他们都具有特性和行为是非常合适的。
从过程型编程语言到面向对象型编程语言过度的重点就是,提供了更多的表述问题空间中的元素!!从而逐渐脱离了计算机思维,转向面向对象思维!
Alan Kay曾经总结了第一个成功的面向对象语言,同时也是Java所基于的语言之一—SmallTalk的五个基本特性,这些特性表示了一种纯粹的面向对象的设计方式。
①、万物皆为对象:
将对象视为神奇的变量,它可以存储数据(变量),它还可以要求它在自身上执行操作(方法),理论上讲,你可以抽取待求解问题的任何概念化构件(狗,建筑物,服务等,将其表示为程序中的对象)
②、程序是对象的集合,他们通过发送消息来告诉彼此所要做的:要想请求一个对象,即必须对该对象发送一条消息。更具体的说,可以把消息想象为对某个特定对象的方法的调用请求。
③、每个对象都有自己的由其他对象构成的存储:换句话说,可以通过创建包含现有对象的包的方式来创建新类型的对象,因此,可以在程序中构建复杂的体系,同时将其复杂性隐藏在对象的简单性背后。
④、每个对象都拥有其类型:按照通用的说法,每个对象都是某个类的一个实例(你创建的类类型代码的一个实例),这里,类就是"类型"的同义词,每个类最重要的区别于其他类的特性就是"可以发送什么样的消息给它(也就这个类型都有什么方法。)"?
⑤、某一特定类型的所有对象都可以接收同样的消息:这是一句意味深长的表述,因为"圆形"类型的对象同时也是"几何类型"的对象,所以一个"圆形"类型的对象必定能够接受发送给"几何类形"对象的消息,这意味着可以编写与"几何类型"交互并自动处理所有与几何类型性质相关的事物代码,这种"可替代性"是OOP中最强有力的概念之一(这里表述的就是后面最实用的"接口"概念,创建一个接口类型,并编写与这个接口类型交互的代码,如果需要新增其他类型的对象,直接实现这个接口再创建对象就行。那么,之前编写的与接口交互的代码,同样也对这个对象适用!)。
还有另外一个对对象更精准的表述:对象具有状态、行为、标识。这意味着每一个对象可以拥有内部数据(状态),方法(行为),并且每一个对象都可以唯一的与其他对象区分开来,具体来说,就是每一个对象在内存中都有一个唯一的地址(一旦创建了一个对象,那么这个对象就会自动在堆中分配到一个唯一的内存地址,所以同一个类型可以创建多个对象,那么就有多个内存地址,这个内存地址是存储在堆栈中,使用对象的时候,先通过你在程序中声明的变量名称找到堆栈存储区,再读取这个存储区存储的地址值,继续通过这个地址值去堆中找实际的对象,再执行最终的代码[状态或者行为])。
⑵、每个对象都有一个接口
亚里士多德大概是第一个深入研究类型的哲学家,他曾提出过"鱼类"和"鸟类"这样的概念。所有的对象都是唯一的(就算是双胞胎,看起来几乎一样,但是从面向对象角度出发,他们都是唯一的人类对象),但同时也是具有相同的特性和行为的对象所归属的类的一部分,这种思想被直接应用于第一个面向对象语言Simula-67,它在程序中使用基本关键字"class"来引入新的类型。
Simula,诸如其名字一样,是为了开发诸如经典的银行出纳员问题:在银行出纳员问题中,有"出纳",“客户”,“账户”,"交易"和"货币单位"等许多对象,在程序执行期间,具有不同的状态而其他方面都相似的对象会被分组到对象的类中,这就是关键字"class"的由来,创建抽象数据类型"class"是面向对象程序设计的基本概念之一,抽象数据类型的运行方式与内置类型(八种基本类型)几乎一致,你可以创建某一类型的变量(按照面向对象的说法,称其为对象或者实例),然后操作这些变量(称其为发送消息或请求,发送消息,对象就知道要做什么。),每个类的成员或元素都具有某种共性:每个账户都有余额,每个出纳都能处理存款请求等,同时,每个成员都有其自身的状态:每个账户都有不同的结余金额,每个出纳都有自己的姓名,因此,“出纳”、“客户”、“账户”、"交易"等都可以在计算机程序中被表示成唯一的实体。这些实体就是对象。每一个对象都属于定义了特性和行为的某个特定的类。
实际上,我们在面向对象程序设计中进行的就是创建新的数据类型(用"class"关键字在代码中表示新的类型)。
因为类描述了具有相同特性(数据元素)和行为(功能)的对象集合。所以,一个类实际上就是一个数据类型,例如:所有的浮点数类型都具有相同的特性和行为。
说到这里,可以总结一下面向对象编程的先进以及适应性:程序员可以通过定义新的类型来适应问题,而不再被迫只能使用现有的用来表示机器中存储单元的数据类型(针对Java,就是八种基本数据类型)。实际上,你正在通过添加新的数据类型扩展编程语言,同时,编程系统也欣然接受新的类,并且像对待内置类型一样照管它们并进行类型检查!
一旦类被建立(特性或行为都已经确定),就可以随心所欲地创建类的任意个对象,然后去操作它们,就像它们是存在于你的待求解问题中的元素一样,事实上,面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。
创建了对象之后,怎样使用对象呢?必须有某种方式产生对对象的请求,使对象完成各种任务,如完成一笔交易,在屏幕上画图,打开开关等等。每个对象都只能满足某些请求,这些请求由对象的接口(interface)所定义,决定接口的便是类型。以电灯泡为例来做一个简单的比喻:

 JavaSE基础知识(五)--面向对象代码实现基础(从待求解问题到代码过度)_第1张图片

 

类型    Light
接口    on() off() brighten() dim()
创建对象    Light it = new Light()
操纵对象/给对象发送请求    it.on()
接口确定了对某一特定对象所能发出的请求,在程序中,必须有满足这些请求的代码(这里的代码指的是方法实现的代码,比如on()接口中的具体代码),这些代码与隐藏的数据一起构成了实现(隐藏的数据指的是权限为private的变量,还记得我在上篇博文中提到,方法实际上就是不断地在改变对象的状态,也就是在不断地改变变量的值),从过程型编程的观点来看,这并不复杂,在类型中,每一个可能的请求都有一个方法与之相关联,当向对象发送请求时,与之相关联的方法就会被调用,此过程通常被概括为:向某个对象"发送消息"(产生请求),这个对象便知道此消息的目的,然后执行对应程序的代码。
回到上面的例子Light,类型/类的名称是Light,创造出的特定对象的名称是it(在这里,继续反复强调一下:it是堆栈存储区域的名称,它存储的是这个特定对象在堆上的存储地址值。),可以向it对象发送的请求由它的接口决定:分别是打开它(it.on()),关闭它(it.off()),将它调亮(it.brighten()),将它调暗(it.dim())。
你以下列方式创建了一个对象:
Light It = new Light();
定义这个对象的引用:it–一定要明白它的实际意义。
然后用new关键字以及Light类型的构造方法来创建该类型的新对象。
为了向对象发送消息,需要声明对象的名称(引用):it;
并以圆点符号连接一个消息请求:it.on();
以上内容几乎就是用对象来进行设计的全部。
⑶、每个对象都提供服务:
当正在试图开发或理解一个程序设计时,最好的方法之一就是将对象想象为"服务提供者",程序本身将向用户提供服务,它将调用其他对象提供的服务来实现这一目的(我在上文中提到过,当你在创建新类型时,首先考虑现有对象是否能满足需要,如果不行,再考虑从最底层开始),你的目标就是去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。
举个例子:首先你必须问一下自己,如果可以将问题从表象中抽取出来,那么,什么样的对象可以立马解决这个问题?
假设你正在创建一个簿记系统,系统应该具有某些包括了预定义的簿记输入屏幕的对象,一个执行簿记计算的对象集合,以及一个处理在不同的打印机上打印支票和开发票的对象,也许上述对象中的某些已经存在了,但是对于那些并不存在的对象,他们看起来应该是什么样子?他们能够提供哪些服务?他们需要哪些其他的对象才能提供他们的服务?如果持续这样做,那么最终你会发现,实际上,这个对象看起来很简单,只不过是多个已存在对象的组合而已。到这里,你可以坐下来写代码了,以上是将在实际的软件设计中,针对以上内容,还有一个标准,就是软件的高内聚性,换句话说,你的对象集合必须有内聚性。也就是你的对象(或者是一个方法、一个库等)的各个方面必须"组合"的很好。
人们在设计一个对象时所面临的一个问题是:将过多的功能都塞在了一个对象中。
例如,在检查打印模式的模块中,你可以这样设计一个对象:让它了解所有的格式和打印技术,但是,这些功能对于一个对象来说可能太多了。你需要三个甚至更多的对象:其中,一个对象可以是所有可能的支票排版的目录,它可以被用来查询有关如何打印一张支票的信息,另一个对象(或者是对象集合)可以是一个通用的打印接口,它知道所有不同类型的打印机的信息(它不应该包含任何有关簿记的内容,实际上,它更应该是一个需要购买而不是自己编写的对象),第三个对象调用前面两个对象的服务来完成打印任务,这样,每个对象都有一个它所能提供服务的内聚的集合,在良好的面向对象程序设计中,每个对象都可以很好地完成一项任务,它并不试图做更多的事,就像在这里看到的,不仅可以通过购买获得某些对象(打印机接口对象),而且还可以创建能够在别处复用的新对象(支票排版目录对象)。
将对象作为服务提供者看待是一件伟大的简化工具,这不仅在设计过程中非常有用,当其他人试图理解你的代码或重用某个对象时,如果他们看出了这个对象所能提供的服务的价值,会导致调整对象以适应其设计变得简单的多。
⑷、被隐藏的具体实现:
将程序开发人员按照角色分为类创建者(那些创造新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消费者)是大有裨益的。
客户端程序员的目标是搜集各种用来实现快速应用开发的类。
类创建者的目标是构建类。这种类只向客户端程序员暴露必须的部分,而隐藏其他部分,为什么要这样呢?因为,暴露给客户端程序员的部分是让客户端程序员使用的(比如一个方法),一旦被客户端程序员使用(客户端程序员调用了这个方法),那么这部分是无论如何在任何情况下都不能被改变(如果改变了方法名称,则客户端程序会出现BUG,因为找到方法。),所以,类的创建者需要通过一种机制将不能暴露的部分隐藏起来,避免被客户端程序员使用,以此来保证程序后续修改升级的灵活性(类创建者可以任意修改被隐藏的部分,而不会对客户端程序员的代码造成任何影响,被隐藏的部分通常代表对象内脆弱的部分,他们很容易被不知情或粗心的客户端程序员所毁坏,因此隐藏他们可以减少BUG),而将允许暴露的部分让客户端程序员使用,并保证这部分始终不会被修改(这里提到的机制就是访问权限控制)。
在任何的相互关系中,具有关系所涉及的各方都遵守的边界是十分重要的事情,当创建一个类库时,就创建了与客户端程序员之间的关系,他们也同样是程序员,但是他们是使用你的类库来构建应用,或者更大的类库的程序员,如果所有的类成员对任何人都是可用的,你会失去对你自己所创建类的控制权(它最终被谁修改,以及修改成什么样,你都不可控,这是不能容忍的事情。),客户端程序员可以对类做任何事情,而不受任何约束,即使你希望客户端程序员不要直接操作你的类中的某些成员,但是,如果没有任何访问控制,将无法阻止此事发生。
访问控制存在的第一个原因就是让客户端程序员无法触及他们不该触及的部分—这部分对类内的操作来说是必须的,但并不是用户解决特定问题所需的接口的一部分,这对客户端程序员来说,其实是一项服务,因为他们可以很容易就看出哪些东西对他们来说很重要,而哪些东西可以忽略。
访问控制存在的第二个原因就是允许类设计者改变类内部的工作方式而不用担心会影响到客户端程序员的代码。例如,你最初为了赶工时选择了一种比较简单的方式实现了一个特定的类,过段时间发现你可以改写它,使它运行得更快,如果接口和实现可以清晰地分离并得以保护,那么以上情况对你不会造成任何干扰,直接改写就行。
Java用三个关键字在类的内部设定边界:public,private,protected,这些访问指定词指定了紧跟其后被定义的内容可以被谁使用,public表示任何人都可以使用,private表示仅类内部可以使用(类内部是指类型创建者或者是类内的方法)。private实际上就是你与客户端程序员之间的一堵墙。如果有人试图访问private成员,代码无法通过编译。protected作用与private几乎相当,差别仅在于继承的类可以访问protected成员(继承的类不能访问private成员),后续博文会详细介绍。
Java还有一种默认的访问权限,当你没有使用public,private,protected任何一个关键字时,它也将发挥作用,这种权限是包访问权限,也就是说,没有使用任何权限控制关键字修饰的内容仅允许同一个包中的其他类访问,但是在包之外,对于其他包中的类来说,它相当于是private(其它包中的类无法访问)。
总结:

知识点    内容描述    涉及的关键字
1、抽象过程    你需要通过分析具体的问题,将问题中包含的每一种元素都对应一个类型,那么,在代码中,这些问题都将会有一个一一对应的对象。    class + 类名称 + { }
2、每个对象都有一个接口    实际上就是每种类类型应该具备哪些方法,所有的方法组成的集合就是这个类类型创建的对象的接口    所有的方法名称
3、每个对象都提供服务    在实际编程中,我们直接调用的是方法,再精准一些,我们实际上只需要知道方法名就能调用方法了,但是我们不知道这个方法的具体实现,严格来讲,更优的实现能提供更好的服务,每个对象的方法都提供服务,你需要根据自己的开发需求去选择服务!    对象名称+" . “+方法名称+” () "
4、被隐藏的具体实现    这里体现的就是接口与实现分离的想法,你只需要知道方法名称,并且通过固定的调用代码,你就能享受这个方法带来的服务,而它的具体实现你是完全不可见的,如果这个方法长期有人维护的话,你甚至不需要去修改你的代码,你就能继续享受越来越好的实现,这正是接口与实现分离带来的好处!    public 、private 、protected 、默认权限
 

你可能感兴趣的:(java学习笔记,java)