摘要
在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。一个Java对象的创建过程往往包括类初始化和类实例化两个阶段。
1 Java对象创建时机
我们知道,一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象,这种方式在Java规范中被称为 : 由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。
1.1 使用new关键字创建对象
这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。:
Object object = new Object();
1.2 使用Class类的newInstance方法(反射机制)
我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法是调用无参的构造器创建对象
Student student = (Student)Class.forName("Student类全限定名").newInstance();
//或者
Student stu = Student.class.newInstance();
1.3 使用Constructor类的newInstance方法(反射机制)
java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象,该方法和Class类中的newInstance方法很像,但是相比之下,Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数
public class Student {
private int id;
public Student(Integer id) {
this.id = id;
}
public static void main(String[] args) throws Exception {
Constructor<Student> constructor = Student.class
.getConstructor(Integer.class);
Student stu3 = constructor.newInstance(123);
}
}
- 使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法
1.4 使用clone方法创建对象
无论何时我们调用一个对象的clone方法,JVM都会帮我们创建一个新的、一样的对象,特别需要说明的是,用clone方法创建对象的过程中并不会调用任何构造函数。要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法,这也是原型模式的应用。
public class Child implements Cloneable {
public Child() {
}
@Override
public Object clone(){
Child child = null
try {
child = (Child) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return child;
}
public static void main(String[] args) throws Exception {
Constructor<Child> childConstructor = Child.class.getConstructor();
Child child = childConstructor.newInstance();
Child cloneChild = (Child) child.clone();
System.out.println(cloneChild==child);//false
}
}
1.5 使用(反)序列化机制创建对象
当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口,
public class Child implements Serializable {
public static void main(String[] args) throws Exception {
Child child = new Child();
//写对象
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("aaa"));
output.writeObject(child);
output.close();
//读对象
ObjectInputStream input = new ObjectInputStream(new FileInputStream("aaa"));
Child c = (Child) input.readObject();
System.out.println(child);//cn.zck.jvm.Child@6d6f6e28
System.out.println(c);//cn.zck.jvm.Child@7ba4f24f
}
}
2 Java对象创建过程
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
在编译生成的字节码中,构造函数会被命名成
我们知道,Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用
public class Parent {
private Integer a;
private String b;
private Date c;
private Double d;
public Parent() {
this(2);
}
public Parent(int a) {
this.a = a;
this();//Call to 'this()' must be first statement in constructor body
}
public Parent(String b) {
super();
this();//Call to 'this()' must be first statement in constructor body
}
public Parent(Date c) {
this();
super();//Call to 'super()' must be first statement in constructor body
}
public Parent(Double d) {
//会自动生成super()
this.d = d;
}
}
- 总而言之,实例化一个类的对象的过程是一个典型的递归过程,进一步地说,在实例化一个类的对象时,具体过程是这样的:
在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各l进行实例化,直到完成对目标类的实例化。
具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。对象实例化过程 public Child(int a, String b){ super(); // 实例变量或实例代码块 // 构造函数本身代码 }

总的来说,类实例化的一般过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数<init>() -> 子类的成员变量和实例代码块 -> 子类的构造函数<init>()。