web-dev-qa-db-ja.com

Javaジェネリック型の消去:いつ何が起こりますか?

Javaの型消去について読みました OracleのWebサイトで

型消去はいつ発生しますか?コンパイル時または実行時?クラスがロードされるとき?クラスがインスタンス化されるとき

多くのサイト(上記の公式チュートリアルを含む)は、コンパイル時に型消去が発生すると言います。型情報がコンパイル時に完全に削除された場合、ジェネリックを使用するメソッドが型情報なしまたは間違った型情報で呼び出された場合、JDKはどのように型の互換性をチェックしますか?

次の例を考えてみましょう。クラスAにはメソッドempty(Box<? extends Number> b)があるとします。 A.Javaをコンパイルし、クラスファイルA.classを取得します。

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

次に、別のクラスBを作成します。このクラスは、パラメータ化されていない引数(rawタイプ)でメソッドemptyを呼び出します:empty(new Box())。クラスパスでB.Javaを使用してA.classをコンパイルすると、javacは十分に賢く警告を発します。したがって、A.classには、いくつかのタイプ情報が格納されています

public class B {
    public static void invoke() {
        // Java: unchecked method invocation:
        //  method empty in class A is applied to given types
        //  required: Box<? extends Java.lang.Number>
        //  found:    Box
        // Java: unchecked conversion
        //  required: Box<? extends Java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

私の推測では、クラスがロードされたときに型の消去が行われますが、それは単なる推測です。それでそれはいつ起こりますか?

227
afryingpan

型消去は、ジェネリックのseに適用されます。クラスファイルには、メソッド/タイプisジェネリックかどうか、および制約が何であるかなどを示すメタデータが間違いなくあります。しかし、ジェネリックがsedの場合、変換されますコンパイル時のチェックと実行時のキャストに。したがって、このコード:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

にコンパイルされます

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

実行時には、リストオブジェクトのT=Stringを見つける方法はありません。その情報は失われています。

...しかし、List<T>インターフェース自体は、それ自体を汎用であると宣伝しています。

編集:明確にするために、コンパイラは変数List<String>であるという情報を保持しますが、リストオブジェクト自体のT=Stringを見つけることはできません。

230
Jon Skeet

コンパイラーは、コンパイル時にジェネリックを理解する責任があります。コンパイラは、type erasureと呼ばれるプロセスで、この汎用クラスの「理解」を破棄する責任もあります。すべてはコンパイル時に発生します。

注:Java開発者の大半の信念に反して、コンパイル時の型情報を保持し、この情報を取得することができます。非常に制限された方法にもかかわらず、ランタイム。言い換えると、Javaは、非常に制限された方法で具体化されたジェネリックを提供します

型消去について

コンパイル時には、コンパイラは完全な型情報を利用できますが、この情報は意図的に削除されます一般バイトコードが生成されると、type erasureと呼ばれるプロセスで=。これは、互換性の問題によりこのように行われます。言語設計者の意図は、プラットフォームのバージョン間で完全なソースコード互換性と完全なバイトコード互換性を提供することでした。別の方法で実装した場合、プラットフォームの新しいバージョンに移行するときにレガシーアプリケーションを再コンパイルする必要があります。それが行われた方法、すべてのメソッドシグネチャは保持され(ソースコードの互換性)、何も再コンパイルする必要はありません(バイナリ互換性)。

Javaの具象ジェネリックについて

コンパイル時の型情報を保持する必要がある場合は、匿名クラスを使用する必要があります。ポイントは、匿名クラスの非常に特殊なケースでは、実行時に完全なコンパイル時の型情報を取得することが可能である、つまり、具体化されたジェネリックです。これは、匿名クラスが関係しているときにコンパイラが型情報を破棄しないことを意味します。この情報は生成されたバイナリコードに保持され、ランタイムシステムではこの情報を取得できます。

このテーマに関する記事を書きました。

http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

上記の記事で説明した手法についての注意点は、この手法はほとんどの開発者にとってわかりにくいことです。ほとんどの開発者は、うまく機能し、うまく機能していますが、この手法に混乱または不快感を覚えています。共有コードベースがある場合、またはコードを公開する予定がある場合、上記の手法はお勧めしません。一方、コードの唯一のユーザーである場合は、この手法が提供するパワーを活用できます。

サンプルコード

上記の記事には、サンプルコードへのリンクがあります。

90
Richard Gomes

ジェネリック型のフィールドがある場合、その型パラメーターはクラスにコンパイルされます。

ジェネリック型を取得または返すメソッドがある場合、それらの型パラメーターはクラスにコンパイルされます。

この情報は、Box<String>empty(Box<T extends Number>)メソッドに渡すことができないことを伝えるためにコンパイラが使用するものです。

APIは複雑ですが、リフレクションAPIを介して getGenericParameterTypesgetGenericReturnType 、およびフィールドの場合 getGenericType などのメソッドを使用して、このタイプ情報を検査できます。

ジェネリック型を使用するコードがある場合、コンパイラは必要に応じて(呼び出し側で)キャストを挿入して型をチェックします。ジェネリックオブジェクト自体は単なる生の型です。パラメータ化されたタイプは「消去」されます。したがって、new Box<Integer>()を作成すると、IntegerオブジェクトにはBoxクラスに関する情報はありません。

Angelika LangerのFAQ は、Java Genericsで見た中で最高のリファレンスです。

33
erickson

Java言語のジェネリック は、このトピックに関する非常に優れたガイドです。

ジェネリックは、消去と呼ばれるフロントエンド変換としてJavaコンパイラーによって実装されます。 (ほぼ)ソースからソースへの変換と考えることができます。これにより、loophole()の汎用バージョンが非汎用バージョンに変換されます。

そのため、コンパイル時です。 JVMは、どのArrayListを使用したかを知ることはありません。

Javaのジェネリックの消去の概念は何ですか? に対するスキート氏の答えもお勧めします==

13
Eugene Yokota

型の消去はコンパイル時に発生します。タイプ消去とは、すべてのタイプではなく、ジェネリックタイプを忘れることを意味します。その上、ジェネリック型についてのメタデータがまだあります。例えば

Box<String> b = new Box<String>();
String x = b.getDefault();

に変換されます

Box b = new Box();
String x = (String) b.getDefault();

コンパイル時に。コンパイラがジェネリックな型を知っているためではなく、逆に、十分な知識がないために型の安全性を保証できないため、警告が表示される場合があります。

さらに、コンパイラーは、メソッド呼び出しのパラメーターに関する型情報を保持します。これは、リフレクションを介して取得できます。

この ガイド は、このテーマで見つけた最高のものです。

6
Vinko Vrsalovic

「型消去」という用語は、実際にはジェネリックに関するJavaの問題の正しい説明ではありません。型の消去自体は悪いことではありません。実際、パフォーマンスに非常に必要であり、C++、Haskell、Dなどのいくつかの言語でよく使用されます。

嫌悪感を抱く前に、 Wiki からタイプ消去の正しい定義を思い出してください

タイプ消去とは何ですか?

型消去とは、実行時に実行される前に、明示的な型注釈がプログラムから削除されるロード時プロセスを指します。

型消去とは、バイナリコードでコンパイルされたプログラムに型タグが含まれないように、設計時に作成された型タグまたは推定された型タグをコンパイル時に破棄することです。これは、ランタイムタグが必要な場合を除き、すべてのプログラミング言語がバイナリコードにコンパイルする場合に当てはまります。これらの例外には、たとえば、すべての存在タイプ(サブタイプ化可能なJava参照タイプ、多くの言語の任意のタイプ、ユニオンタイプ)が含まれます。型が消去される理由は、型は抽象化のみであり、値の構造とそれらを処理する適切なセマンティクスをアサートするため、プログラムは何らかのユニタイプ(ビットのみを許可するバイナリ言語)の言語に変換されるためです。

ですから、これは見返りであり、通常の自然なことです。

Javaの問題は異なり、Javaの具体化方法に起因します。

Javaにジェネリックが具体化されていないことについて頻繁に行われるステートメントも間違っています。

Javaは具体化しますが、下位互換性のために間違った方法で機能します。

具体化とは何ですか?

Wiki から

具象化とは、コンピュータープログラムに関する抽象的な概念を、プログラミング言語で作成された明示的なデータモデルまたはその他のオブジェクトに変換するプロセスです。

具象化とは、特殊化により、抽象的なもの(パラメトリックタイプ)を具体的なもの(コンクリートタイプ)に変換することです。

これを簡単な例で説明します。

定義付きのArrayList:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

抽象、具体的には型コンストラクタです。具体的な型に特化したときに「具体化」されます。たとえば、Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

ここで、ArrayList<Integer>は実際には型です。

しかし、これはexactlyJavadoes not !!!、代わりに、常に抽象型を境界で具体化します。つまり、特殊化のために渡されたパラメーターとは無関係に、同じ具体的な型を生成します。

ArrayList
{
    Object[] elems;
}

ここでは、暗黙的にバインドされたオブジェクト(ArrayList<T extends Object> == ArrayList<T>)で具体化されています。

それにもかかわらず、ジェネリック配列が使用できなくなり、生の型に対して奇妙なエラーが発生します:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

それは多くのあいまいさを引き起こします

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

同じ機能を参照してください:

void function(ArrayList list)

したがって、Javaでは汎用メソッドのオーバーロードを使用できません。

4
iconfly

Androidでタイプ消去に遭遇しました。本番環境では、gradify with minifyオプションを使用します。縮小後、致命的な例外が発生しました。オブジェクトの継承チェーンを表示する単純な関数を作成しました。

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

そして、この関数には2つの結果があります。

縮小されていないコード:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: Android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: Android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class Java.lang.Object

D/Reflection: this class: Java.lang.Object
D/Reflection: superClass: null

縮小コード:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class Android.support.v7.g.e

D/Reflection: this class: Android.support.v7.g.e
D/Reflection: superClass: class Java.lang.Object

D/Reflection: this class: Java.lang.Object
D/Reflection: superClass: null

したがって、縮小コードでは、実際のパラメーター化されたクラスは、型情報のない生のクラス型に置き換えられます。私のプロジェクトの解決策として、すべてのリフレクション呼び出しを削除し、関数の引数で渡された明示的なparams型でそれらを置き換えました。

2
porfirion