08.面向对象的特性

简介: 本文详细介绍了面向对象编程的核心特性:封装、抽象、继承和多态。封装通过限制访问保护数据,提高代码可维护性和易用性;抽象隐藏实现细节,关注功能本身,增强代码扩展性;继承表示类间的“is-a”关系,解决代码复用问题,但需谨慎使用以避免复杂性;多态允许子类替换父类,提升代码扩展性和复用性,是设计模式的基础。此外,文章还探讨了面向对象分析与设计(OOA/OOD)、UML建模语言以及这些特性在实际编程中的应用。最后提供了多个学习资源链接,涵盖Java、C/C++、Android开发及算法等内容,适合开发者深入学习面向对象思想及相关技术。

08.面向对象的特性

目录介绍

  • 01.面向对象编程和语言
    • 1.1 面向对象的介绍
    • 1.2 面向对象编程
    • 1.3 面向对象的特性
    • 1.4 编程语言的划分
  • 02.面向对象分析和设计
    • 2.1 OOA和OOD设计
    • 2.2 UML统一建模语言
  • 03.面向对象之封装
    • 3.1 什么是封装
    • 3.2 看一个封装案例
    • 3.3 封装的语法机制
    • 3.4 封装的意义
  • 04.面向对象之抽象
    • 4.1 什么是抽象
    • 4.2 抽象案例分析
    • 4.3 抽象的语法机制
    • 4.4 抽象的意义
  • 05.面向对象之继承
    • 5.1 什么是继承
    • 5.2 继承的意义
  • 06.面向对象之多态
    • 6.1 什么是多态
    • 6.2 多态案例分析
    • 6.3 多态的意义
  • 07.四大特性的总结
    • 7.1 封装特性总结
    • 7.2 抽象特性总结
    • 7.3 继承特性总结
    • 7.4 多态特性总结

01.面向对象编程和语言

1.1 面向对象的介绍

提到面向对象,相信很多人都不陌生,随口都可以说出面向对象的四大特性:封装、抽象、继承、多态

实际上,面向对象这个概念包含的内容还不止这些。当谈论面向对象的时候,经常会谈到的一些概念和知识点,为学习后面的几节更加细化的内容做一个铺垫。

如果你看了之后,对某个概念和知识点还不是很清楚,那也没有关系。在后面的博客中,会花更多的篇幅,对讲到的每个概念和知识点,结合具体的例子,一一做详细的讲解。

1.2 面向对象编程

面向对象编程中有两个非常重要、非常基础的概念:那就是类(class)和对象(object)。

这两个概念最早出现在 1960 年,在 Simula 这种编程语言中第一次使用。而面向对象编程这个概念第一次被使用是在 Smalltalk 这种编程语言中。Smalltalk 被认为是第一个真正意义上的面向对象编程语言。

1980 年左右,C++ 的出现,带动了面向对象编程的流行,也使得面向对象编程被越来越多的人认可。

直到今天,如果不按照严格的定义来说,大部分编程语言都是面向对象编程语言,比如 Java、C++、Go、Python、C#、Ruby、JavaScript、Objective-C、Scala、PHP、Perl 等等。

除此之外,大部分程序员在开发项目的时候,都是基于面向对象编程语言进行的面向对象编程。

面向对象编程和面向对象编程语言。那究竟什么是面向对象编程?什么语言才算是面向对象编程语言呢?如果非得给出一个定义的话,我觉得可以用下面两句话来概括。

  1. 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
  2. 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。

1.3 面向对象的特性

理解面向对象编程及面向对象编程语言两个概念,其中最关键的一点就是理解面向对象编程的四大特性。

这四大特性分别是:封装、抽象、继承、多态。关于面向对象编程的特性,也有另外一种说法,那就是只包含三大特性:封装、继承、多态,不包含抽象。为什么会有这种分歧呢?抽象为什么可以排除在面向对象编程特性之外呢?

关于这个问题,话说回来,实际上,我们没必要纠结到底是四大特性还是三大特性,关键还是理解每种特性讲的是什么内容、存在的意义以及能解决什么问题。

而且,在技术圈里,封装、抽象、继承、多态也并不是固定地被叫作“四大特性”(features),也有人称它们为面向对象编程的四大概念(concepts)、四大基石(cornerstones)、四大基础(fundamentals)、四大支柱(pillars)等等。

