【解惑】JVMがJava汎用クラスをどのように理解するか


//    
public class Pair<T>{
       private T first=null;
       private T second=null;

       public Pair(T fir,T sec){
            this.first=fir;
	    this.second=sec;
       }
       public T getFirst(){
             return this.first;
       }
       public T getSecond(){
             return this.second;
       }
       public void setFirst(T fir){
	     this.first=fir;
       }
}

 上は典型的な汎用(generic)コードです.Tはタイプ変数であり、任意の参照タイプであってもよい.
 
1.Generic classオブジェクトの作成            Pair pair1=new Pair("string",1);           ...①             Pair pair2=new Pair("string",1)    ...②       興味深い現象があります  ①コードはコンパイル中にエラーが発生しない、②コードはコンパイル中にエラーが検出されます.      この問題は実は簡単だ.      (1)JVM自体に汎用オブジェクトという特別な概念はない.すべての汎用クラスオブジェクトは、コンパイラですべて一般クラスオブジェクトになります(この点は以下で詳しく説明します).      例えば①,②両コードコンパイラはすべてPair(Object fir,Object sec)のようなコンストラクタを呼び出す.      したがって、コード1のnew Pair(「string」,1)はコンパイラでは問題ありません.コンパイラは、作成したPairタイプのうち、具体的にどのタイプ変数Tなのか分かりません.また、コンパイラは、StringオブジェクトとIntegerオブジェクトがObjectタイプに属していることを確認します.       ただし、pair 1.getSecond()を実行するとClassCastException異常が放出されます.これは、JVMが最初のパラメータ「string」に基づいてTタイプ変数をStringタイプと推定するため、getSecondもStringタイプを返すべきであり、コンパイラはsecondのオペランドが1の値のIntegerタイプであることをデフォルト化しているからです.もちろんJVMの実行要件に合致しないので、プログラムを終了しないのはおかしいです.       (2)ただし,コード2がコンパイラで誤報されるのは,new Pair("string",1)がオブジェクトpair 2を作成するタイプ変数TがStringであるべきであることを示しているためである.コンパイラでは、2番目のパラメータIntegerでエラーが発生していることがわかります.       まとめてみます.       汎用オブジェクトを作成する場合は、必ずタイプ変数Tの具体的なタイプを指摘します.JVMの実行時に例外を投げ出すのではなく、コンパイラにエラーをチェックさせるようにします.
 
2、JVMがどのように汎用概念を理解するか——タイプ消去    実際、JVMは汎用性を知らず、すべての汎用性はコンパイル段階で一般的なクラスと方法に処理されています.    処理方法は簡単で,タイプ変数Tの消去(erased)と呼ぶ.    汎用タイプをどのように定義しても、元のタイプが自動的に提供されます.元のタイプの名前は、消去タイプパラメータの汎用タイプの名前です.         汎用型の型変数が限定されていない場合()、元の型としてObjectを使用します.         限定()があれば、XClassを元のタイプとします.         複数の制限()がある場合は、最初の境界のタイプ変数XClass 1クラスを元のタイプとして使用します.    たとえば、上記のPairの例では、コンパイラはObjectの元のタイプに置き換えられた一般的なクラスとして置き換えられます.
 //    :       
public class Pair{
        private Object first=null;
        private Object second=null;

        public Pair(Object fir,Object sec){
            this.first=fir;
	    this.second=sec;
        }
       public Object getFirst(){
             return this.first;
       }
       public void setFirst(Object fir){
	     this.first=fir;
       }
    }

 
3、汎用制約と限界——タイプ消去によるトラブル
(1)  汎用タイプのマルチステート面倒を継承します.(——子に親を上書きする方法はありません)
     
     次のクラスのSonPairを見てみましょう
class SonPair extends Pair<String>{
          public void setFirst(String fir){....}
}

     明らかに,プログラマーの本意はSonPairクラスで親クラスPairのsetFirst(T fir)という方法を上書きしようとすることである.しかし、実際には、SonPairのsetFirst(String fir)メソッドは、Pairのこのメソッドを上書きしていません.     理由は簡単で、Pairはコンパイル段階でタイプによってPairに消去され、そのsetFirst方法はsetFirst(Object fir)になった.ではSonPairのsetFirst(String)はもちろん親のsetFirst(Object)を上書きすることはできません.
     これはマルチステートにとって確かに大きなトラブルであり、コンパイラがこの問題をどのように解決しているかを見てみましょう.
     コンパイラは、SonPairでブリッジメソッド(bridge method)を自動的に生成します.           public void setFirst(Object fir){                    setFirst((String) fir)             }       これにより、SonPairのブリッジメソッドは確かに汎用親のsetFirst(Object)を上書きすることができます.また、ブリッジメソッドの内部では、実はサブクラスバイトsetFirst(String)メソッドが呼び出されます.マルチステートには問題ありません.
      問題はまだ終わっていないので、多態の中の方法でカバーすればいいのですが、橋の方法には疑問があります.
      今、SonPairでgetFirst()メソッドを上書きしたいとしたら?
