>  기사  >  Java  >  Java 객체 생성: 클래스 초기화 타이밍 및 프로세스

Java 객체 생성: 클래스 초기화 타이밍 및 프로세스

php是最好的语言
php是最好的语言원래의
2018-08-08 11:45:091958검색

요약:

Java에서는 객체를 사용하기 전에 적절하게 초기화해야 하며 이는 Java 사양에 규정되어 있습니다. 객체를 인스턴스화할 때 JVM은 먼저 해당 유형이 로드되고 초기화되었는지 확인합니다. 그렇지 않은 경우 JVM은 즉시 이를 로드하고 클래스 생성자를 호출하여 클래스 초기화를 완료합니다. 클래스 초기화 과정 중 또는 초기화가 완료된 후 특정 상황에 따라 클래스가 인스턴스화됩니다. 본 글에서는 자바 객체 생성 과정을 자바 가상 머신의 관점에서 명확하게 분석하기 위해 JVM이 수행하는 클래스 초기화 및 인스턴스화 과정을 자세하고 심도 있게 소개하고자 한다.

1. Java 객체 생성 타이밍

우리는 객체를 사용하기 전에 올바르게 인스턴스화해야 한다는 것을 알고 있습니다. Java 코드에는 객체 생성을 유발할 수 있는 동작이 많이 있습니다. 가장 직관적인 방법은 new 키워드를 사용하여 클래스 생성자를 호출하여 객체를 명시적으로 생성하는 것입니다. By 클래스 인스턴스 생성 표현식을 실행하여 객체 생성 이 발생합니다. 또한 리플렉션 메커니즘(Class 클래스의 newInstance 메서드, Constructor 클래스의 newInstance 메서드 사용), Clone 메서드, deserialization 등을 사용하여 객체를 생성할 수도 있습니다. 저자는 아래에서 이들 각각을 하나씩 소개합니다:

1) new 키워드를 사용하여 객체를 생성합니다.

이것은 객체를 생성하는 가장 일반적이고 간단한 방법입니다. 매개변수가 없고 매개변수화된) 객체를 생성합니다. 예:

Student student = new Student();

2) Class 클래스의 newInstance 메소드를 사용합니다(반사 메커니즘)

또한 Class 클래스의 newInstance 메소드를 사용하여 Java의 반사 메커니즘을 통해 객체를 생성할 수도 있습니다. 매개변수 없이 호출 생성자는 다음과 같은 객체를 생성합니다.

Student student2 = (Student)Class.forName("Student类全限定名").newInstance(); 
或者:
Student stu = Student.class.newInstance();

3) 생성자 클래스의 newInstance 메서드를 사용합니다(반사 메커니즘)

java.lang.relect.Constructor 클래스에도 객체를 생성하는 newInstance 메서드가 있습니다. 이 메소드는 Class 클래스와 매우 유사하지만, 이에 비해 Constructor 클래스의 newInstance 메소드가 더 강력합니다. 이 newInstance 메소드를 통해 다음과 같은 매개변수화된 생성자를 호출할 수 있습니다. newInstance 메소드를 사용하는 이 두 가지 방법 Java의 리플렉션 메커니즘은 실제로 클래스의 newInstance 메소드가 생성자의 newInstance 메소드를 내부적으로 호출하는 데 사용됩니다.

4) Clone 메소드를 사용하여 객체 생성

객체의 clone 메소드를 호출할 때마다 JVM은 새롭고 동일한 객체를 생성하는 데 도움을 줍니다. 특히, clone 메소드는 객체를 생성하는 데 사용됩니다. 프로세스 중에는 생성자가 호출되지 않습니다. 간단히 말해서, clone 메소드를 사용하려면 먼저 Cloneable 인터페이스를 구현하고 이에 정의된 clone 메소드를 구현해야 합니다. 이 역시 프로토타입 패턴의 적용입니다. 예:

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