对于这四大特性,光知道它们的定义是不够的,我们还要知道每个特性存在的意义和目的,以及它们能解决哪些编程问题。

对于这四大特性,尽管大部分面向对象编程语言都提供了相应的语法机制来支持,但不同的编程语言实现这四大特性的语法机制可能会有所不同。

在讲解四大特性的时候,并不与具体某种编程语言的特定语法相挂钩,同时,也希望你不要局限在你自己熟悉的编程语言的语法思维框架里。

1.4 编程语言的划分

“如果不按照严格的定义来说,大部分编程语言都是面向对象编程语言”。为什么要加上“如果不按照严格的定义”这个前提呢?

那是因为,如果按照刚刚我们给出的严格的面向对象编程语言的定义,前面提到的有些编程语言,并不是严格意义上的面向对象编程语言。

比如 JavaScript,它不支持封装和继承特性,按照严格的定义,它不算是面向对象编程语言,但在某种意义上,它又可以算得上是一种面向对象编程语言。我为什么这么说呢?

到底该如何判断一个编程语言是否是面向对象编程语言呢?

实际上,面向对象编程从字面上,按照最简单、最原始的方式来理解,就是将对象或类作为代码组织的基本单元,来进行编程的一种编程范式或者编程风格,并不一定需要封装、抽象、继承、多态这四大特性的支持。更多博客。但是,在进行面向对象编程的过程中,人们不停地总结发现,有了这四大特性,我们就能更容易地实现各种面向对象的代码设计思路。

只要某种编程语言支持类或对象的语法概念,并且以此作为组织代码的基本单元,那就可以被粗略地认为它就是面向对象编程语言了。至于是否有现成的语法机制,完全地支持了面向对象编程的四大特性、是否对四大特性有所取舍和优化,可以不作为判定的标准。

基于此,我们才有了前面的说法,按照严格的定义,很多语言都不能算得上面向对象编程语言,但按照不严格的定义来讲,现在流行的大部分编程语言都是面向对象编程语言。

02.面向对象分析和设计

2.1 OOA和OOD设计

跟面向对象编程经常放到一块儿来讲的还有另外两个概念,那就是面向对象分析(OOA)和面向对象设计(OOD)。

  1. 面向对象分析英文缩写是 OOA,全称是 Object Oriented Analysis;
  2. 面向对象设计的英文缩写是 OOD,全称是 Object Oriented Design。
  3. OOA、OOD、OOP 三个连在一起就是面向对象分析、设计、编程(实现),正好是面向对象软件开发要经历的三个阶段。

什么是面向对象分析和设计。这两个概念相对来说要简单一些。

面向对象分析与设计中的“分析”和“设计”这两个词,我们完全可以从字面上去理解,不需要过度解读,简单类比软件开发中的需求分析、系统设计即可。

面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做。两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等等

不过,你可能会说,那为啥前面还加了个修饰词“面向对象”呢?有什么特殊的意义吗?

在前面加“面向对象”这几个字,是因为我们是围绕着对象或类来做需求分析和设计的。更多博客。

分析和设计两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法,类与类之间如何交互等等。它们比其他的分析和设计更加具体、更加落地、更加贴近编码,更能够顺利地过渡到面向对象编程环节。这也是面向对象分析和设计,与其他分析和设计最大的不同点。

面向对象分析、设计、编程到底都负责做哪些工作呢?简单点讲,面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的的结果翻译成代码的过程。

2.2 UML统一建模语言

什么是 UML?我们是否需要 UML?

面向对象分析、设计、编程,我们就不得不提到另外一个概念,那就是 UML(Unified Model Language),统一建模语言。

很多讲解面向对象或设计模式的书籍,常用它来画图表达面向对象或设计模式的设计思路。

实际上,UML 是一种非常复杂的东西。它不仅仅包含我们常提到类图,还有用例图、顺序图、活动图、状态图、组件图等。即便仅仅使用类图,学习成本也是很高的。就单说类之间的关系,UML 就定义了很多种,比如泛化、实现、关联、聚合、组合、依赖等。

要想完全掌握,并且熟练运用这些类之间的关系,来画 UML 类图,肯定要花很多的学习精力。而且,UML 作为一种沟通工具,即便你能完全按照 UML 规范来画类图,可对于不熟悉的人来说,看懂的成本也还是很高的。

