Javaでの型消去の説明
1. 概要
この簡単な記事では、型消去と呼ばれるJavaのジェネリックスの重要なメカニズムの基本について説明します。
2. 型消去とは何ですか?
型消去は、コンパイル時にのみ型制約を適用し、実行時に要素型情報を破棄するプロセスとして説明できます。
例えば:
public static boolean containsElement(E [] elements, E element){
for (E e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}
コンパイルされると、バインドされていないタイプEは、実際のタイプObjectに置き換えられます。
public static boolean containsElement(Object [] elements, Object element){
for (Object e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}
コンパイラは、コードのタイプセーフを保証し、ランタイムエラーを防ぎます。
3. 型消去の種類
型消去は、クラス(または変数)レベルおよびメソッドレベルで発生する可能性があります。
3.1. クラス型消去
クラスレベルでは、クラスの型パラメーターはコードのコンパイル中に破棄され、最初のバインドに置き換えられます。型パラメーターがバインドされていない場合はObjectに置き換えられます。
配列を使用してStackを実装しましょう。
public class Stack {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
コンパイル時に、バインドされていない型パラメーターEはObjectに置き換えられます。
public class Stack {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void push(Object data) {
// ..
}
public Object pop() {
// ..
}
}
タイプパラメータEがバインドされている場合:
public class BoundStack> {
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
コンパイルされると、バインドされた型パラメーターEは、最初のバインドされたクラス(この場合はComparable)に置き換えられます:
public class BoundStack {
private Comparable [] stackContent;
public BoundStack(int capacity) {
this.stackContent = (Comparable[]) new Object[capacity];
}
public void push(Comparable data) {
// ..
}
public Comparable pop() {
// ..
}
}
3.2. メソッドタイプ消去
メソッドレベルの型消去の場合、メソッドの型パラメータは保存されませんが、バインドされていない場合、またはバインドされたときに最初にバインドされたクラスの場合は、親タイプObjectに変換されます。
特定の配列の内容を表示する方法を考えてみましょう。
public static void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
コンパイル時に、型パラメーターEはObjectに置き換えられます。
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}
バインドされたメソッドタイプパラメータの場合:
public static > void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
タイプパラメータEが消去され、Comparable:に置き換えられます
public static void printArray(Comparable[] array) {
for (Comparable element : array) {
System.out.printf("%s ", element);
}
}
4. エッジケース
型消去プロセス中に、コンパイラは合成メソッドを作成して類似のメソッドを区別します。 これらは、同じ最初のバインドクラスを拡張するメソッドシグネチャから取得できます。
以前のStackの実装を拡張する新しいクラスを作成しましょう。
public class IntegerStack extends Stack {
public IntegerStack(int capacity) {
super(capacity);
}
public void push(Integer value) {
super.push(value);
}
}
次に、次のコードを見てみましょう。
IntegerStack integerStack = new IntegerStack(5);
Stack stack = integerStack;
stack.push("Hello");
Integer data = integerStack.pop();
型の消去後、次のものがあります。
IntegerStack integerStack = new IntegerStack(5);
Stack stack = (IntegerStack) integerStack;
stack.push("Hello");
Integer data = (String) integerStack.pop();
IntegerStackは親クラスStackからpush(Object)を継承しているため、IntegerStackにStringをプッシュする方法に注目してください。 もちろん、これは正しくありません。integerStackはStack<Integer>型であるため、整数である必要があります。
したがって、当然のことながら、popをStringに割り当てて、Integerに割り当てようとすると、コンパイラーによってpush中に挿入されたキャストからClassCastExceptionが発生します。
4.1. ブリッジメソッド
上記のエッジケースを解決するために、コンパイラーはブリッジメソッドを作成することがあります。 これは、パラメータ化されたクラスを拡張したり、メソッドシグネチャがわずかに異なるかあいまいなパラメータ化されたインターフェイスを実装するクラスまたはインターフェイスをコンパイルするときにJavaコンパイラによって作成される合成メソッドです。
上記の例では、Javaコンパイラは、IntegerStackのpush(Integer)メソッドとStackのpush(Object)メソッドの間でメソッドシグネチャの不一致がないことを保証することにより、消去後のジェネリック型のポリモーフィズムを保持します。
したがって、コンパイラはここでブリッジメソッドを作成します。
public class IntegerStack extends Stack {
// Bridge method generated by the compiler
public void push(Object value) {
push((Integer)value);
}
public void push(Integer value) {
super.push(value);
}
}
その結果、型消去後のStackクラスのpushメソッドは、IntegerStackクラスの元のpushメソッドに委任します。
5. 結論
このチュートリアルでは、型パラメータの変数とメソッドの例を使用して、型消去の概念について説明しました。
これらの概念についての詳細を読むことができます:
いつものように、この記事に付属するソースコードはover on GitHubで入手できます。