Javaのジェネリックの消去の概念は何ですか?


回答:


200

これは基本的に、ジェネリックがコンパイラのトリックを介してJavaで実装される方法です。コンパイルされたジェネリックコードは、実際にjava.lang.Objectあなたが話すところT(または他の型パラメーター)を使用するだけです-そして、それが本当にジェネリック型であることをコンパイラーに伝えるメタデータがあります。

ジェネリック型またはジェネリックメソッドに対していくつかのコードをコンパイルすると、コンパイラーは実際に何を意味するか(つまり、型の引数が何であるか)を計算し、コンパイル時に正しいことTを確認しますが、出力されたコードはもう一度話しますに関してjava.lang.Object-コンパイラは必要に応じて追加のキャストを生成します。実行時には、a List<String> とa List<Date>はまったく同じです。追加の型情報はコンパイラによって消去されました。

、言うと情報コードは以下のような式を含むことができるように、実行時に保持されているC#、この比較typeof(T)に相当するT.class後者が無効であることを除いては- 。(ご注意ください。.NETジェネリックとJavaジェネリックの間にはさらに違いがあります。)型消去は、Javaジェネリックを処理するときの「奇数」の警告/エラーメッセージの多くの原因です。

その他のリソース:


6
@Rogerio:いいえ、オブジェクトは異なるジェネリック型を持ちません。フィールドは、タイプを知っているが、オブジェクトにはありません。
Jon Skeet、

8
@ロジェリオ:絶対に- Object(弱く型付けされたシナリオで)としてのみ提供されるものが実際List<String>にたとえば)であるかどうかを実行時に見つけることは非常に簡単です。Javaではそれは現実的ではありません。それがであることがわかりますがArrayList、元のジェネリック型はそうではありません。この種のことは、たとえば、シリアライゼーション/デシリアライゼーションの状況で発生する可能性があります。別の例は、コンテナーがそのジェネリック型のインスタンスを構築できる必要がある場合です-その型をJavaで(としてClass<T>)個別に渡す必要があります。
Jon Skeet、

6
私はそれが常にまたはほとんど常に問題であると主張したことはありませんが、私の経験では少なくともかなり頻繁に問題でした。Class<T>Javaがその情報を保持していないという理由だけで、コンストラクター(またはジェネリックメソッド)にパラメーターを追加せざるを得ないところがいくつかあります。見てEnumSet.allOf例えば-メソッドにジェネリック型引数は十分なはずです。なぜ「通常の」引数も指定する必要があるのですか?答え:型消去。この種のものはAPIを汚染します。興味がありましたが、.NETジェネリックを多用しましたか?(続き)
Jon Skeet、

5
.NETジェネリックを使用する前に、さまざまな方法でJavaジェネリックが扱いにくいことに気付きました(「呼び出し元指定」の分散形式には確かに利点がありますが、ワイルドカードは依然として頭痛の種です)-.NETジェネリックを使用した後でのみしばらくの間、Javaジェネリックスではいくつのパターンがぎこちなくなったり、不可能になったりするのかを見ました。再びブラブのパラドックスです。残念ながら、.NETジェネリックにも欠点がないと言っているわけではありません。残念ながら、表現できないさまざまな型の関係がありますが、Javaジェネリックよりも優先されます。
Jon Skeet、

5
@Rogerio:リフレクションでできることはたくさんあります、Javaジェネリックスではできないことと同じくらい頻繁にやりたいことはありません。私は、フィールドの型引数を見つけるためにしたくないほとんどのように、多くの場合、私は実際のオブジェクトの型引数を知りたいと。
Jon Skeet、

41

余談ですが、コンパイラーが消去を実行するときに実際に何をしているのかを実際に確認するのは興味深い演習です。概念全体が少しわかりやすくなります。ジェネリックを消去してキャストを挿入したjavaファイルを出力するためにコンパイラーに渡すことができる特別なフラグがあります。例:

javac -XD-printflat -d output_dir SomeFile.java

-printflatファイルを生成するコンパイラに渡されますフラグです。(この-XD部分はjavac、実際に単にコンパイルするのではなく、実際にコンパイルを行う実行可能jarに渡すように指示するものjavacですが、余談です...)-d output_dirコンパイラーが新しい.javaファイルを配置する場所を必要とするため、これが必要です。

もちろん、これは単に消去するだけではありません。コンパイラが行うすべての自動処理はここで行われます。たとえば、デフォルトのコンストラクターも挿入され、新しいforeachスタイルのforループは通常のforループに拡張されます。自動的に発生している小さなことを確認できるのは素晴らしいことです。


29

消去とは、文字通り、ソースコードに存在する型情報がコンパイル済みバイトコードから消去されることを意味します。いくつかのコードでこれを理解しましょう。

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();

    }
} 

.classファイルから型を消去した後、javaデコンパイラを使用してコードを表示しようとしましたが、.classファイルにはまだ型情報が含まれています。試してみましjigawotたが、うまくいきました。
正直

25

すでに非常に完全な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();
}

2
後方互換性は、型を消去せずに実現できた可能性がありますが、Javaプログラマーが新しいコレクションのセットを学習しなければ実現できなかったことに注意してください。それがまさに.NETがたどったルートです。つまり、重要なのはこの3番目の弾丸です。(続き)
ジョン・スキート

