Javaクラスの初期化とインスタンス化における2つの「雷域」

5191 ワード

クラスの初期化を考慮すると、サブクラスの初期化を行う場合、親が初期化していない場合は、サブクラスを初期化する必要があることがわかります.しかし、事は一言も簡単ではない.まず、Javaの初期化トリガの条件を見てみましょう.
(1)newインスタンス化オブジェクトを用いて静的データとメソッドにアクセスする場合、すなわちnew,getstatic/putstaticとinvokestaticの命令に遭遇する場合.(2)反射を用いてクラスを呼び出す場合;(3)クラスを初期化する場合、親が初期化していない場合は、親の初期化をトリガーします.(4)エントリmainメソッドが存在するクラスを実行する.(5)JDK1.7動的言語サポートのメソッドハンドルが存在するクラスは、初期化がなければ初期化をトリガーします.
コンパイルした後に1つの方法を生成して、クラスの初期化はこの方法の中で行って、この方法はただ実行して、JVMがこの点を保証して、そして同期制御を行います;ここで条件(3)は、メソッド呼び出しの観点から、サブクラスが開始時に再帰的に親クラスを呼び出すことであり、これは、サブクラスコンストラクタで最初に親クラスを呼び出さなければならないコンストラクタと同様である.ただし、トリガは初期化を完了するわけではありません.これは、子クラスの初期化が親クラスの初期化よりも早く終了する可能性があることを意味します.これが「危険」です.
1.クラス初期化の例:この例では、周辺クラスの初期化と静的メンバークラスに因果関係がないため、2つの継承関係のある静的メンバークラスを含む周辺クラスを使用します.親クラスAと子クラスBはそれぞれmain関数を含み、上記のトリガ条件(4)から、この2つのmain関数をそれぞれ呼び出すことで異なるクラス初期化経路をトリガすることが分かる.この例の問題は、親クラスが子クラスのstatic参照を含み、定義で初期化される問題です.

public class WrapperClass { 
  private static class A { 
    static { 
      System.out.println(" A     ..."); 
    } 
    //       static   
    private static B b = new B(); 
    protected static int aInt = 9; 
 
    static { 
      System.out.println(" A     ..."); 
    } 
 
    public static void main(String[] args) { 
 
    } 
  } 
 
  private static class B extends A { 
    static { 
      System.out.println(" B     ..."); 
    } 
    //            
    private static int bInt = 9 + A.aInt; 
 
    public B() { 
      //       static  
      System.out.println(" B       " + "bInt  " + bInt); 
    } 
 
    static { 
      System.out.println(" B     ... " + "aInt  :" + bInt); 
    } 
 
    public static void main(String[] args) { 
 
    } 
  } 
} 

シナリオ1:エントリがクラスBのmain関数の場合出力結果:

/** 
   *  A     ... 
   *  B       bInt  0 
   *  A     ... 
   *  B     ... 
   *  B     ... aInt  :18 
   */ 

分析:main関数の呼び出しがクラスBの初期化をトリガし、クラスBに入る方法が見られ、クラスAはその親クラスとして先に初期化を開始してAに入った方法で、その中に文new B()がある.このときBのインスタンス化が行われます.これはクラスBの中にあります.mainスレッドはすでにロックを取得してクラスBを実行し始めました.JVMは1つのクラスの初期化方法が1回しか実行されないことを保証します.JVMはnew命令を受け取ってからクラスBに入らない方法で直接インスタンス化しますが、この場合クラスBはまだクラス初期化が完了していないので、bIntの値は0であることがわかります.(この0は、クラスロードにおいてメソッド領域のメモリをフェーズ割り当てて準備した後に行われるゼロ設定初期化である).したがって、親クラスにサブクラスタイプのstaticドメインを含めて付与動作を行うと、サブクラスインスタンス化がクラス初期化が完了する前に行われる可能性がある.
シナリオ2:エントリがクラスAのmain関数の場合出力結果:

/** 
   *  A     ... 
   *  B     ... 
   *  B     ... aInt  :9 
   *  B       bInt  9 
   *  A     ... 
   */ 

