• 浅谈面向对象


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

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

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

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

    多态怎么实现的?

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

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

    • 继承多态
    • 接口多态

    所谓继承多态:

    1. class Son extends Father {
    2. @Overrid
    3. public void smoke() {
    4. System.out.print("儿子抽烟");
    5. }
    6. }
    7. class Daughter extends Father {
    8. @Overrid
    9. public void smoke() {
    10. System.out.print("女儿抽烟");
    11. }
    12. }
    13. // 继承多态,因为Son、Daughter继承了Father
    14. Father obj = new Son();
    15. obj.smoke(); // 打印:儿子抽烟
    16. obj = new Daughter();
    17. obj.smoke(); // 打印:女儿抽烟

    所谓接口多态:

    1. class Son implements Swimmer {
    2. @Override
    3. public void swim() {
    4. System.out.print("儿子游泳");
    5. }
    6. }
    7. class Daughter implements Swimmer {
    8. @Overrid
    9. public void swim() {
    10. System.out.print("女儿游泳");
    11. }
    12. }
    13. // 接口多态,因为Son、Daughter实现了Swimmer
    14. Swimmer obj = new Son();
    15. obj.swim(); // 打印:儿子游泳
    16. obj = new Daughter();
    17. 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

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

  • 相关阅读:
    lambda表达式怎么用?(人话版)
    Elasticsearch 保姆级入门篇
    【数据结构初阶(3)】双向带头结点循环链表
    Java网络编程----通过实现简易聊天工具来聊聊BIO模型
    spring cloud之负载均衡
    Caldera(二)高级实战
    专题 递归与递推
    基于springboot小型车队管理系统毕业设计源码061709
    邮件协议SMTP、POP3和IMAP
    【C语言】循环结构习题
  • 原文地址:https://blog.csdn.net/smart_an/article/details/134501780