类加载和初始化顺序

这个博客是我看Thinking In Java的笔记与记录

简单介绍类加载:

在很多编程语言中,程序是作为启动过程的一部分立刻被加载出来的,然后是初始化工作,然后是程序开始。  这些语言必须严格控制初始化的过程,这样才能保证static变量的初始化不会出问题。比如像C++,就有可能出现一个static变量在初始化的过程中,需要另一个static变量已经成功初始化并已经有效,不然就会有问题。而Java不会出现这样的问题。因为它采用一个比较特别的方法去加载。

万物在Java中都是object,每个类的编译代码都在自己的那个独立的文件中,也就是每个.class文件,这些文件或者说是编译代码只有在代码第一次被使用的时候会加载。

什么叫第一次被使用?第一个这个类的对象被构造出来,或者是类中的static方法或者是static变量被调用或者是使用了,这两个情况随便发生一个,类加载就发生了。(但其实一个对象要被构造出来,就要调用这个类的构造函数,而构造函数也是static,所以其实可以说只要有static方法或者变量的调用或使用,类就被加载)

然后有个一定要清楚的是,初次使用的时候,也是static变量初始化的时候。在类加载的时候,所有的static对象会按照你在代码中定义的顺序一次初始化,可以看作static对象的初始化和类加载是一起进行的。当然static对象只会初始化一次。

类变量的自动初始化和初始化顺序:

Java为了保证类变量在使用之前已经得到初始化,有个自动赋值的机制。就如果你在定义这个类变量的时候没有给它初始化,new出来这个对象后这些类变量会被自动初始化赋值。基本数据类型会被自动赋值为默认值(这里就不说明了),对象的引用会被自动赋值为null。

构造器的其中一个用处之一,就是为类变量提供初始化的服务。但注意,构造器的初始化也就是构造器中为类变量赋值的这个动作,并不能阻止类变量自动赋值的机制,也就是说,在进入构造器之前,类变量其实已经赋值完毕了。

public class Counter {
  int i;
  Counter() { i = 7; }
  // ...
} 

如果你new一个Counter对象,那么i先被自动赋值为0,再是进入构造器,被赋值为7.也就是说,类变量的初始化(自动)在构造器之前就完成了。

在一个类中,类变量的初始化顺序就是代码中他们的位置,这些类变量初始化完之后,就到构造器中的工作。这个例子可以很好得体现类变量的初始化顺序。

