【Java核心知识】类中属性的初始化、static关键字性质、类的加载是什么、类属性的初始化何时发生、类中属性初始化顺序

static关键字、类的加载、属性的初始化问题

一、属性的初始化

1.1 方法中属性的初始化

  • 对于方法中定义的属性(准确说是局部变量),一定要对其进行初始化操作之后才能对其进行其他操作。

  • 例:

    void f() {
        int i;
        i++;	// 会报错,得先对i进行初始化操作。
    }
    

    对于方法的局部变量,如果不对其进行初始化,会在编译时报错。

1.2 类中属性的初始化

  • 对于类中的属性,情况就会不同,Java允许不对其进行初始化,对于那些未被程序员初始化的类中属性,Java会在为它们赋予该类型的默认值。

  • Java类型的默认值:

    • 8大基本数据类型:byte:0;short:0;int:0;long:0L;float:0.0f;double:0.0d;boolean:false;char:" ";(空字符)
    • 引用类型(对象、数组):null
  • 例:

    public class InitialValues {
        int val;  // 在类被加载时,会被初始化为0
        
        void addOne() {
            System.out.println("before_addOne:"+val);
            val++;
        }
        void printInitialValues() {
    		System.out.println("printInitialValues():"val);
        }
    
        public static void main(String[] args) {
            InitialValues a = new InitialValues();
            a.addOne();
            a.printInitialValues();
        }
    }
    /* 
    程序执行结果:
        before_addOne:0
        printInitialValues():1
    */
    
  • 对于未手动初始化的类中属性,其自动初始化赋值是在构造方法之前的。例:

    class InitialValues {
        int val;  // 在类被加载时,会被初始化为0,且优先于构造方法中的赋值
    
        InitialValues() {
            System.out.println("in_constructor:"+val);
            val=10;
        }
        void printInitialValues() {
            System.out.println("printInitialValues():"+val);
        }
    
        public static void main(String[] args) {
            InitialValues a = new InitialValues();
            a.printInitialValues();
        }
    }
    /*
    程序执行结果:
    	in_constructor:0
    	printInitialValues():10
    */
    

1.3 属性初始化的顺序(非静态)

  • 在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造方法)被调用时得到初始化。

  • 例:

    class Window {
        Window(int marker) {
            System.out.println("Window(" + marker + ")");
        }
    }
    
    class House {
        Window w1 = new Window(1); 		// 在构造方法被调用时
    
        House() {
            System.out.println("in_constructor");
            w3 = new Window(33);
        }
    
        Window w2 = new Window(2);	 	// 在构造方法被调用时
    
        void f() {
            System.out.println("f()");
        }
    
        Window w3 = new Window(3); 		// 在构造方法被调用时
    }
    
    public class OrderOfInitialization {
        public static void main(String[] args) {
            House h = new House();
            h.f(); 
        }
    }
    /*
    程序执行结果:
        Window(1)
        Window(2)
        Window(3)
        in_constructor
        Window(33)
        f()
    */
    

二、static关键字

  • static关键字可以对属性、方法、代码块、类(static内部类)进行声明。

  • static关键字的基本属性

    • static关键字定义的成员属于这个类所有类共有的。
    • static关键字定义的成员可以使用类名直接调用,所以没有实例化对象时也可以调用static成员
    • 被static关键字声明的属性将被放在全局数据区(普通属性被放在堆内存中)
    • 不能使用static关键字定义局部变量(非类成员属性)
    • static成员只能访问static成员,非static成员此无限制
  • Java中主要的4块内存空间:

    • 栈内存空间:保存所有的对象名称(准确的说是保存引用的堆内存空间的地址)
    • 堆内存空间:保存每个对象的具体属性内容
    • 全局数据区:保存static类型的属性
    • 全局代码区:保存所有的方法代码
  • static内部类:在一个类的内部定义一个static类,这个类就称为static内部类;内部类其实就是一个外部类,因为他可以使用“类名.类名”直接实例化对象。

  • static关键字修饰代码块时,该代码块被称为"静态子句"(有时叫做静态块),静态块只会在类被加载的时候执行一次。

三、类的加载与静态数据的初始化

