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


237

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

型消去はいつ発生しますか?コンパイル時または実行時ですか?クラスが読み込まれると?クラスがインスタンス化されるのはいつですか?

多くのサイト(上記の公式チュートリアルを含む)では、型の消去はコンパイル時に行われるとしています。コンパイル時に型情報が完全に削除された場合、ジェネリックを使用するメソッドが型情報なしまたは誤った型情報で呼び出された場合、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> {}

次に、パラメーター化されていない引数(rawタイプ)を使用しBてメソッドを呼び出す別のクラスを作成します。クラスパスでコンパイルすると、javacは十分に賢く警告を発します。だから、持ってそれに保存されているいくつかのタイプの情報を。emptyempty(new Box())B.javaA.classA.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());
    }
}

私の推測では、クラスがロードされると型の消去が発生しますが、それは単なる推測です。それでそれはいつ起こるのですか?



@afryingpan:私の回答で言及されている記事は、型消去がいつどのように発生するかを詳細に説明しています。タイプ情報が保持されるタイミングについても説明します。言い換えると、広まった信念に反して、具体化されたジェネリックはJavaで使用できます。参照:rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

回答:


240

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

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>インターフェース自体それ自体が汎用であることをまだ宣伝しています。

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


6
いいえ、ジェネリック型を使用している場合でも、実行時にメタデータが利用できる場合があります。ローカル変数はReflectionを介してアクセスできませんが、 "List <String> l"として宣言されたメソッドパラメーターの場合、実行時に型メタデータがあり、Reflection APIを介して利用できます。多くの人が考えるよううん、「型消去は」単純としてではありません...
ホジェリオ

4
@ロジェリオ:私があなたのコメントに返信したとき、変数の型を取得できることとオブジェクトの型を取得できることの間で混乱していると思います。フィールド自体は知っているが、オブジェクト自体はその型引数を知らない。
Jon Skeet、

もちろん、オブジェクト自体を見ただけでは、それがList <String>であることはわかりません。しかし、オブジェクトはどこからともなく現れるだけではありません。それらはローカルで作成され、メソッド呼び出し引数として渡されるか、メソッド呼び出しからの戻り値として返されるか、またはいくつかのオブジェクトのフィールドから読み取られます...これらすべての場合で、実行時にジェネリック型が何であるかを知ることができます。暗黙的に、またはJavaリフレクションAPIを使用して。
ホジェリオ

13
@ロジェリオ:オブジェクトがどこから来たのか、どうやってわかるの?タイプのパラメーターList<? extends InputStream>がある場合、作成時にそれがどのタイプであったかをどのようにして知ることができますか?参照が格納されているフィールドのタイプを確認できたとしても、なぜそうする必要があるのでしょうか?実行時にオブジェクトに関する残りの情報はすべて取得できるが、ジェネリック型の引数は取得できないのはなぜですか?あなたは型の消去をこの小さなものにしようとしているようですが、実際には開発者には影響を与えませんが、場合によっては非常に重大な問題であることがわかります。
Jon Skeet、

しかし、型消去は小さなことであり、開発者には実際には影響しません!もちろん、私は他の人のために話すことはできませんが、私の経験では、それは決して大したことではありませんでした。私は実際に、JavaモックAPI(JMockit)の設計でランタイム型情報を利用しています。皮肉なことに、.NETモックAPIは、C#で利用可能なジェネリック型システムをあまり活用していないようです。
ホジェリオ

99

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

注:ほとんどのJava開発者の信念に反して、非常に制限された方法にもかかわらず、コンパイル時に型情報を保持し、実行時にこの情報を取得することが可能です。つまり、Javaは具体化されたジェネリックを非常に制限された方法で提供します

型消去について

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

Javaで具体化されたジェネリックについて

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

私はこの主題についての記事を書きました:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

上記の記事で説明されている手法についての注意は、この手法は大部分の開発者にとってあいまいであることです。それが機能し、うまく機能しているにもかかわらず、ほとんどの開発者はこのテクニックに混乱している、または不快に感じています。共有コードベースがある場合、またはコードを公開する予定がある場合は、上記の手法はお勧めしません。一方、コードの唯一のユーザーである場合は、この手法が提供する機能を利用できます。

サンプルコード

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


1
@ will824:回答を大幅に改善し、いくつかのテストケースへのリンクを追加しました。乾杯:)
Richard Gomes

1
:実際には、彼らは両方のバイナリとソースの互換性を維持しませんでしたoracle.com/technetwork/java/javase/compatibility-137462.html私は彼らの意図についてもっと読むことができますか?ドキュメントは型消去を使用すると言っていますが、その理由は述べていません。
Dzmitry Lazerka 2013

@リチャード確かに、素晴らしい記事!ローカルクラスも機能し、両方のケース(匿名クラスとローカルクラス)では、コンパイラーは型情報を保持しないため、直接アクセスの場合にのみ目的の型引数に関する情報が保持されnew Box<String>() {};、間接アクセスの場合には保持されないことを追加できます。void foo(T) {...new Box<T>() {};...}囲むメソッド宣言。
ヤン・ガエル・Guéhéneuc

記事へのリンク切れを修正しました。私はゆっくりと自分の人生をグーグル除去し、自分のデータを取り戻しています。:-)
Richard Gomes

33

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

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

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

APIは複雑ですが、次のような方法でリフレクションAPIを介して、この種の情報を調べることができgetGenericParameterTypesgetGenericReturnType、、および、フィールドのgetGenericType

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

Angelika LangerのFAQは、Java Genericsについて私が見た中で最高のリファレンスです。


2
実際、クラスにコンパイルされるのは、フィールドとメソッドの正式な総称型、つまり典型的な "T"です。ジェネリック型の実際の型を取得するには、「匿名クラスのトリック」を使用する必要があります。
ヤン・ガエル・Guéhéneuc

13

Generics in Java Languageは、このトピックに関する非常に優れたガイドです。

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

つまり、コンパイル時です。JVMはArrayListあなたがどちらを使用したかを決して知ることはありません。

私はまた、Javaのジェネリックスにおける消去の概念とは何かに関するスキート氏の回答をお勧めします。


6

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

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

に変換されます

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

コンパイル時に。コンパイラがジェネリックのタイプを知っているためではなく、逆に、十分な情報が得られないためタイプセーフを保証できないため、警告が表示される場合があります。

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

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


6

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

嫌がる前に、Wikiから型消去の正しい定義を思い出してください

型消去とは何ですか?

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

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

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

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

Javaについて頻繁に行われる発言には具体化されたジェネリックスがないことも間違っています。

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

具象化とは何ですか?

私たちのウィキから

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

具象化とは、特殊化によって抽象(パラメトリックタイプ)を具体(コンクリートタイプ)に変換することを意味します。

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

定義付きのArrayList:

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

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

ArrayList<Integer>
{
    Integer[] elems;
}

どこArrayList<Integer>本当にタイプです。

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

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では使用できません。


2

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

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型でそれらをreplcedしました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.