class Window {
    Window(int marker) { print("Window(" + marker + ")"); }
}
class House { Window w1 = new Window(1); // Before constructor House() { // Show that we’re in the constructor:     print("House()");     w3 = new Window(33); // Reinitialize w3   }   Window w2 = new Window(2); // After constructor   void f() { print("f()"); }   Window w3 = new Window(3); // At end }
public class OrderOfInitialization {   public static void main(String[] args) {     House h = new House();     h.f(); // Shows that construction is done   } }

/* Output: Window(1) Window(2) Window(3) House() Window(33) f()

static对象的初始化:

static对象只有一块公共的内存,static关键字只能用于类变量,所以static也会被自动初始化,初始化的规则也和普通的类变量一样,这里就不重复讲了。

 然后就是static对象的初始化是在类加载的时候就进行的,下面看个比较长的例子,来深刻理解下static对象的初始化:

class Bowl {
    Bowl(int marker) {
        print("Bowl(" + marker + ")");
    }
    void f1(int marker) {
        print("f1(" + marker + ")");
    }
}

class Table {
    static Bowl bowl1 = new Bowl(1);
    Table() {
        print("Table()");
        bowl2.f1(1);
    }
    void f2(int marker) {
        print("f2(" + marker + ")");
    }
    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);
    Cupboard() {
        print("Cupboard()");
        bowl4.f1(2);
    }
    void f3(int marker) {
        print("f3(" + marker + ")");
    }
    static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
    public static void main(String[] args) {
        print("Creating new Cupboard() in main");
        new Cupboard();
        print("Creating new Cupboard() in main");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
}
    static Table table = new Table();
    static Cupboard cupboard = new Cupboard();
} 

输出是:

/* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*///:~

现在我们来一步一步说明下这些输出:

首先是类StaticInitialization类中的main函数,这是程序启动的入口。而这个main函数是个static的对象,也就是说,现在是第一次运行了StaticInitialization类的一个static对象,这个类便要加载,然后static变量按顺序初始化。     我们看到,这个类的static对象有:main函数,table(Table类的对象引用),cupboard(Cupboard的对象引用),也就是说要加载StaticInitialization类,需要先初始化table和cupboard。

而要也就是说现在要创建Table对象还有Cupboard对象。

new Table(),这个语句就是第一次构造Table对象,或者说要调用Table的构造器,static函数(第一次),所以构造这个对象,要先加载Table类和初始化Table类中的static对象:static Bowl bowl1和static Bowl bowl2  

而这个时候我们又要第一次构造Bowl对象,调用了Bowl的static构造器,又是要加载Bowl类,初始化Bowl类的static对象,Bowl类中没有static对象,然后加载就完成,进入构造对象阶段。先初始化类变量,先赋值为默认值,再执行赋值语句。但Bowl中没有非static的类变量,所以接着进入Bowl的构造器——于是便有了我们的第一和第二个输出——Bowl(1)和Bowl(2)。

好的现在Table的两个static变量初始化完成了,也就是说Table类的加载完成,进入构造对象阶段,Table类也没有能够初始化的非static类变量,所以进入Table的构造器——第三个输出——Table()。构造器中还有个语句是bowl2.f1(1);,这个是个非static的方法,于是第四个输出——f1(1)

然后是new Cupboard()语句,一样的,因为是第一次构造Cupboard对象,所以先加载Cupboard类和初始化Cupboard类中的static对象——static Bowl bowl4和static Bowl bowl5。由于这个时候已经不是第一次用到Bowl类的对象,所以不用加载Bowl类了,也就是跳过类加载阶段,直接进入构造对象阶段,一样没有类变量要初始化,直接构造器——第五和第六个输出——Bowl(4)和Bowl(5)。然后Cupboard的类加载就结束了,进入构造Cupboard的阶段,先是初始化类变量,先赋值为默认值,也就是bowl3 = null; 然后再执行赋值语句,也就是Bowl bowl3 = new Bowl(3),为这个普通类变量了bowl3赋值。Bowl已经加载类并且没有普通类变量,所以直接构造器打印——第六个输出——bowl(3)。Cupboard的类变量初始化之后,便进入Cupboard的构造器,一个pirnt语句和bowl4.f1(1),于是有了第七个和第八个输出——Cupboard()和f1(2)

好了,到了这里,StaticInitialization类的两个static变量都初始化成功了,也就是说StaticInitialization类的加载完成了。(现在思路回到这个类调用main方法那)因为这个类加载是因为运行了static方法,而不是new对象(调用构造器),所以不用构造对象。接下来就是进入到main函数里面了。先第个输出——Creating new Cupboard in main()然后main方法里面new了个Cupboard对象,因为这里已经不是第一次用这个Cupboard类,所以不用类加载,直接构造Cupboard对象。一样先初始化类变量,然后构造器,所以有了第十个、十一个和十二个输出——Bowl(3)和Cupboard()还有f1(2)。后面又new了个Cupboard对象和直接调用对象的方法,后面几个输出就不讲解了。

所以这里来稍微总结下普通类的加载与初始化顺序:

  1. 用到static变量或者是static的方法(注意构造器也是static方法),第一次用new创建某个类的对象(也就是第一次用static的构造器方法),Java编译器会找到这个类的.class文件也就是编译代码(在classpath中),然后加载这个类。
  2. static变量的初始化立刻开始,可以看作类中static变量的初始化也是类加载的一部分。
  3. 如果你是用new Dog()的方法,也就是创建了一个对象,或者说是static的构造器方法触发了构造对象这个动作,那么首先会在堆上为这个对象分配足够的内存,然后这块内存会被清零。这么做的后果就是,类变量会被自动地赋值为默认值,像0和null。
  4. 然后是执行类变量的赋值语句,像是Leg leg = new Leg()类似的,就是为类变量赋值。
  5. 类变量的初始化结束后,便进入构造器中,跑构造器中的代码。

(我觉得3,4步可以就简单看作是:进行类变量的初始化工作,定义时没赋值的就自动赋默认值,有手动赋值的就执行赋值操作~)

涉及继承的类加载和初始化顺序:

首先要知道的一个概念是:当你创建一个子类的的对象的时候,里面其实也包含了一个基类的对象。(可能不止一个基类)所以,基类正确的初始化就变成了一个很重要的工作。Java怎样保证基类的初始化呢?通过基类的构造器。Java会自动在子类构造器中第一时间call基类的构造器。(当然是自动调用无参的那个构造器,如果你自己重载了一个有参的构造器,然后又没有写无参的构造器,那么你要在子类构造器的第一行用super显示调用你自己写的那个构造器)

好的知道这些信息后,就直接看这个例子吧:

package www.com.thinkInJava.reusdingClasses;


class Insect {
    private int i = 9;
    