03.面向对象之封装

3.1 什么是封装

来看封装特性。封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。

这句话怎么理解呢?我们通过一个简单的例子来解释一下。

需求:你要设计一个钱包,可以获取钱包id,创建时间,余额,最后交易时间。用户可以在钱包中存钱。

分析:这个需求中,除了金额可以变动设置,其他id,交易时间,创建时间都是不允许外部修改的。

3.2 看一个封装案例

下面这段代码是金融系统中一个简化版的虚拟钱包的代码实现。在金融系统中,我们会给每个用户创建一个虚拟钱包,用来记录用户在我们的系统中的虚拟货币量。

public class Wallet {
   
  private String id;
  private long createTime;
  private BigDecimal balance;
  private long balanceLastModifiedTime;
  // ...省略其他属性...

  public Wallet() {
   
     this.id = IdGenerator.getInstance().generate();
     this.createTime = System.currentTimeMillis();
     this.balance = BigDecimal.ZERO;
     this.balanceLastModifiedTime = System.currentTimeMillis();
  }

  // 注意:下面对get方法做了代码折叠,是为了减少代码所占文章的篇幅
  public String getId() {
    return this.id; }
  public long getCreateTime() {
    return this.createTime; }
  public BigDecimal getBalance() {
    return this.balance; }
  public long getBalanceLastModifiedTime() {
    return this.balanceLastModifiedTime;  }

  public void increaseBalance(BigDecimal increasedAmount) {
   
    if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
   
      throw new InvalidAmountException("...");
    }
    this.balance.add(increasedAmount);
    this.balanceLastModifiedTime = System.currentTimeMillis();
  }

  public void decreaseBalance(BigDecimal decreasedAmount) {
   
    if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
   
      throw new InvalidAmountException("...");
    }
    if (decreasedAmount.compareTo(this.balance) > 0) {
   
      throw new InsufficientAmountException("...");
    }
    this.balance.subtract(decreasedAmount);
    this.balanceLastModifiedTime = System.currentTimeMillis();
  }
}

从代码中,我们可以发现,Wallet 类主要有四个属性(也可以叫作成员变量),也就是我们前面定义中提到的信息或者数据。

其中,id 表示钱包的唯一编号,createTime 表示钱包创建的时间,balance 表示钱包中的余额,balanceLastModifiedTime 表示上次钱包余额变更的时间。

参照封装特性,对钱包的这四个属性的访问方式进行了限制。调用者只允许通过下面这六个方法来访问或者修改钱包里的数据。

String getId()

long getCreateTime()

BigDecimal getBalance()

long getBalanceLastModifiedTime()

void increaseBalance(BigDecimal increasedAmount)

void decreaseBalance(BigDecimal decreasedAmount)

从业务的角度来说,id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动,所以,我们并没有在 Wallet 类中,暴露 id、createTime 这两个属性的任何修改方法,比如 set 方法。而且,这两个属性的初始化设置,对于 Wallet 类的调用者来说,也应该是透明的,所以,我们在 Wallet 类的构造函数内部将其初始化设置好,而不是通过构造函数的参数来外部赋值。

对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所以,我们在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。对于 balanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候,这个属性才会被修改。所以,我们把 balanceLastModifiedTime 这个属性的修改操作完全封装在了 increaseBalance() 和 decreaseBalance() 两个方法中,不对外暴露任何修改这个属性的方法和业务细节。这样也可以保证 balance 和 balanceLastModifiedTime 两个数据的一致性。

3.3 封装的语法机制

对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。

  1. 这个语法机制就是访问权限控制。例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法。
  2. private 关键字修饰的属性只能类本身访问,可以保护其不被类之外的代码直接访问。
  3. 如果 Java 语言没有提供访问权限控制语法,所有的属性默认都是 public 的,那任意外部代码都可以通过类似 wallet.id=123; 这样的方式直接访问、修改属性,也就没办法达到隐藏信息和保护数据的目的了,也就无法支持封装特性了。

3.4 封装的意义

封装特性的定义讲完了,我们再来看一下,封装的意义是什么?它能解决什么编程问题?

如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。

