咱们知道,一个对象在能够被使用以前必需要被正确地实例化。在Java代码中,有不少行为能够引发对象的建立,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地建立对象,这种方式在Java规范中被称为 : 由执行类实例建立表达式而引发的对象建立。除此以外,咱们还可使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式建立对象。java
1). 使用new关键字建立对象程序员
这是咱们最多见的也是最简单的建立对象的方式,经过这种方式咱们能够调用任意的构造函数(无参的和有参的)去建立对象。好比:算法
Student student = new Student();
2). 使用Class类的newInstance方法(反射机制)编程
咱们也能够经过Java的反射机制使用Class类的newInstance方法来建立对象,事实上,这个newInstance方法调用无参的构造器建立对象,好比:ide
Student student2 = (Student)Class.forName("Student类全限定名").newInstance(); 或者: Student stu = Student.class.newInstance();
3). 使用Constructor类的newInstance方法(反射机制)函数
java.lang.relect.Constructor类里也有一个newInstance方法能够建立对象,该方法和Class类中的newInstance方法很像,可是相比之下,Constructor类的newInstance方法更增强大些,咱们能够经过这个newInstance方法调用有参数的和私有的构造函数,好比:this
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方法。spa
4). 使用Clone方法建立对象.net
不管什么时候咱们调用一个对象的clone方法,JVM都会帮咱们建立一个新的、同样的对象,特别须要说明的是,用clone方法建立对象的过程当中并不会调用任何构造函数。关于如何使用clone方法以及浅克隆/深克隆机制,笔者已经在博文《 Java String 综述(下篇)》作了详细的说明。简单而言,要想使用clone方法,咱们就必须先实现Cloneable接口并实现其定义的clone方法,这也是原型模式的应用。好比:设计
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<Student> constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); Student stu4 = (Student) stu3.clone(); } }
5). 使用(反)序列化机制建立对象
当咱们反序列化一个对象时,JVM会给咱们建立一个单独的对象,在此过程当中,JVM并不会调用任何构造函数。为了反序列化一个对象,咱们须要让咱们的类实现Serializable接口,好比:
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<Student> 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); } }
6). 完整实例
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<Student> 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指令直接建立对象的。
当一个对象被建立时,虚拟机就会为其分配内存来存放对象本身的实例变量及其从父类继承过来的实例变量(即便这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成以后,Java虚拟机就会开始对新建立的对象按照程序猿的意志进行初始化。在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是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,而且不容许顺序靠前的实例代码块初始化在其后面定义的实例变量,好比:
public class InstanceInitializer { { j = i; } private int i = 1; private int j; } public class InstanceInitializer { private int j = i; private int i = 1; }
上面的这些代码都是没法经过编译的,编译器会抱怨说咱们使用了一个未经定义的变量。之因此要这么作是为了保证一个变量在被使用以前已经被正确地初始化。可是咱们仍然有办法绕过这种检查,好比:
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初始化以前和构造函数调用以前。
二、构造函数初始化
咱们能够从上文知道,实例变量初始化与实例代码块初始化老是发生在构造函数初始化以前,那么咱们下面着重看看构造函数初始化过程。众所周知,每个Java中的对象都至少会有一个构造函数,若是咱们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。
咱们知道,Java要求在实例化类以前,必须先实例化其超类,以保证所建立实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)以外的全部对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其余的构造函数,若是咱们既没有调用其余的构造函数,也没有显式调用超类的构造函数,那么编译器会为咱们自动生成一个对超类构造函数的调用,好比:
public class ConstructorExample { }
对于上面代码中定义的类,咱们观察编译以后的字节码,咱们会发现编译器为咱们生成一个构造函数,以下,
aload_0 invokespecial #8; //Method java/lang/Object."<init>":()V
return
上面代码的第二行就是调用Object类的默认构造函数的指令。也就是说,若是咱们显式调用超类的构造函数,那么该调用必须放在构造函数全部代码的最前面,也就是必须是构造函数的第一条指令。正由于如此,Java才可使得一个对象在初始化以前其全部的超类都被初始化完成,并保证建立一个完整的对象出来。
特别地,若是咱们在一个构造函数中调用另一个构造函数,以下所示,
public class ConstructorExample { private int i; ConstructorExample() { this(1); .... } ConstructorExample(int i) { .... this.i = i; .... } }
对于这种状况,Java只容许在ConstructorExample(int i)内调用超类的构造函数,也就是说,下面两种情形的代码编译是没法经过的:
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; .... } }
或者,
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经过对构造函数做出这种限制以便保证一个类的实例可以在被使用以前正确地初始化。
三、 小结
总而言之,实例化一个类的对象的过程是一个典型的递归过程,以下图所示。进一步地说,在实例化一个类的对象时,具体过程是这样的:
在准备实例化一个类的对象前,首先准备实例化该类的父类,若是该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对如下各种进行实例化,直到完成对目标类的实例化。具体而言,在实例化每一个类时,都遵循以下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,而且这些代码会被放在对超类构造函数的调用语句以后,构造函数自己的代码以前。
Ps: 关于递归的思想与内涵的介绍,请参见个人博文《 算法设计方法:递归的内涵与经典应用》。
四、实例变量初始化、实例代码块初始化以及构造函数初始化综合实例
笔者在《 JVM类加载机制概述:加载时机与加载过程》一文中详细阐述了类初始化时机和初始化过程,并在文章的最后留了一个悬念给各位,这里来揭开这个悬念。建议读者先看完《 JVM类加载机制概述:加载时机与加载过程》这篇再来看这个,印象会比较深入,如若否则,也没什么关系~~
//父类
class Foo { int i = 1; Foo() { System.out.println(i); -----------(1) int x = getValue(); System.out.println(x); -----------(2) } { i = 2; } protected int getValue() { return i; } } //子类
class Bar extends Foo { int j = 1; Bar() { j = 2; } { j = 3; } @Override protected int getValue() { return j; } } public class ConstructorExample { public static void main(String... args) { Bar bar = new Bar(); System.out.println(bar.getValue()); -----------(3) } }/* Output: 2 0 2 *///:~
根据上文所述的类实例化过程,咱们能够将Foo类的构造函数和Bar类的构造函数等价地分别变为以下形式:
//Foo类构造函数的等价变换:
Foo() { i = 1; i = 2; System.out.println(i); int x = getValue(); System.out.println(x); }
//Bar类构造函数的等价变换
Bar() { Foo(); j = 1; j = 3; j = 2 }
这样程序就好看多了,咱们一眼就能够观察出程序的输出结果。在经过使用Bar类的构造方法new一个Bar类的实例时,首先会调用Foo类构造函数,所以(1)处输出是2,这从Foo类构造函数的等价变换中能够直接看出。(2)处输出是0,为何呢?由于在执行Foo的构造函数的过程当中,因为Bar重载了Foo中的getValue方法,因此根据Java的多态特性能够知道,其调用的getValue方法是被Bar重载的那个getValue方法。但因为这时Bar的构造函数尚未被执行,所以此时j的值仍是默认值0,所以(2)处输出是0。最后,在执行(3)处的代码时,因为bar对象已经建立完成,因此此时再访问j的值时,就获得了其初始化后的值2,这一点能够从Bar类构造函数的等价变换中直接看出。
关于类的初始化时机,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》已经介绍的很清楚了,此处再也不赘述。简单地说,在类加载过程当中,准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,而初始化阶段是真正开始执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程。更直接地说,初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态代码块static{}中的语句合并产生的,其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。
类构造器<clinit>()与实例构造器<init>()不一样,它不须要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行以前,父类的类构造<clinit>()执行完毕。因为父类的构造器<clinit>()先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来讲并非必需的,若是一个类中没有静态代码块,也没有对类变量的赋值操做,那么编译器能够不为这个类生产类构造器<clinit>()。此外,在同一个类加载器下,一个类只会被初始化一次,可是一个类能够任意地实例化对象。也就是说,在一个类的生命周期中,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用屡次,只要程序员还在建立对象。
注意,这里所谓的实例构造器<init>()是指收集类中的全部实例变量的赋值动做、实例代码块和构造函数合并产生的,相似于上文对Foo类的构造函数和Bar类的构造函数作的等价变换。
一、一个实例变量在对象初始化的过程当中会被赋值几回?
咱们知道,JVM在为一个对象分配完内存以后,会给每个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。若是咱们在声明实例变量x的同时对其进行了赋值操做,那么这个时候,这个实例变量就被第二次赋值了。若是咱们在实例代码块中,又对变量x作了初始化操做,那么这个时候,这个实例变量就被第三次赋值了。若是咱们在构造函数中,也对变量x作了初始化操做,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程当中,一个实例变量最多能够被初始化4次。
二、类的初始化过程与类的实例化过程的异同?
类的初始化是指类加载过程当中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类彻底加载到内存中后建立对象的过程。
三、假如一个类还未加载到内存中,那么在建立一个该类的实例时,具体过程是怎样的?
咱们知道,要想建立一个类的实例,必须先将该类加载到内存并进行初始化,也就是说,类初始化操做是在类实例化操做以前进行的,但并不意味着:只有类初始化操做结束后才能进行类实例化操做。例如,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》中所提到的下面这个经典案例:
public class StaticTest { public static void main(String[] args) { staticFunction(); } static StaticTest st = new StaticTest(); static { //静态代码块
System.out.println("1"); } { // 实例代码块
System.out.println("2"); } StaticTest() { // 实例构造器
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 *///:~
总的来讲,类实例化的通常过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。