概要:
Java では、オブジェクトを使用する前にオブジェクトを適切に初期化する必要があり、これは Java 仕様で規定されています。オブジェクトをインスタンス化するとき、JVM はまず、関連する型がロードおよび初期化されているかどうかを確認します。そうでない場合は、JVM はすぐにそれをロードし、クラス コンストラクターを呼び出してクラスの初期化を完了します。クラスの初期化プロセス中、または初期化の完了後、クラスは特定の状況に応じてインスタンス化されます。この記事では、Java 仮想マシンの観点から Java オブジェクトの作成プロセスを明確に分析するために、JVM によって実行されるクラスの初期化とインスタンス化のプロセスを詳細かつ徹底的に紹介します。
オブジェクトは使用する前に正しくインスタンス化される必要があることがわかっています。 Java コードでは、オブジェクトの作成を引き起こす可能性のある動作が多数あります。最も直感的な動作は、 new キーワードを使用してクラスのコンストラクターを呼び出し、オブジェクトを明示的に作成することです。このメソッドは Java 仕様で呼び出されます。クラス インスタンス作成式の実行によって発生するオブジェクト作成 。さらに、リフレクション機構 (Class クラスの newInstance メソッド、Constructor クラスの newInstance メソッドを使用)、Clone メソッド、逆シリアル化などを使用してオブジェクトを作成することもできます。著者は以下でそれらを 1 つずつ紹介します:
1) new キーワードを使用してオブジェクトを作成します
これは、オブジェクトを作成するための最も一般的で最も簡単な方法です。パラメータ化された) を使用してオブジェクトを作成します。例:Student student = new Student();
2) Class クラスの newInstance メソッドを使用します (リフレクション メカニズム)
実際、この newInstance メソッドは Java のリフレクション メカニズムを通じてオブジェクトを作成するために使用することもできます。パラメータなしの呼び出し コンストラクターは次のようなオブジェクトを作成します: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); } }です。 newInstance メソッドを使用するこれら 2 つの方法 オブジェクトの作成には Java のリフレクション メカニズムが使用されます。実際、Class の newInstance メソッドは内部で Constructor の newInstance メソッドを呼び出します。
4). Clone メソッドを使用してオブジェクトを作成します
オブジェクトの clone メソッドを呼び出すたびに、JVM が新しい同一のオブジェクトを作成することに注意する必要があります。オブジェクトの作成 プロセス中にコンストラクターは呼び出されません。簡単に言うと、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 はこのプロセス中に別のオブジェクトを作成します。 。オブジェクトを逆シリアル化するには、次のような 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); } }
完全な例
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] *///:~他のすべてのメソッドは、invokevirtual 命令に変換することでオブジェクトを直接作成します。 2. Java オブジェクトの作成プロセス オブジェクトが作成されると、仮想マシンはオブジェクト自身のインスタンス変数と親クラスから継承されたインスタンス変数を保存するためにメモリを割り当てます (これらが親クラスから継承された場合でも)。スーパークラス) 渡されたインスタンス変数は非表示になり、スペースが割り当てられる場合があります)。
これらのインスタンス変数にメモリが割り当てられている間、これらのインスタンス変数にはデフォルト値 (ゼロ値) も割り当てられます。 メモリ割り当てが完了すると、Java 仮想マシンはプログラマの意志に従って、新しく作成されたオブジェクトの初期化を開始します。 Java オブジェクトの初期化プロセスでは、オブジェクトの初期化の実行に主に 3 つの構造 (インスタンス変数の初期化、インスタンス コード ブロックの初期化、コンストラクターの初期化) が関係します。
1. インスタンス変数の初期化とインスタンスコードブロックの初期化
インスタンス変数を定義(宣言)する際、インスタンス変数に直接値を代入したり、インスタンスコードブロックを使用して値を代入したりすることもできます。これら 2 つの方法でインスタンス変数を初期化すると、コンストラクターが実行される前にインスタンス変数が初期化されます。実際、インスタンス変数に値を直接割り当てるか、インスタンス コード ブロックを使用して値を割り当てる場合、コンパイラはコードをクラスのコンストラクターに配置し、これらのコードはスーパークラス コンストラクターへの呼び出しステートメントに配置されます。コンストラクター自体のコードの後 (覚えていますか? 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 オブジェクトの作成: クラスの初期化のタイミングとプロセスの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。