• 类加载和初始化顺序


    这个博客是我看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类中的类变量赋值为默认值,就清空内0啊null什么的,然后执行赋值语句,为类变量赋值。然后进入Animal的构造器中,执行代码。(注意这里和前面不涉及inheritance的顺序不一样,前面是先执行那个类的类变量赋值再进入构造器,而这里第三步类变量内存清空后就进入基类对象的构造)
    5. Animal对象的初始化与构造完成,继续Dog的初始化与对象的构造,执行Dog中类变量的赋值语句,最后进入Dog的构造器中执行相应的代码。

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

  • 相关阅读:
    第三方网站实现绑定微信登陆
    安卓微信中bootstrap下拉菜单无法正常工作的解决方案
    一个Web钢琴谱记忆工具
    腾讯实习生面试经历-15年3月-Web前端岗
    AngularJS自定义指令三种scope
    AngularJS在自定义指令中传递Model
    Canvas文本绘制的浏览器差异
    AngularJS学习笔记
    善用width:auto以及white-space:nowrap以防止布局被打破
    Timeline中frame mode帧模式中idle占据大片位置
  • 原文地址:https://www.cnblogs.com/wangshen31/p/9608925.html
Copyright © 2020-2023  润新知