解析:シナリオ1の解析を経て、クラスBの初期化によってクラスAの初期化がトリガーされると、クラスAにおけるクラス変数bのインスタンス化がクラスBの初期化が完了する前に行われることがわかります.では、クラスAを初期化すると、クラス変数がインスタンス化される前にクラスBの初期化がトリガーされ、インスタンス化前に初期化されるのではないでしょうか.問題.出力によると、クラスBの初期化はクラスAの初期化が完了する前に行われたため、クラス変数aIntのような変数はクラスBの初期化が完了した後に初期化されるため、クラスBのドメインbIntが取得したaIntの値は「0」であり、私たちの予定の「18」ではない.
結論:以上から,親クラスに子クラス型のクラス変数を含め,インスタンス化を定義することは非常に危険な行為であり,具体的には例のように直白にならない可能性があり,呼び出し方法は定義に値を付けるように危険を隠しているが,子クラス型のstaticドメインを含める場合でもstaticメソッドで値を割り当てるべきである.JVMはstatで保証できるからである.icメソッド呼び出し前にすべての初期化動作を完了します(もちろん、static B b=new B()を含むべきではありません.このような初期化動作).
2.インスタンス化の例:まずオブジェクト作成のプロセスを知る必要があります:(1)new命令に遭遇し、クラスがロード、検証、準備、解析、初期化を完了したかどうかを確認します.(解析プロセスはシンボル参照を直接参照に解析することである.例えば、メソッド名はシンボル参照であり、初期化が完了した後にこのシンボル参照を使用することができるのは、動的バインドをサポートするためである.)これらのプロセスが完了していない.(2)メモリを割り当て、空きリストやポインタ衝突の方法を採用し、新しく割り当てられたメモリを「ゼロにする」したがって、すべてのインスタンス変数は、このセクションでデフォルトで0(null参照)に初期化されるプロセスが行われる.(3)インスタンス変数が定義した付与動作、インスタンス化器が順次実行され、最後にコンストラクタ内の動作が呼び出される親をチェックする方法(コンストラクタ)を含む実行方法.
この例は、「コンストラクタ、cloneメソッド、readObjectメソッドで上書き可能なメソッドを呼び出さない」ことに違反していることがよく知られているかもしれません.Javaのマルチステート、すなわち動的バインドに起因します.親Aのコンストラクタには、クラスBがそのサブクラスであるprotectedメソッドが含まれています.

public class WrongInstantiation { 
  private static class A { 
    public A() { 
      doSomething(); 
    } 
 
    protected void doSomething() { 
      System.out.println("A's doSomething"); 
    } 
  } 
 
  private static class B extends A { 
    private int bInt = 9; 
 
    @Override 
    protected void doSomething() { 
      System.out.println("B's doSomething, bInt: " + bInt); 
    } 
  } 
 
  public static void main(String[] args) { 
    B b = new B(); 
  } 
} 

出力結果:

/** 
   * B's doSomething, bInt: 0 
   */ 

解析:まず、コンストラクタの提供が表示されていない場合、Javaコンパイラはデフォルトのコンストラクタを生成し、親のコンストラクタを最初に呼び出すため、クラスBのコンストラクタはクラスAのコンストラクタを先に呼び出すことを知っておく必要があります.クラスAではprotectedメソッドdoSomethingが呼び出され、出力結果から実際に呼び出されたのはサブクラスのメソッド実装であり、このときサブクラスのインスタンス化はまだ始まっていないため、bIntは「予想」のように9ではなく0である.これは、動的バインドのため、doSomethingはprotectedメソッドであるため、オブジェクトインスタンスのタイプに応じて対応するメソッド実装(ここではBのインスタンスオブジェクトであり、対応するメソッドはクラスBのメソッド実装である)を見つけるinvokevirtualコマンドによって呼び出される.を選択して実行します.
結論:「コンストラクタ,cloneメソッド,readObjectメソッドで上書き可能なメソッドを呼び出すな」と述べたように.
以上、Javaクラスの初期化とインスタンス化の2つの「雷区」についてご紹介しましたが、皆さんの学習に役立つことを願っています.