深入java重载和重写的内幕

本文通过具体的Java代码示例介绍了方法重载与重写的区别。重点分析了静态分派(重载)与动态分派(重写)在字节码层面的不同表现形式,并深入探讨了它们的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、重载

Method Overload Resolution,也称Dispath,国内一般翻译为“静态分派”,但是严格来说,一般很少用再静态环境中。

1、静态类方法中的演示

package jdbc.test.es;

/**
 * 方法静态分派演示
 * @author Administrator
 *
 */
public class MyStaticTest {

    static abstract class Human{

    }

    static class Man extends Human{

    }
    static class Woman extends Human{

    }
    public void sayHell(Human guy) {
        System.out.println("hello guy!");
    }
    public void sayHell(Man guy) {
        System.out.println("hello man!");
    }
    public void sayHell(Woman guy) {
        System.out.println("hello lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        MyStaticTest sr = new MyStaticTest();
        sr.sayHell(man);
        sr.sayHell(woman);

//      MyStaticTest sr = new MyStaticTest();
//      Human man = new Man();
//      Human woman = new Woman();
//      sr.sayHell((Man)man);
//      sr.sayHell((Woman)woman);
    }
}

main函数的数据结果:

hello guy!
hello guy!

但是把main函数中的内容换成注释的内容则结果如下:

hello man!
hello lady!

2、非静态类方法中的演示

package jdbc.test.es;

public abstract class Human1 {

    public Human1() {
        // TODO Auto-generated constructor stub
    }

}
package jdbc.test.es;

public class Man1 extends Human1 {

    public Man1() {
        // TODO Auto-generated constructor stub
    }

}
package jdbc.test.es;

public class Woman1  extends Human1 {

    public Woman1() {
        // TODO Auto-generated constructor stub
    }

}
package jdbc.test.es;
import org.junit.Test;

public class MyTest1 {

    public void sayHell(Human1 guy) {
        System.out.println("hello guy!");
    }
    public void sayHell(Man1 guy) {
        System.out.println("hello man!");
    }
    public void sayHell(Woman1 guy) {
        System.out.println("hello lady!");
    }

    @Test
    public void test1(){
        Human1 man = new Man1();
        Human1 woman = new Woman1();
        MyTest1 sr = new MyTest1();
        sr.sayHell(man);
        sr.sayHell(woman);

    }
}

执行结果如下:

hello guy!
hello guy!

同样把test1()的内容换成

Human1 man = new Man1();
Human1 woman = new Woman1();
MyTest1 sr = new MyTest1();
sr.sayHell((Man1)man);
sr.sayHell((Woman1)woman);

执行结果如下:

hello man!
hello lady!

3、解释原理

可能在面试的时候,包括面试官在内,不一定知道原理,只是照本宣科的问问重载如何来区别的,比如参数个数,参数类型,以及能不能用会返回值来区别等。这些都是比较浅层次的东西,如果你能把内部原理的实现说出来,无疑就会脱颖而出。
其实对java虚拟机有一些了解的人都知道,在类的加载中,有个比较有名的模型,叫做双亲委派模型,类加载器是通过复合的方式而非继承来灵活实现双亲委派的,首先类加载器会递归到最上层的启动类加载器(Bootstrap ClassLoader)开始,在逐层来实现类的初始化(如果在某层能实现类的初始化,就直接返回结果,不行就逐层往下,直到成功为止)。
再有些了解的人都知道,虚拟机加载的过程中,类/接口,以及类常量都是等都是在编译期间已经完成了,重载实际上不是由虚拟机来执行的。静态方法、私有方法、实例构造器、父类方法(统称非虚方法),他们在类加载的时候就会把符号引用解析为该方法的直接引用(也包括final修饰的方法,虽然final方法是invokevirtural修饰的),所以可以看到没有强制转换类型的就会默认为编译期间的Human类,所以输入man和women其实调用的都是human匹配的方法。这符合“编译期可知,运行期不可变”的要求。编译为字节码的实现是调用invokestatic,invokespecial指令来实现的(感兴趣的可以用javac之后javap来查看详细信息)。

一、重写

1、代码演示

package jdbc.test.es;

/**
 * 方法静态分派演示
 * @author Administrator
 *
 */
public class MyStaticTest {

