Javaエッセイ-Java汎用の少しの学習

8420 ワード

Java汎用
Java汎用(generics)は、クラスとインタフェースを定義するときにタイプパラメータ(type parameter)を使用できるJDK 5に導入された新しい特性です.宣言されたタイプパラメータは、使用時に特定のタイプで置き換えられます.汎用型の最も主要な応用はJDK 5における新しい集合クラスフレームワークである.汎用的な導入は、コンパイラがコンパイル時にタイプチェックを通過し、潜在的なリスクを回避できるため、JDK 5以前の集合クラスフレームワークの使用中に発生するランタイムタイプ変換異常を解決することができる.
JDK 5の前に集合フレームワークを使用する場合、タイプ情報がなく、統一的にObjectを使用していたので、JDK 4 Listインタフェースの方法署名を探しました.以下のようにJDK 5が汎用型、Listインタフェースの変更、新しい方法署名、タイプパラメータの導入を開始しました.
boolean add(E e);

JDK 5の前に、集合クラスを使用する場合、その中に任意の要素を追加することができます.その中のタイプはObjectなので、取り出した段階で強制変換を行い、これによって、以下のコードのような意図しない実行時強制変換エラーを引き起こす可能性があります.
public class Test1 {
    public static void main(String[] args) {
        List a = new ArrayList();
        a.add("123");
        a.add(1);             
        //             ,       Integer   String  
        for (int i = 0 ; i < a.size(); i++) {
            int result = (Integer)a.get(i);    //        Object      
            System.out.println(result);
        }
    }
}

このようなコードは実行時段階で強い回転異常をもたらし,コンパイル時間に潜在的なリスクを検出できない.汎用メカニズムを使用すると、コンパイル中にリストのタイプ挿入に問題があることを確認し、以下のコードを回避できます.
public class Test1 {
    public static void main(String[] args) {
        List a = new ArrayList();
        a.add("123");          //      
        a.add(1);
    }
}

汎用型を導入すると、コンパイラはコンパイル時にタイプパラメータに基づいてタイプチェックを行い、潜在的なリスクを排除します.なぜコンパイル時にチェックするかというと、実行時に反射することができるため、タイプパラメータに合わないデータをlistに挿入します.以下のコードで示します.
public class Test1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List a = new ArrayList();
        List b = new ArrayList();
        a.getClass().getMethod("add",Object.class).invoke(a,"abc");    
        //         ,    
    }
}

汎用を導入するとともに、JDK 5以前のクラスライブラリと互換性を持たせるために、JDK 5が導入を開始したのは実は擬似汎用であり、生成されたJavaバイトコードには汎用中のタイプ情報は含まれていない.汎用型を使用するときに付けたタイプパラメータは、コンパイラがコンパイルするときに削除されます.このプロセスをタイプ消去と呼びます.コードに定義されているListなどのタイプは,コンパイル後もListとなり,JDK 5以前のコードと自然に互換性がある.Javaの汎用メカニズムはC++などの汎用メカニズムとは異なり,Javaの汎用はタイプ消去に頼るか,ターゲットコードは1部しか生成されず,実行速度を犠牲にする.C++のテンプレートは、異なるテンプレートパラメータに対して静的にインスタンス化され、ターゲットコードのボリュームがやや大きくなり、実行速度が大幅に速くなります.
タイプ消去を行うと、タイプパラメータの元のタイプ(raw type)は、汎用情報を消去し、最後にバイトコード内のタイプ変数の真のタイプである.汎用タイプを定義するたびに、対応する元のタイプが自動的に提供されます.タイプ変数は消去され、その限定タイプ(無限の変数はObject)で置き換えられます.
class Pair {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T  value) {
        this.value = value;
    }
} 
  
Pair      :
class Pair {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object  value) {
        this.value = value;
    }
}

Pairでは、タイプ消去、Objectを使用して、javaプログラミング言語に汎用的に追加する前に実装されたように、その結果は一般的なクラスです.プログラムには、PairやPairなどの異なるタイプのPairを含めることができますが、消去タイプは元のPairタイプになり、元のタイプはObjectになります.ArrayListが消去されると元のタイプもObjectになり、反射することで文字列を格納できます.
汎用メソッドを呼び出すときは、汎用を指定してもよいし、汎用を指定しなくてもよい.汎用を指定しない場合、汎用変数のタイプは、メソッド内のいくつかのタイプの同じ親クラスの最小レベルであり、Objectまでです.汎用を指定する場合、このメソッドのいくつかのタイプは、汎用インスタンスタイプまたはそのサブクラスである必要があります.
public class Test1 {
 