比如某个同事在不了解业务逻辑的情况下,在某段代码中“偷偷地”重设了 wallet 中的 balanceLastModifiedTime 属性,这就会导致 balance 和 balanceLastModifiedTime 两个数据不一致。

除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。

相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多。

这就好比,如果一个冰箱有很多按钮,你就要研究很长时间,还不一定能操作正确。相反,如果只有几个必要的按钮,比如开、停、调节温度,你一眼就能知道该如何来操作,而且操作出错的概率也会降低很多。

封装的设计意义是将实现细节隐藏起来,提供简洁的接口,以提高代码的可维护性、重用性和安全性。

04.面向对象之抽象

4.1 什么是抽象

封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

在面向对象编程中,我们常借助编程语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。

把编程语言提供的接口语法叫作“接口类”而不是“接口”。之所以这么做,是因为“接口”这个词太泛化,可以指好多概念,比如 API 接口等,所以,我们用“接口类”特指编程语言提供的接口语法。

4.2 抽象案例分析

对于抽象这个特性,我举一个例子来进一步解释一下。

public interface IPictureStorage {
   
  void savePicture(Picture picture);
  Image getPicture(String pictureId);
  void deletePicture(String pictureId);
  void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}

public class PictureStorage implements IPictureStorage {
   
  // ...省略其他属性...
  @Override
  public void savePicture(Picture picture) {
    ... }
  @Override
  public Image getPicture(String pictureId) {
    ... }
  @Override
  public void deletePicture(String pictureId) {
    ... }
  @Override
  public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) {
    ... }
}

在上面的这段代码中,我们利用 Java 中的 interface 接口语法来实现抽象特性。调用者在使用图片存储功能的时候,只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑。

实际上,抽象这个特性是非常容易实现的,并不需要非得依靠接口类或者抽象类这些特殊语法机制来支持。换句话说,并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage),才叫作抽象。即便不编写 IPictureStorage 接口类,单纯的 PictureStorage 类本身就满足抽象特性。

之所以这么说,那是因为,类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。

调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以直接使用了。

比如,我们在使用 C 语言的 malloc() 函数的时候,并不需要了解它的底层代码是怎么实现的。

抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。

4.3 抽象的语法机制

抽象的语法机制是一种编程语言的特性,用于隐藏底层实现细节,提供更高层次的抽象和简化编程过程。它允许开发者通过定义抽象的概念、规则和模式来描述问题领域,而不必关注具体的实现细节。

通过抽象的语法机制,开发者可以定义抽象类、接口、函数、模块等,以提供更高级别的抽象。这些抽象可以隐藏底层的复杂性,使代码更易于理解、维护和重用。抽象的语法机制还可以提供代码的可扩展性和灵活性,使开发者能够以更高层次的抽象思考和解决问题。

4.4 抽象的意义

抽象特性的定义讲完了,我们再来看一下,抽象的意义是什么?它能解决什么编程问题?

实际上,如果上升一个思考层面的话,抽象及其前面讲到的封装都是人类处理复杂性的有效手段。在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息。

除此之外,抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等。

抽象的意义是通过隐藏细节和提供概念上的简化,使复杂问题更易理解和处理。

换一个角度来考虑,我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。

举个简单例子,比如 getAliyunPictureUrl() 就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl(),那即便内部存储方式修改了,我们也不需要修改命名。

05.面向对象之继承

5.1 什么是继承

如果你熟悉的是类似 Java、C++ 这样的面向对象的编程语言,那你对继承这一特性,应该不陌生了。继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。

从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。

为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承,C++ 使用冒号(class B : public A),Python 使用 paraentheses(),Ruby 使用 <。

不过,有些编程语言只支持单继承,不支持多重继承,比如 Java、PHP、C#、Ruby 等,而有些编程语言既支持单重继承,也支持多重继承,比如 C++、Python、Perl 等。

为什么有些语言支持多重继承,有些语言不支持呢?这个问题留给你自己去研究,你可以针对你熟悉的编程语言,写一写具体的原因。

5.2 继承的意义

继承特性的定义讲完了,我们再来看,继承存在的意义是什么?它能解决什么编程问题?

继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。

如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。

继承的概念很好理解,也很容易使用。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。

所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用,甚至不用。关于这个问题,在后面讲到“多用组合少用继承”这种设计思想的时候,我会非常详细地再讲解,这里暂时就不展开讲解了。

