文章目录
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 类,它的静态属性 table 和 cupboard 随后被初始化,这会导致它们对应的类也被加载,而由于它们都包含静态的 Bowl 对象,所以 Bowl 类也会被加载。因此,在这个特殊的程序中,所有的类都会在main()
方法之前被加载。 -
概括一下创建对象的过程,假设有个名为 Dog 的类:
- 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 Dog.class。
- 当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次。
- 当用
new Dog()
创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。 - 分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null。
- 执行所有出现在字段定义处的初始化动作。
- 执行构造器。
-
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 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。
- 当执行 java Beetle,首先会试图访问 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 */
- Glyph 的
draw()
被设计为可重写,在 RoundGlyph 这个方法被重写。但是 Glyph 的构造器里调用了这个方法,结果调用了 RoundGlyph 的draw()
方法,这看起来正是我们的目的。输出结果表明,当 Glyph 构造器调用了draw()
时,radius 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。 - 前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:
- 在所有事发生前,分配给对象的存储空间会被初始化为对应类型的初始值。
- 类被加载,初始化static属性。
- 初始化基类非静态属性。
- 调用基类构造器。此时调用重写后的
draw()
方法。 - 初始化派生类非静态属性。
- 最终调用派生类的构造器。
- Glyph 的
-
例:
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 */
- 执行Test类中的main()方法
- 发现要调用B类的构造器,于是得先加载B类,然后又发现B类继承A类,所以得先加载A类
- A类加载时,对A类的static属性p1进初始化。
- 然后开始加载B类,对B类的static属性p1进行初始化。
- 加载完两个类后,开始创建对象。
- 首先得初始化A类中的非静态属性p2。
- 然后调用A类的构造方法。
- 然后再初始化B类的非静态属性P2。
- 最后调用B类的构造方法。