面向对象与Java

面向对象与Java

面向对象之我见

个人认为,面向对象程序设计(Object Oriented Programming, OOP)应当属于一种思想,一种程序设计的思想、一种抽象的思想。而且我认为生活中处处都用到这样的思想,比如说:我们会把素鸡、腐竹、兰花干都叫做豆制品,尽管它们确实不是一个东西(多态);我们会在床上放置床单、枕头、被子等,但是我们依然把这一块地儿叫做床(抽象);小吃摊前我们总会说来两串烤鱿鱼,但是很少说来两串重400克且经过国家检查合格的鱿鱼、烤制时80克洋葱为辅、烤5分钟、20克辣椒粉均匀撒上之类的细节(封装)。

当然了,面向对象作为计算机编程中的专有名词,除了其本质的思想,还额外包含了对编码或设计的一些要求。所谓 Object-oriented,就是以对象为目标。何谓“对象”?看得见的、摸得着的、存在于概念中的任何事物,都是对象;或者说,我们想要研究什么,那什么就是对象;亦或者说,万物皆对象

既然要以对象为目标,那么首先要知道这个对象有什么特点和行为,即“抽象”(Abstract)。抽象的过程就是对我们研究的目标的属性或特征、行为或功能进行抽取,然后归纳,最后总结。例如我们想要研究超市里面的水果,它有名字、大小、重量、颜色、口味、产地、价格等属性或特征,也有被售卖、被进货、被摆放、过期等功能或行为。然后将这些属性、行为组合起来,就得到了一个水果模型,这就是对超市里面的水果的抽象。我们将抽象完成后的结果称为“”(Class),在这里就是水果类,表明超市里的一切水果都具有水果类定义的属性、特点、功能、行为等。

抽象完成后,也只是停留在概念或框架层面,我们要丰富其中的内容,并做详细的定义,这个过程就是“封装”(Encapsulation)。封装的过程就是隐藏对象的属性和实现细节,控制这些属性或行为的访问与修改权限。还是以上面的水果为例,我们不仅需要考虑每个属性的数据类型,还要考虑具体行为的逻辑,此外还需要考虑这些属性或行为是公开的还是非公开的、如果是公开的那么公开到什么程度。超市要尽可能地保证这些数据的修改权限是非公开的,但是访问权限是公开的;诸如售卖、进货、摆放等方法是非公开的,但是购买、退货、砍价等方法是公开的。

抽象并封装完成后,我们讨论这个问题,还是拿上面我们谈论的水果说事:超市里面的水果一般都有分区,有家常的水果,也有高档水果,那么我们是不是要抽象成两个对象——普通水果、高档水果呢?看起来确实需要,但是我们会发现,普通水果有名字、价格、大小等属性,高档水果也有,是不是感觉重复了,好麻烦?那么怎么解决呢?对于这个问题,面向对象的方案是:要求程序需要具有“继承”(Inheritance)的特点。也就是说,我们可以让普通水果、高档水果都继承水果的部分属性和方法,于是就不用重复定义一些共同的东西了。在这里,高档水果和普通水果是水果的子类,水果是高档水果和普通水果的父类。于是我们说,继承就是指子类继承父类的成员和方法,使得子类也能具有父类父类相同的行为

既然我们要求有“继承”这个特点,那么就会带来另一个问题:普通水果和高档水果的确都继承了被摆放这个行为,但是普通水果和高档水果排放的逻辑往往不一样(例如区域不一样、包装不一样),如果都继承了父类的方法,那它们的摆放逻辑就一样了呀!所以,为了解决这个问题,面向对象又提出了一个要求,即“多态”(Polymorphism)。我们可以允许高档水果重新定义被摆放的逻辑,或者说重写被摆放的逻辑。这样子,虽然说它们都继承了父类被摆放的逻辑,但是高档水果按照自己的需要重写了逻辑。于是我们说,多态就是允许同一个行为具有不同的表现形式或形态。就好像花钱这个行为有很多形式,例如做慈善、下馆子、充648、买电影票……。

至此,我们就在思想层面上基本讲完了面向对象的四大特性:AEIP(抽象、封装、继承、多态),建议读者结合自己的编码经验,凝练总结,以形成自己的理解。