06.面向对象之多态

6.1 什么是多态

再来看面向对象编程的最后一个特性,多态。

多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。

6.2 多态案例分析

对于多态这种特性,纯文字解释不好理解,我们还是看一个具体的例子。

public class DynamicArray {
   
  private static final int DEFAULT_CAPACITY = 10;
  protected int size = 0;
  protected int capacity = DEFAULT_CAPACITY;
  protected Integer[] elements = new Integer[DEFAULT_CAPACITY];

  public int size() {
    return this.size; }
  public Integer get(int index) {
    return elements[index];}
  //...省略n多方法...

  public void add(Integer e) {
   
    ensureCapacity();
    elements[size++] = e;
  }

  protected void ensureCapacity() {
   
    //...如果数组满了就扩容...代码省略...
  }
}

public class SortedDynamicArray extends DynamicArray {
   
  @Override
  public void add(Integer e) {
   
    ensureCapacity();
    int i;
    for (i = size-1; i>=0; --i) {
    //保证数组中的数据有序
      if (elements[i] > e) {
   
        elements[i+1] = elements[i];
      } else {
   
        break;
      }
    }
    elements[i+1] = e;
    ++size;
  }
}

public class Example {
   
  public static void test(DynamicArray dynamicArray) {
   
    dynamicArray.add(5);
    dynamicArray.add(1);
    dynamicArray.add(3);
    for (int i = 0; i < dynamicArray.size(); ++i) {
   
      System.out.println(dynamicArray[i]);
    }
  }

  public static void main(String args[]) {
   
    DynamicArray dynamicArray = new SortedDynamicArray();
    test(dynamicArray); // 打印结果:1、3、5
  }
}

多态这种特性也需要编程语言提供特殊的语法机制来实现。在上面的例子中,我们用到了三个语法机制来实现多态。

第一个语法机制是编程语言要支持父类对象可以引用子类对象,也就是可以将 SortedDynamicArray 传递给 DynamicArray。

第二个语法机制是编程语言要支持继承,也就是 SortedDynamicArray 继承了 DynamicArray,才能将 SortedDyamicArray 传递给 DynamicArray。

第三个语法机制是编程语言要支持子类可以重写(override)父类中的方法,也就是 SortedDyamicArray 重写了 DynamicArray 中的 add() 方法。

通过这三种语法机制配合在一起,我们就实现了在 test() 方法中,子类 SortedDyamicArray 替换父类 DynamicArray,执行子类 SortedDyamicArray 的 add() 方法,也就是实现了多态特性。

对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外,我们还有其他两种比较常见的的实现方式,一个是利用接口类语法,另一个是利用 duck-typing 语法。不过,并不是每种编程语言都支持接口类或者 duck-typing 这两种语法机制,比如 C++ 就不支持接口类语法,而 duck-typing 只有一些动态语言才支持,比如 Python、JavaScript 等。

接下来,我们先来看如何利用接口类来实现多态特性。我们还是先来看一段代码。

public interface Iterator {
   
  String hasNext();
  String next();
  String remove();
}

public class Array implements Iterator {
   
  private String[] data;

  public String hasNext() {
    ... }
  public String next() {
    ... }
  public String remove() {
    ... }
  //...省略其他方法...
}

public class LinkedList implements Iterator {
   
  private LinkedListNode head;

  public String hasNext() {
    ... }
  public String next() {
    ... }
  public String remove() {
    ... }
  //...省略其他方法... 
}

public class Demo {
   
  private static void print(Iterator iterator) {
   
    while (iterator.hasNext()) {
   
      System.out.println(iterator.next());
    }
  }

  public static void main(String[] args) {
   
    Iterator arrayIterator = new Array();
    print(arrayIterator);

    Iterator linkedListIterator = new LinkedList();
    print(linkedListIterator);
  }
}

在这段代码中,Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator。我们通过传递不同类型的实现类(Array、LinkedList)到 print(Iterator iterator) 函数中,支持动态的调用不同的 next()、hasNext() 实现。

具体点讲就是,当我们往 print(Iterator iterator) 函数传递 Array 类型的对象的时候,print(Iterator iterator) 函数就会调用 Array 的 next()、hasNext() 的实现逻辑;当我们往 print(Iterator iterator) 函数传递 LinkedList 类型的对象的时候,print(Iterator iterator) 函数就会调用 LinkedList 的 next()、hasNext() 的实现逻辑。

