web-dev-qa-db-ja.com

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

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

136
Tushu

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

ジェネリック型またはメソッドに対してコードをコンパイルすると、コンパイラは実際の意味(つまり、Tの型引数が何であるか)を計算し、compileあなたが正しいことをしているが、放出されたコードは_Java.lang.Object_の観点から再び話します-コンパイラは必要に応じて追加のキャストを生成します。実行時には、_List<String>_と_List<Date>_はまったく同じです。余分な型情報は、コンパイラによって消去されています。

これを、たとえば、実行時に情報が保持されるC#と比較して、コードが_T.class_と同等のtypeof(T)などの式を含むことを許可します。ただし、後者は無効です。 (.NETジェネリックとJavaジェネリック、さらに注意してください。)型消去は、Javaジェネリック。

その他のリソース:

192
Jon Skeet

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

javac -XD-printflat -d output_dir SomeFile.Java

-printflatは、ファイルを生成するコンパイラーに渡されるフラグです。 (-XDパートは、javacjavacではなく、実際にコンパイルを実行する実行可能jarに渡すように指示するものですが、私は脱線します...)-d output_dirは、コンパイラが新しい.Javaファイルを置くための場所を必要とするために必要です。

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

40
jigawot

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

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 decompilerで逆コンパイルすると、次のような結果が得られます。逆コンパイルされたコードには、元のソースコード

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

    }
} 
28
Parag

すでに非常に完全なJon Skeetの答えを完了するには、type erasureの概念がJavaの以前のバージョンとの互換性

EclipseCon 2007で最初に発表された(使用できなくなりました)互換性には、次の点が含まれていました。

  • ソースの互換性(必要なもの...)
  • バイナリ互換性(持っている必要があります!)
  • 移行の互換性
    • 既存のプログラムは引き続き機能する必要があります
    • 既存のライブラリはジェネリック型を使用できる必要があります
    • 持つ必要があります!

元の回答:

したがって:

new ArrayList<String>() => new ArrayList()

より大きな具体化の命題があります。 「抽象概念を本物と見なす」ことを具体化する。ここで、言語の構成要素は単なる構文糖ではなく概念でなければならない。

また、指定されたコレクションの動的に型保証されたビューを返すJava 6の checkCollection メソッドに言及する必要があります。間違ったタイプの要素を挿入しようとすると、すぐにClassCastExceptionになります。

言語のジェネリックメカニズムはコンパイル時(静的)の型チェックを提供しますが、未チェックのキャストでこのメカニズムを無効にすることができます。

通常、これは問題ではありません。コンパイラは、このようなすべての未チェックの操作に対して警告を発行するからです。

ただし、次のような静的型チェックだけでは不十分な場合があります。

  • コレクションがサードパーティのライブラリに渡され、ライブラリコードが間違ったタイプの要素を挿入することによってコレクションを破損しないことが不可欠です。
  • プログラムはClassCastExceptionで失敗し、誤って入力された要素がパラメーター化されたコレクションに配置されたことを示します。残念ながら、エラー要素が挿入された後はいつでも例外が発生する可能性があるため、通常、問題の実際の原因に関する情報はほとんどまたはまったく提供されません。

ほぼ4年後の2012年7月の更新:

API Migration Compatibility Rules(Signature Test)) で詳細になりました(2012)

Javaプログラミング言語は消去を使用してジェネリックを実装します。これにより、型に関するいくつかの補助情報を除き、レガシーバージョンとジェネリックバージョンは通常同一のクラスファイルを生成します。クライアントコードを変更または再コンパイルせずに、汎用クラスファイルを含むレガシークラスファイル。

非ジェネリックレガシーコードとのインターフェイスを容易にするために、パラメーター化された型の消去を型として使用することもできます。このようなタイプは、rawタイプJava Language Specification 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();
}
25
VonC

すでに補完されたJon Skeetの答えを補完する...

消去を介してジェネリックを実装すると、いくつかの迷惑な制限が生じることが言及されています(例:no new T[42])。また、このように物事を行う主な理由は、バイトコードの後方互換性であると言われています。これも(ほとんど)真実です。生成されるバイトコード-target 1.5は、単に脱糖したキャスト-target 1.4とは多少異なります。技術的には、(巨大な策略を通じて)ジェネリック型のインスタンス化実行時にアクセスすることさえ可能であり、バイトコードに実際に何かがあることを証明します。

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

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

8
Daniel Spiewak

私が理解しているように( 。NET guy) [〜#〜] jvm [〜#〜] にはジェネリックの概念がないため、コンパイラは型パラメータをオブジェクトとすべてのキャストを実行します。

これは、Javaジェネリックは構文シュガーに過ぎず、参照渡しでボックス化/ボックス化解除を必要とする値型のパフォーマンス改善を提供しないことを意味します。

5
Andrew Kennan

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

元のクラス、

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
snr