当前位置 : 首页 > 车辆数据

Java对象创建与初始化机制深度解析

<|begin▁of▁sentence|># 1. 概述 - 在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖Java中创建对象的关键点。 # 2. Java对象创建时机 - 我们知道,一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象。这种方式在Java规范中被称为 : 由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。下面笔者分别对此进行一一介绍: ## 2.1. 使用new关键字创建对象 - 这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。比如: ```java   Student student = new Student(); ``` ## 2.2. 使用Class类的newInstance方法(反射机制) - 我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象,比如: ```java   Student student2 = (Student)Class.forName("Student类全限定名").newInstance();  或者:   Student stu = Student.class.newInstance(); ``` ## 2.3. 使用Constructor类的newInstance方法(反射机制) - java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象,该方法和Class类中的newInstance方法很像,但是相比之下,Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数,比如: ```java public class Student { private int id; public Student(Integer id) { this.id = id; } public static void main(String[] args) throws Exception { Constructor constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); } } ``` - 使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。 ## 2.4. 使用Clone方法创建对象 - 无论何时我们调用一个对象的clone方法,JVM都会帮我们创建一个新的、一样的对象,特别需要说明的是,用clone方法创建对象的过程中并不会调用任何构造函数。简单而言,要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法,这也是原型模式的应用。比如: ```java public class Student implements Cloneable{ private int id; public Student(Integer id) { this.id = id; } @Override protected Object clone() throws CloneNotSupportedException { // TODO Auto-generated method stub return super.clone(); } public static void main(String[] args) throws Exception { Constructor constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); Student stu4 = (Student) stu3.clone(); } } ``` ## 2.5. 使用(反)序列化机制创建对象 - 当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口,比如: ```java public class Student implements Cloneable, Serializable { private int id; public Student(Integer id) { this.id = id; } @Override public String toString() { return "Student [id=" + id + "]"; } public static void main(String[] args) throws Exception { Constructor constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); // 写对象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("student.bin")); output.writeObject(stu3); output.close(); // 读对象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "student.bin")); Student stu5 = (Student) input.readObject(); System.out.println(stu5); } } ``` ## 2.6. 完整实例 ```java public class Student implements Cloneable, Serializable { private int id; public Student() { } public Student(Integer id) { this.id = id; } @Override protected Object clone() throws CloneNotSupportedException { // TODO Auto-generated method stub return super.clone(); } @Override public String toString() { return "Student [id=" + id + "]"; } public static void main(String[] args) throws Exception { System.out.println("使用new关键字创建对象:"); Student stu1 = new Student(123); System.out.println(stu1); System.out.println("\n---------------------------\n"); System.out.println("使用Class类的newInstance方法创建对象:"); Student stu2 = Student.class.newInstance(); //对应类必须具有无参构造方法,且只有这一种创建方式 System.out.println(stu2); System.out.println("\n---------------------------\n"); System.out.println("使用Constructor类的newInstance方法创建对象:"); Constructor constructor = Student.class .getConstructor(Integer.class); // 调用有参构造方法 Student stu3 = constructor.newInstance(123); System.out.println(stu3); System.out.println("\n---------------------------\n"); System.out.println("使用Clone方法创建对象:"); Student stu4 = (Student) stu3.clone(); System.out.println(stu4); System.out.println("\n---------------------------\n"); System.out.println("使用(反)序列化机制创建对象:"); // 写对象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("student.bin")); output.writeObject(stu4); output.close(); // 读对象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "student.bin")); Student stu5 = (Student) input.readObject(); System.out.println(stu5); } }/* Output: 使用new关键字创建对象: Student [id=123] --------------------------- 使用Class类的newInstance方法创建对象: Student [id=0] --------------------------- 使用Constructor类的newInstance方法创建对象: Student [id=123] --------------------------- 使用Clone方法创建对象: Student [id=123] --------------------------- 使用(反)序列化机制创建对象: Student [id=123] *///:~ ``` - 从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。 # 3. Java对象创建过程 - 当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。**在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。**在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。 ## 3.1. 实例变量初始化与实例代码块初始化 - 我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。例如: ```java public class InstanceVariableInitializer { private int i = 1; private int j = i + 1; public InstanceVariableInitializer(int var){ System.out.println(i); System.out.println(j); this.i = var; System.out.println(i); System.out.println(j); } { // 实例代码块 j += 3; } public static void main(String[] args) { new InstanceVariableInitializer(8); } }/* Output: 1 5 8 5 *///:~ ``` - 上面的例子正好印证了上面的结论。特别需要注意的是,Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量,比如: ```java public class InstanceInitializer { { j = i; } private int i = 1; private int j; } public class InstanceInitializer { private int j = i; private int i = 1; } ``` - 上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如: ```java public class InstanceInitializer { private int j = getI(); private int i = 1; public InstanceInitializer() { i = 2; } private int getI() { return i; } public static void main(String[] args) { InstanceInitializer ii = new InstanceInitializer(); System.out.println(ii.j); } } ``` - 如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,这一动作发生在实例变量i初始化之前和构造函数调用之前。 ## 3.2. 构造函数初始化 - 我们可以从上文知道,实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成()方法,参数列表与Java语言书写的构造函数的参数列表相同。 - 我们知道,Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用,比如: ```java public class ConstructorExample { } ``` - 对于上面代码中定义的类,我们观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下, ```java aload_0 invokespecial #8; //Method java/lang/Object."":()V return ``` - 上面代码的第二行就是调用Object类的默认构造函数的指令。也就是说,如果我们显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面,也就是必须是构造函数的第一条指令。正因为如此,Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。 - 特别地,如果我们在一个构造函数中调用另外一个构造函数,如下所示, ```java public class ConstructorExample { private int i; ConstructorExample() { this(1); .... } ConstructorExample(int i) { .... this.i = i; .... } } ``` - 对于这种情况,Java只允许在ConstructorExample(int i)内调用超类的构造函数,也就是说,下面两种情形的代码编译是无法通过的: ```java public class ConstructorExample { private int i; ConstructorExample() { super(); this(1); // Error:Constructor call must be the first statement in a constructor .... } ConstructorExample(int i) { .... this.i = i; .... } } ``` 或者, ```java public class ConstructorExample { private int i; ConstructorExample() { this(1); super(); //Error: Constructor call must be the first statement in a constructor .... } ConstructorExample(int i) { this.i = i; } } ``` - Java通过对构造函数作出这种限制以便保证一个类的实例能够在被使用之前正确地初始化。 ## 3.3. 小结 - 总而言之,实例化一个对象的过程中,实例变量初始化与实例代码块初始化发生在构造函数初始化之前,也就是说Java编译器会将实例变量初始化与实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前;而构造函数初始化时,会先调用超类构造函数来保证整个对象被完整初始化。特别地,如果我们在构造函数中调用其他构造函数,那么Java要求被调用的构造函数代码必须放在构造函数所有代码的最前面。 # 4. 类的初始化 - 在本文的第2部分中,我们介绍了对象的创建过程,但是并未就类初始化相关过程做详细介绍,这部分内容将在本节详细介绍。 - 我们知道,在一个类被实例化之前,该类必须被JVM加载并初始化,也就是说,类的初始化是发生在对象实例化之前,那么类的初始化过程到底做了些什么呢?实际上,类的初始化过程就是为类的各种变量赋予正确的初始值。事实上,我们可以从两个角度来考虑类的初始化: - 从JVM的角度看,类的初始化过程就是为类的静态变量赋予正确的初始值。这里的“正确”初始值指的是,程序员希望这个静态变量所具有的值,而不是Java默认赋予的零值。因此,类的初始化过程就是调用类构造器()方法的过程,该方法是由编译器自动生成的,它是由类中所有类变量的赋值动作和静态代码块中的语句合并产生的。 - 从程序猿的角度看,类的初始化过程就是为类的静态变量赋予程序员所期望的初始值。因此,我们可以通过静态代码块或者直接为静态变量赋值来为类变量赋予我们所期望的初始值。 - 例如: ```java public class StaticInitializer { private static int i = 1; static{ i = 2; } public StaticInitializer(){ i = 4; } static{ i = 3; } public static void main(String[] args) { System.out.println(StaticInitializer.i); } }/* Output: 3 *///:~ ``` - 上面的程序输出为3,这是因为编译器会将所有的类变量的赋值语句和静态代码块中的语句全部提取出来,合并成一个()方法,该方法中语句的顺序与它们在源代码中的顺序一致。也就是说,上面的代码等价于: ```java public class StaticInitializer { private static int i = 1; public StaticInitializer(){ i = 4; } private static void clinit(){ i = 2; i = 3; } public static void main(String[] args) { System.out.println(StaticInitializer.i); } } ``` - 因此,程序首先将i赋值为1,然后按顺序执行静态代码块中的语句,第一次执行时i被赋值为2,第二次执行时i被赋值为3,最后输出为3。 - 特别地,如果类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成()方法。需要注意的是,()方法与类的构造函数不同,它不需要显式调用父类的构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。 - 由于父类的()方法先执行,也就意味着父类中定义的静态代码块/静态变量的赋值要优先于子类的静态代码块/静态变量的赋值执行。特别地,对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 - 最后,我们通过一个例子来说明类的初始化过程: ```java public class StaticInitializer2 { public static void main(String[] args) { staticFunction(); } static StaticInitializer2 st = new StaticInitializer2(); static{ //静态代码块 System.out.println("1"); } { // 实例代码块 System.out.println("2"); } StaticInitializer2() { // 实例构造器 System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void staticFunction() { // 静态方法 System.out.println("4"); } int a = 110; // 实例变量 static int b = 112; // 静态变量 }/* Output: 2 3 a=110,b=0 1 4 *///:~ ``` - 这个例子中,JVM首先会执行main方法,而main方法中调用了StaticInitializer2的静态方法staticFunction,此时JVM会查看StaticInitializer2是否已经被初始化,结果发现没有被初始化,那么JVM就会立即对StaticInitializer2类进行初始化。在初始化过程中,JVM会按照顺序对静态变量进行赋值和静态代码块中的语句进行执行。因此,首先对静态变量st进行赋值,而new StaticInitializer2()就会引起对象实例化,在对象实例化过程中,会首先执行实例代码块中的语句,然后执行实例构造器中的语句。因此,此时会输出2和3,并且在实例构造器中,a已经被赋值为110,而b仅仅被赋予默认值0,因此输出a=110,b=0。对象实例化完成之后,JVM继续对类进行初始化,按顺序执行静态代码块中的语句,输出1,然后为静态变量b赋值为112。至此,类的初始化过程

栏目列表