类的初始化过程与finalize()

155 阅读4分钟

一、new一个对象的初始化过程

我们看看下面的类

public class Crate {
    public static void main(String[] args) {
        Parent.say("xml");
    }
}
class Parent {
    private int age;
    Dog dog = new Dog();
    {
        System.out.println("parent 的非静态代码块");
    }
    static Dog staticDog = new Dog();
    static {
        System.out.println("parent 的静态代码块");
    }
    static void say(String text) {
        System.out.println(text);
    }
    Parent(int age) {
        System.out.println("parent(" + age + ")");
    }
}
class Dog {
    Dog() {
        System.out.println("dog()");
    }
}

首先有以下几个注意点

  • 如果不写构造器的话,编译器会自动生成一个无参数的默认构造器,写了就不会生成了
  • 构造器也可以调用其他的构造器,比如父类(使用super关键字)的或者是本类(使用this关键字)的构造器,且必须在第一行
  • 类的成员变量可以不初始化,交给编译器来赋成默认值。但是局部变量不行,因为局部变量不初始化往往是程序员的疏忽

执行main方法控制台打印的顺序如下:

dog()
parent 的静态代码块
xml

把main方法改成这样,再次执行

public class Crate {
    public static void main(String[] args) {
        new Parent(1);
    }
}

控制台打印如下:

dog()
parent 的静态代码块
dog()
parent 的非静态代码块
parent(1)

可以看到,第一次的main方法没有生成对象,只是使用了类的静态方法
第二次的main方法new了一个对象,因此得到了不同结果。
因为,static字段和方法是属于类的,这个Jvm中只有一份,使用静态方法不需要生成对象。
而非static字段是属于对象的,new多少个对象就有多少份。

那么这些字段的加载顺序如何呢?为什么会有这种打印顺序?

  1. 找到Parent.class并且载入,这个时候有关静态初始化的动作都会被按代码中出现的先后顺序执行,因此,静态初始化只在class首次加载的时候进行一次,第二次就不会执行了,因为类只有一份。
  2. new Parent() 的时候,会在堆上未这个对象分配足够的空间。并且这块存储空间会被清零,也就是基本数据类型和引用都变成了默认值(例如 0 和 null)。
  3. 执行定义在字段处的初始化动作,例如(int age = 666)。因此这个age会被赋值两次,先变成0(因为第二步的存储空间清零),后面才被赋值成666。
  4. 执行构造器。

按照这个执行顺序我们在分析一边第二次执行main方法控制台为何这样打印。

第一:static Dog staticDog = new Dog()先执行,因为在静态代码块前面。所以打印出了第一个dog()
第二:静态代码块执行,打印出 parent 的静态代码块
第三:分配空间并清零,生成对象,对象的字段进行初始化,执行 Dog dog = new Dog(),因为其在非静态代码块前面, 打印出了第二个dog()
第四:执行非静态代码块,打印出 parent 的非静态代码块。
第五:执行构造器,打印出 parent(1)。

现在是不是清晰多了 知道这个加载过程之后我们可以干一件非常沙雕的事情,准备下面几个类

public class Crate {
    public static void main(String[] args) {
        new A();
    }
}
class A {
    static {
        new B(); // 1号点
    }
    A() {
        System.out.println("A"); // 2号点
    }
}
class B {
    static {
        new A(); // 3号点
    }
    B() {
        System.out.println("B"); // 4号点
    }
}

执行main方法会发生什么?还是说会异常? 执行结果如下:

A
B
A

分析如下:
第一:执行main方法的new A(),首先时加载类A进内存 (此时A类已经进入的内存),再执行A的静态代码块。
第二:执行1号点代码,加载B类进内存 (此时B类已经进入内存),执行B的静态代码块。
第三:执行3号点代码,此时A类已经被加载了,类只会被加载一次,此时直接生成A对象,执行构造器,打印出A。
第四:执行流程回到1号点代码,B类已经被加载了,生成B对象,执行构造器,打印出B。
第五:流程回到main方法,打印出A。
不知道你们有没有发现一个细节,我们在A类的静态代码块没执行完成时,就已经生成了一个A对象。

二、finalize()方法

这是Object的一个方法,我们可以覆盖它。这个方法会在垃圾回收器回收之前被执行,但是不保证一定能执行。
也可能执行到一半对象就已经被回收了。
因为如果在这个方法里面发生了死循环,会导致整个JVM垃圾回收器奔溃,sun公司肯定不会这样设计,显然不可能每次保证这个方法会执行完成。
对于这个方法的一个用处就是尽量不用他。