Java 对象的创建过程
类的初始化与实例化
一个 Java 对象的创建过程往往包括类的初始化 和 实例化 两个阶段。
Java 规范规定一个对象在可以被使用之前必须要被正确地初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。在实例化一个对象时,JVM 首先会检查相关类型是否已经加载并初始化,如果没有,则 JVM 立即进行加载并调用类构造器完成类的初始化。
Java 对象的创建方式
一个对象在可以被使用之前必须要被正确地实例化。在 Java 程序中,有多种方法可以创建对象,最直接的一种就是使用 new 关键字来调用一个类的构造函数显式地创建对象。这种方式是由执行类的实例创建表达式创建对象。除此之外,还可以使用反射机制 (Class 类的 newInstance 方法、Constructor 类的newInstance 方法)、使用 Clone 方法、使用反序列化等方式创建对象。
- 使用 new 关键字创建对象
这是最常见、最简单的创建对象的方式,通过这种方式可以调用任意的构造函数(无参的和有参的)创建对象。 - 使用 Class 类的 newInstance 方法 (反射机制) 。事实上 Class 类的 newInstance 方法内部调用的是 Constructor 类的 newInstance 方法,相当于是调用无参的构造器创建对象。
- 使用 Constructor 类的 newInstance 方法 (反射机制) 。该方法和 Class 类中的 newInstance 方法类似,不同的是 Constructor 类的 newInstance 方法可以调用有参数的和私有的构造函数。
- 使用Clone方法创建对象
调用一个对象的 clone 方法,JVM 都会创建一个新的、一样的对象。特别需要说明的是,用 clone 方法创建对象的过程中并不会调用任何构造函数。如何使用 clone 方法以及浅克隆/深克隆机制。简单而言,要想使用 clone 方法,就必须先实现 Cloneable 接口并实现其定义的 clone 方法,这也是原型模式的应用。 - 使用 (反) 序列化机制创建对象
当反序列化一个对象时,JVM会创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,对应的类需要实现 Serializable 接口。
从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的。
Java 对象的创建过程
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其继承父类的实例变量 (即使继承超类的实例变量有可能被隐藏也会被分配空间) 。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值。在内存分配完成之后,Java 虚拟机就会开始对新创建的对象进行初始化。在 Java 对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是实例变量初始化、实例代码块初始化以及构造函数初始化。
实例变量初始化与实例代码块初始化
在定义(声明)实例变量的同时,可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后 (构造函数的第一条语句必须是超类构造函数的调用语句) ,构造函数本身的代码之前。
特别需要注意的是,Java 是按照先后顺序来执行实例变量初始化和实例初始化器中的代码,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量。这么做是为了保证一个变量在被使用之前已经被正确地初始化。
构造函数初始化
实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前。Java 中的每一个类中都至少会有一个构造函数,如果没有显式定义构造函数,那么 JVM 会为它提供一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成 () 方法 (参数列表与 Java 语言中构造函数的参数列表相同) 。Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。
事实上,这一点是在构造函数中保证的:Java 强制要求除 Object 类 (Object 是 Java 的顶层类,没有超类) 之外所有类的构造函数中的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数。如果既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会自动生成一个对超类构造函数的调用。
如果显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面。正因为如此,Java 才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。特别地,如果在一个构造函数中调用另外一个构造函数则不能显式调用超类的构造函数,而且要另一个构造函数放在构造函数所有代码的最前面。
Java 通过对构造函数作出上述限制保证一个类的实例能够在被使用之前正确地初始化。