刚刚讲的是用接口类来实现多态特性。现在,我们再来看下,如何用 duck-typing 来实现多态特性。我们还是先来看一段代码。这是一段 Python 代码。

class Logger:
    def record(self):
        print(“I write a log into file.)

class DB:
    def record(self):
        print(“I insert data into db.)

def test(recorder):
    recorder.record()

def demo():
    logger = Logger()
    db = DB()
    test(logger)
    test(db)

从这段代码中,我们发现,duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,既不是继承关系,也不是接口和实现的关系,但是只要它们都有定义了 record() 方法,就可以被传递到 test() 方法中,在实际运行的时候,执行对应的 record() 方法。

也就是说,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口。

6.3 多态的意义

多态特性讲完了,我们再来看,多态特性存在的意义是什么?它能解决什么编程问题?

多态特性能提高代码的可扩展性和复用性。为什么这么说呢?我们回过头去看讲解多态特性的时候,举的第二个代码实例(Iterator 的例子)。

在那个例子中,我们利用多态的特性,仅用一个 print() 函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据。当再增加一种要遍历打印的类型的时候,比如 HashMap,我们只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext()、next() 等方法就可以了,完全不需要改动 print() 函数的代码。所以说,多态提高了代码的可扩展性。

如果我们不使用多态特性,我们就无法将不同的集合类型(Array、LinkedList)传递给相同的函数(print(Iterator iterator) 函数)。我们需要针对每种要遍历打印的集合,分别实现不同的 print() 函数,比如针对 Array,我们要实现 print(Array array) 函数,针对 LinkedList,我们要实现 print(LinkedList linkedList) 函数。而利用多态特性,我们只需要实现一个 print() 函数的打印逻辑,就能应对各种集合数据的打印操作,这显然提高了代码的复用性。

除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。关于这点,在学习后面的章节中,你慢慢会有更深的体会。

07.四大特性的总结

7.1 封装特性总结

封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。它需要编程语言提供权限访问控制语法来支持,例如 Java 中的 private、protected、public 关键字。

封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。

7.2 抽象特性总结

封装主要讲如何隐藏信息、保护数据,那抽象就是讲如何隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现的。

抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持。

抽象存在的意义,一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。

7.3 继承特性总结

继承是用来表示类之间的 is-a 关系,分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类。

为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。

7.4 多态特性总结

多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。

多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。

08.更多内容推荐