更进一步地,抽象可以理解为“建立模型”,封装可以理解为“定义模型细节”,继承是允许模型可以被复用,多态是允许模型可以被重定义。其实这种思维,我们可以称之为模型化思维(Modeling),即一种将事物进行抽象、归纳、总结,形成一个模型的思维。Java 中我们自己定义的每一个类,都是我们建立并定义的模型。有了模型,自然要应用模型,这种把模型应用到具体事物上的思维,就是实例化思维(Instantiation)

面向对象就是按照一定规则不断地抽象并封装类,然后按照另一套规则不断地将类应用在具体的某个事物上。因此我们可以说,面向对象的思维就是模型化与实例化思维的统一(M&I)。而面向对象所具有的各种特性,只是这两种思维统一后的应有之义。

在Java中,模型化体现在类的定义和设计,实例化体现在不同类的具体应用。实例化过程需要模型化提供构造方法,在计算机内部相当于承认当前实例的存在合法性,并为当前实例申请一段内存以供其存在(否则就是不存在,不存在就是null)。所以,我们可以把构造方法理解为“承认其在计算机内存中具有存在合法性的方法”,内存就是这些实例平时的活动空间。

在C++中,还存在除了构造函数,还存在析构函数,一个实例可以使用析构函数,来抹去自己在内存中的存在。所以,我们可以把析构函数理解为“离开内存并注销该实例在内存中存在合法性的方法”。或者形象一点,构造函数就是“上班打卡”的方法,析构函数就是“下班”的方法,内存就是“工作的环境”。

Java面向对象特性

抽象类与接口

在谈论 Java 面向对象特性之前,需要详细理解一下什么是抽象类,什么是接口。抽象类和接口的概念并不独属于 Java,而是面向对象编程里面的概念。

在封装对象的时候,我们可能会遇到一个问题,就是当前对象的功能我们只需要去定义即可,但是没有必要给出具体实现细节。例如水果的“被摆放”方法,考虑到苹果、西瓜、香蕉等水果的摆放方式都不一样,因此水果类作为父类没必要“一厢情愿”地去设计摆放细节,只需要把这个方法交给各个子类实现就好。针对这个问题,面向对象给出的方案就是:只给出方法的定义就好,无需给出方法的实现细节。这就是我们熟知的抽象方法

为了将普通类和包含抽象方法的类加以区分,我们定义包含一个或多个抽象方法的类就是抽象类。考虑到我们定义抽象方法的目的就是,让子类去琢磨具体的实现细节,那么父类还有必要实例化吗?答案是没有必要,子类实例化就可以了,父类仅仅是子类所遵循的一个抽象的模型而已,没必要“一厢情愿”地实例化。(该交给儿子的交给儿子,做父亲的没必要总是出面)

我们需要注意的是,抽象类还是可以包含具体数据和具体方法的。那么这说明了什么呢?说明了抽象类是对象自然属性的抽象模型。所谓自然属性就是对象的特性和方法,仅限于特定的对象;所谓的具体数据和具体方法,就是对象自身的数据和方法。但是对象与对象之间是否还有联系呢?答案是肯定有的。对象与对象之间的联系就是对象的社会属性,而社会属性最大的特点就是多样性。即不同的对象之间存在共同的联系类型,但是具体的联系细节不尽相同。因此我们就需要一个类,其中只包含对象与对象之间联系的数据和方法,而不包含对象自身的具体数据和具体方法。这样的类就是接口,我们可以说接口是对象社会属性的抽象模型。在这个层面上,接口也是一种抽象类,接口中的静态数据是社会属性的共有数据,接口中的默认方法是社会属性的共有方法,剩下的就是待实现的普遍联系方法。

抽象类是需要被子类继承的,子类在继承的时候,需要实现抽象类中的抽象方法。接口也是需要被“继承”的,只不过我们习惯称为“实现”,实现类需要实现接口中定义的所有方法(除了默认方法非必要实现)。抽象类是不能实例化的,接口自然也不能实例化,可以通过其实现类进行接口的调用。正因这个过程类似于选用不同厂家的 USB 插头接入设备的 USB 接口,所以这种特殊的抽象类被自然而然地命名为“接口”。