class SonPair extends Pair<String>{
      public String getFirst(){....}
}

      親クラスのgetFirstを上書きするブリッジメソッドが必要なため、コンパイラは自動的にSonPairでpublic Object getFirst()ブリッジメソッドを生成します.      しかし、疑問が出てくると、SonPairには2つのメソッドが署名されているように見えます(ただし、戻りタイプが異なるだけです):
            ①String getFirst()  //自分で定義した方法
            ②Object getFirst() //  コンパイラ生成ブリッジメソッド      まさか、コンパイラはメソッド署名が同じ複数のメソッドが1つのクラスに存在することを許可しますか?
      実際には皆さんが知らない知識があるかもしれません.      ①メソッド署名は確かにメソッド名+パラメータリストのみです.これは間違いありません!      ②メソッド署名のような複数のメソッドを絶対に作成することはできません.このようにプログラムを書くと、コンパイラは見逃されません.これも間違いありません!      ③最も重要な点は、JVMがパラメータタイプと戻りタイプで1つのメソッドを決定することです.コンパイラがメソッド署名のような2つのメソッドを何らかの方法で自分でコンパイルすると(コンパイラ自身がこのような奇跡を創造するしかありませんが、私たちのプログラマーはこのコードを人為的に書くことはできません).JVMは、戻りタイプが異なることを前提として、これらの方法を明確にすることができます.
 
(2)汎用型におけるメソッド競合
 
       コードを見てみましょう
//        equals  
public class Pair<T>{
      public boolean equals(T value){
            return (first.equals(value));
      }
}

        このように見ると、問題のないコードはコンパイラにも合格できないようです.
       【Error】    Name clash: The method equals(T) of type Pair has the same erasure as equals(Object) of type Object but does not override it.
        コンパイラはあなたの方法がObjectの方法と衝突したと言っています.これはなぜですか.
 
        最初はこの問題もよく分かりませんでしたが、コンパイラがequals(T)のような方法でObjectのequals(Object)をカバーしてくれたような気がします.皆さんの議論を経て、この問題をこのように説明すべきだと思いますか?
 
        まず、サブクラスメソッドを上書きするには、親メソッドと同じメソッド署名(メソッド名+パラメータリスト)が必要であることを知っています.また、サブクラスのアクセス権>=親メソッドのアクセス権を保証する必要があります.これはよく知られている事実です.
        そして、上記のコードでは、コンパイラがPairのequals(T)メソッドを見たとき、第1の反応は、当然、equals(T)が親Objectのequals(Object)を上書きしていないことである.
        次に、コンパイラが汎用コードのTをObjectに置き換え(消去).消去後にequals(T)がequals(Object)になってしまったことに突然気づき、しまった.この方法はObjectクラスのequalsと同じである.上書きされていないと判断し始めたことに基づいて、コンパイラは完全に狂ってしまった(精神分裂)それから2つの結論を出しました:①元の思想を堅持します:カバーしていません.しかし今同じように方法の衝突をもたらしました.  ②このプログラムを書いたプログラマーは気が狂った(ハハ).
 
        それに、PairオブジェクトとTオブジェクトをequalsと比べると、牛の頭が馬の口を比べているように、ははは、論理的にも通じませんね.
 
(3)汎用配列なし
 
      Pair[] stringPairs=new Pair[10];
      Pair[] intPairs=new Pair[10];
      この書き方コンパイラは、Cannot create a generic array of Pairのエラーを指定します.
 
      汎用消去後、Pair[]はPair[]になり、Object[]に変換できると述べた.
      汎用配列が存在すると仮定すると
            Object[0]=stringPairs[0]; Ok
            Object[1]=intPairs[0]; Ok
       これは面倒で、理論的にはObject[]はすべてのPairオブジェクトを格納することができるが、これらのPairオブジェクトは汎用オブジェクトであり、彼らのタイプ変数は異なるので、各Object[]配列要素を呼び出すオブジェクト方法は異なる記憶を得ることができるかもしれない.文字列かもしれないし、整形かもしれない.これはJVMには予想できない.
 
      配列はその要素タイプ、つまりすべての要素オブジェクトが同じでなければならないことを覚えておいてください.汎用タイプではできません.PairでもPair...はPairタイプですが、彼らは違います.
 
まとめ:汎用コードとJVM    ①仮想マシンには汎用型はなく、普通のクラスと方法しかありません.    ②コンパイル段階では、すべての汎用クラスのタイプパラメータがObjectまたはそれらの限定境界に置き換えられます.(タイプ消去)    3汎用型を継承する場合,ブリッジ法の合成は型変数消去による多態災害を回避するためである.