java类的初始化

Posted Monkey_Dog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java类的初始化相关的知识,希望对你有一定的参考价值。

首先整理一下一些初始化的细节:

对于一个类的构造方法,我们把这样子的方法叫做默认构造器或者无参构造器,这个方法没有返回值,即使有这个返回值,那么编译器也并不知道如何处理这个返回值,从实质来说,构造方法其实就是一个隐式的static方法

当我们指定了其他的构造方法,而整个类有且仅有一个这个构造方法:编译器将不允许你以其他任何方式创建没有在类定义的构造器对象。相当于我们对类讲:我已经为你定义了构造你的方法,除了这个方法以外,你不需要用其他的方法去创造对象,即使它曾经是默认构造器

你可以重载构造方法,使他能用不同的方式构造对象:

public Dog(){}

public Dog(int Id){}

也可以在一个构造方法里面调用另外一个构造方法,但是禁止在其他地方调用构造方法

public Dog(int Id){

Dog();

//do other thing

}

关于重载——类名和参数,不能使用返回值作为评判标准,因为当你调用的时候,没有指定返回值,编译器不知道你要调用哪一个,如果调换形参的顺序十一中不规范的重载写法,即使他是可行的

成员的初始化,如果没有指定值,我们的编译器会在你创建成员变量的地方为他们提供初始化的值

以上是一些关于java类构造的小部分细节,平时写java的代码但是却很少留意这些事情


java编译器在处理类的初始化的时候其实是经过三个阶段的:(From 《java编程思想》)

1、加载:由类加载器执行,查找字节码并从字节码创建一个class对象(目前先撇开类加载器是怎么工作的,可以这样子说:类在初次使用的时候才会发生加载

2、链接:在链接阶段将验证类中的字节码,为静态区域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用

3、初始化:首先初始化静态块、静态变量、静态方法,如果该类有超类,则对其初始化

如果对这三步需要更深一步的理解,可以参考资料:http://news.newhua.com/news/2011/0727/128305.shtml


在加载过程中,类的初次使用有如下几种形式:(http://www.cnblogs.com/zhguang/p/3154584.html )
1) 创建类的实例
2) 访问某个类或者接口的静态变量,或者对该静态变量赋值(如果访问静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)
3) 调用类的静态方法
4) 反射(Class.forName(xxx.xxx.xxx))
5) 初始化一个类的子类(相当于对父类的主动使用),不过直接通过子类引用父类元素,不会引起子类的初始化
6) Java虚拟机被标明为启动类的类(包含main方法的)
类与接口的初始化不同,如果一个类被初始化,则其父类或父接口也会被初始化,但如果一个接口初始化,则不会引起其父接口的初始化。


接下来就是对空间的分配,但此时仅仅是分配空间并把所有内存置为0

在初始化的过程中,我们必须关注初始化的顺序,在同一个概念层次来说,即static层次、普通成员变量层次等,变量定义的先后顺序决定了初始化的先后顺序

举个例子,house.java和dog.java:

Dog.java

public class Dog {
	private int Id;//用来标识dog
	public Dog(int Id) {
		this.Id = Id;
		System.out.println("I'm dog "+ this.Id);
	}
}

House.java:

public class House {
	public House(){
		System.out.println("In the house:");
		Dog dog6 = new Dog(6);
	}
	static {
		Dog dog5 = new Dog(5);
	}
	private Dog dog7 = new Dog(7);
	private static Dog dog1 = new Dog(1);
	private static Dog dog3 = new Dog(3);
	private static Dog dog2 = new Dog(2);
	
	public static void createDog(){
		Dog dog4 = new Dog(4);
	}
}

main方法:

public class Main {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		House h = new House();
	}
}

运行输出:

I‘m dog 5
I‘m dog 1
I‘m dog 3
I‘m dog 2
I‘m dog 7
In the house:
I‘m dog 6

在house里面定义了,1——7个Dog对象,其中1、2、3在static对象层次上用来证明这个原则:

当你把1、3、2顺序调换一下,换成1、2、3,输出顺序也为1、2、3。在类里面static方法如果没有被用到,则不会进行初始化