3.1 类的加载与数据的初始化

  • 类的加载:类的加载会在类中的静态成员被调用或构造方法被调用时发生,且只会被加载一次。(其实准确的说构造方法是一种隐式的static方法)

  • 类中的static属性的初始化会在类被加载的时候进行。

  • 类中的非static属性的初始化会在构造方法被调用时进行

  • 例:

    class Window {
        Window(int marker) {
            System.out.println("Window(" + marker + ")");
        }
    }
    
    class House {
        static Window w1 = new Window(1); 	// 在类被加载时初始化
        Window w2 = new Window(2);	 	    // 在构造方法被调用时初始化
        House() {
            System.out.println("in_constructor");
            w3 = new Window(33);
        }
        static void f() {
            System.out.println("f()");
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            House h;						// 没有实例化对象,类没有被加载
            House.f();						// 调用类中的static成员,类被加载,初始化static属性
            h = new House();				// 调用构造方法,初始化所以没被初始化的属性
        }
    }
    /*
    程序执行结果:
        Window(1)
        f()
        Window(2)
        in_constructor
        Window(33)
    */
    

3.2 属性的初始化顺序

  • 前面我们说过:类中的static属性的初始化会在类被加载的时候进行,static属性的初始化会在构造方法被调用时进行。

  • 问题:构造方法也是被调用时也会使类被加载,那么构造被调用时,各种属性的初始化顺序如何呢?

  • 答:static属性会先按其定义的顺序进行初始化,非static属性会在static属性初始化完成后进行初始化。

  • 例:

    class Bowl {
        Bowl(int marker) {
            System.out.println("Bowl(" + marker + ")");
        }
        
        void f1(int marker) {
            System.out.println("f1(" + marker + ")");
        }
    }
    
    class Table {
        static Bowl bowl1 = new Bowl(1);
        
        Table() {
            System.out.println("Table()");
            bowl2.f1(1);
        }
        
        void f2(int marker) {
            System.out.println("f2(" + marker + ")");
        }
        
        static Bowl bowl2 = new Bowl(2);
    }
    
    class Cupboard {
        Bowl bowl3 = new Bowl(3);
        static Bowl bowl4 = new Bowl(4);
        
        Cupboard() {
            System.out.println("Cupboard()");
            bowl4.f1(2);
        }
        
        void f3(int marker) {
            System.out.println("f3(" + marker + ")");
        }
        
        static Bowl bowl5 = new Bowl(5);
    }
    
    public class StaticInitialization {
        public static void main(String[] args) {
            System.out.println("main creating new Cupboard()");
            new Cupboard();
            System.out.println("main creating new Cupboard()");
            new Cupboard();
            table.f2(1);
            cupboard.f3(1);
        }
        
        static Table table = new Table();
        static Cupboard cupboard = new Cupboard();
    }
    /*
    程序执行结果:
    	Bowl(1)
        Bowl(2)
        Table()
        f1(1)
        Bowl(4)
        Bowl(5)
        Bowl(3)
        Cupboard()
        f1(2)
        main creating new Cupboard()
        Bowl(3)
        Cupboard()
        f1(2)
        main creating new Cupboard()
        Bowl(3)
        Cupboard()
        f1(2)
        f2(1)
        f3(1)
    */
    

    要执行 main() 方法,必须加载 StaticInitialization 类,它的静态属性 tablecupboard 随后被初始化,这会导致它们对应的类也被加载,而由于它们都包含静态的 Bowl 对象,所以 Bowl 类也会被加载。因此,在这个特殊的程序中,所有的类都会在 main() 方法之前被加载。

  • 概括一下创建对象的过程,假设有个名为 Dog 的类:

    1. 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 Dog.class
    2. 当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次。
    3. 当用 new Dog() 创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。
    4. 分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null
    5. 执行所有出现在字段定义处的初始化动作。
    6. 执行构造器。
  • static关键字修饰代码块时,该代码块被称为"静态子句"(有时叫做静态块),静态块只会在类被加载的时候执行一次。

    class Cup {
        Cup(int marker) {
            System.out.println("Cup(" + marker + ")");
        }
        
        void f(int marker) {
            System.out.println("f(" + marker + ")");
        }
    }
    
    class Cups {
        static Cup cup1;
        static Cup cup2;
        
        static {
            cup1 = new Cup(1);
            cup2 = new Cup(2);
        }
        
        Cups() {
            System.out.println("Cups()");
        }
    }
    
    public class ExplicitStatic {
        public static void main(String[] args) {
            System.out.println("Inside main()");
            Cups.cup1.f(99); // [1]
        }
    }
    /*
    程序执行结果:
        Inside main()
        Cup(1)
        Cup(2)
        f(99)
    */
    