5). (역)직렬화 메커니즘을 사용하여 객체를 만듭니다

객체를 역직렬화하면 JVM은 이 프로세스 동안 별도의 객체를 생성합니다. . 객체를 역직렬화하려면 클래스가 직렬화 가능 인터페이스를 구현하도록 해야 합니다(예:

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

6). 전체 예제

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 가상 머신 관점에서 보면 new 키워드를 사용하여 생성합니다. 객체, 다른 모든 메소드는 Invokevirtual 명령어로 변환하여 직접 객체를 생성합니다.

2. Java 객체 생성 프로세스

객체가 생성되면 가상 머신은 객체 자체의 인스턴스 변수와 상위 클래스에서 상속된 인스턴스 변수를 저장하기 위해 메모리를 할당합니다. 슈퍼 클래스) 전달된 인스턴스 변수는 숨겨지고 공간이 할당될 수 있습니다.

이러한 인스턴스 변수에 메모리가 할당되는 동안 이러한 인스턴스 변수에도 기본값(0 값)이 할당됩니다.

메모리 할당이 완료되면 Java Virtual Machine은 프로그래머의 의지에 따라 새로 생성된 객체를 초기화하기 시작합니다. Java 객체 초기화 프로세스에는 객체 초기화 실행과 관련된 구조가 크게 인스턴스 변수 초기화, 인스턴스 코드 블록 초기화, 생성자 초기화의 세 가지 구조가 있습니다.

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初始化之前和构造函数调用之前。 

2、构造函数初始化

我们可以从上文知道,实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成7e51f00a783d7eb8f68358439dee7daf()方法,参数列表与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通过对构造函数作出这种限制以便保证一个类的实例能够在被使用之前正确地初始化。

3、 小结

  总而言之,实例化一个类的对象的过程是一个典型的递归过程,如下图所示。进一步地说,在实例化一个类的对象时,具体过程是这样的:

  在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。

4、实例变量初始化、实例代码块初始化以及构造函数初始化综合实例

//父类
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类构造函数的等价变换中直接看出。

三. 类的初始化时机与过程

  简单地说,在类加载过程中,准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,而初始化阶段是真正开始执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程。更直接地说,初始化阶段就是执行类构造器583d030be372af71281df966e84181a5()方法的过程。583d030be372af71281df966e84181a5()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。

  类构造器583d030be372af71281df966e84181a5()与实例构造器7e51f00a783d7eb8f68358439dee7daf()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器583d030be372af71281df966e84181a5()执行之前,父类的类构造583d030be372af71281df966e84181a5()执行完毕。由于父类的构造器583d030be372af71281df966e84181a5()先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。特别地,类构造器583d030be372af71281df966e84181a5()对于类或者接口来说并不是必需的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器583d030be372af71281df966e84181a5()。此外,在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器583d030be372af71281df966e84181a5()最多会被虚拟机调用一次,而实例构造器7e51f00a783d7eb8f68358439dee7daf()则会被虚拟机调用多次,只要程序员还在创建对象。

  注意,这里所谓的实例构造器7e51f00a783d7eb8f68358439dee7daf()是指收集类中的所有实例变量的赋值动作、实例代码块和构造函数合并产生的,类似于上文对Foo类的构造函数和Bar类的构造函数做的等价变换。

四. 总结

1、一个实例变量在对象初始化的过程中会被赋值几次?

  我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。

2、类的初始化过程与类的实例化过程的异同?

  类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类完全加载到内存中后创建对象的过程。

3、假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?

  我们知道,要想创建一个类的实例,必须先将该类加载到内存并进行初始化,也就是说,类初始化操作是在类实例化操作之前进行的,但并不意味着:只有类初始化操作结束后才能进行类实例化操作。

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
*///:~

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

相关推荐:

java对象初始化的顺序

Java 객체 초기화 순서

위 내용은 Java 객체 생성: 클래스 초기화 타이밍 및 프로세스의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.