    protected int j;
    
    protected int h = printInit("Insect.h initialized and j=" + j);
    
    Insect() {
        System.out.println("i = " + i + ", j = " + j);
        j = 39;
    }
    
    private static int x1 = printInit("static Insect.x1 initialized");
    
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
    
}


public class Beetle extends Insect {
    private int k = printInit("Beetle.k initialized");
    
    public Beetle() {
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }
    
    private static int x2 = printInit("static Beetle.x2 initialized");
    
    public static void main(String[] args) {
        System.out.println("Beetle constructor");
        Beetle b = new Beetle();
    }
}

输出是:

/**
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
Insect.h initialized and j=0
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
**/

好了现在也来一步一步分析下输出,理解下涉及继承的类加载和初始化顺序:

首先,运行程序,是从main函数进去的,也就是先调用了Beetle类的static方法——main方法。第一次用static方法,编译器找到Beetle类的编译代码也就是Beetle.class,然后加载这个类。但在加载这个类的过程中,发现了extends关键字,所以按逻辑关系,编译器现在得先加载它的基类:Insect类。

加载Insect类的过程和加载普通类的过程一样,找到.class文件,加载,并立刻初始化static对象。(如果Insect还有基类,那么也要先加载基类)Insect有个private static int x1 = printlnt(static Insect.x1 initialized),这个static field的初始化用到了一个static的方法,于是有了第一句输出——static Insect.x1 initialized

然后Insect的类加载过程就完成了,现在可以继续进行Beetle类的加载。   这里补一句,之所以要先加载基类,是为了保证加载子类的时候,static对象的初始化时如果需要用到基类的static对象,不会出现基类的static对象还没有初始化或者invalid的。       所以现在就开始初始化Beetle类中的static对象。private static int x2 = printInit("static Beetle.x2 initialized");    于是有了第二句输出——static Beetle.x2 initialized然后Beetle的加载过程就完成了,因为这个类加载是由main方法引起的,所以没有构造对象的对象。所以是运行main方法里剩下的内容。所以第三句输出——Beetle constructor。然后下一句是Beetle b = new Beetle()。但由于刚刚已经第一次用了Beetle类中是static方法(main),这里已经不是第一次了,所以没有类加载的过程,直接进入构造对象的过程。

构造对象的过程和普通的类似乎有点不一样:首先是分配足够的内存给Beetle对象,然后再清空,也就是把Beetle类中的类变量的值自动赋值为默认值,也就是k = 0。然后调用基类的构造器(我觉得不一样就是在这里,因为上面的普通类的过程是赋默认值后,运行了赋值语句才到构造器的)。这里是自动调用,你也可以用super显示调用。由于基类刚刚加载过了,所以这里不用进行类的加载,而是直接进行基类对象的构造:分配足够的内存,清空自动赋默认值,也就是Insect中的i = 0,j = 0,h = 0,x1 = 0。然后再执行相应的赋值语句——第四句输出——Insect.h initialized and j=0。   这里是执行了类变量h的赋值语句protected int h = printInt("Insect.h initialized and j = " + j); ,还有执行对i的赋值语句,private int i = 9。    然后再进入基类的构造器中,执行构造器中的代码——第五句输出——i = 9, j = 0。  

基类对象的初始化完成了,构造完成了,就继续子类对象的构造,刚刚做到所有类变量赋默认值,现在是执行类变量的赋值语句——第六句输出——Beetle.k initialized。 然后才是Beetle的构造器中的代码——第七句输出——k =47 (回车)j = 39 

也来总结一下,父类Animal,子类Dog,new Dog():

  1. 先是第一次用Dog的对象,也是static的构造方法的第一次使用,所以加载Dog.class,发现有基类,加载Animal.class
  2. 立刻初始化Animal的static field,完成Animal的类加载,然后继续Dog的类加载
  3. 因为是new语句,所以开始构造Dog对象,先分配足够内存,然后清0,相当于为所有的Dog中的类变量赋值为默认值,0啊null这些。
  4. 然后调用基类的构造器,进行基类对象的构造,一样先为Animal类中的类变量赋值为默认值,然后执行赋值语句,为类变量赋值。然后进入Animal的构造器中,执行代码。
  5. Animal对象的初始化与构造完成,继续Dog的初始化与对象的构造,执行Dog中类变量的赋值语句,最后进入Dog的构造器中执行相应的代码。

大概的类加载和类变量的初始化顺序有点头绪了,再具体的细节还有一些问题,待到学习到JVM的时候再深入去理解与学习。

猜你喜欢

转载自www.cnblogs.com/wangshen31/p/9608925.html