Polymorphic 梦里花落知多少

e44eb382000fbaba3bedebf47aa4ab0b.jpeg

Java特性

封装,继承,多态

谈到Java语言,就会自然的想到:面向对象,符合人们的思维模式。同时,它自身的特性更是被广泛的应用。三大特性:封装、继承、多态。

三大特性

► 概念理解:

1 封装

   主要是用来 隐藏细节,保证安全性

2 继承

   主要是用来子父类之间的继承关系,利于代码规范以及扩展

3 多态

    是以继承为基础,体现的是 类与类之间的关系

    多态,多态,从字面上理解就是:多种表现形态

   (向上转型、向下转型)

很多人也许都有这样的意识,谈到多态,自然而然的脱口而出:重载和重写。在这里,就不纠结多态的具体含义。因为多态从其自身而言,分为了 编译时多态以及运行时多态。在这里,具体的来谈谈重载和重写。

1 重载剖析

2 重写剖析

3d6b51a65b953aa795da6d8b3fff1794.jpeg

前景引入

     自己的下载的英雄联盟8个G,放到了自己定义的磁盘上了,要运行起来这游戏,而自己的内存却只有8个G。运行游戏的时候,是怎么吃我的内存的呢?一次性全部吃完 or 一点点的慢慢吃?


在Java世界里,程序的运行,必然是经过了类的加载机制:加载,验证,解析,准备,初始化,使用,卸载。其中加载的这一过程,就是Java文件编译为了.class文件。这个时候会问一下自己,这个编译的时候会发生什么事情呢?这就涉及到Javac的知识了,这里不做深究。这里只想说,如玩英雄联盟一样,这里的程序是怎样运行的?多态的两种定义:编译时多态,运行时多态到底是怎么一个区分法。重载和重写到底是属于哪一种呢?


重载 (理论以及抽象理解)

一个类里,方法名相同,参数类型以及个数不同即为重载

1  在一个类里 模拟重载 三个同名方法 但参数不同

32b9cc17444b632e3d42ae90bae88173.jpeg

2 这个类,没有main方法,但是调用 javac Main类里面的main方法对这个HelloWorld进行编译

b0ae565a0d110de5b25ab7279e22ed83.jpeg

3 编译结束之后 生成.class文件

ecede6ab62d9cb175068157979ed4eba.jpeg

从生成的.class文件 我们可以看到

《1》 生成了都知道的 默认构造方法

默认构造方法:方法名与类名一致,没有返回值
 public HelloWorld() {
    }

《2》 同时还有三个 同名但参数不一样的方法

public void printTest() {
        System.out.println("无参方法:说好不哭");
    }
    public void printTest(String var1, String var2) {
        System.out.println("参数str: " + var1 + " 参数string: " 
        + var2 + " 说好不哭");
    }
    public void printTest(int var1, String var2) {
        System.out.println("参数a: " + var1 + " 参数string: " 
        + var2 + " 说好不哭");
    }

重载字节码 剖析

字节码剖析class文件

81201bc4b6b41f38afc11f7e9300a409.jpeg

23ba39af0317fed841b2106038e18063.jpeg

从字节码中可以看到:

默认构造方法:
 public HelloWorld() {
    }
    字节码:
  public HelloWorld();
    descriptor: ()V
无参方法
 public void printTest() {
        System.out.println("无参方法:说好不哭");
    }
    字节码:
   public void printTest();
    descriptor: ()V