15
個人的には、これは近視の間違いだったと思います-短期的な利点と長期的な欠点がありました。
Jon Skeet、

8

すでに補完されているジョンスキートの回答を補完しています...

消去によるジェネリックの実装は、いくつかの厄介な制限(たとえば、no new T[42])につながることが言及されています。このように処理を行う主な理由は、バイトコードの下位互換性であるとも述べられています。これも(ほとんど)真実です。生成されたバイトコード-target 1.5は、脱糖されたキャスト-target 1.4とは多少異なります。技術的には、実行時に一般的な型のインスタンス化にアクセスすることも可能です(計り知れないトリックを通じて)であり、実際にバイトコードに何かがあることを証明。

より興味深い点(これは提起されていません)は、消去を使用してジェネリックを実装すると、高レベルの型システムで実現できることについて、かなり柔軟性が高まることです。これの良い例は、CLRに対するScalaのJVM実装です。JVMでは、JVM自体が総称型に制限を課さない(これらの「型」が事実上存在しないため)ため、上位の種類を直接実装することが可能です。これは、パラメーターのインスタンス化に関する実行時の知識を持つCLRとは対照的です。このため、CLR自体にはジェネリックを使用する方法の概念が必要であり、予期しないルールでシステムを拡張する試みを無効にします。その結果、CLR上のScalaのより高い種類は、コンパイラー自体の内部でエミュレートされる奇妙な形の消去を使用して実装されます。

実行時にいたずらをしたい場合、消去は不便かもしれませんが、コンパイラー作成者に最も柔軟性を提供します。私はそれがすぐに消えない理由の一部だと思います。


6
不便なのは、実行時に「いたずら」をしたいときではありません。実行時に完全に合理的なことをしたいときです。実際、型消去を使用すると、List <String>をListにキャストしてから、警告のみを指定してList <Date>にキャストするなど、いたずらなことを行うことができます。
Jon Skeet、

5

私が理解しているように(.NETの男なので)JVMはジェネリックの概念がないため、コンパイラーは型パラメーターをObjectに置き換え、すべてのキャストを実行します。

つまり、Javaジェネリックは構文糖衣にすぎず、参照渡しの際にボックス化/ボックス化解除を必要とする値型のパフォーマンスは向上しません。


3
いずれにしても、Javaジェネリックは値型を表すことができません。List<int>などはありません。しかし、参照渡し、Javaの全てではありません-それは厳密値渡し'S(その値を基準とすることができる。)
ジョンスキート

2

良い説明があります。型消去が逆コンパイラでどのように機能するかを示す例を追加するだけです。

オリジナルクラス、

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);


   }
}

2

Genericesを使用する理由

簡単に言うと、ジェネリックスは、クラス(インターフェースとメソッド)を定義するときに、タイプ(クラスとインターフェース)をパラメーターにすることができます。メソッド宣言で使用されるより一般的な仮パラメーターと同様に、型パラメーターは、同じコードを異なる入力で再利用する方法を提供します。違いは、仮パラメーターへの入力は値であるのに対し、型パラメーターへの入力は型です。ジェネリックを使用するodeには、非ジェネリックコードに比べて多くの利点があります。

  • コンパイル時のより強力な型チェック。
  • キャストの排除。
  • プログラマが汎用アルゴリズムを実装できるようにします。

型消去とは

ジェネリックはJava言語に導入され、コンパイル時により厳密な型チェックを提供し、ジェネリックプログラミングをサポートします。ジェネリックを実装するために、Javaコンパイラーは型消去を以下に適用します。

  • ジェネリック型のすべての型パラメーターをその境界または型パラメーターが無制限の場合はObjectで置き換えます。したがって、生成されたバイトコードには、通常のクラス、インターフェース、およびメソッドのみが含まれます。
  • 型の安全性を維持する必要がある場合は、型キャストを挿入します。
  • 拡張ジェネリック型のポリモーフィズムを維持するためのブリッジメソッドを生成します。

【NB】-ブリッジ方式とは?簡単に言うと、などのパラメータ化されたインターフェースの場合Comparable<T>、これにより追加のメソッドがコンパイラによって挿入される可能性があります。これらの追加メソッドはブリッジと呼ばれます。

消去のしくみ

型の消去は次のように定義されます。パラメーター化された型からすべての型パラメーターを削除し、型変数をその境界の消去、または境界がない場合はObjectに、またはそれがある場合は左端の境界の消去に置き換えます。複数の境界。ここではいくつかの例を示します。

  • 消去List<Integer>List<String>List<List<String>>ありますList
  • の消去はList<Integer>[]ですList[]
  • の消去Listはそれ自体であり、生のタイプと同様です。
  • intの消去自体は、他のプリミティブ型と同様です。
  • の消去Integerはそれ自体であり、型パラメーターのないすべての型について同様です。
  • 消去Tの定義ではasListありませんObjectので、T 何も結合しました。
  • T定義での消去maxComparableT にバインドされてComparable<? super T>いるためです。
  • T最後の定義での消去はmaxですObject。なぜなら、 TObjectComparable<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つのメソッド呼び出しと他のメソッド呼び出しを区別することができないため、両方のメソッドに同じ名前を付けて、オーバーロードによってそれらを区別しようとすることはできません。

うまくいけば、読者はお楽しみいただけます :)

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