前言

本篇主要讲解了类的初始化、实例化、静态代码块、构造器、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 子类静态代码块




ideaij 看看java 及其子类 diagrams idea查看子类_java多态的理解


老铁们,目前还跟得上吧?

对象实例化

类初始化之后,就是对象实例化了,因为我们调用了构造函数去初始化,所以我们看看 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 父类无参构造器


ideaij 看看java 及其子类 diagrams idea查看子类_idea查看一个类的子类_02


看到这里,有眼尖的老铁就懵了,怎么回事,我们之前看到编译器这里打印的是


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 快捷键。

不清楚的老铁再复习一下


ideaij 看看java 及其子类 diagrams idea查看子类_idea 查看一个类的子类_03


重写的了解完了,我们再看看 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?


ideaij 看看java 及其子类 diagrams idea查看子类_idea 查看一个类的子类_04


老铁们还跟得上不

继承关系中的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 的运行时类型。

如果编译时类型和运行时类型不一致,会出现所谓的多态,看到没有,老铁,这就是多态!!!我们自己去百度多态的定义,一大堆文字描述,都给人绕晕了,看这里,敲黑板!!!编译时类型和运行时类型不一致就是多态

第二行,编译时类型和运行时类型都一样,他不是多态。


ideaij 看看java 及其子类 diagrams idea查看子类_java多态的理解


稍微总结下我们目前了解的,多态存在的三个必要条件:

多态存在的三个必要条件:

  • 继承: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));
}


ideaij 看看java 及其子类 diagrams idea查看子类_idea查看类的子类_06