汎用-消去実装Java汎用

6527 ワード

Javaの汎用は言語の内在的なメカニズムではなく、後に追加された特性であり、非汎用コードと汎用コードの互換性という問題をもたらす.汎用型はJDK 1である.5 Javaに追加されたばかりで、以前のコードはすべて非汎用で、JDK 1でどのように実行されていましたか.5以降のVM上?このような互換性を実現するために、Java汎用型は狭い場所に限られ、同時に理解しにくくなり、Java言語の中で最も理解しにくい文法とも言える.
消去
非汎用コードとの互換性を実現するために、Java言語の汎用は消去(Erasure)で実現される.すなわち、汎用は基本的にコンパイラによって実現され、コンパイラによってタイプチェックとタイプ推定が実行され、バイトコードを生成する前に消去され、仮想マシンは汎用の存在を知らない.これにより,汎用と非汎用のコードが混在して実行できるようになり,もちろん,かなり混乱しているように見える.
汎用型を使用すると、対応するタイプがオリジナルタイプ(raw type)と呼ばれ、汎用型はオリジナルタイプに消去されます.例えば、GenericはGenericに、ListはListに、消去のため、仮想マシンではどのタイプの情報も得られず、仮想マシンはオリジナルタイプしか知りません.次のコードは、Java汎用の真実-消去を示します.
class Erasure<T> {
	private T t;
	
	public void set(T t) {
		this.t = t;
	}
	
	public T get() {
		return t;
	}
	
	public static void main(String[] args) {	
		Erasure<String> eras = new Erasure<String>();
		eras.set("not real class type");
		String value = eras.get();
		
	}
}

Javapを使用してclassファイルを逆コンパイルし、次のコードを得ます.
class com.think.generics.Erasure<T> {
  com.think.generics.Erasure();
    Code:
       0: aload_0       
       1: invokespecial #12                 // Method java/lang/Object."<init>":()V
       4: return        

  public void set(T);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #23                 // Field t:Ljava/lang/Object;
       5: return        

  public T get();
    Code:
       0: aload_0       
       1: getfield      #23                 // Field t:Ljava/lang/Object;
       4: areturn       

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class com/think/generics/Erasure
       3: dup           
       4: invokespecial #30                 // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: ldc           #31                 // String not real class type
      11: invokevirtual #33                 // Method set:(Ljava/lang/Object;)V
      14: aload_1       
      15: invokevirtual #35                 // Method get:()Ljava/lang/Object;
      18: checkcast     #37                 // class java/lang/String
      21: astore_2      
      22: return        
}

逆コンパイルされたバイトコードから,汎用ErasureはErasureに消去され,その内部のフィールドTはObjectに消去され,getとsetメソッドではtがObjectとして用いられていることがわかる.最も注目すべきは,逆コンパイルコードの最後から3行目であり,Javaコードに対応するのがString value=erasである.get();コンパイラはタイプ変換を実行します.これがJava汎用の本質です.
転送された値を追加のコンパイル期間チェックし、転送された値の変換を挿入します.
.このような汎用型は本当に汎用型ですか?
Javaの汎用型は確かに本当の汎用型ではないと言っても、Javaのタイプを安全に一歩前進させ、プログラマーが明示的に制御するタイプ変換が必要だったが、コンパイラによって実現され、汎用型の規範に従ってコードを書くだけで、安全な保障を受けることができる.ここでは、Javaの汎用性を理解する問題を考えなければなりません.では、その核心的な目的は何ですか.個人的には、Java汎用の核心的な目的は安全性にあり、特に汎用ワイルドカードを理解する際、すべての奇妙なルールは、結局安全な目的にあると思います.
タイプ情報の消失
消去のため、汎用コードの内部では、汎用パラメータタイプに関する情報は取得できません.実行時に仮想マシンは正確なタイプの情報を取得できず、instanceof操作やnew式などの正確なタイプの情報の作業はすべて完了しません.
class  Erasure<T>  {
    public void f() {
        if(arg instanceof T) //Error
        T ins = new T();//Error
        T[] array = new T[10];//error
    }
}
では、特定のタイプ情報が必要な場合、クラスオブジェクトを覚えて実装します.実行時にタイプ情報が必要な場合は、クラスオブジェクトを使用して操作します.たとえば、次のようにします.
class Erasure<T> {
    private Class<T> clazz;
    Erasure(Class<T> kind) {
        clazz = kind;
    }
    public void f() {
        if(clazz.isInstance(arg)) {}
        T t = clazz.newInstance();//          
    }
}

