本文是读周志明先生著的《深入理解Java虚拟机》一书有感后写的一篇读书笔记。毕竟好记性不如烂笔头,如果想要更多了解相关内容建议去读此书。 言归正传,众所周知,面向对象的三个基本特征是继承,封装,多态,本文将介绍在Java虚拟机中如何实现的“重载”与“重写”
重载
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同,重载方法都必须是独一无二的,最常见的是类构造器重载,重载规则如下
- 被重载的方法必须改变参数列表(参数个数或类型不一样)
- 被重载的方法可以改变返回类型
- 被重载的方法可以改变访问修饰符
- 被重载的方法可以声明新的或更广的检查异常
- 方法能够在同一个类中或者在一个子类中被重载
- 无法以返回值类型作为重载函数的区分标准
看下面的一个实例 ,《深入理解Java虚拟机》书中提到几个概念
“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),静态类型是在编译器可知的,
“Man”则称为变量的实际类型(Actual Type),因为编译程序的时候不知道对象的实际类型是什么,因此实际类型变化的结果在运行期可知,
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
//概念Human静态类型或外观类型 , Man 实际类型
//静态类型编译器可知, 实际类型是运行期才可知
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
//实际类型变化
Human man2 = new Man();
Human woman2 = new Woman();
sr.sayHello((Man) man2);
sr.sayHello((Woman) woman2);
}
}
运行结果
结果为白色字体,圈选后才能看到
hello,guy!
hello,guy!
hello,gentleman!
hello,lady!
上面的示例主要是为了测试使用sayHello重载,使用哪个版本重载完全取决于参数的数量和参数的数据类型,重载是通过参数的静态类型作为判断依据。编译阶段, 编译器就会根据参数的静态类型判断使用的重载版本。
所有依赖静态类型来决定方法版本的分派叫做静态分派,静态分派发生在编译期,很多情况下重载不是惟一的,编译器会按照一定规则找到更加适合的版本,
1.运行的时候因为参数是char类型 a ,那么执行结果是hello char:a
public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello object;"+arg);
}
public static void sayHello(int arg) {
System.out.println("hello int:"+arg);
}
public static void sayHello(char arg) {
System.out.println("hello char:"+arg);
}
public static void main(String[] args) {
sayHello('a');
}
}
2. 注释掉最匹配的char方法,执行结果是hello int:97 ,因为'‘a’ 处除了表示一个字符串还可以代表‘a’的Unicode编码 97
public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello object;"+arg);
}
public static void sayHello(int arg) {
System.out.println("hello int:"+arg);
}
/*
public static void sayHello(char arg) {
System.out.println("hello char:"+arg);
}
*/
public static void main(String[] args) {
sayHello('a');
}
}
3. 注释掉char和int 的方法, 执行结果是hello object;a ,因为char装箱后转型为父类,查找是从下往上找找到了 Object
public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello object;"+arg);
}
/*
public static void sayHello(int arg) {
System.out.println("hello int:"+arg);
}
public static void sayHello(char arg) {
System.out.println("hello char:"+arg);
}
*/
public static void main(String[] args) {
sayHello('a');
}
}
重写
重写(Override)是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变,重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
执行结果
man say hello
woman say hello
woman say hello
虚拟机是如何知道调用那个方法的呢,看看类的字节码
C:\Windows\system32>d:
D:\>javac DynamicDispatch.java
D:\>javap -c DynamicDispatch
Compiled from "DynamicDispatch.java"
public class com.jvm.gc.DynamicDispatch extends java.lang.Object{
public com.jvm.gc.DynamicDispatch();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2; //class com/jvm/gc/DynamicDispatch$Man
3: dup
4: invokespecial #3; //Method com/jvm/gc/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4; //class com/jvm/gc/DynamicDispatch$Woman
11: dup
12: invokespecial #5; //Method com/jvm/gc/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6; //Method com/jvm/gc/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6; //Method com/jvm/gc/DynamicDispatch$Human.sayHello:()V
24: new #4; //class com/jvm/gc/DynamicDispatch$Woman
27: dup
28: invokespecial #5; //Method com/jvm/gc/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6; //Method com/jvm/gc/DynamicDispatch$Human.sayHello:()V
36: return
}
调用方法17行,21行,33行都是使用invokevirtual 指令实现invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回
java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
总结
面试题:重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分