Java继承中的隐藏和覆盖
背景
某一天和同事某超一起聊起一个子类调用父类方法,父类又调用被子类覆盖了的方法的。。。。超说Java皆对象,那个对象的实例就调用那个方法。最近遇到了下面的一段代码:
//父类
public class Parent {
int x = 10;
public Parent() {
add(2);
}
public void add(int y) {
x += y;
}
}
//子类
public class Child extends Parent {
int x = 9;
public Child() {
}
@Override
public void add(int y) {
x += y;
}
}
上面代码不仅进行了方法的覆盖了,也有同名变量的覆盖。
变量的覆盖和方法的覆盖规则会是一直的么?
//运行代码
public class Main {
public static void main(String[] args) {
Parent parent = new Child();
System.out.println("parent.x="+ parent.x);
}
}
我们先思考一下上面代码的运行结果~!
知识点回顾
类的初始化
在程序运行时,当类被首次访问或创建实例时,Java虚拟机会按照一定的规则来对类进行加载。
类的加载过程包括以下阶段:
- 加载(Loading):查找并加载类的二进制数据,这一步通常由类加载器完成。
- 验证(Verification):确保加载的类的正确性,比如检查文件格式、语法错误等。
- 准备(Preparation):为类的静态变量分配内存,并设置默认初始值。
- 解析(Resolution):将类中的符号引用转换为直接引用,比如将方法名转换为方法的指针。
- 初始化(Initialization):执行类的初始化方法,包括静态变量的显示赋值和静态代码块的执行等。
类的初始化方法是由Java虚拟机在必要时自动调用的,它会保证线程安全和只调用一次。对于初始化阶段,虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要再此之前开始):
- 遇到
new
、getstatic
、putstatic
或invockstatic
这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
- 生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用
java.lang.reflect
包的方法对类进行发射调用的时候,如果类么有进行过初始化,则需要先触发其初始化。 - 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要制定一个要执行的主类(包含
main()
方法的那个类),虚拟机会先初始化这个主类。
在准备阶段,变量已经赋值过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
- 类构造器方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序决定的。
- 类构造器方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法已经执行完毕。因此在虚拟机中第一个被执行的类构造器方法的类肯定是
java.lang.Object
。 - 由于父类的类构造器方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
- 类构造器方法对于类或接口来说并不是必须得,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以部位这个类生成类构造器方法。
- 接口中不能使用静态语句块,但任然有变量初始化的赋值操作,因此接口与类一样都会生成类构造器方法。但接口于类不同的是,执行接口的类构造器方法不需要先执行父接口的类构造器方法。只有当付接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的类构造器方法。
- 虚拟机会保证一个类的类构造器方法在多线程环境中被正确的加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其他线程都行需要阻塞等待,知道活动线程执行类构造器方法完毕。
对象的实例化
- 方法区加载
Parent
父类,再加载Child
子类; - 在栈中申请空间声明对象;
- 在堆内存中开辟空间并分配地址,这块内存空间的大小取决于对象的类型和实例变量的数量。
- 对象的实例变量会被初始化为默认值(包括父类),数值类型(如
int
、double
)初始化为0
,布尔类型初始化为false
,引用类型初始化为null
。 - 子类构造函数方法进栈;
- 显示初始化父类的实例变量;
- 父类构造方法进栈,执行完出栈。执行构造方法中的语句,对实例变量进行进一步初始化。这可以包括对变量赋初值,也可以包括执行一些初始化逻辑。
- 显示初始化子类的实例变量;
- 子类初始化结束后,执行子类的构造方法体。
- 将对内存中的地址赋值给引用变量,然后子类构造函数方法出栈;
初始化顺序:
- 父类静态代码块(类加载);
- 子类静态代码块(类加载);
- 父类非静态代码块(实例变量);
- 父类构造方法;
- 子类非静态代码块(实例变量);
- 子类构造方法;
结果分析
输出结果:
parent.x=10
过程分析
- 调用
new Child()
的时候,先进行类加载(这里没有静态代码,所以类加载不影响最后结果); - 初始化父类的实例变量(父类中的
Parent.x
赋值为10); - 调用父类的构造方法,执行父类构造方法的代码块
add(2)
; -
add
函数被子类覆盖,调用的是子类的add
方法。此时子类的实例变量只在开辟内存的时候进行过默认值的赋值,还没有进行代码赋值。所以此时Child.x
为0,调用add(2)
之后Child.x
值为0; - 初始化子类的实例变量(子类中的
Child.x
赋值为9); - 将创建好的
Child
实例赋值给Parent parent
变量;
好了到这里就分析完了,最后的答案是10
。
这里或许有些同学有问题,为什么答案是10
,而不是9
。
这里就需要说一下我写这篇文章的目的了(PS:因为上面的知识点好多书籍都能找到)。
Java继承中的隐藏和覆盖
方法和变量在继承时的隐藏与覆盖
隐藏:若Child
隐藏了Parent
的变量或方法,那么Parent
不能访问Parent
被隐藏的变量或方法,但将child
转换成Parent
后可以访问Parent
被隐藏的变量或者方法。
覆盖:若Child
覆盖了Parent
的变量或者方法,那么不仅Child
不能访问Parent
被覆盖的变量或者方法,将Child
转换成Parent
后同样不能访问Parent
被覆盖的变量或者方法。
隐藏与覆盖规则:
- 父类的实例变量和类变量能被子类的同名变量隐藏。
- 父类的静态方法被子类的同名静态方法隐藏,父类的实例方法被子类的同名实例方法覆盖。
- 不能用子类的静态方法隐藏父类的实例方法,也不能用子类的实例方法覆盖父类的静态方法,否则编译器会异常。
- 用
final
关键字修饰的最终方法不能被覆盖。 - 变量只能被隐藏不会被覆盖,子类的实例变量可以隐藏父类的类变量,子类的类变量也可以隐藏父类的实例变量。
以上代码只是在一篇文章中看到的,目前看了一些Java相关的专业书籍并没有找到对应的内容讲解(PS:如果有哪位同学在某本书上看到过相关的内容讲解可以告诉我一下~!)。
上面声明的是Parent
在访问Parent.x
的时候虽然实现是Child
,但此时依旧可以访问到Parent
中的x
,所以最后答案是10
。
参考文章:
c+++多态和java的区别