文档:https://files.cnblogs.com/files/blogs/847696/动手动脑4.zip?t=1761298729&download=true

class Parent {
int myValue = 0;
public void printValue() {System.out.println("Parent: myValue = " + myValue);
}
}
class Child extends Parent {
int myValue = 1; // 隐藏父类的myValue
@Override
public void printValue() {System.out.println("Child: myValue = " + myValue);
}
}
- 左边的程序运行结果是什么?
运行ParentChildTest类的main方法,输出结果如下:
Parent: myValue = 0
Child: myValue = 1
Child: myValue = 1
Child: myValue = 1
Child: myValue = 2
2. 你如何解释会得到这样的输出?
输出结果可以通过Java的变量隐藏和方法重写机制来解释:
第1行输出:Parent parent = new Parent(); parent.printValue(); 创建Parent对象并调用printValue,输出Parent类的myValue(初始值为0)。
第2行输出:Child child = new Child(); child.printValue(); 创建Child对象并调用printValue,输出Child类的myValue(初始值为1)。注意,Child类重写了printValue方法,因此调用的是子类的方法。
第3行输出:parent = child; parent.printValue(); 将parent引用指向Child对象。由于方法调用是动态绑定的,parent.printValue()会调用Child类的printValue方法,输出Child类的myValue(仍为1)。
第4行输出:parent.myValue++; parent.printValue(); 通过parent引用访问myValue。变量访问是静态绑定的,基于引用类型(Parent),因此parent.myValue++增加的是Parent类的myValue变量(从0变为1),但Child类的myValue未被修改。所以parent.printValue()仍然输出Child类的myValue(1)。
第5行输出:((Child)parent).myValue++; parent.printValue(); 将parent强制转换为Child类型,然后访问myValue。这时访问的是Child类的myValue变量,将其从1增加到2。因此parent.printValue()输出Child类的myValue(2)。
- 从这些运行结果中,你能总结出Java的哪些语法特性?
从运行结果中可以总结出Java的以下语法特性:
继承:子类(如Child)可以继承父类(如Parent)的字段和方法。
方法重写(Override):子类可以重写父类的方法。在运行时,方法调用根据实际对象类型动态绑定,即多态性的体现。例如,parent.printValue()在parent指向Child对象时调用的是Child类的printValue方法。
变量隐藏(Field Hiding):如果子类定义了与父类同名的实例变量,则子类的变量会隐藏父类的变量。变量访问是静态绑定的,基于引用类型(编译时类型),而不是实际对象类型。例如,通过Parent引用访问myValue时,总是访问Parent类的变量;通过Child引用访问时,总是访问Child类的变量。
类型转换:可以将父类引用强制转换为子类类型(向下转型),以访问子类特有的成员。但需要确保实际对象是子类类型,否则会抛出ClassCastException。例如,((Child)parent).myValue++允许访问Child类的myValue。
多态:方法调用表现出多态行为,但字段访问没有多态性。这强调了Java中方法和字段在继承体系中的不同处理方式。
代码测试验证
class Parent {
int myValue = 0;
public void printValue() {System.out.println("Parent: myValue = " + myValue);
}
}
class Child extends Parent {
int myValue = 1;
@Override
public void printValue() {System.out.println("Child: myValue = " + myValue);
}
}
public class ParentChildTest {
public static void main(String[] args) {
Parent parent = new Parent();
parent.printValue();
Child child = new Child();
child.printValue();
parent = child;
parent.printValue();
parent.myValue++;
parent.printValue();
((Child) parent).myValue++;
parent.printValue();
}
}

class Mammal {}
class Dog extends Mammal {}
class Cat extends Mammal {}
public class TestCast {
public static void main(String args[]) {
Mammal m;
Dog d = new Dog();
Cat c = new Cat();
m = d;
// d = m; // 语句2
d = (Dog) m; // 语句3
// d = c; // 语句4
// c = (Cat) m; // 语句5
}
}
分析语句:
m = d;
这是向上转型(upcasting),将子类引用赋值给父类引用。在Java中,这是安全的,不会引起编译错误或运行时错误。
d = m;
这将父类引用赋值给子类引用。由于父类引用可能指向其他子类对象(如Cat),编译器无法保证类型安全,因此会引发编译错误。错误信息类似于:incompatible types: Mammal cannot be converted to Dog。
d = (Dog) m;
这是向下转型(downcasting),将父类引用强制转换为子类类型。在编译时不会错误,但运行时行为取决于m实际指向的对象。在代码中,m被赋值为d(Dog对象),因此转换是安全的,不会抛出异常。但如果m指向其他类型(如Cat),则会抛出ClassCastException。
d = c;
这将Cat引用赋值给Dog引用。Dog和Cat是兄弟类,没有继承关系,因此类型不兼容,会引发编译错误。错误信息类似于:incompatible types: Cat cannot be converted to Dog。
c = (Cat) m;
这是向下转型,将父类引用强制转换为Cat类型。在编译时不会错误,但运行时,由于m实际指向Dog对象(因为之前有m = d),因此会抛出ClassCastException,因为Dog对象不能转换为Cat类型。
总结:
引起编译错误的语句:d = m 和 d = c。
原因:d = m 需要显式类型转换,而编译器无法保证m总是Dog类型;d = c 因为类型不兼容,Dog和Cat没有继承关系。
引起运行时错误的语句:c = (Cat) m。
原因:m实际指向Dog对象,强制转换为Cat类型会抛出ClassCastException。
验证:
class Mammal {}
class Dog extends Mammal {}
class Cat extends Mammal {}
public class TestCast {
public static void main(String args[]) {
Mammal m;
Dog d = new Dog();
Cat c = new Cat();
m = d; // 正常
// d = m; // 取消注释会导致编译错误
d = (Dog) m; // 正常,因为m指向Dog
// d = c; // 取消注释会导致编译错误
// c = (Cat) m; // 取消注释会导致运行时ClassCastException
}
}
实际运行结果:
如果取消注释d = m;或d = c;,编译将失败。
如果取消注释c = (Cat) m;,运行时将抛出异常:Exception in thread "main" java.lang.ClassCastException: Dog cannot be cast to Cat。