3.3 继承与初始化

  • 问题:前面我们说类会在类中的static成员首次调用时被加载,那么如果一个类继承了一个基类,问这个基类何时被加载?

  • 答:基类会在派生类前被加载。

  • 例:

    // reuse/Beetle.java
    // The full process of initialization
    class Insect {
        private int i = 9;
        protected int 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
        i = 9, j = 0
        Beetle.k initialized
        k = 47
        j = 39
    */
    
    • 当执行 java Beetle,首先会试图访问 Beetle 类的 main() 方法(一个静态方法),加载器启动并找出 Beetle 类的编译代码(在名为 Beetle.class 的文件中)。在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类对象的代码注释掉证明这点。)
    • 如果基类还存在自身的基类,那么第二个基类也将被加载,以此类推。接下来,根基类(例子中根基类是 Insect)的 static 的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类中 static 的初始化可能依赖基类成员是否被正确地初始化。
    • 至此,必要的类都加载完毕,对象可以被创建了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为 null —— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用 super 调用指定的基类构造器(在 Beetle 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。
  • 现在有另一个问题,如果基类的构造方法调用了子类中的方法,且该方法调用了子类的属性。按理说在创建子类对象时,应该先初始化基类的属性,再创造基类对象;然后再初始化子类属性,最后创建子类对象。所以说这里是在子类属性初始化之前就调用了,这样会产生什么问题呢?

    class Glyph {
        void draw() {
            System.out.println("Glyph.draw()");
        }
    
        Glyph() {
            System.out.println("Glyph() before draw()");
            draw();		// 由于子类覆写了draw()方法,所以这里调用的是子类的draw()方法
            System.out.println("Glyph() after draw()");
        }
    }
    
    class RoundGlyph extends Glyph {
        private int radius = 1;
    
        RoundGlyph(int r) {
            radius = r;
            System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
        }
    
        @Override
        void draw() {
            System.out.println("RoundGlyph.draw(), radius = " + radius);
        }
    }
    
    public class PolyConstructors {
        public static void main(String[] args) {
            new RoundGlyph(5);
        }
    }
    /*
    程序执行结果:
        Glyph() before draw()
        RoundGlyph.draw(), radius = 0
        Glyph() after draw()
        RoundGlyph.RoundGlyph(), radius = 5
    */
    
    • Glyphdraw() 被设计为可重写,在 RoundGlyph 这个方法被重写。但是 Glyph 的构造器里调用了这个方法,结果调用了 RoundGlyphdraw() 方法,这看起来正是我们的目的。输出结果表明,当 Glyph 构造器调用了 draw() 时,radius 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。
    • 前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:
      1. 在所有事发生前,分配给对象的存储空间会被初始化为对应类型的初始值。
      2. 类被加载,初始化static属性。
      3. 初始化基类非静态属性。
      4. 调用基类构造器。此时调用重写后的 draw() 方法。
      5. 初始化派生类非静态属性。
      6. 最终调用派生类的构造器。
  • 例:

    class Print {
        String str;
        Print(String str) {
            this.str = str;
            System.out.println(str);
        }
        public String toString() {
            return str;
        }
    }
    
    class A {
        static Print p1 = new Print("static A.p1");
        Print p2 = new Print("static A.p2");
        A() {
            fun();
            System.out.println("constructorA");
        }
        public void fun() {}
    
    }
    
    class B extends A {
        static Print p1 = new Print("static B.p1");
        Print p2 = new Print("static B.p2");
        B() {
            System.out.println("constructorB");
        }
        public void fun() {
            System.out.println(p1+" fun()");
            System.out.println(p2+" fun()");
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            B b = new B();
        }
    }
    /*
    程序执行结果:
        static A.p1
        static B.p1
        static A.p2
        static B.p1 fun()
        null fun()
        constructorA
        static B.p2
        constructorB
    */
    
    1. 执行Test类中的main()方法
    2. 发现要调用B类的构造器,于是得先加载B类,然后又发现B类继承A类,所以得先加载A类
    3. A类加载时,对A类的static属性p1进初始化。
    4. 然后开始加载B类,对B类的static属性p1进行初始化。
    5. 加载完两个类后,开始创建对象。
    6. 首先得初始化A类中的非静态属性p2。
    7. 然后调用A类的构造方法。
    8. 然后再初始化B类的非静态属性P2。
    9. 最后调用B类的构造方法。
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大枫树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值