回答:
これは基本的に、ジェネリックがコンパイラのトリックを介してJavaで実装される方法です。コンパイルされたジェネリックコードは、実際にjava.lang.Object
あなたが話すところT
(または他の型パラメーター)を使用するだけです-そして、それが本当にジェネリック型であることをコンパイラーに伝えるメタデータがあります。
ジェネリック型またはジェネリックメソッドに対していくつかのコードをコンパイルすると、コンパイラーは実際に何を意味するか(つまり、型の引数が何であるか)を計算し、コンパイル時に正しいことT
を確認しますが、出力されたコードはもう一度話しますに関してjava.lang.Object
-コンパイラは必要に応じて追加のキャストを生成します。実行時には、a List<String>
とa List<Date>
はまったく同じです。追加の型情報はコンパイラによって消去されました。
、言うと情報コードは以下のような式を含むことができるように、実行時に保持されているC#、この比較typeof(T)
に相当するT.class
後者が無効であることを除いては- 。(ご注意ください。.NETジェネリックとJavaジェネリックの間にはさらに違いがあります。)型消去は、Javaジェネリックを処理するときの「奇数」の警告/エラーメッセージの多くの原因です。
その他のリソース:
Object
(弱く型付けされたシナリオで)としてのみ提供されるものが実際List<String>
にたとえば)であるかどうかを実行時に見つけることは非常に簡単です。Javaではそれは現実的ではありません。それがであることがわかりますがArrayList
、元のジェネリック型はそうではありません。この種のことは、たとえば、シリアライゼーション/デシリアライゼーションの状況で発生する可能性があります。別の例は、コンテナーがそのジェネリック型のインスタンスを構築できる必要がある場合です-その型をJavaで(としてClass<T>
)個別に渡す必要があります。
Class<T>
Javaがその情報を保持していないという理由だけで、コンストラクター(またはジェネリックメソッド)にパラメーターを追加せざるを得ないところがいくつかあります。見てEnumSet.allOf
例えば-メソッドにジェネリック型引数は十分なはずです。なぜ「通常の」引数も指定する必要があるのですか?答え:型消去。この種のものはAPIを汚染します。興味がありましたが、.NETジェネリックを多用しましたか?(続き)
余談ですが、コンパイラーが消去を実行するときに実際に何をしているのかを実際に確認するのは興味深い演習です。概念全体が少しわかりやすくなります。ジェネリックを消去してキャストを挿入したjavaファイルを出力するためにコンパイラーに渡すことができる特別なフラグがあります。例:
javac -XD-printflat -d output_dir SomeFile.java
-printflat
ファイルを生成するコンパイラに渡されますフラグです。(この-XD
部分はjavac
、実際に単にコンパイルするのではなく、実際にコンパイルを行う実行可能jarに渡すように指示するものjavac
ですが、余談です...)-d output_dir
コンパイラーが新しい.javaファイルを配置する場所を必要とするため、これが必要です。
もちろん、これは単に消去するだけではありません。コンパイラが行うすべての自動処理はここで行われます。たとえば、デフォルトのコンストラクターも挿入され、新しいforeachスタイルのfor
ループは通常のfor
ループに拡張されます。自動的に発生している小さなことを確認できるのは素晴らしいことです。
消去とは、文字通り、ソースコードに存在する型情報がコンパイル済みバイトコードから消去されることを意味します。いくつかのコードでこれを理解しましょう。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class GenericsErasure {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("Hello");
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
}
}
このコードをコンパイルして、Javaデコンパイラで逆コンパイルすると、次のような結果になります。逆コンパイルされたコードには、元のソースコードに存在する型情報のトレースが含まれていないことに注意してください。
import java.io.PrintStream;
import java.util.*;
public class GenericsErasure
{
public GenericsErasure()
{
}
public static void main(String args[])
{
List list = new ArrayList();
list.add("Hello");
String s;
for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
s = (String)iter.next();
}
}
jigawot
たが、うまくいきました。
すでに非常に完全なJon Skeetの回答を完了するには、型消去の概念が以前のバージョンのJavaとの互換性の必要性から派生していることを理解する必要があります。
EclipseCon 2007で最初に発表されました(現在は利用できません)、互換性には以下の点が含まれていました。
元の答え:
したがって:
new ArrayList<String>() => new ArrayList()
より具体化するための命題があります。「抽象的な概念を本物と見なす」ことを具体化し、言語構造は単なる構文糖ではなく概念であるべきです。
またcheckCollection
、指定したコレクションの動的に型保証されたビューを返すJava 6 のメソッドについても触れておきます。間違ったタイプの要素を挿入しようとすると、すぐにになりClassCastException
ます。
言語のジェネリックメカニズムはコンパイル時(静的)の型チェックを提供しますが、チェックされていないキャストでこのメカニズムを無効にすることができます。
通常、これは問題ではありません。コンパイラは、このようなチェックされていないすべての操作に対して警告を発行するためです。
ただし、次のように静的型チェックだけでは不十分な場合があります。
ClassCastException
、誤って型指定された要素がパラメーター化されたコレクションに入れられたことを示します。残念ながら、この例外はエラーのある要素が挿入された後はいつでも発生する可能性があるため、通常、問題の実際の原因に関する情報はほとんどまたはまったく提供されません。ほぼ4年後の2012年7月の更新:
「API移行互換性ルール(署名テスト)」で詳細(2012)になっています
Javaプログラミング言語は、消去を使用してジェネリックスを実装します。これにより、レガシーバージョンとジェネリックバージョンが、型に関するいくつかの補助情報を除いて、通常は同じクラスファイルを生成するようになります。クライアントコードを変更または再コンパイルせずに、レガシークラスファイルをジェネリッククラスファイルに置き換えることができるため、バイナリ互換性は損なわれません。
非ジェネリックなレガシーコードとのインターフェースを容易にするために、パラメーター化された型の消去を型として使用することもできます。このような型は、raw型(Java言語仕様3 / 4.8)と呼ばれます。rawタイプを許可すると、ソースコードの下位互換性も保証されます。
これによると、次の
java.util.Iterator
クラスのバージョンはバイナリコードとソースコードの両方に下位互換性があります。
Class java.util.Iterator as it is defined in Java SE version 1.4:
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
Class java.util.Iterator as it is defined in Java SE version 5.0:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
すでに補完されているジョンスキートの回答を補完しています...
消去によるジェネリックの実装は、いくつかの厄介な制限(たとえば、no new T[42]
)につながることが言及されています。このように処理を行う主な理由は、バイトコードの下位互換性であるとも述べられています。これも(ほとんど)真実です。生成されたバイトコード-target 1.5は、脱糖されたキャスト-target 1.4とは多少異なります。技術的には、実行時に一般的な型のインスタンス化にアクセスすることも可能です(計り知れないトリックを通じて)であり、実際にバイトコードに何かがあることを証明。
より興味深い点(これは提起されていません)は、消去を使用してジェネリックを実装すると、高レベルの型システムで実現できることについて、かなり柔軟性が高まることです。これの良い例は、CLRに対するScalaのJVM実装です。JVMでは、JVM自体が総称型に制限を課さない(これらの「型」が事実上存在しないため)ため、上位の種類を直接実装することが可能です。これは、パラメーターのインスタンス化に関する実行時の知識を持つCLRとは対照的です。このため、CLR自体にはジェネリックを使用する方法の概念が必要であり、予期しないルールでシステムを拡張する試みを無効にします。その結果、CLR上のScalaのより高い種類は、コンパイラー自体の内部でエミュレートされる奇妙な形の消去を使用して実装されます。
実行時にいたずらをしたい場合、消去は不便かもしれませんが、コンパイラー作成者に最も柔軟性を提供します。私はそれがすぐに消えない理由の一部だと思います。
良い説明があります。型消去が逆コンパイラでどのように機能するかを示す例を追加するだけです。
オリジナルクラス、
import java.util.ArrayList;
import java.util.List;
public class S<T> {
T obj;
S(T o) {
obj = o;
}
T getob() {
return obj;
}
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("Hello");
// for-each
for(String s : list) {
String temp = s;
System.out.println(temp);
}
// stream
list.forEach(System.out::println);
}
}
バイトコードから逆コンパイルされたコード、
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;
public class S {
Object obj;
S(Object var1) {
this.obj = var1;
}
Object getob() {
return this.obj;
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("Hello");
// for-each
Iterator iterator = var1.iterator();
while (iterator.hasNext()) {
String string;
String string2 = string = (String)iterator.next();
System.out.println(string2);
}
// stream
PrintStream printStream = System.out;
Objects.requireNonNull(printStream);
var1.forEach(printStream::println);
}
}
Genericesを使用する理由
簡単に言うと、ジェネリックスは、クラス(インターフェースとメソッド)を定義するときに、タイプ(クラスとインターフェース)をパラメーターにすることができます。メソッド宣言で使用されるより一般的な仮パラメーターと同様に、型パラメーターは、同じコードを異なる入力で再利用する方法を提供します。違いは、仮パラメーターへの入力は値であるのに対し、型パラメーターへの入力は型です。ジェネリックを使用するodeには、非ジェネリックコードに比べて多くの利点があります。
型消去とは
ジェネリックはJava言語に導入され、コンパイル時により厳密な型チェックを提供し、ジェネリックプログラミングをサポートします。ジェネリックを実装するために、Javaコンパイラーは型消去を以下に適用します。
【NB】-ブリッジ方式とは?簡単に言うと、などのパラメータ化されたインターフェースの場合Comparable<T>
、これにより追加のメソッドがコンパイラによって挿入される可能性があります。これらの追加メソッドはブリッジと呼ばれます。
消去のしくみ
型の消去は次のように定義されます。パラメーター化された型からすべての型パラメーターを削除し、型変数をその境界の消去、または境界がない場合はObjectに、またはそれがある場合は左端の境界の消去に置き換えます。複数の境界。ここではいくつかの例を示します。
List<Integer>
、List<String>
とList<List<String>>
ありますList
。List<Integer>[]
ですList[]
。List
はそれ自体であり、生のタイプと同様です。Integer
はそれ自体であり、型パラメーターのないすべての型について同様です。T
の定義ではasList
ありませんObject
ので、T
何も結合しました。T
定義での消去max
はComparable
、T
にバインドされてComparable<? super T>
いるためです。T
最後の定義での消去はmax
ですObject
。なぜなら、
T
はObject
&Comparable<T>
をバインドしており、左端のバインドの消去を取っているからです。ジェネリックを使用する場合は注意が必要
Javaでは、2つの異なるメソッドが同じシグネチャを持つことはできません。ジェネリックは消去によって実装されるため、2つの異なるメソッドが同じ消去のシグネチャを持つことはできません。クラスは、シグネチャが同じ消去を持つ2つのメソッドをオーバーロードできません。また、クラスは、同じ消去を持つ2つのインターフェイスを実装できません。
class Overloaded2 {
// compile-time error, cannot overload two methods with same erasure
public static boolean allZero(List<Integer> ints) {
for (int i : ints) if (i != 0) return false;
return true;
}
public static boolean allZero(List<String> strings) {
for (String s : strings) if (s.length() != 0) return false;
return true;
}
}
このコードは次のように動作するように意図しています。
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
ただし、この場合、両方の方法の署名の消去は同じです。
boolean allZero(List)
したがって、名前の衝突はコンパイル時に報告されます。消去後は、1つのメソッド呼び出しと他のメソッド呼び出しを区別することができないため、両方のメソッドに同じ名前を付けて、オーバーロードによってそれらを区別しようとすることはできません。
うまくいけば、読者はお楽しみいただけます :)