- 编译代码并查看字节码
首先,编译Java文件:
javac TestPolymorphism.java
然后,使用javap反编译类文件,查看字节码指令:
javap -c TestPolymorphism
TestPolymorphism 的 main 方法字节码:
public static void main(java.lang.String[]);
Code:
0: new #2 // class Parent
3: dup
4: invokespecial #3 // Method Parent."
7: astore_1
8: aload_1
9: invokevirtual #4 // Method Parent.Introduce:()V
12: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_1
16: getfield #6 // Field Parent.value:I
19: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
22: new #8 // class Son
25: dup
26: invokespecial #9 // Method Son."
29: astore_1
30: aload_1
31: invokevirtual #4 // Method Parent.Introduce:()V
34: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
37: aload_1
38: getfield #6 // Field Parent.value:I
41: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
44: new #10 // class Daughter
47: dup
48: invokespecial #11 // Method Daughter."
51: astore_1
52: aload_1
53: invokevirtual #4 // Method Parent.Introduce:()V
56: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
59: aload_1
60: getfield #6 // Field Parent.value:I
63: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
66: return
关键字节码指令说明:
invokevirtual #4:用于调用实例方法(如 Introduce)。这里的 #4 是常量池索引,指向 Parent.Introduce 方法。但 invokevirtual 指令支持多态,运行时会根据对象的实际类型调用正确的方法。
getfield #6:用于访问字段(如 value)。这里的 #6 指向 Parent.value 字段。字段访问不是多态的,基于引用类型(编译时类型)而非实际对象类型。
new、dup、invokespecial:用于创建对象并调用构造函数。
- 多态的实现机制:从字节码到 JVM 运行时
Java 中的多态是通过 虚方法表(vtable) 和 invokevirtual 指令 实现的。以下是详细解释:
(1)编译时的“滞后绑定”
在编译阶段,编译器为方法调用生成 invokevirtual 指令,并引用常量池中的方法符号(如 Parent.Introduce)。编译器无法知道实际对象类型,因此采用“滞后绑定”(late binding),将方法解析推迟到运行时。
从字节码可以看出,所有 p.Introduce() 调用都使用相同的 invokevirtual #4 指令,尽管 p 指向不同子类对象。
(2)运行时的多态解析
当 JVM 执行 invokevirtual 指令时,它会:
获取对象的实际类型(通过对象头中的类指针)。
查找该类的虚方法表(vtable)。每个类都有一个 vtable,其中存储了方法的实际入口地址。
根据方法引用(常量池索引)在 vtable 中找到对应的方法实现。
调用该方法。
例如:
当 p 指向 Son 对象时,invokevirtual #4 会查找 Son 类的 vtable,找到 Son.Introduce 方法并调用。
当 p 指向 Daughter 对象时,同样过程会调用 Daughter.Introduce。
(3)字段访问没有多态
字段访问使用 getfield 指令,直接基于引用类型(编译时类型)解析。因此,p.value 总是访问 Parent 类中定义的 value 字段,即使 p 指向子类对象。这就是为什么输出中 value 总是 100,而不是子类的 101 或 102。
- 代码运行结果验证
运行 TestPolymorphism 程序,输出如下:
I'm father
100
I'm son
100
I'm daughter
100
方法调用体现多态:Introduce 方法根据实际对象类型输出不同内容。
字段访问没有多态:value 字段始终输出 Parent 类的值。
- 总结 Java 的语法特性
从多态的实现中,可以总结出以下 Java 特性:
方法重写(Override):子类可以重写父类的方法,运行时通过多态调用正确版本。
动态绑定:方法调用在运行时根据对象实际类型决定,通过 invokevirtual 指令和 vtable 实现。
字段隐藏:如果子类定义了与父类同名的字段,则子类字段会隐藏父类字段。字段访问是静态绑定的,基于引用类型。
编译时与运行时差异:编译器处理方法引用基于声明类型,但 JVM 在运行时动态解析方法。
多态的限制:多态仅适用于实例方法,不适用于字段、静态方法或私有方法。
