重载和重写的实现原理

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语言中重写的本质

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值