其次,我们能发现静态块是会被初始化,尝试把静态块语句放到后面,输出的顺序也会变,就是说静态块的初始化符合“变量定义的先后顺序决定了初始化的先后顺序”这个原则
注意:在dog1、2、3这几个对象,如果没有new,对象是不会被初始化的
在上面提到:构造方法实质上也是一个静态方法,构造方法与定义先后无关,构造方法会在所有的变量初始化后进行初始化,而普通成员变量会在static成员变量初始化之后初始化
所以初始化顺序:静态对象——非静态对象——构造方法
假设有一个Dog的类:
1、即使没有显式的使用static关键字,构造器实际上也是静态方法,因此,在首次创造Dog对象额时候,或者Dog类的静态方法/静态域首次被访问,java解释器必须查找类路径,定位Dog.class
2、载入Dog.class,有关静态初始化的所有动作都会执行,因此,静态初始化只有在class对象首次加载的时候进行一次
3、当使用new Dog()来创建对象的时候,首先在堆里为Dog对象分配足够的存储空间
4、存储空间会清零,这就自动的将Dog对象的所有基本类型数据设置为默认值,引用为null

5、执行出现在定义处的初始化动作

6、执行构造器


为了更好理解初始化顺序,引用一道阿里巴巴的面试题:http://my.oschina.net/chape/blog/299533?fromerr=G7bNLvjy

public class InitializeDemo {
	private static int k = 1;
	private static InitializeDemo t1 = new InitializeDemo("t1");
	private static InitializeDemo t2 = new InitializeDemo("t2");
	private static int i = print("i");
	private static int n = 99;
	static {
		print("静态块");
	}
	private int j = print("j");
	{
		print("构造块");
	}
	public InitializeDemo(String str) {
		System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
		++i;
		++n;
	}
	public static int print(String str) {
		System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
		++n;
		return ++i;
	}
	public static void main(String args[]) {
		new InitializeDemo("init");
	}
}

我们可以看到他的输出结果:

1:j   i=0    n=0
2:构造块   i=1    n=1
3:t1   i=2    n=2
4:j   i=3    n=3
5:构造块   i=4    n=4
6:t2   i=5    n=5
7:i   i=6    n=6
8:静态块   i=7    n=99
9:j   i=8    n=100
10:构造块   i=9    n=101
11:init   i=10    n=102

在main方法开始加载这个类的时候,所有的static属性的成员、方法都已经在链接阶段分配好存储控件并且进行默认值的初始化,可以结合上面的总结来看,就是说static域除k根据执行顺序初始化为1,i、n、静态块均未执行到,他们只有自己的空间,提供默认值0,此时执行到t1,所以t1也就剩下自己的内部普通成员——j,值得注意的是j并不是只有一份。

其次,构造方法执行在构造块之后,在输出的1——3行,我们可以看作是t1对自己内部的处理,并使static成员i、n的值变化,同理,在第4——6,这是t2的处理,执行到private static int i = print("i");,i正式被初始化,接下来到n、静态块、以init为标识的类构造块和构造方法


除了通过new的方法来初始化,在通过反射来初始化的时候有一种情况被称为编译时常量:

Initable.java

public class Initable {
	static final int staticFinal = 47;
	static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
	static{
		System.out.println("Initializing Initable");
	}
}
Initable2.java

public class Initable2 {
	static int staticNonFinal = 147;
	static{
		System.out.println("Initializing Initable2");
	}
}

Initable3.java

public class Initable3 {
	static int staticNonFinal = 74;
	static{
		System.out.println("Initializing Initable3");
	}
}
ClassInitialization.java
public class ClassInitialization {
	public static Random rand = new Random(47);
	public static void main(String[] args) throws ClassNotFoundException {
		// TODO Auto-generated method stub
		Class initable = Initable.class;
		System.out.println("After creating Initable ref");
		System.out.println(Initable.staticFinal);
		System.out.println(Initable.staticFinal2);
		
		System.out.println(Initable2.staticNonFinal);
		
		Class initable3 = Class.forName("com.example.test.Initable3");
		System.out.println("After creating Initable3 ref");
		System.out.println(Initable3.staticNonFinal);
	}
}

运行输出:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

为了更好的理解,在这里引入一个概念:编译时常量——形如static final int initable = 47; 这样的语句

1)、Class initable = Initable.class;这句话创造了一个引用,但是并不会发生类的加载和初始化,但是访问类里面的编译时常量却能仅仅读取这个值47,但不会发生初始化,但是其他的就并不这样子保证了,static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);这个不算是编译时常量,所以在使用时就进行了Initable的初始化,生成了staticFinal2,输出static块,然后在main里面syso出来staticFinal2。

