测试案例
重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里式替换原则,重写有以下三个限制:
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中:
- 子类方法访问权限为 public,大于父类的 protected。
- 子类的返回类型为 ArrayList,是父类返回类型 List 的子类。
- 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
- 子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。
class SuperClass {
protected List<Integer> func() throws Throwable {
return new ArrayList<>();
}
}
class SubClass extends SuperClass {
@Override
public ArrayList<Integer> func() throws Exception {
return new ArrayList<>();
}
}Copy to clipboardErrorCopied
在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有查找到再到父类中查看,看是否有继承来的方法。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:
- this.func(this)
- super.func(this)
- this.func(super)
- super.func(super)
/*
A
|
B
|
C
|
D
*/
class A {
public void show(A obj) {
System.out.println("A.show(A)");
}
public void show(C obj) {
System.out.println("A.show(C)");
}
}
class B extends A {
@Override
public void show(A obj) {
System.out.println("B.show(A)");
}
}
class C extends B {
}
class D extends C {
}Copy to clipboardErrorCopied
public static void main(String[] args) {
A a = new A();
B b = new B();
C c = new C();
D d = new D();
// 在 A 中存在 show(A obj),直接调用
a.show(a); // A.show(A)
// 在 A 中不存在 show(B obj),将 B 转型成其父类 A
a.show(b); // A.show(A)
// 在 B 中存在从 A 继承来的 show(C obj),直接调用
b.show(c); // A.show(C)
// 在 B 中不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其父类 C
b.show(d); // A.show(C)
// 引用的还是 B 对象,所以 ba 和 b 的调用结果一样
A ba = new B();
ba.show(c); // A.show(C)
ba.show(d); // A.show(C)
}
2. 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
深度解析:
1. 解析方式
分为动态解析和静态解析。静态解析是有些符号引用,在类加载阶段或者第一次使用时,转化为直接引用;
另外一些符号引用则是在每次运行期间转化为直接引用,这种引用叫做动态解析,体现了java的多态性。
静态解析的四种情形:(invokestatic,invokespecial)
1.静态方法
2.父类方法
3.构造方法
4.私有方法(不可以被重写)
他们是在类加载阶段,就将符号应用转换为直接引用的
2.重载
重载对与类的解析方式则是属于静态解析,是在类加载的过程中将符号引用,转变为具体的直接引用,指向具体
方法。重载是个类中含有多个同名方法,参数个数、类型、顺序不同的方法,invokevirtual在重载方法中的作用,
则是根据是根据参数的静态类型去调用重载方法。
查看代码的执行结果:
遮挡住执行结果. method()
方法是重载方法,参数不同,可以传递3中类型的参数,在主函数中调用方法,参数分别是Grandpa g1 = new Father(); Grandpa g2 = new Son();
执行结果,可以看到,定位的方法都是method(Grandpa g);
方法的静态分配
Grandpa g1 = new Father();
以上代码,g1的静态类型是Grandpa,而实际类型(真正指向的类型)是Father。
结论:
执行结果都是执行的Grandpa,根据上面的解释,这属于方法的静态分配,重载方法的分配,
是根据变量的静态类型确定的,在编译期间那参数的静态类型去和方法中的参数类型进行匹配。
结论
一个类中含有多个同名方法,参数个数、类型、顺序不同的方法是方法的重载方法, 对于重载的方法调用,是一种
静态行为,是根据参数的静态类型去调用重载方法。`对于jvm,方法重载,是一种静态行为,是根据变量的类型去
确定方法,在加载期间,确定调用那个重载方法(是根据参数类型,调用那个重载方法,不是根据对象类型,去调
用对象的具体方法,这里是静态的。
可以参看代码
package byteCode;
/***
* * <p>
* * 符号引用 直接引用
* * <p>
* * 有些符号引用在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析,;
* * 另外一些符号引用则是在每次运行期间转换为直接引用,这种转换叫做动态解析,这体现了
* * java的多态性。
* *
* * 静态解析的四种情形:(invokestatic,invokespecial)
* * 1.静态方法
* * 2.父类方法
* * 3.构造方法
* * 4.私有方法(不可以被重写)
* * 他们是在类加载阶段,就将符号应用转换为直接引用的
*/
/**
* 方法重载与invokeVirtual字节码指令的关系:
* <p>
* 方法的静态分配:
* <p>
* Grandpa g1 = new Father();
* <p>
* 以上代码,g1的静态类型是Grandpa,而实际类型(真正指向的类型)是Father。
* 结论:
* 变量的静态类型是不会发生变化的,而变量的实际类型会随着实际类型的而变化(多态的体现),
* 实际类型是在方法运行期间方可确定的。
* <p>
* 一个类中含有多个同名方法,参数个数、类型、顺序不同的方法是方法的重载方法,
* 对于重载的方法调用,是一种静态行为,是根据参数的静态类型去调用重载方法。
* `对于jvm,方法重载,是一种静态行为,是根据变量的类型去确定方法,在加载期间,
* 确定调用那个重载方法(是根据参数类型,调用那个重载方法,不是根据对象类型,去调用对象的具体方法,这里是静态的)`。
*/
public class Demo5 {
public void method(Grandpa g) {
System.out.println("Grandpa");
}
public void method(Father f) {
System.out.println("Father");
}
public void Son(Son s) {
System.out.println("Son");
}
/*
method方法是重载的,创建两个对象,静态类型是Grandpa,实际类型是Father和Son
调用method方法,执行结果?
*/
/**
* 执行结果都是执行的Grandpa,根据上面的解释,这属于方法的静态分配,重载方法的分配,是根据变量的静态类型确定的,
* 在编译期间那参数的静态类型去和方法中的参数类型进行匹配。
*
* 》》》 字节码层次的分析是,这个invokevirtual指令,直接获取传入参数的静态类型,然后去确定调用的那个具体的重载方法,然后指令
* 在根据确定的方法,进行从低向上的实际对象的多态流程查找。
*/
public static void main(String[] args) {
Grandpa g1 = new Father();
Grandpa g2 = new Son();
Demo5 d = new Demo5();
d.method(g1);
d.method(g2);
/*
Grandpa
Grandpa
*/
}
/**字节码
* 0 new #7 <byteCode/Father>
* 3 dup
* 4 invokespecial #8 <byteCode/Father.<init>>
* 7 astore_1
*
* 8 new #9 <byteCode/Son>
* 11 dup
* 12 invokespecial #10 <byteCode/Son.<init>>
* 15 astore_2
*
* 16 new #11 <byteCode/Demo5>
* 19 dup
* 20 invokespecial #12 <byteCode/Demo5.<init>>
* 23 astore_3
*
* //invokevirtual: 调用虚方法,运行期间动态查找的过程,用静态类型去判定
* 24 aload_3
* 25 aload_1
* 26 invokevirtual #13 <byteCode/Demo5.method>
*
* 29 aload_3
* 30 aload_2
* 31 invokevirtual #13 <byteCode/Demo5.method>
* 34 return
*/
}
class Grandpa {
}
class Father extends Grandpa {
}
class Son extends Father {
}
3.重写
方法的动态分配:
方法的动态分派涉及到一个重要概念:方法的接收者。
invokevirtual字节码指令多态查找流程:
1.去操作数栈的栈顶,找到到栈顶的元素,然后获取栈顶元素的实际指向对象的实际类型
2.根据实际对象的类型,在这个类型中会去寻找invokevirtual参数中的方法(方法名称和描述符一样),如果权限通
过,则直接返回这个对象的类型的方法的引用。
3.如果实际对象类型中找寻不到,则会根据当前实际对象继承体系去向上寻找,寻找父类中是否有方法,查找到,
则返回方法引用,否则抛出异常;
package byteCode;
/**
* 方法重写与invokeVirtual字节码指令的关系:
* <p>
* 方法的动态分配:
* <p>
* 方法的动态分派涉及到一个重要概念:方法的接收者。
* <p>
* invokevirtual字节码指令多态查找流程:
* 1.去操作数栈的栈顶,找到到栈顶的元素,然后获取栈顶元素的实际指向对象的实际类型
* 2.根据实际对象的类型,在这个类型中会去寻找invokevirtual参数中的方法(方法名称和描述符一样),如果权限通过,则直接返回这个
* 对象的类型的方法的引用。
* 3.如果实际对象类型中找寻不到,则会根据当前实际对象继承体系去向上寻找,寻找父类中是否有方法,查找到,则返回方法
* 引用,否则抛出异常;
*
* 可以看到下面字节码第17行和21行,他们有相同的符号引用,在运行期间,被解析到不同的直接引用上面了,
*
*
* 重点:
* 方法重载和方法重写的区别:
*
* 1.调用方:
* 重载对方法调用是同一个对象纠结调用那一个方法,是同一个对象中,确定调用哪一个重载方法。
* 重写是纠结哪一个对象去调用这个方法,是有一个方法,确定是那个对象来调用了。
* 2.执行顺序:
* 重载是在编译期间确定使用哪一个具体的方法(确定方法描述符),是先于重写的,他不需要运行就可以确定,是根据参数的静态类型。
* 重写则是在运行期间确定这个方法具体由那个对象调用,是有对象本身还是有父类对象,会根据多态查找流程去确定调用对象。
* 联系:
* 方法的重载执行先于重写,先通过重载,去确定执行的方法的名称,描述符,访问权限等方法标识,然后通过这个确定的方法,去寻找
* 具体的执行对象。
*
*/
public class Demo6 {
/**
* 0 new #2 <byteCode/Apple> //1.分配内存空间
* 3 dup //压栈
* 4 invokespecial #3 <byteCode/Apple.<init>> //2.调用构造方法
* 7 astore_1 //3.存储对象的引用变量
* <p>
* 8 new #4 <byteCode/Bear>
* 11 dup
* 12 invokespecial #5 <byteCode/Bear.<init>>
* 15 astore_2
* <p>
* 16 aload_1
* 17 invokevirtual #6 <byteCode/Fruit.test>
* 20 aload_2
* 21 invokevirtual #6 <byteCode/Fruit.test>
* 24 return
*/
public static void main(String[] args) {
Fruit f1 = new Apple();
Fruit f2 = new Bear();
f1.test();
f2.test();
/*
Apple
Bear
*/
}
}
class Fruit {
public void test() {
System.out.println("Fruit");
}
}
class Apple extends Fruit {
public void test() {
System.out.println("Apple");
}
}
class Bear extends Fruit {
public void test() {
System.out.println("Bear");
}
}
方法重载和方法重写的区别:
* 1.调用方:
* 重载对方法调用是同一个对象纠结调用那一个方法,是同一个对象中,确定调用哪一个重载方法。
* 重写是纠结哪一个对象去调用这个方法,是有一个方法,确定是那个对象来调用了。
* 2.执行顺序:
* 重载是在编译期间确定使用哪一个具体的方法(确定方法描述符),是先于重写的,他不需要运行就可以确定,是根据参数的静态类型。
* 重写则是在运行期间确定这个方法具体由那个对象调用,是有对象本身还是有父类对象,会根据多态查找流程去确定调用对象。
* 联系:
* 方法的重载执行先于重写,先通过重载,去确定执行的方法的名称,描述符,访问权限等方法标识,然后通过这个确定的方法,去寻找
* 具体的执行对象。