模块 描述 备注
GitHub 多个YC系列开源项目,包含Android组件库,以及多个案例 GitHub
博客汇总 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 YCBlogs
设计模式 六大设计原则,23种设计模式,设计模式案例,面向对象思想 设计模式
Java进阶 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM Java高级
网络协议 网络实际案例,网络原理和分层,Https,网络请求,故障排查 网络协议
计算机原理 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 计算机基础
学习C编程 C语言入门级别系统全面的学习教程,学习三到四个综合案例 C编程
C++编程 C++语言入门级别系统全面的教学教程,并发编程,核心原理 C++编程
算法实践 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 Leetcode
Android 基础入门,开源库解读,性能优化,Framework,方案设计 Android
目录
相关文章
|
9月前
|
机器学习/深度学习 传感器 搜索推荐
《解析鸿蒙原生智能:解锁精准用户意图理解的密码》
鸿蒙系统以其原生智能特性,通过多维感知、大模型赋能、知识图谱、机器学习及意图框架五大核心技术,精准理解用户意图,开创智能交互新篇章。多维感知融合语音、图像与传感器数据,捕捉用户需求;大模型深度解析语言语义,提供个性化服务;知识图谱构建信息网络,提升搜索精度;机器学习持续优化交互体验;意图框架高效匹配服务。鸿蒙原生智能不仅提升了人机交互效率,更为未来智能化生活奠定了坚实基础。
261 10
|
9月前
|
XML JSON API
掌握 Postman:高级 GET 请求技术与响应分析
本指南详细讲解了如何在 Postman 中发送 GET 请求并解析 API 响应,帮助开发者提升 API 测试与开发能力。Postman 是一款强大的工具,可简化请求发送和响应分析流程,并支持团队协作及多版本管理。通过创建集合、配置请求参数、设置身份验证与请求头等步骤,开发者能够高效测试 API。同时,理解响应体、Cookie、响应头等内容有助于深入分析 API 行为,确保高质量的软件交付。掌握 Postman 不仅提高效率,还能加深对 Web 通信机制的理解。
|
8月前
|
存储 安全 算法
《深度剖析:软总线设备认证机制如何筑牢互联安全防线》
在智能设备互联互通的时代,软总线作为数据交互的核心,其安全性尤为重要。设备认证机制通过生成唯一身份标识(如椭圆曲线公私钥对)确保合法设备接入。信任关系的建立包括PIN码认证和无交互认证等方式,结合加密与完整性保护技术,防止信息被窃取或篡改。此外,通过信任链拓展与管理,软总线实现跨设备安全互联。这一机制为智能家居、智能办公等场景提供了可靠保障,并将随技术发展持续优化,适应复杂网络环境。
200 7
|
9月前
|
机器学习/深度学习 人工智能 安全
一篇关于DeepSeek模型先进性的阅读理解
本文以DeepSeek模型为核心,探讨了其技术先进性、训练过程及行业影响。首先介绍DeepSeek的快速崛起及其对AI行业的颠覆作用。DeepSeek通过强化学习(RL)实现Time Scaling Law的新范式,突破了传统大模型依赖算力和数据的限制,展现了集成式创新的优势。文章还提到开源的重要性以及数据作为制胜法宝的关键地位,同时警示了业务发展中安全滞后的问题。
1335 176
一篇关于DeepSeek模型先进性的阅读理解
|
9月前
|
Oracle 关系型数据库 Java
【YashanDB知识库】Mybatis-Plus适配崖山配置
【YashanDB知识库】Mybatis-Plus适配崖山配置
|
7月前
|
存储 机器学习/深度学习 缓存
vLLM 核心技术 PagedAttention 原理详解
本文系统梳理了 vLLM 核心技术 PagedAttention 的设计理念与实现机制。文章从 KV Cache 在推理中的关键作用与内存管理挑战切入,介绍了 vLLM 在请求调度、分布式执行及 GPU kernel 优化等方面的核心改进。PagedAttention 通过分页机制与动态映射,有效提升了显存利用率,使 vLLM 在保持低延迟的同时显著提升了吞吐能力。
3624 19
vLLM 核心技术 PagedAttention 原理详解
|
9月前
|
关系型数据库 BI OLAP
一招解决数据库中报表查询慢的痛点
本文旨在解决传统数据库系统如PostgreSQL在处理复杂分析查询时面临的性能瓶颈问题。
1542 164
一招解决数据库中报表查询慢的痛点
|
9月前
|
人工智能 缓存 安全
大模型无缝切换,QwQ-32B和DeepSeek-R1 全都要
通义千问最新推出的QwQ-32B推理模型,拥有320亿参数,性能媲美DeepSeek-R1(6710亿参数)。QwQ-32B支持在小型移动设备上本地运行,并可将企业大模型API调用成本降低90%以上。本文介绍了如何通过Higress AI网关实现DeepSeek-R1与QwQ-32B之间的无缝切换,涵盖环境准备、模型接入配置及客户端调用示例等内容。此外,还详细探讨了Higress AI网关的多模型服务、消费者鉴权、模型自动切换等高级功能,帮助企业解决TPS与成本平衡、内容安全合规等问题,提升大模型应用的稳定性和效率。
1198 136
大模型无缝切换,QwQ-32B和DeepSeek-R1 全都要
|
机器学习/深度学习 人工智能 移动开发
一文搞懂 FFN / RNN / CNN 的参数量计算公式 !!
一文搞懂 FFN / RNN / CNN 的参数量计算公式 !!
1029 3
|
9月前
|
人工智能 边缘计算 监控
《解锁分布式软总线:构建AI分布式推理临时集群的密钥》
分布式软总线是一种创新的软件定义通信技术,通过自发现与自组网特性,构建高效的人工智能分布式推理临时计算集群。它能自动识别并连接多设备,优化任务分配与数据交互,广泛应用于智能安防、工业制造及科学研究等领域,大幅提升推理效率与性能,为人工智能发展开辟新路径。
354 6