    static abstract class Human{
        protected abstract void sayHello();
    }

    static class Man extends Human{

        @Override
        protected void sayHello() {
            // TODO Auto-generated method stub
            System.out.println("hi man");
        }

    }
    static class Woman extends Human{

        @Override
        protected void sayHello() {
            // TODO Auto-generated method stub
            System.out.println("hi lady");
        }

    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();

    }
}

执行结果如下:

hi man
hi lady

2、原理剖析

javac MyStaticTest.java
javap -verbose MyStaticTest
通过javac编译之后,用javap查看详细信息如下:

Classfile /F:/Dev/myWorkSpace/jdbc.test.es/src/test/java/jdbc/test/es/MyStaticTest.class
  Last modified 2017-11-21; size 535 bytes
  MD5 checksum ecee8b49f9300fcbbb9c27aa2428e8a2
  Compiled from "MyStaticTest.java"
public class jdbc.test.es.MyStaticTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // jdbc/test/es/MyStaticTest$Man
   #3 = Methodref          #2.#22         // jdbc/test/es/MyStaticTest$Man."<init>":()V
   #4 = Class              #24            // jdbc/test/es/MyStaticTest$Woman
   #5 = Methodref          #4.#22         // jdbc/test/es/MyStaticTest$Woman."<init>":()V
   #6 = Methodref          #12.#25        // jdbc/test/es/MyStaticTest$Human.sayHello:()V
   #7 = Class              #26            // jdbc/test/es/MyStaticTest
   #8 = Class              #27            // java/lang/Object
   #9 = Utf8               Woman
  #10 = Utf8               InnerClasses
  #11 = Utf8               Man
  #12 = Class              #28            // jdbc/test/es/MyStaticTest$Human
  #13 = Utf8               Human
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               MyStaticTest.java
  #22 = NameAndType        #14:#15        // "<init>":()V
  #23 = Utf8               jdbc/test/es/MyStaticTest$Man
  #24 = Utf8               jdbc/test/es/MyStaticTest$Woman
  #25 = NameAndType        #29:#15        // sayHello:()V
  #26 = Utf8               jdbc/test/es/MyStaticTest
  #27 = Utf8               java/lang/Object
  #28 = Utf8               jdbc/test/es/MyStaticTest$Human
  #29 = Utf8               sayHello
{
  public jdbc.test.es.MyStaticTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
        line 23: 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class jdbc/test/es/MyStaticTest$Man
         3: dup
         4: invokespecial #3                  // Method jdbc/test/es/MyStaticTest$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class jdbc/test/es/MyStaticTest$Woman
        11: dup
        12: invokespecial #5                  // Method jdbc/test/es/MyStaticTest$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method jdbc/test/es/MyStaticTest$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method jdbc/test/es/MyStaticTest$Human.sayHello:()V
        24: return
      LineNumberTable:
        line 34: 0
        line 35: 8
        line 36: 16
        line 37: 20
        line 39: 24
}
SourceFile: "MyStaticTest.java"
InnerClasses:
     static #9= #4 of #7; //Woman=class jdbc/test/es/MyStaticTest$Woman of class jdbc/test/es/MyStaticTest
     static #11= #2 of #7; //Man=class jdbc/test/es/MyStaticTest$Man of class jdbc/test/es/MyStaticTest
     static abstract #13= #12 of #7; //Human=class jdbc/test/es/MyStaticTest$Human of class jdbc/test/es/MyStaticTest

可以看到在mian方法调用的的实现上用的是invokevirtual 指令,而这个指令的含义就是在运行期间确定接收者的实际类型,所以两次实际调用会把常量池中的类方法应用解析到了不同的直接应用上,这个过程就是java重写的本质。
这也就是java开发人员熟悉的重写。
当然,重载的编译字节码文件,我没有详细拿出来分析,感兴趣也可以javac之后用javap查看详细信息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值