浅谈面向对象

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

从某个角度说,你是对的,多态最大的作用就是为了传参提供便利,但我们不应该只看到这一层,还要往下再走走:为什么要用父类引用指向子类实例呢?就好比你看到一把刀很锋利,可以切菜,你不应该疑惑“难道刀就是拿来切菜的吗”,而应该关注“为什么刀可以如此锋利”...

回到你的问题上来,我们更应该关心:为什么可以使用多态机制,以及为什么需要多态?

多态怎么实现的?

我并非计算机专业,所以对于这个问题,只给出一个大概的解释。多态从语法表面上看,就是子类对象可以赋值给父类引用,并且通过该引用可以动态地调用不同子类的方法。

多态按实际用法又可以分为:

  • 继承多态
  • 接口多态

所谓继承多态:

class Son extends Father { 
    @Overrid
    public void smoke() {
        System.out.print("儿子抽烟");
    }
}
class Daughter extends Father {
    @Overrid
    public void smoke() {
        System.out.print("女儿抽烟");
    }
}

// 继承多态,因为Son、Daughter继承了Father
Father obj = new Son();
obj.smoke(); // 打印:儿子抽烟
obj = new Daughter();
obj.smoke(); // 打印:女儿抽烟

所谓接口多态:

class Son implements Swimmer {
    @Override
    public void swim() {
        System.out.print("儿子游泳");
    }
}
class Daughter implements Swimmer {
    @Overrid
    public void swim() {
        System.out.print("女儿游泳");
    }
}

// 接口多态,因为Son、Daughter实现了Swimmer
Swimmer obj = new Son();
obj.swim(); // 打印:儿子游泳
obj = new Daughter();
obj.swim(); // 打印:女儿游泳

实际开发接口多态更常用。多态的实现,依赖于2个大方面:

  • 机制上的支持
  • 编码上的支持

机制支持

首先,编译器要允许这种赋值方式,不然把son赋值给swimmer就会像把 int a赋值给String b一样报错。

其次,运行时要支持并且能通过某种机制找到真正的子类方法。

编码支持

必须存在继承(实现)关系 + 子类必须重写(实现)父类的方法

我们一般所说的多态,其实都是指方法的多态


什么意思呢?以上面Swimmer的代码为例(假设整个工程只有这么几个类),当程序运行时,JVM中实际上并不存在一个对象叫Swimmer,自始至终只有Son和Daughter两个对象,而且Son和Daughter都实现了Swimmer,且重写了swim()方法。当JVM运行到16行时:

JVM是怎么知道要打印“儿子游泳”的呢?换句话说,JVM怎么知道调用Son#swim()而不是Swimmer#swim()或者Daughter#swim()的呢?

这就涉及到所谓的“虚方法”和“虚方法表”了。大家都知道JVM有个所谓的“类加载子系统”,专门负责类的加载(下图最上面的部分)。

而在类加载过程中,有loading、linking、initialization三个阶段,其中linking(链接)阶段又包括3个小阶段:

  • verify(验证)
  • prepare(准备)
  • resolve(解析)

其中在resolve阶段,JVM会针对类或接口、字段、类方法、接口方法等进行相应解析,其中方法信息会形成所谓的“虚方法表”。

也就是说,当出现多态方法调用时,底层会多一次“查表”的过程,也就是通过搜索虚方法表,确定本次实际应该调用的方法(实际指向对象+实例对应的类有无重写父类方法),如果子类Override了父类方法,那么就会执行子类方法。

多态与设计模式

很多初学编程的人,一定会记住两句话,即使他们并不懂得其中含义:

  • 面向对象的三大特性是:封装、继承、多态
  • 万物皆对象

但在我眼里,封装、继承这俩货和多态根本不是一个档次的(就好比李云迪和郎朗),多态才是面向对象的核心和根本,甚至没有多态就没有面向对象。举个例子,C语言没有封装吗?不也是可以抽取方法吗?也有结构体呢,看起来不像对象吗?再者,你问问自己,你使用继承是为了什么?不就是为了贪图父类的那一点点已经写好的方法,为了偷点懒吗?既然是为了少写一点代码,我抽取成方法不行吗?

所以,到底什么是面向对象呢?

这就回到了我上面说的,多态才是面向对象的核心(当然,面向对象本质是一种编程思想的转变)。当我们有了多态,才能写出更加抽象的代码,而抽象代表稳定

假设世界末日,外星人占领地球了,它们觉得必须杀鸡儆猴,我们因为真的打不过,只能任由宰割。此时我们签订契约:你们可以杀一个动物。于是我们送了一只实验室的小白鼠,因为小白鼠也是动物呀。动物这个词是抽象的,后面我们送啥都可以,只要不送人。

再举个编程的例子。假设在写好的一个类文件中,你写下这样一段代码:

如果后期接入拼多多,你就需要修改代码。但如果使用策略模式,就可以用增量的方式代替修改(开闭原则):

其实,这就是策略模式。而所谓的设计模式,其实有一本书的书名,恰恰点破了设计模式的本质:

是的,设计模式本质是围绕着“在面向对象的基础上,如何复用设计”这个原则展开的...所以本质又回到了面向对象。为什么设计模式这么牛逼,能把很多看起来像“屎山一样”的代码优化得清晰、简洁?本质上就是多态!

所谓“屎山一样”的代码,大概率就是因为后期需求不断迭代,开发人员在未经思考的情况下肆意使用if else添加逻辑分支导致的!但分支是不会无缘无故消失的,只是借助设计模式把分支下推,最终交给了多态——JVM,你给我去查虚方法表。

换句话说就是:JVM,这坨屎你来吃。

最终,JVM带着虚方法表承受了一切,而我们的上层代码一扫阴霾,看起来干净而整洁,也就是所谓的clean code...

所以,最后再问一句:

多态真的就是用来传参吗?

Ps.也正因为多态调用底层需要查虚方法表,所以大部分设计模式的引入其实反而会降低执行效率(可以忽略),也可能增加内存负担(子类和子类对象增多)。但我们必须清楚,设计模式本来就不是为了解决效率问题,而是为了解决扩展问题,让编码复用性更高、更清晰。只有极少部分设计模式的初衷是为了效率和内存经济性,比如享元模式(Integer、Long这些包装类底层有缓存池)。

补充说明

有同学容易把虚方法表和对象方法调用搞混了,这两个其实是完全不同维度的东西,这里补充解释一下。

上一篇《对象与this》,我们解释了一个问题:

Person的changeUser()方法是所有Person实例p1、p2共有的,那么p1.changeUser()为什么不会处理p2的数据呢?

根本原因是p1.changeUser()会隐式传递this,那么执行changeUser()时,虽然方法是p1、p2共用的,但只会处理p1的。

根据Person类,可以new出无数个对象 p1、p2...pn,但每个对象调用changeUser()时都会传递不同的this,那么changeUser()同一套指令(方法),处理的数据(对象字段)就是不同的。

而上面所说的虚方法表,它的创建时机是类加载阶段,而所谓类加载,可以认为是类层次的,此时并没有业务对象被创建(比如Person对象),但确实又是在运行时起作用。两者的关系其实是这样的:

调父还是子的方法(重载),与这个方法处理哪个对象(隐式this),是两个不同维度的东西。

最后,无论new多少个对象,虚方法表都是不变的(类层次)。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值