深入理解Java对象的创建过程:类的初始化与实例化

深入理解Java对象的创建过程:类的初始化与实例化

参考:
深入理解Java对象的创建过程:类的初始化与实例化
类的初始化&实例化顺序

一、Java对象创建方式

1). 使用new关键字创建对象

Student student = new Student();

2). 使用Class类的newInstance方法(反射机制)

我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象,比如:

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方法调用有参数的和私有的构造函数,比如:

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);
    }
}

经测试,上面的代码,把Student的构造函数改为私有运行错误,报java.lang.NoSuchMethodException。需使用getDeclaredConstructor。

使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。

4). 使用Clone方法创建对象

  无论何时我们调用一个对象的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);
    }
}

二、JAVA对象创建过程

在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是:

  1. 实例变量初始化、
  2. 实例代码块初始化
  3. 构造函数初始化。

1、实例变量初始化与实例代码块初始化

我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗?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初始化之前和构造函数调用之前。

类的初始化&实例化顺序

从大流程来说,类肯定是先初始化,再实例化的,这里得出第一个顺序:
静态域 --> 实例域 --> 构造函数。另外要符合任何子类的动作都会触发父类:父类 --> 子类。所以得出原则:【先静态后实例;先父类后子类】

而且同一个域的顺序可以分成两步: 创建-->赋值

对于静态域,其先经过链接创建静态变量,赋default值;再到初始化阶段给静态变量赋assign值和执行静态代码块。同理于实例域,也是分成创建和赋值两个部分,不同的只是加入构造函数(形参和代码块):先创建实例变量和构造函数形参以default值,然后对变量和形参赋assign值和执行实例代码块,最后执行构造函数的代码。

前提先要执行父类,再到子类;另外同一层次的就按从上到下顺序执行,如下面例子。

package com.jscai.java.classLoader;


public class LoaderLazy {
    {
        System.out.println("Parent Instance Code");
    }
    private PrintTmp p1 = new PrintTmp("Parent Instance Member");
    
    static {
        System.out.println("Parent Static Code");
    }
    private static PrintTmp p2 = new PrintTmp("Parent Static Member");


    public LoaderLazy() {
        System.out.println("Parent Constuctor");
    }
}


class SubLoaderLazy extends LoaderLazy {
    {
        System.out.println("Sub Instance Code");
    }
    private PrintTmp p1 = new PrintTmp("Sub Instance Member");
    
    static {
        System.out.println("Sub Static Code");
    }
    private static PrintTmp p2 = new PrintTmp("Sub Static Member");


    public SubLoaderLazy() {
        System.out.println("Sub Constuctor");
    }
}


class PrintTmp {
    public PrintTmp(String strOut) {
        System.out.println(strOut);
    }
}
SubLoaderLazy test = new SubLoaderLazy();

输出:

console:
Parent Static Code
Parent Static Member
Sub Static Code
Sub Static Member
Parent Instance Code
Parent Instance Member
Parent Constuctor
Sub Instance Code
Sub Instance Member
Sub Constuctor

另外我们看看下面的代码,说明了静态域先赋default值;然后按顺序赋assign值,先执行tester = new Test(),这时候count1和count2还是0,所以自加后都是1。

     private static Test tester = new Test();
     private static int count1;
     private static int count2 = 2;
     public Test() {
         count1++;
         count2++;
         System.out.println("c1=" +count1 + "; c2=" +  count2);
     }

输出:

console:
c1=1; c2=1

顺序:

父类静态代码块(按static声明的先后顺序)--》子类静态代码块(按static声明的先后顺序)--》

父类实例代码块(按声明的先后顺序)--》父类构造函数 --》

子类实例代码块(按声明的先后顺序)--》子类构造函数

posted @ 2019-12-01 11:28  王的博客园12  阅读(2863)  评论(0编辑  收藏  举报