汎用クラスの配列
配列はJava言語の組み込み特性であり,汎用性と配列を組み合わせると理解しにくい問題がある.まずJavaの配列はコヒーレントであり、IntegerはNumberのサブクラスであるため、Integer[]もNumber[]のサブクラスであり、Number[]を使用する場合、Integer[]を使用して代用することができるが、汎用型はコヒーレントではない.例えば、ListがListではないサブクラスは、ワイルドカードでは、これらの状況を詳細に議論する.
正確なタイプ情報が得られないため、汎用配列をどのように作成しますか?Javaでは、すべてのクラスの親がObjectであるため、汎用配列の代わりにObjectタイプの配列を作成できます.
public class Array<T> {
	private int size = 0;
	private Object[] array;
	
	public Array(int size) {
		this.size = size;
		array = new Object[size];
	}
	//                
	public void put(int index,T item) {
		array[index] = item;
	}
	
	//       
	public T get(int index) {
		return (T)array[index];
	}
	
	public T[] rep() {
		return (T[])array;
	}
	
	private static class Father {}
	private static class Son extends Father {}
	
	public static void main(String[] args) {
		Array<String> instance = new Array<String>(10);
		String[] array = instance.rep();//  
		
	}
}
上記のコードではget()もput()も正しく動作し、コンパイラはタイプの正確性を保証します.しかしrep()が戻るとString[]タイプの配列に付与され、ClassCastException異常が放出され、このような異常が放出されるのは予想外である.Javaでは、配列は実際にはオブジェクトであり、各タイプの配列には対応するクラスがあります.このクラスは仮想マシン生成です.たとえば、上記のコードでは、実行時に「[Ljava.lang.Object」というクラスが生成されます.これは、Objectの1次元配列を表します.同様に、String[]配列を定義します.対応するクラスは[[Ljava.lang.String].クラス名から分かるように、これらの配列を表すクラスはすべて合法的なJavaクラス名ではなく、仮想マシンによって生成され、仮想マシンが生成するクラスは実際に構築された配列タイプに基づいており、あなたが構築したのはObjectタイプの配列であり、それが生成したのはObjectタイプの配列を表すクラスであり、あなたがそれをどんなタイプに変換しても.すなわち,下位配列のタイプをひっくり返す方法はない.前述したように、配列はコヒーレントです.つまり、[Ljava.lang.Objectは[Ljava.lang.Stringの親です.たとえば、次のコードでtrueが得られます.
String[] array = new String[10];
System.out.println(array instanceof Object[]);
だからrep()戻り値をString[]タイプに割り当てると、確かにタイプ変換が発生しますが、このタイプ変換にすぎません.
配列要素の変換ではなく、Objectタイプの要素をStringに変換するのではなく、それは[Ljava.lang.ObjectがLjava.lang.Stringに変換されたのは、親オブジェクトが子クラスに変換されたので、必然的に異常を投げ出す必要があります.では、問題は出てきます.私たちが汎用的なタイプを得るために使用しているのは、Arrayと宣言した以上、格納されている要素はStringであり、得られた要素もStringであり、私が得た配列もStringであるべきだと思います.String[]ですが、私がそうすれば、あなたは私に異常を投げてくれます.これはいくつかの意味です.
この問題の原因はやはり消去であり,消去のために配列をこのように定義することはできない:T[]array=new T[size];特定のタイプの配列を生成するには、Classオブジェクトのみを使用します.Javaクラスライブラリで提供されるArrayクラスでは、配列要素タイプのClassオブジェクトと配列の長さを必要とする配列を作成する方法があります.
private Class<T> kind;
	public ArrayMaker(Class<T> kind ) {
		this.kind = kind;
	}
	
	@SuppressWarnings("unchecked")
	T[] create(int size) {
		T[] array = (T[])Array.newInstance(kind, size);
		System.out.println(array.getClass().getName());
		return array;
	}
は、Stringなどの具体的なタイプの配列である.class、createメソッドを呼び出すと印刷されます:[Ljava.lang.String、下層構造では確かにStringタイプの配列です.このような方法で配列を作成するのは、より優雅で、より安全な方法であるはずです.
以上、Java汎用の本質を紹介しました.その汎用は文法糖のようなもので、コンパイラに含まれる文法糖のようなものです.コンパイラによって実現される汎用型には多くの奇妙な制限があり、汎用型の機能はこのように強く、使用はこのように頻繁であるため、汎用型に対する愚痴はずっと続いており、同時に、汎用型は迂回できない曲がりである.
転載は明記してください:喩紅葉『汎型-消去実現Java汎型』