前言
本篇主要讲解了类的初始化、实例化、静态代码块、构造器、getClass()、super、this 等相关的知识点,做一个总结。
demo
老规矩,看代码:
Father.java
public class Father {
private int i = test();
private static int j = method();
static {
System.out.println("1 父类静态代码块");
}
Father() {
System.out.println("2 父类无参构造器");
}
{
System.out.println("3 父类代码块");
}
public int test() {
System.out.println("4 父类 test 方法");
return 1;
}
public static int method() {
System.out.println("5 父类 method 方法");
return 1;
}
}
Son.java
public class Son extends Father {
private int i = test();
private static int j = method();
static {
System.out.println("6 子类静态代码块");
}
Son() {
System.out.println("7 子类无参构造函数");
}
{
System.out.println("8 子类代码块");
}
@Override
public int test() {
System.out.println("9 子类 test 方法");
return 1;
}
public static int method() {
System.out.println("10 子类 method 方法");
return 1;
}
}
MainClass.java
public class MainClass {
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
大家先思考一下 main 方法执行之后的打印应该是什么样的,
打印结果:
5 父类 method 方法
1 父类静态代码块
10 子类 method 方法
6 子类静态代码块
9 子类 test 方法
3 父类代码块
2 父类无参构造器
9 子类 test 方法
8 子类代码块
7 子类无参构造函数
9 子类 test 方法
3 父类代码块
2 父类无参构造器
9 子类 test 方法
8 子类代码块
7 子类无参构造函数
如果你计算的打印结果和我的不一样,那么看看我下面的讲解吧,如果一样的,老铁,你基础还不错啊。
讲解
基础知识
在讲解这个题目之前,我先总结一下需要了解的前提知识点。
- 任何 java 程序的入口都是 main 方法
- main 方法所在的类需要先加载和初始化
- 类初始化先执行类的
<clinit>()
方法,大家查看 class 文件的字节码时可以找到这个方法。
-
<clinit>()
方法由静态类变量显式赋值代码【注意变量还没创建】和静态代码块组成。【包含且只包含这些。】 - 类变量显式赋值代码和静态代码块从上到下顺序执行,谁在前谁先执行
-
<clinit>()
方法只执行一次。【因为 class 类只被加载一次】
- 如果有父类,先加载和初始化父类,即先执行
father.<clinit>()
方法,再执行子类的clinit方法 - 用构造方法实例化对象时,会调用
<init>
方法,且每 new 一次就会调用一次
- 几个构造方法就有几个 init 方法,可能有参,可能无参
- 由非静态实例变量显式赋值代码、非静态代码块、对应构造器代码组成
- 构造器最后执行,另外 2 个谁在上面谁先执行
- 每 new 一次对象,调用对应的构造器,就是执行对应的()方法
- init 方法首行是 super()或者 super(参数), 代表要先执行父类的 () 方法【所以说会先调用父类构造器】
开始分析
由我们上面总结的 main 方法所在类先加载和初始化,因为我们main 方法写在了 MainClass
文件,没有其他变量或者方法,直接看 main 方法的代码。
类初始化
第一行代码
Son s1 = new Son();
我们知道=号右边先执行,所以去 new 一个 son 对象,这个步骤就叫实例化对象,并且会把对象进行初始化。
我们知道在实例化对象前,jvm 需要去方法区找有没有这个对象对应的 class
文件。从我们的代码看,是没有的,所以他要去装载这个 class 文件,就是进行类初始化操作。
时刻记住 class 文件按需加载,如果你整个程序运行阶段都用不到这个类,那么 jvm 从启动到结束都不会去加载这个 class文件。
在加载 class 的步骤时就是我们说的类初始化
,而我们知道类初始化时会调用类的<clinit>()
方法。
但是在执行Son.<clinit>()
方法的时候,我们看到 Son 是继承自 Father 的,它有个父类,按照规则又会先去看看父类初始化没有。
从我们代码看,第一行前面没有 father 相关的代码,说明 father 类没有初始化,所以又先去调用Father.<clinit>()
方法。
按照()规则:
- 该方法由静态类变量显式赋值代码、静态代码块组成。
- 这 2 个谁在前,谁先执行。
我们看 Father 的代码,踢出不需要的,我们的 方法会执行下面 2 句
private static int j = method();
static {
System.out.println("1 父类静态代码块");
}
第一行会去调用 method 方法,所以打印了5 父类 method 方法
。
然后是静态代码块,打印1 父类静态代码块
下面没有代码了,Father.<clinit>()
结束,开时执行子类的Son.<clinit>()
方法
private static int j = method();// 子类的 method 方法
static {
System.out.println("6 子类静态代码块");
}
同理我们知道打印了10 子类 method 方法
,6 子类静态代码块
老铁们,目前还跟得上吧?
对象实例化
类初始化之后,就是对象实例化了,因为我们调用了构造函数去初始化,所以我们看看 Son 的构造函数
Son() {
System.out.println("7 子类无参构造函数");
}
一般老铁看到这里,哈!简单啊,就一个打印,over。
但是我既然在这里列出来了,肯定不简单啊。
其实这里被隐藏了一句代码:
Son() {
// 这句写或者不写,都一定会执行,子类构造器一定会调用父类构造器。
// super();
System.out.println("7 子类无参构造函数");
}
我们看看 Son.class 的字节码,里面先调用了Father.<init> ()V
,后面的 V 标识无返回值
<init>()V
L0
LINENUMBER 39 L0
ALOAD 0
INVOKESPECIAL top/ybq87/Father.<init> ()V
L1
...
那我们就去父类呗
Father() {
System.out.println("2 父类无参构造器");
}
保险起见,我们再去看看父类的字节码,果然 father 还有一个父类!Object.<init> ()V
,我们知道 Object 是所有类的父类
<init>()V
L0
LINENUMBER 22 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
...
Object 的字节码,此时就再也没有父类了。
public <init>()V
L0
LINENUMBER 37 L0
RETURN
MAXSTACK = 0
MAXLOCALS = 1
...
好了,说了这么一大堆,总结:Son.<init>
执行前先调用Father.<init>
我们知道方法每次构造会执行:
- 非静态实例变量显式赋值代码,有点绕口,其实就是成员变量的赋值代码,哈哈,啥又是成员变量呢?实例变量=成员变量
- 非静态代码块,和第一条,谁先出现,谁先执行
- 对应构造器代码【最后执行】
看看 father 的代码
private int i = test();
{
System.out.println("3 父类代码块");
}
Father() {
System.out.println("2 父类无参构造器");
}
调用 test 方法,打印4 父类 test 方法
,代码块打印3 父类代码块
,构造函数打印2 父类无参构造器
。
看到这里,有眼尖的老铁就懵了,怎么回事,我们之前看到编译器这里打印的是
9 子类 test 方法 // 我们按照预期以为应该是打印的:4 父类 test 方法
3 父类代码块
2 父类无参构造器
和我们分析的不一样啊???这里就涉及到另外一个知识点,我们后面讲解,先分析完代码。
父类的方法执行完毕,轮到子类了,流程是一样的,这里就不再列出。
9 子类 test 方法
8 子类代码块
7 子类无参构造函数
到此,第一行代码执行完毕。
Son s2 = new Son();
这个就留给大家继续巩固下,我们继续说关于 test()
的问题。
方法重写
我们学习 java 的时候,知道 java 有三大特性【封装、继承、多态】,很多人对封装和继承比较容易理解,但是这个多态就会一脸懵了。什么叫多态?在理解多态之前,我先讲讲继承中的方法重写。
方法重写,什么情况下会有重写?我们知道子类继承父类,是可以重写父类方法的,我们从不能被重写的定义去理解能被重写的
- final 修饰的方法,不允许重写
- 静态方法不允许重写
- private 等子类中不可见的方法不能被重写
- 其他的都可以被重写
大家可以自己验证一下,在 father 写一个 public 的方法,然后利用编辑器提供的提示功能,看能否找到提示代码。
IDEA 找到 Second Basic Completion 快捷键。
不清楚的老铁再复习一下
重写的了解完了,我们再看看 this 关键字,学海无涯啊。为了讲好重写和多态,不得不了解一下 this 关键字。我们一般写代码的时候,编辑器会自动将一些 this 省略了,导致我们理解代码有一定的误差
推荐 idea 的插件:save action,设置格式化的时候加上 this 关键字,勾上:Add this to field access 和 Add this to method access。当然基础熟悉之后,可以不需要了。
经过这么一改造,我们发现 father 和 son 中的部分代码发生了一些变化
private int i = this.test();
原来方法调用是通过 this 关键字来调用的,这个 this 看来就影响了我们的打印结果,还记得我们分析Father.<init>
时的预期打印嘛?
9 子类 test 方法 // 我们按照预期以为应该是打印的:4 父类 test 方法
3 父类代码块
2 父类无参构造器
这里的结果显示的居然是调用了子类的 test() 方法的打印,也就是说 Father 中的 this 居然是子类 Son?
老铁们还跟得上不
继承关系中的this关键字
非继承的类中的 this,就是这个实例本身,这个没有什么怀疑了。
但是继承关系下的 this 关键字,子类中的 this 还是子类本身,但是父类中如果用到了 this,就有一定的区别
【注意,我们分析的是:使用子类class进行实例化一个对象,而不是直接实例化父类时,在父类中的 this 关键字
的作用】
- this(1,2); 访问的是本类的其他构造方法,无论子类是否有相同参数的构造方法,父类中这里都是访问自己的构造方法
- this.paramName; 可以访问类中的成员变量,在父类中使用 this.param 访问的始终是父类的成员变量
- this.func(params..); 访问类中的成员方法时【也就是调用了 test 方法时】
- 子类重写了父类方法,那么这里就是调用的子类的方法。
看到了吧!
此时我们在父类用 this.test()调用的是子类重写过的 test()方法。 - 子类没有重写,那么这里调用的还是父类自己的方法。
- this; 当前类【或者叫
运行时类型
】对象的引用,this 始终代表的是子类的对象
- 这个怎么理解?因为 this 的用法除了调用方法,调用参数,还可以被当成参数传递
- 比如在父类的构造器 System.out.println(this); 那么这个 this 其实是指向的是子类。
- 或者 System.out.println(this.getClass()); this 也是子类。
虽然看到这里我们理解了为什么在父类的Father.<init>
时会调用子类的重写过的方法,因为 java 这么规定的,是代码规则。
但是我们又引出了新的概念!运行时类型???
编译时类型和运行时类型
我们说到哪里了?嗯,上面不是说怎么理解多态么,怎么又扯到这个了?老铁们坚持住啊,胜利就在眼前了。
java 的引用变量有 2 种类型
啥是引用变量?本例中 main 方法中的Son s1 = new Son();其中 s1 就是引用变量。不了解的老铁补一下基础
- 编译时类型:由声明该变量时使用的类型决定
- 运行时类型:实际赋给该变量的对象决定
光文字说明不容易理解,
Father son1 = new Son();
Son son2 = new Son();
第一行代码,我们知道 son 是 father 的子类,所以这么声明 son1 是没有问题的。
那么 Father 就是 son1 的编译时类型,而 Son 是 son1 的运行时类型。
如果编译时类型和运行时类型不一致,会出现所谓的多态
,看到没有,老铁,这就是多态!!!我们自己去百度多态的定义,一大堆文字描述,都给人绕晕了,看这里,敲黑板!!!编译时类型和运行时类型不一致就是多态
。
第二行,编译时类型和运行时类型都一样,他不是多态。
稍微总结下我们目前了解的,多态存在的三个必要条件:
多态存在的三个必要条件:
- 继承:son 继承自 father
- 重写:son 重写了 father 的 test 方法
- 父类引用指向之类对象:父类中 this 的指向概念
到这里你再去网上 google 一下多态的定义,我想老铁们肯定思路清晰很多了吧。
进一步理解
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。
说人话就是:son1 在编译阶段只能调用 Father 所具有的方法,但运行时则执行 Son 所具有的方法。
假设我们在Son 写了一个新的方法,
class Son{
...
public void hello(){
System.out.println("hello");
}
}
// 定义
Father son1 = new Son();
// 我们可以使用 son1 直接调用 father 存在的方法,
son1.test();
// 但是如果你这么写,编译就会报错,也就是前半句:son1 在编译阶段只能调用 Father 所具有的方法
son1.hello();
// 程序跑起来之后,代码执行到了 son1.test()
// 则执行 Son 所具有的方法[如果子类有重写的话]
因此,编写Java代码时,引用变量只能调用声明该变量所用类里包含的方法。与方法不同的是,对象的属性则不具备多态性。通过引用变量来访问其包含的实例属性时,系统总是试图访问它编译时类所定义的属性,而不是它运行时所定义的属性,属性不能被重写。
super.getClass() 和 this.getClass()
既然说到了 this,就不能不说下 super。
按照官方的定义,this()调用构造器方法必须放在构造器的第一行,但是 super 也必须放在第一行,所以 2 个关键字调用不允许出现在同一个方法中,且都不能出现多次。
super 关键字的作用是在于当子类覆盖了父类的某个成员变量或者方法,还想要访问到父类的这个变量或者方法时,用 super。
super在一个类中用来引用其父类的成员,它是在子类中访问父类成员的一个桥梁,并不是任何一个对象的引用。
而this则表示当前类对象的引用。在代码中Object o = super;是错误的,Object o = this;则是允许的。
那么说说 getClass() ,看到这个方法的名称我们知道是获取当前对象的 class 的。那么我们打印一下
Father son1 = new Son();
...
Father() {
System.out.println(this.getClass().getName());
System.out.println("2 父类无参构造器");
}
// 打印
...
top.ybq87.Son
2 父类无参构造器
...
java 中 getClass() 方法返回的是该对象的运行时类,因为我们执行的是第一行代码,son1 的运行时类是 Son。那么我们就像知道真实的父类是啥呢?
使用:
System.out.println(getClass().getSuperclass().getName());
结束
咱们结束之前在看看看这个
public class TestClass {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b));
System.out.println(a1.show(c));
System.out.println(a1.show(d));
System.out.println(a2.show(b));
System.out.println(a2.show(c));
System.out.println(a2.show(d));
System.out.println(b.show(b));
System.out.println(b.show(c));
System.out.println(b.show(d));
}
}
class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
class B extends A {
public String show(B obj) {
return ("B and B");
}
@Override
public String show(A obj) {
return ("B and A");
}
}
class C extends B {
}
class D extends B {
}
是不是很简单了呢
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
// A and A,A 没有 show(B obj) 方法,但是 B 是继承 A,所以找到 show(A obj)。
System.out.println(a1.show(b));
// A and A,同理 C 继承 B,B 继承 A,可以找到 show(A obj)
System.out.println(a1.show(c));
// A and D,直接找到 show(D obj)
System.out.println(a1.show(d));
// B and A,a2 编译时类型是 A,但是 A 没有 show(B obj) 方法,但是 b 继承 a,
// 可以转为 show(A obj),但是因为多态原因,这里调用的是运行时类 B 的 show(A obj)方法。
System.out.println(a2.show(b));
// B and A,同上 c 转为了 a,调用运行时类的 show 方法
System.out.println(a2.show(c));
// A and D,编译时类的方法可以直接调用,所以这里直接调用 show(D obj)
System.out.println(a2.show(d));
// B and B,直接找到 show(B obj)
System.out.println(b.show(b));
// B and B,c 继承自 b,找到 show(B obj)
System.out.println(b.show(c));
// A and D,因为 B 继承自 A,但是 b 又没有重写 show(D obj),而且 A 中 show(D obj)是个 public 的方法
// 所以此方法被 B 继承过来,可以直接调用。
System.out.println(b.show(d));
}