重载和重写的实现原理

Java具有面向对象的三大特征:继承、封装、和多态。多态性特征的最基本体现有“重载”和“重写”,其实这两个体现在Java虚拟机中时分派的作用。

分派又分为静态分派和动态分派,静态分派是指所有依赖静态类型来定位方法执行版本的分派动作,动态分派是指在运行期根据实际类型确定方法执行版本的分派过程。

 Animal animal = new Bird();

  在上面代码中Animal是父类,Bird是继承Animal的子类;那么在定义animal对象时前面的“Animal”称为变量的静态类型(Static Type),或者叫外观类型(Apparent Type),后面的“Bird”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译器可知的;而实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际对象是什么。例如下面代码:

//实际类型变化
Animal bird = new Bird();
Animal eagle = new Eagle();
//静态类型变化
sd.sayHello((Bird)bird);
sd.sayHello((Eagle)eagle);

 1.重载

我们先看一段代码,想想会输出什么,然后围绕该类的重载方法来分析。

package test;

/**
* @Description: 方法静态分派演示
* @version: v1.0.0
 */
public class StaticDispatch {
    
	static abstract class Animal{
		
	}
	
	static class Bird extends Animal{
		
	}
	
	static class Eagle extends Animal{
		
	}
	
	public void sayHello(Animal animal) {
		System.out.println("hello,animal");
	}
	
	public void sayHello(Bird bird) {
		System.out.println("hello,I'm bird");
	}
	
	public void sayHello(Eagle eagle) {
		System.out.println("hello,I'm eagle");
	}
	
	public static void main(String[] args){
	    Animal bird = new Bird();
	    Animal eagle = new Eagle();
	    StaticDispatch sd = new StaticDispatch();
	    sd.sayHello(bird);
	    sd.sayHello(eagle);
	}
}

运行结果:

hello,animal
hello,animal

看到输出内容之后我们知道,程序选择了参数类型为Animal的重载;在main()里面的两次sayHello()方法的调用,在方法接收者已经确定是对象“sd”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确的是编译器)在重载时是通过参数的静态类型而不是实际类型来作为判断依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,因此选择了sayHello(Animal)作为调用目标。

方法重载是通过静态分派实现的,并且静态分派是发生在编译阶段,所以确定静态分派的动作实际上不是由虚拟机来执行的;另外,编译器虽然能确定出方法重载的版本,但在很多情况下这个版本并不是“唯一的”,往往只能确定一个“更加适合”的版本。

2.重写

    方法的重写与虚拟机中动态分派的过程有着密切联系。再看一段代码例子

package test;

/**
* @Description: 方法动态分派演示
* @version: v1.0.0
 */
public class DynamicDispatch {
    
	static abstract class Animal{
		protected abstract void sayHello();
	}
	
	static class Bird extends Animal{

		@Override
		protected void sayHello() {
			System.out.println("Bird say hello");
		}
		
	}
	
	static class Eagle extends Animal{
		@Override
		protected void sayHello() {
			System.out.println("Eagle say hello");
		}
		
	}
	
	public static void main(String[] args){
	    Animal bird = new Bird();
	    Animal eagle = new Eagle();
	    bird.sayHello();
	    eagle.sayHello();
	    bird = new Eagle();
	    bird.sayHello();
	}
}

运行结果:

Bird say hello
Eagle say hello
Eagle say hello

这是一个经典的学习多态的例子,相信很多人都知道结果,但是我们我们现在要知道虚拟机是如何知道要调用哪个方法的。显然这里不是通过静态类型来确定的,因为静态类型都相同的两个对象调用同一个方法时执行了不同的行为。看了类型的讲解我们知道是因为这两个变量实际类型不同。我们通过javap反编译命令来看一段该代码的字节码:

 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           #16                 // class test/DynamicDispatch$Bird
        3: dup
        4: invokespecial #18                 // Method test/DynamicDispatch$Bird."<init>":()V
        7: astore_1
        8: new           #19                 // class test/DynamicDispatch$Eagle
       11: dup
       12: invokespecial #21                 // Method test/DynamicDispatch$Eagle."<init>":()V
       15: astore_2
       16: aload_1
       17: invokevirtual #22                 // Method test/DynamicDispatch$Animal.sayHello:()V
       20: aload_2
       21: invokevirtual #22                 // Method test/DynamicDispatch$Animal.sayHello:()V
       24: new           #19                 // class test/DynamicDispatch$Eagle
       27: dup
       28: invokespecial #21                 // Method test/DynamicDispatch$Eagle."<init>":()V
       31: astore_1
       32: aload_1
       33: invokevirtual #22                 // Method test/DynamicDispatch$Animal.sayHello:()V
       36: return

0~15行的字节码是准备动作,作用时建立bird和eagle的内存空间、调用Bird和Eagle类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表Slot之中,这个动作也就对应了代码中的这两句:

	    Animal bird = new Bird();
	    Animal eagle = new Eagle();

接下来的16~21行时关键部分;这部分把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);17和21句是方法调用命令,这两条调用命令单从字节码角度来看,无论是指令(invokevirtual)还是参数(都是常量池中第22项的常量,注释显示了这个常量是sayHello方法的符号引用)完全一样的,但是这两句执行的目标方法并不同,这是因为invokevirtual指令的多态查找过程引起的,该指令运行时的解析过程可分为以下几个步骤:

  1. 找到操作数栈第一个元素所指向的对象的实际类型,记为C。
  2. 如果在类型C中找到了与常量中描述符和简单名称都一样的方法,则进行访问权限校验,如果通过则返回该方法的的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终都没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中重写的本质

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
重载(Overloading)和重写(Overriding)是Java中两个重要的方法相关的机制。 重载是指在一个类中定义多个方法名相同但参数列表不同的方法。重载方法通过参数的类型、个数或顺序的不同来区分。在Java中,编译器会根据调用的方法的参数类型来确定具体调用哪个重载方法。重载方法并不会改变方法的返回值类型。 重载实现原理是通过在编译阶段静态绑定生成不同的字节码来实现。编译器会根据调用方法的参数类型和数量的不同来选择合适的方法。 重写是指子类中定义一个与父类中方法名、返回值类型和参数列表完全相同的方法,并在该方法内部提供新的实现。重写方法通过动态绑定在运行时确定具体调用哪个方法。重写方法必须与父类方法具有相同的返回值类型或其子类型。 重写实现原理是通过在运行时动态绑定在方法调用时确定具体调用的方法。在调用重写方法时,Java虚拟机会根据实际对象的类型来选择合适的方法。 Java中的方法表是一个用于存储类的方法信息的数据结构,包括方法名、返回值类型、参数列表、访问修饰符等。每个类都有一个方法表来记录其方法的信息。 当调用一个方法时,Java虚拟机会根据方法的调用方式(静态绑定还是动态绑定)来查找方法表,找到对应的方法并执行。对于重载方法,Java虚拟机会根据方法的参数类型来选择合适的重载方法。 总结起来,重载重写是Java中处理方法多态性的两个机制。重载通过编译器的静态绑定实现,重写通过运行时的动态绑定实现。方法表是记录类方法信息的数据结构,用于在方法调用时确定具体的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值