有参方法 String var1, String var2
public void printTest(String var1, String var2) {
        System.out.println("参数str: " + var1 + " 参数string: " +
         var2 + " 说好不哭");
    }
    字节码:
    public void printTest(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
有参方法 
 public void printTest(int var1, String var2) {
        System.out.println("参数a: " + var1 + " 参数string: " +
         var2 + " 说好不哭");
    }
    字节码:
    public void printTest(int, java.lang.String);
    descriptor: (ILjava/lang/String;)V

综上,在生成的字节码中,每个方法下面都有一个 descriptor

其实这个 descriptor 就是作为 JVM 识别的方法描述符

同一方法名 printTest()

产生的不同 方法描述符

descriptor: ()V

descriptor: (Ljava/lang/String;Ljava/lang/String;)V

descriptor: (ILjava/lang/String;)V

重载 why? 作用 以及总结

所以,为啥要有重载呢?

Java中方法的重载为 :方法名相同,参数不同的方法。

这样设计的好处?

因为 参数的有无以及参数类型的不确定性,但都是针对于同一方法名来说的。意思就是说,比如上方的,同一方法名 printTest(),都是这一动作行为的方法,但是呢?参数类型不一样。像这样的,我们就可以用重载来完成

由上述的 字节码层面看到的:

同一方法名 printTest()

产生的不同 方法描述符

所以,自己就再来自己总结一下,重载 重载 字面上看,无非就是 重新加载喽,既然是加载,是在由 java文件编译为class文件,此时虽然没有被调用,没有被运行,但是实际上 已经产生了 作为该方法的描述符descriptor,有了这个,其实在后续的调用以及运行中,JVM是根据这方法的描述符,去判断调用以及运行这同名方法中的具体其中的哪一个。

再来看看 这个类中有main方法,执行这个类里面的方法的时候

8f7c75a30534cfe44923a60bdc2b8a19.jpeg

HelloWorld helloWorld = new HelloWorld();
 helloWorld.printTest();//调用无参方法
 helloWorld.printTest("杰伦","信");//调用string string 方法
 helloWorld.printTest(1,"信");//调用int stirng 方法

在编译的过程中,已经产生了该方法的描述符

在调用过程中,人为显现的调用(传参 or  不传参)

为什么java中重载是以方法名和参数作为方法的描述符的

因为 在方法调用执行中 有时候 注重执行过程 而不 注重返回值,

比如构造方法,有一个默认的构造方法。但是想对属性做一下赋值,可以重载对属性操作。

所以 用方法名和方法的参数作为方法的描述符

综上 总结

Java中的方法的重载 在编译时期就已经产生了 该方法的描述符(通过字节码可知,每个方法都有自己的描述符)

在执行被调用的的时候,根据方法的描述符和传入的参数选择方法执行

                                        ------------------------------- 编译时多态

所以

重载  是 编译时多态


说完了 重载 接下来 说说 重写

重写(理论介绍 以及 抽象理解)

跟重载一样,这里自己也来说说重写,重写,重写,重新写呗aa0b6cbf45488c5007444ef9a4132807.png

重写Override:在Java中,

1 通过继承 extends  子父类的继承 并且只能是 单继承

   可以在子类中 Override 父类中的方法

   此时是重写父类中已经有的方法

2 通过实现 implements  实现接口 

   可以在该类中 Override 接口中定义的方法

   此时是重写具体的实现方式

区分开来了以上的两点,这里谈的重写,是第一种子父类继承的重写

重载 是强调的是  同一个类里面  方法名相同  参数的类型以及个数不同

那么 重写呢?(继承的重写)

强调的是 不同的类之间的 继承 关系,重写该继承过来的方法

1 三个不同的类,模拟重写

   其中 Child 继承了Parent

f4d3e9ff7ec2f2c8e9587d9d1cf8f316.jpeg

2 编译后,生成了三个class文件

1118f7ba01f3a4f598c25ce8f471a9f0.jpeg

从生成的.class文件 我们可以看到

1 HelloWorld 类
public class HelloWorld {
    public HelloWorld() {
    }
}
2 Parent 类
class Parent {
    Parent() {
    }
    public void testSay(String var1) {
        System.out.println("父亲说:说好不哭");
    }
3 Child 类 (继承了Parent )    
 class Child extends Parent {
    Child() {
    }
    public void testSay(String var1) {
        System.out.println("孩子说:说好不哭");
    }
}

重写 字节码

4ad43a6286d64e783dfeb6bc07d70b00.jpeg

67b6abe47675c87c2945beb5cd0c679f.jpeg

91936c1555022857ef72042e47e33301.jpeg

从字节码文件 我们可以看到

1 HelloWorld
 public HelloWorld();
    descriptor: ()V
2 Parent
  public void testSay(java.lang.String);
    descriptor: (Ljava/lang/String;)V
3 Child(继承了Parent)    
  public void testSay(java.lang.String);
    descriptor: (Ljava/lang/String;)V

同样都会有 descriptor

这个时候,是否会问下:重写也是编译时多态?

其实 这里的 descriptor 并不是判断依据

由上面的可以知道,重载 是编译时多态,是由于在同一个类中,同一的方法名  不同的参数 在编译的时候,就已经确定好了,在调用的时候,通过传递参数就可以具体的指定为 是哪一个方法了。

那么,重写呢?

重写 现在知道的是,在不同的类中,子父类,同样的方法名,子类可以继承后,方法名以及参数不改,修改的是 方法体里面的内容。那么在调用的时候,是否可以知道其具体的是哪一个方法呢?

猜想:如果知道是 具体调用的是哪一个方法,那么就是编译时多态

          否则,就为 运行时多态

既然是 子父类继承,那么父类永远只有一个,但是孩子可以是多个

da9993fbecf7f09b0974db47f8043119.jpeg

673ecb264d61b635de683dc853fb67b1.jpeg

//这里模拟的是  父类的引用 指向到底是哪一个子类的对象
 Parent parent = h.testWay();
 //然后 子类对象的方法打印
 parent.testSay("杰伦");

打印结果剖析

1 由编译时多态(重载)的结论。在调用的时候,根据参数的不同,就已经知道具体的是要调用的是哪一个方法了

2 那么在重写的时候,这个时候调用的时候,由于是子父类继承,方法名以及参数是一样的,这里的关注点是,在调用的时候,是否就已经知道调用的是具体的哪一个方法

3 打印的结果:

   多次运行main方法

  《1》孩子3说:说好不哭

  《2》孩子1说:说好不哭

  《3》孩子2说:说好不哭

  《4》孩子1说:说好不哭

    通过以上的打印结果,运行了 四次 main方法

     每次打印的结果都不一样,这个时候能说,重写是 编译时多态吗?因为 编译时多态,不管运行多少次,打印的结果都是一样的

所以:

     重写(继承关系的重写) 是 运行时多态,因为在编译的时候,确定不好。  

重写 why? 作用 以及总结

综上,重写(继承关系的重写) 是 运行时多态。

//这里模拟的是  父类的引用 指向到底是哪一个子类的对象
 Parent parent = h.testWay();

所以 运行时多态 : 父类的引用  指向了  子类的对象

同时 在撸过的代码中

List<> list  = new  ArrayList<>();

应该说 都遇到过这种写法

撸过ArrayList源码都知道,ArrayList 实现了List接口

所以  接口的引用 指向了 实现该接口的类的对象

这里为啥不是 

List<> list  = new  List<>();  

或者

ArrayList<> list  = new  ArrayList<>();

因为这两种 都是 实例化当前类,是具体的。

设计模式中  "代码尽量依赖于抽象,不依赖于具体"

面向接口,依赖抽象,才是符合思维模式,代码可以方便替换。

再来谈谈 关于重写(继承关系的重写)注意的地方:

1 父类的引用指向子类的对象

ec2cc4050278ea3c78f81287857a05b5.jpeg

至于会打印出来什么?

理解了 父类的引用  指向了 子类的对象

就可以大声的喊出来,打印的会是什么了

父类的引用指向子类的对象,但是调用的方法是子类的,父类没有

a9cf725d94d3aa7dc0f5668fa35a5563.jpeg

这个时候,会问 是否可以编译通过,以及会打印什么

其实,理解了 父类的引用  指向了 子类的对象

就可以大声的说出来了

当 父类的引用  指向了 子类的对象 时,调用的方法 一定要满足 子父类二者中的方法都要有

向上转型  向下转型

说到这个 其实就是形态状态的改变

向上转型   (子类 继承了 父类  ,有了 父类里面的所有方法,子类还可以 定义自己的方法 所以 子类的作用方法范围 必然比 父类大)

                  所以 这里的 向上,指的是  范围的意思

                   既然是 向上(范围变大了)  所以 就是   父类引用 ---指向--》 子类对象

Parent p = new Child();
 //这就叫 upcasting (向上转型)
// 现在 父类引用p 引用指向一个Child 子类对象

那相应的

向下转型

                   既然是 向下(范围变小了 必须要强转)  所以 就是   子类引用 ---指向---》子类对象的父类引用

Child c= (Child )p;   
// 这就叫 downcasting (向下转型)
// 现在p 还是指向 Child 子类对象

注意:

子类的引用 不能指向 父类对象

493edda96b37e7375480f5a9120c35d7.jpeg

288fb2c465d80a4fa9133fb5452944f9.jpeg

不管怎样,通过这样的分析

就可以明确的了解到了

1 多态 ----  编译时多态   运行时多态

2 向上 向下转型

在其他地方,有的也会有说 :多态 是运行时 父类引用指向子类对象。是类与类的关系,而重载 是在一个类中的。所以 重载不是多态。甚至还有 “晚绑定”,“早绑定”。

在这里呢,多态这个概念 每个人的理解不一样,但是,对于原理一定要掌握

所以 Polymorphic 梦里花落知多少,知多知少,原理最重要~

  Hey~ 

我们又见面啦~

你还好吗?

a9d569018168793420f6527ff6d2cdc1.jpeg

b88bf3aea1047c9b27ddff3d8313f443.jpeg

2019.09.22

ffb08451c916c03684aca94bd9c14abb.png

281f14a65046aa33b91e6d2033f78514.gif

喜欢记得来一个

0af220f726d57c2c8e6f22a906017476.gif

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值