Java源码(.java文件)编译后生成字节码(.class文件),一个.java文件如果定义了多个类,则会为每个类生成一个.class文件。然后jvm虚拟机加载并运行字节码文件。jvm在运行一个Java程序时,采用的是懒加载,并不会把CLASSPATH下的所有类文件一次性加载进来。一个类在第一次被使用时将被jvm加载进来,第一次使用一般有这几种情况:
1.创建这个类的第一个对象;
2.第一次使用这个类的static属性或static方法;
3.加载某个类时,它的超类还没被加载,此时需要先加载超类;
4.反射操作,如Class.forName()
类的初始化
当一个类被java虚拟机加载时,它的static属性将被初始化,它的static方法块将被执行,且这个初始化动作只会执行一次。
class Marker {
public Marker(int i) {
System.out.println(i);
}
public Marker(String s) {
System.out.println(s);
}
}
class Foo {
public static Marker marker = new Marker("foo static field");
static {
System.out.println("foo static block");
}
}
public class LearnTest extends TestCase {
public static void main(String[] args) {
Foo foo1 = new Foo();
Foo foo2 = new Foo();
}
}
执行上述代码,输出:
foo static field
foo static block
可以看到当为类Foo创建第一个对象时(此时类Foo将被jvm加载),它的static属性及static方法块将初始化,且只会初始化一次。除了生成第一个对象时会加载类,第一次使用类的static属性或方法也将加载类。
class Marker {
public Marker(int i) {
System.out.println(i);
}
public Marker(String s) {
System.out.println(s);
}
}
class Foo {
public static int i = 1;
public static Marker marker = new Marker("foo static field");
static {
System.out.println("foo static block");
}
}
public class LearnTest extends TestCase {
public static void main(String[] args) {
System.out.println(Foo.i);
System.out.println(Foo.i);
}
}
执行上述代码,输出:
foo static field
foo static block
1
1
可以看到当第一次使用到类Foo的static属性时(此时类Foo将被jvm加载),它的static属性及static方法块将初始化,且只会初始化一次。
结论:static属性及static方法块将在类被加载进jvm时执行初始化,由于类的加载只发生一次,所以static也只会初始化一次。
对象的初始化
java提供了constructor(构造器)来执行对象的初始化,每个类都有一个默认的空构造器,不执行任何动作,你也可以自己定义任意数量的构造器来定义对象的初始化行为。但是一个对象在初始化时并不仅仅是执行构造器中定义的代码,而是按顺序完成初始化动作,执行构造器仅仅是这初始化动作中的一个。
直接看一段示例代码:
class Marker {
public Marker(int i) {
System.out.println(i);
}
public Marker(String s) {
System.out.println(s);
}
}
class Super {
private static Marker marker = new Marker(0);
static {
System.out.println(1);
}
private Marker marker1 = new Marker(4);
public Super() {
System.out.println(5);
}
}
class Sub extends Super {
private static Marker marker = new Marker(2);
static {
System.out.println(3);
}
private Marker marker1 = new Marker(6);
public Sub() {
System.out.println(7);
}
}
public class LearnTest extends TestCase {
public static void main(String[] args) {
Super s1 = new Sub();
Super s2 = new Sub();
}
}
以上程序将输出:
0
1
2
3
4
5
6
7
4
5
6
7
先说一下继承,当一个子类对象被实例化出来时,会隐式地自动实例化一个它的父类对象,这个父类对象只能由子类对象通过super关键字访问;如果父类是另一个类的子类,则继续向上传递进行实例化,最后实例化出来的子类对象在内存中如下图(其中子类继承父类,父类继承基类):
如图所示,子类对象内部隐式地存在一个父类对象,父类对象内部隐式地存在一个基类对象。
理解了继承,再来看下对象的初始化顺序:
- 由于是第一次实例化Sub类的对象,将加载Sub类,而Sub类继承Super类,所以先把Super类加载进jvm,此时Super类的static属性及static方法块初始化,对应输出0和1;
- Super类加载完毕后,开始加载Sub类,此时Sub类的static属性及static方法块初始化,对应输出2和3;
- 类加载完毕后开始实例化对象,由于Sub类继承Super类,所以Sub类对象包含Super类对象,先将Super类对象实例化出来。Super类对象的属性被初始化,对应输出4。接着Super类对象的构造器被执行,对应输出5;
- Super类的对象实例化完毕后,开始实例化Sub类的对象。Sub类对象的属性被初始化,对应输出6,最后Sub类对象的构造器被执行,对应输出7。整个对象实例化动作结束。
- 当再次实例化另一个Sub类对象时,由于类已经被加载过,static属性及方法块不再初始化,仅执行属性及构造器初始化动作,对应输出4、5、6、7。
其他
以上对类的加载介绍其实是很简略的,实际上jvm对类的加载还分为加载、链接、初始化等步骤,而这些步骤也是可以分步完成的,比如我们第一次使用类的static final常量,仅仅会执行加载、链接两个步骤,但不会初始化类,所以类的static属性及static方法块将不会初始化。
网友评论