   public static void main(String[] args) {
 
      /**          */
      int i = Test1.add(1, 2); //        Integer,  T Integer  
      Number f = Test1.add(1, 1.2);//         Integer,    Float,           , Number
      Object o = Test1.add(1, "asd");//         Integer,    Float,           , Object
 
      /**         */
      int a = Test1. add(1, 2);//    Integer,     Integer       
      int b = Test1. add(1, 2.2);//     ,   Integer,   Float
      Number c = Test1. add(1, 2.2); //    Number,     Integer Float
   }
 
   //            
   public static  T add(T x, T y) {
      return y;
   }
}

タイプ消去の問題で、すべての汎用タイプ変数は最後に元のタイプに置き換えられますが、汎用の使用では、取り出したデータを強制的に変換する必要はありません.
public class Test1 {
 
    public static void main(String[] args) {
        List a = new ArrayList();
        a.add(1);
 
        for (int i = 0 ; i < a.size(); i++) {
            int result = a.get(i);
            System.out.println(result);
        }
    }
}

バイトコードの観点から探索してみましょう.
public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      18: pop
      19: iconst_0
      20: istore_2
      21: iload_2
      22: aload_1
      23: invokeinterface #6,  1            // InterfaceMethod java/util/List.size:()I
      28: if_icmpge     58
      31: aload_1
      32: iload_2
      33: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      38: checkcast     #8                  // class java/lang/Integer         JVM    
      41: invokevirtual #9                  // Method java/lang/Integer.intValue:()I
      44: istore_3
      45: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
      48: iload_3
      49: invokevirtual #11                 // Method java/io/PrintStream.println:(I)V
      52: iinc          2, 1
      55: goto          21
      58: return

オフセット量38の位置から分かるように、JVMはcheckcast命令を使用しており、コンパイル時にタイプ消去が行われているものの、JVMにはタイプパラメータのメタ情報が残っており、取り出し時に自動的に強転していることを説明しているので、汎用的な使い勝手と言えるでしょう.
他の人の例では,タイプ消去と多態の衝突を見て,一例を挙げた.
public class Test1 {
 
   public static void main(String[] args) {
      DateInter dateInter = new DateInter();
      dateInter.setValue(new Date());
      dateInter.setValue(new Object());//     
   }
}
 
class Pair {
   private T value;
 
   public T getValue() {
      return value;
   }
 
   public void setValue(T value) {
      this.value = value;
   }
}
 
class DateInter extends Pair {
   @Override
   public Date getValue() {
      return super.getValue();
   }
 
   @Override
   public void setValue(Date value) {
      super.setValue(value);
   }
}

タイプが消去されると、親は次のように一般的なクラスになります.
class Pair {
   private Object value;
 
   public Object getValue() {
      return value;
   }
 
   public void setValue(Object value) {
      this.value = value;
   }
}

しかし、setValueは書き換えからリロードに変わり、明らかに達成したい目的を破ったが、JVMはどのようにしてこの衝突を解決したのだろうか.答えはJVMがブリッジを作ってくれたので、具体的にはバイトコードの角度から見てみましょう.
class DateInter extends Pair {
  DateInter();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Pair."":()V
       4: return
 
  public java.util.Date getValue();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method Pair.getValue:()Ljava/lang/Object;
       4: checkcast     #3                  // class java/util/Date
       7: areturn
 
  public void setValue(java.util.Date);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #4                  // Method Pair.setValue:(Ljava/lang/Object;)V
       5: return
 
  public void setValue(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #3                  // class java/util/Date
       5: invokevirtual #5                  // Method setValue:(Ljava/util/Date;)V
       8: return
 
  public java.lang.Object getValue();
    Code:
       0: aload_0
       1: invokevirtual #6                  // Method getValue:()Ljava/util/Date;
       4: areturn
}

コンパイルの結果から,setValueとgetValueメソッドのサブクラスを書き換えることを意図し,4つのメソッドがあり,最後の2つのメソッドは,コンパイラ自身が生成したブリッジメソッドである.ブリッジメソッドのパラメータタイプはすべてObjectであることがわかります.つまり、サブクラスで親クラスを本当にカバーする2つのメソッドは、私たちが定義したsetvalueメソッドとgetValueメソッドの上にある@Oveerrideは仮象にすぎません.ブリッジメソッドの内部実装は、私たちが自分で書き直した2つのメソッドを呼び出すだけです.したがって、仮想マシンは巧みな方法を巧みに使用して、タイプ消去とマルチステートの衝突を解決しました.
最後に、最近他の人の経験を見てtipsを得たことを添付します.
  • JSONシリアルを使用してオブジェクトセットを逆シーケンス化する場合は、オブジェクトのclassタイプをマークしてください.そうしないと、元のタイプ、すなわちObjectのみのセットが得られ、特にサービス呼び出しのこのようなシーンでは、タイプ変換エラーが発生する可能性があります.
  • コンパイラからの警告情報を重視する.