型消去について説明する前に、まず Java ジェネリックを理解する必要があります。いわゆるジェネリックは、パラメーター化された型です。これは、具象型をパラメーターとしてメソッド、クラス、およびインターフェースに渡すことができることを意味します。
なぜジェネリックが必要なのですか?まず第一に、Java では Object がオブジェクトの親クラスであることは誰もが知っています。オブジェクトは、任意のタイプのオブジェクトを参照できます。しかし、これは型安全性の問題を引き起こします。ジェネリックの出現により、型安全の機能が java にもたらされます。
- 一般的な方法:
class Test {
static <T> void helloworld(T t){
}
}
- ジェネリック クラス
class Test<T> {
T obj
Test(T obj){
this.obj = obj;
}
}
- 汎用インターフェース
interface Test<T> {
T getData();
}
ジェネリックを使用すると、コードの再利用が可能になり、1 つのコードを使用してさまざまな型に適用できます。第 2 に、ジェネリックは型の安全性も保証します (コンパイル時にチェックできます)。例えば:
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
// 编译器会阻止下面的操作,从而保证了我们的类型安全
list.add(100); // error
ジェネリックのもう 1 つの利点は、次のような個別の型変換が必要ないことです。
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
String s0 = (String)list.get(0); // 类型转换是不需要做的
// 下面展示了,我们不需要进行单独的类型转换
String s1 = list.get(0);
String s2 = list.get(1);
予兆は以前に行われており、型消去について話す時が来ました。
型消去とは いわゆる型消去は、コンパイル時にのみ型制約を強制し、実行時に型情報を破棄するプロセスです。このような一般的なメソッドがあるため、次のようになります。
public static <T> boolean myequal(T t1, T t2){
return t1.equals(t2);
}
コンパイラは、次のように型 T を Object に置き換えます。
public static <Object> boolean myequal(Object t1, Object t2){
return t1.equals(t2);
}
ジェネリック クラスの型消去
クラス レベルでの型消去は次の規則に従います。まず、コンパイラはクラスの型パラメーターを破棄し、それを最初にバインドされた型、または型パラメーターがバインドされていない場合は Object に置き換えます。
- パラメータの型がバインドされていません
public class MyClass<T> {
private T[] elements;
public void doSomething(T element){
}
public T getSomething(){
}
}
MyClass の型パラメーター T はどの型にもバインドされていないため、T は Object に置き換えられ、置き換えの結果は次のようになります。
public class MyClass {
private Object[] elements;
public void doSomething(Object element){
}
public Object getSomething(){
}
}
- パラメータの型はバインドされています
interface MyT {
}
public class MyClass<T extends MyT> {
private E[] elements;
public void doSomething(T element){
}
public T getSomething(){
}
}
MyTClass は、MyClass の型パラメーター T がバインドされる最初の型であるため、T は MyTClass に置き換えられます。
public class MyClass {
private MyT[] elements;
public void doSomething(MyT element){
}
public MyT getSomething(){
}
}
最初のバインディングを取得しても問題ないのはなぜですか? たとえば、MyT にも親クラスがあり、親クラスに親クラスがある場合、型パラメーターには多くの間接バインディングがあり、最初のバインディングはすべての親クラスをカバーするため、最初のバインディングを使用するだけです。
ジェネリック メソッドの型消去
ジェネリック メソッドの場合、その型パラメーターは格納されません。次の規則に従います。まず、コンパイラーはメソッドの型パラメーターを破棄し、最初にバインドされた型に置き換えます。型パラメーターがバインドされていない場合は、単にオブジェクトを使用して交換。
- パラメータの型がバインドされていません
public static <T> void printSomething(T[] arr){
for(T item: arr) {
System.out.printf("%s", item);
}
}
上記のメソッドは、型消去の結果を実行した後:
public static void printSomething(Object[] arr){
for(Object item: arr) {
System.out.printf("%s", item);
}
}
- パラメータの型はバインドされています
public static <T extends MyT> void printSomething(T[] arr){
for(T item: arr) {
System.out.printf("%s", item);
}
}
上記のメソッドは、型消去の結果を実行した後:
public static void printSomething(MyT[] arr){
for(MyT item: arr) {
System.out.printf("%s", item);
}
}
型消去で生成されたブリッジ メソッド
上記の規則に加えて, これらの同様のメソッドについて, コンパイラはそれらを区別するためにいくつかの合成メソッドを作成します. この合成メソッドは、最初にバインドされたクラスの同じメソッド署名を拡張することです. この文は、次の例から得られます.直感的な理解。
まず、そのようなクラスがあります:
class MyQueue<T> {
private T[] elements;
public MyQueue(int size){
this.elements = (T[])new Object[size];
}
public void add(T data){
}
public T dequeue(){
if(elements.length > 0){
return elements[0];
} else {
return null;
}
}
}
上記のクラスの型消去後、T は Object に置き換えられます (具体的な規則については、前の部分を参照してください)。MyQueue を継承するクラスを作成します。
class IntegerQueue extends MyQueue<Integer> {
public IntegerQueue(int size){
super(size);
}
@Override
public void add(Integer data) {
super.add(data);
}
}
次に、合成メソッドの原理を引用するテスト メソッドを記述します。
MyQueue の add メソッドのパラメーターの型が Object に変更されていることがわかりますので、queue.add("Helllo")
それは理にかなっています。IDE は、この型に問題がある可能性も示唆しました。
実際、IntegerQueue は Int 型のみを受け取りたいため、これは私たちが望んでいるものではありません。次に、このテスト ケースを実行すると、次のエラーが表示されます。
この例でも、ジェネリックがタイプ セーフのために導入された関数であることを示しています。コンパイラはどのようにそれを行いますか? それはどのように機能しますか?
実際、コンパイラはブリッジを行うための追加のメソッド (前述の合成メソッド) を生成します。MyQueue が型消去されると、次のようになることは誰もが知っています。
class MyQueue {
private Object[] elements;
public MyQueue(int size){
this.elements = (Object[])new Object[size];
}
public void add(Object data){
}
public Object dequeue(){
if(elements.length > 0){
return elements[0];
} else {
return null;
}
}
}
IntegerQueue
public void add(Integer data){}
とMyQueue
public void add(Object data){}
のメソッドは似ています。コンパイラは、類似のメソッドがそれらの間を橋渡しするための中間メソッドを作成します。ジェネリック ポリモーフィズムの型消去を確実にするために、このメソッドが生成され、IntegerQueue
コンパイラは、この類似したメソッドが誤って一致しないことを保証できます。つまり、コンパイラは、それらの間の合成メソッドを作成します。前述のように、コンパイラによって作成されるブリッジ メソッドは次のとおりです。
static class IntegerQueue extends MyQueue<Integer> {
public void add(Object data){
add((Integer) data);
}
public void add(Integer data) {
super.add(data);
}
}
この例の合成方法は
public void add(Object data){
add((Integer) data);
}
同じ最初にバインドされたクラスのメソッド シグネチャを拡張します。つまり、それらのメソッド名は同じであり、この合成メソッドのパラメーターの型は、型消去後の最初にバインドされたクラスです (具体的には、前のセクションを参照できます)。 )。その機能は、IntegerQueue クラスの add メソッドとその親クラスの add メソッドをブリッジして、継承におけるジェネリックの型安全性の問題を解決することです。