引子:
类的方法重写、属性不重写,只是类加载机制的外在表现形式。
1. 混淆
/**
* OverrideIntro.java分支:
* 3.重写易混淆的点
*/
public class OverrideConfuse{
public static void main(String[] args){
Rose rose = new Rose();
System.out.println(rose.name);
rose.statement();
}
}
//花类
class Flower{
public String name = "花";
public void statement(){
System.out.println("Flower类statement方法...");
}
}
//玫瑰类
class Rose extends Flower{
public String name = "玫瑰";
public void statement(){
System.out.println("Rose类statement方法...");
}
}
//以下是输出结果
玫瑰
Rose类statement方法...
- 上述代码中,子类拥有父类的同名属性及方法,用子类对象访问该属性、方法时,均表现子类的成员。外在表现形式上,父类的属性及方法都被覆盖了,这就是为什么我们容易认为属性也会被重写。
2. 疑惑
/**
* OverrideIntro.java分支:
* 3.重写易混淆的点
*/
public class OverrideConfuse{
public static void main(String[] args){
//Rose向上转型
Flower roseUpFlower = new Rose();
System.out.println(roseUpFlower.name);
roseUpFlower.statement();
}
}
//花类
class Flower{
public String name = "花";
public void statement(){
System.out.println("Flower类statement方法...");
}
}
//玫瑰类
class Rose extends Flower{
public String name = "玫瑰";
public void statement(){
System.out.println("Rose类statement方法...");
}
}
-
上述代码会输出什么呢?先仔细思考
-
输出结果:
花 Rose类statement方法...
-
继承的同名属性、方法有了不同的表现形式,这暗示两者的底层机制是完全不同的。
3. 死记硬背
//Rose向上转型
Flower roseUpFlower = new Rose();
- 上述代码中,
roseUpFlower
的编译类型是Flower
,运行类型是Rose
- 属性的查找看编译类型,方法的查找看运行类型
- 在多态的向上转型中,同名方法表现出了重写的现象,同名属性没有重写的现象。
4. 深入理解
- 问题:为什么属性访问看编译类型,方法访问看运行类型 ?
- 【1.混淆】中的代码(不带转型)在JVM内存中的分布情况如下:
上图解析:
- 继承中,方法继承的本质是添加了方法查询链,属性继承的本质是在子类对象中开辟空间存储父类属性。
- 对象引用在调用方法时,需要从具有继承关系的方法链的某个位置进入查询,这个位置是与对象的地址空间绑定的,这就是多态的动态绑定机制。此例中,对象引用的编译类型和运行类型一致,该特性难以表现。
- 对象引用在调用属性时,没有动态绑定机制,属性也没有重写一说,编译类型是什么类型,就调用什么类的属性;方法在哪个类中,就调用哪个类中的属性。
- 【2.疑惑】中的代码(向上转型)在JVM内存中的分布如下
上图解析:
- 向上转型的引用roseUpFlower具有多态,即编译类型是父类Flower,运行类型是子类Rose,roseUpFlower对于方法、属性的访问就耐人寻味了。
- 按照我们上面所说,对象引用在调用方法时,经由对象的内存地址绑定的方法查询入口来查找方法,也就是仍然从子类Rose开始查找方法,找到了子类的statement(),便不再向父类查找,这就实现了重写。
- roseUpFlower在调用属性时,由于引用的编译类型是Flower,也就是说访问对象空间中的Flower.name属性,也就找到了父类的同名属性,宏观上看来,访问属性没有动态绑定。
- 动态绑定(运行时绑定、后绑定)总结:
- 对象在调用方法时,方法查询入口与该对象的内存地址(运行类型)绑定.
- 当调用属性时,没有动态绑定机制,引用是什么编译类型就调用那个类的属性,方法在哪个类中,方法中使用的属性就调用那个类的属性.
5. 测试实例
public class DynamicBinding{
public static void main(String[] args){
A a = new B();//向上转型
System.out.println(a.sum());
System.out.println(a.sum1());
}//end main
}//end class
//非常经典的动态绑定例子...
class A{//父类
public int i = 10;
public int sum(){ return getI() + 10; }
public int sum1(){ return i + 10; }
public int getI(){ return i; }
}
class B extends A{//子类
public int i = 20;
public int sum(){ return i + 20; }
public int sum1(){ return i + 20; }
@Override
public int getI(){ return i; }
}
测试问题:
-
上述代码输出什么?
40 40 解析:动态绑定,虽然编译类型是父类,但是调用方法绑定了对象的内存地址,对象是子类,自然调用子类的方法。 在子类中,可以找到要调用的方法,于是不向父类访问,实现了重写。
-
注销A类中的sum()方法后,输出什么?
30 40 解析: 1> a.sum()的过程如下: 第一步:动态绑定机制,要求先去子类中寻找是否有sum方法, 由于被注销,没找到,于是启动继承机制,沿着继承方法链向上查找父类中是否有sum方法。 第二步:父类中找到了sum()方法,但是sum中调用了getI()方法,此时仍然启动动态绑定机制,仍然经由绑定对象地址的入口去查询方法, 自然也是先去子类中查找getI()方法,找到了。 第三步:计算,20 + 10 = 30 2> a.sum1()直接在子类中可找到比较简单。
-
再注销A类中的sum1()方法后,输出什么?
30 20 解析: 1> a.sum()的解析与上面一样 2> a.sum1()的过程如下: 第一步:动态绑定机制,要求先去子类中寻找是否有sum方法, 由于被注销,没找到,于是启动继承机制,沿着继承方法链向上查找父类中是否有sum1()方法。 第二步:父类中找到了sum1()方法,但是它调用了一个属性i 由于属性不动态绑定,在什么类中使用,就直接调用那个类的方法,自然是使用父类中的i = 10 第三步:计算,10 + 10 = 20
-
继续注销A类中的getI()方法后,输出什么?
20 20 解析:你自己来吧^ V ^
6.画龙点睛
- 所以,为什么方法会出现重写,属性不会出现重写?
- 从技术机制来讲,子类继承父类时,属性继承与方法继承是不同的底层机制:
1.1 属性继承:子类对象中会开辟空间储存从父类那里继承来的属性,即使同名也会继承过来写入内存,这是实实在在的继承,底层操作与概念一致。
1.2 方法继承:方法继承的本质是,建立一条子类与父类的方法访问链条,实际上是在子类中新建了一个同名方法(),新建的方法实际上并没有覆盖掉父类的方法,只是阻塞了方法链条,使得访问方法时,在子类中就可以访问到该方法了,不会再顺链条往上查找,从宏观来看,就好像父类的方法被覆盖了。 - 从使用角度来看,
2.1 子类的同名方法是为了个性化,动态绑定机制有助于使用父类的同一方法名,在不同的子类对象中表现出不一样的特性(自然世界也是如此,动物叫是一个行为,但是细化到子类猫就是“喵喵喵”,子类狗就是“汪汪汪”)