2)、对于没有final的,也不算编译时常量,那么initable2这里的输出语句也浅显易懂

3)、对于用Class.forName()形式来加载一个类,他是能够马上初始化这个类的,尝试注释掉最后两行代码,仅保留forName这一行,会发现Initable3被初始化了(里面的静态块能输出)


另一种情况:涉及到继承概念:

初始化顺序:基类—— 子类

Insect.java

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

Beetle.java

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 x2 initialized
static Beetle x2 initialized
Beetle constructor
i=9 j=0
Beetle k initialized
k=47
j=39

因为main方法在bettle类里面,在使用加载beetle的过程中,编译器注意到他并不是基类,于是去寻找基类,得到Insect后:

1)、先按照初始化顺序初始化static成员,输出static Insect x2 initialized,然后子类的static成员被初始化(原因是子类的static成员可能依赖于父类的static成员是否能被正确的初始化)

2)、由于本例是因为在加载main的时候就需要对insect初始化,并未执行new Bettle(),所以会先输出Beetle constructor,但是如果把main放在另外的新类,就会最首先输出Beetle constructor这句话。言归正传,初始化父类的普通成员变量后接下来就是父类的构造方法,然后子类的普通成员变量,子类的构造方法

所以:父类静态成员——子类静态成员——父类普通成员以及构造方法——子类普通成员以及构造方法

关于接口:在接口里面的域是隐式的static和final的,会在类第一次加载的时候被初始化


在继承里面,我们可以看看这一种情况:

public class Glyph {
	void draw(){
		System.out.println("Glyph.draw");
	}
	public Glyph() {
		System.out.println("Glyph before draw");
		draw();
		System.out.println("Glyph after draw");
	}
}

public class RoundGlyph extends Glyph {
	private int radius = 1;
	public RoundGlyph(int r) {
		System.out.println("RoundGlyph,radius = "+radius);
	}
	void draw(){
		System.out.println("RoundGlyph,radius = "+radius);
	}
}

public class Main {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new RoundGlyph(5);
	}
}

运行输出:

Glyph before draw
RoundGlyph,radius = 0
Glyph after draw
RoundGlyph,radius = 1


我们可以发现,当调用被覆盖的方法时,该方法仍未被初始化,未初始化的成员是默认值,这也验证了上面那道面试题在初始化时成员会先开辟空间并且提供默认值这种情况,其实,java编程思想里面也详细描述了上面这个代码的过程(这样的优点是所有东西能在调用前至少初始化成0):

1、在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零

2、如前所述那样调用基类的构造器,此时,调用被覆盖后的draw()方法,在调用RoundGlyph构造器之前调用,由于步骤一,我们会发现radius为0

3、按照声明的顺序初始化

4、调用导出类(子类)的构造器主体


简单提及一下java的垃圾回收:我们都知道java有一个垃圾回收器,不需要程序员手动清理多余的内存,关于垃圾回收器仍然有不少细节,例如:

1、垃圾回收器只知道释放那些被new(堆)出来的内存,而不会回收其部分的内存。

2、垃圾回收器不一定释放没用到的对象

3、回收之前会执行finalize()方法,但是这不意味者在finalize()里面做清理工作——因为你不知道他什么时候要回收,也意味着清理工作的执行时间你不可控

4、停止——复制:垃圾回收器回收策略之一,先暂停程序的运行,然后把存活的对象复制一份到另外一个堆,没有被复制的全部是垃圾,在这种方法效率不高,除了复制动作浪费时间,还浪费空间

5、标记——清扫:遍历所有的引用,寻找存活的对象,加以标记,当遍历完成之后,把没有标记的对象释放,但是剩下的空间是不连续的

6、这两种方式是结合使用的,统称“自适应”技术,java虚拟机以“块”来处理这些事,内存大的单独放在一个块,小的多个放在一个块,用代数来记录他是否存活,被引用了代数则增加,很明显,小的对象使用复制的方法整理,而大的对象不需要复制,只会增加代数


以上大多来源于《java编程思想》关于java初始化部分的小细节笔记



以上是关于java类的初始化的主要内容,如果未能解决你的问题,请参考以下文章

Java构造方法及类的初始化

java中无参,有参,默认构造方法的应用及举例

java学习笔记(Core Java)5 继承

java 面向对象:类的结构:构造器简介;属性赋值顺序;JavaBean的概念

Java继承之再谈构造器

java中类的构造方法