多继承

Java 的类是不支持多继承的,但是 Java 的接口是支持多继承的。那么为什么 Java 这样设计呢?

对于一般的类,如果支持多继承的话,会带来一个问题:子类的两个父类有个方法定义是相同的,但是实现细节不同,那么子类应该遵从哪一个呢?这也就是所谓的二义性问题,在 C++ 中可以通过域限定符或覆盖的方式来解决,但是 Java 为了避免产生这种二义性,直接禁止了一般类的多继承。

但是多继承可以有效做到代码复用,帮助我们设计更为高效的模型,这怎么办呢?我们不妨再深入思考一下,产生二义性的根本原因不在于父类都有同样的方法定义,而在于这两种定义相同的方法的实现细节不一样。要想解决,有三种方案:

  1. 设置一个限定符,在调用同样定义的方法时,需要用限定符调用才能成功,否则编译出错。
  2. 子类重新给出这个方法的实现细节,不继承任何一个父类对于该方法的实现细节。
  3. 父类就不对这个方法给出实现细节,具体细节交给子类即可。

而接口这一思想正好可以提供方案二和方案一,因为接口中定义的方法都是没有实现细节的;即使是接口的默认方法,因为默认方法必须要有实现细节,所以在子接口中的默认方法也“无可奈何”地需要重新写实现细节。所以从逻辑上来说,接口天然支持多继承。至于方案一,虽然解决了二义性问题,但是没有消灭二义性,因此就不再考虑了。

这也是为什么 Java 的类是不支持多继承的,但是 Java 的接口是支持多继承的。本质上来说不是这样设计的,而是逻辑上天然如此,Java 所做的,只是果断地彻底地消灭了二义性(采取了“禁止一般类的多继承”这一方案、采取“接口”这一方案,这些方案都是面向对象编程思想所带来的)。当然,一个类也是可以实现多个接口的,道理是一样的。

重载与重写

重载和重写是 Java 多态的两种表现形式(还有接口和实现类、抽象类和实现类这另外两种)。重载是方法定义层面的多态,即同一个方法,有多种定义方式;重写的方法实现层面的多态,即同一个定义的方法,有不同的实现方式。重写就是将方法的具体实现逻辑重新实现;重载就是同名方法具有不同的定义,例如返回类型、参数个数、参数类型不同。在 Java 中,重写方法需要添加 @Override 标识,重载方法要确保返回值、参数个数、各个参数类型至少有一个不一样。

我们需要注意,一个类的构造方法可能有不同的定义,但是对于其中具体的某个定义,绝不能有多种实现方式。(自家的事情不需要别人掺和)。所以类的构造方法可以在当前类有多个重载,但是不能被子类重写。而对于一般的方法,在重写的时候,还需要注意访问权限的问题,如果子类没有权限访问父类的某些方法,那么自然也无从谈起对这个方法的重写了。

面向对象中的静态

在程序设计语言中,有“静态”这一概念,即静态的方法、变量是定义在程序运行空间中的(堆),而不像那些局部变量是定义在局部内存中的(栈)。考虑到静态这一特殊的形态,我们不禁会问:静态的属性、方法可以被继承吗?静态的方法可以有多态吗?

对于继承这个问题,答案是肯定的,即可以被继承。父类定义的静态属性、静态方法是可以被子类继承的。对于多态这个问题,如果是进行重载,自然是没有任何问题,即可以被重载。但是如果是子类重写,由于 Java 采取的方案是直接覆盖原有的,将原有的隐藏起来,自然也无从谈起多态了(因为压根就是两种方法),即无法被重写。(我们之前所说的多态,是同一个东西具有不同形态;但是对于这里的静态而言,看似重写,实则写了两个东西,压根不是同一个东西)

那么为什么会这样呢?很简单,因为当你定义静态的变量或者方法时,计算机就得为它们分配内存,它们不是抽象的模型,而是具体的事物,自然无法按照抽象的模型进行“推理”。但是你定义的非静态的事物,仅仅停留于模型层面,需要用的时候才会按照这个模型进行“推理”,进而动态地分配内存。

  • 21
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HiUniverse_zyfstep

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值