ジェネリクスは、2004年にJavaプログラミング言語のバージョンJ2SE 5.0で追加された汎用プログラミング機能です。これらは、Javaの型システムを拡張して、「コンパイル時の型安全性を確保しながら、さまざまな型のオブジェクトに対して型またはメソッドが操作できるようにする」ことを目的として設計されました。[ 1 ]コンパイル時の型安全性という側面では、型安全性が不可能なため、パラメトリック多相関数をJava仮想マシンに実装しないことが求められました。[ 2 ] [ 3 ]
Javaのコレクションフレームワークは、コレクションインスタンスに格納されるオブジェクトの型を指定するためのジェネリクスをサポートしています。
1998年、Gilad Bracha、Martin Odersky、David Stoutamire、Philip Wadlerは、ジェネリック型をサポートするJava言語の拡張機能であるGeneric Javaを作成しました。[ 4 ] Generic Javaは、ワイルドカードの追加によりJavaに組み込まれました。
Java言語仕様によると:[ 5 ]
java.util.PrimitiveIterator<T, T_CONS>次の Java コードブロックは、ジェネリクスを使用しない場合に発生する問題を示しています。まず、ArrayList型のを宣言しますObject。次に、java.lang.Stringにを追加しますjava.util.ArrayList。最後に、追加されたを取得してにjava.lang.Stringキャストしようとしますが、文字列インスタンスを整数java.lang.Integerにキャストすることは不可能なので、論理エラーになります。
import java.util.ArrayList ; import java.util.List ;List v = new ArrayList (); v.add ( " test" ); // 整数にキャストできない文字列Integer i = ( Integer ) v.get ( 0 ) ; //実行時エラーこれは、ジェネリクスを持たないC言語に似ており、コレクションのようなデータ構造は通常、データをvoid*( void ポインタ) として格納し、そこからオブジェクトを明示的にキャストする必要があります。
java.lang.ClassCastExceptionコードはエラーなくコンパイルされますが、 3 行目のコードを実行すると実行時例外 ( ) が発生します。この種の論理エラーは、ジェネリクス[ 8 ]を使用することでコンパイル時に検出でき、これがジェネリクスを使用する主な動機となっています。[ 7 ]パラメータとして機能する 1 つ以上の型変数を定義します。
上記のコード断片は、ジェネリクスを使用して次のように書き換えることができます。
import java.util.ArrayList ; import java.util.List ;List <String> v = new ArrayList <String> ( ); v.add ( " test" ) ; Integer i = ( Integer ) v.get ( 0 ) ; // (型エラー)コンパイル時エラーjava.lang.String山括弧内の型パラメータは、が(のジェネリック構成要素の子孫)java.util.ArrayListで構成されていることを宣言します。ジェネリクスを使用すると、3行目を特定の型にキャストする必要がなくなります。なぜなら、の結果はコンパイラによって生成されたコードによってとして定義されるからです。java.lang.Stringjava.util.ArrayListjava.lang.Objectv.get(0)java.lang.String
この断片の 3 行目の論理的な欠陥は、コンパイラがの代わりにを返すことを検出するため、コンパイル時エラーとして検出されます (J2SE 5.0 以降の場合) 。[ 8 ]より詳細な例については、参照を参照してください。[ 10 ]v.get(0)java.lang.Stringjava.lang.Integer
パッケージ内のjava.util.Listインターフェースの定義から抜粋した部分を以下に示します。java.util.Iteratorjava.util
interface List < E > { void add ( E x ); Iterator < E > iterator (); }interface Iterator < E > { E next (); boolean hasNext (); }以下は、マップ内の個々のエントリ(キーと値のマッピング)を表すために使用できる汎用的なJavaクラスの例です。
public class Entry < K , V > { private K key ; private V value ;public Entry ( K key , V value ) { this . key = key ; this . value = value ; }public K getKey () { return key ; }public V getValue () { return value ; }public String toString () { return String.format ( " (%s, % s)" , key , value ); } }この汎用クラスは、例えば次のような方法で使用できます。
Entry < String , String > grade = new Entry < String , String > ( "Mike" , "A" ); Entry < String , Integer > mark = new Entry < String , Integer > ( "Mike" , 100 ); System . out . printf ( "grade: %s%n" , grade ); System . out . printf ( "mark: %s%n" , mark );Entry < Integer , Boolean > prime = new Entry <> ( 13 , true ); if ( prime . getValue ()) { System . out . println ( "%s は素数です。%n" , prime . getKey ()); } else { System . out . println ( "%s は素数ではありません。%n" , prime . getKey ()); }出力結果:
成績:(マイク、A) マーク:(マイク、100) 13は素数です。
以下は、上記のジェネリッククラスを使用したジェネリック型に対するジェネリックメソッドの例ですT。
public static < T > Entry < T , T > twice ( T value ) { return new Entry < T , T > ( value , value ); }多くの場合、メソッドのユーザーは型パラメータを指定する必要はありません。型パラメータは推論によって決定されるからです。
Entry < String , String > pair = Entry . twice ( "Hello" );必要に応じて、パラメータを明示的に追加できます。
Entry < String , String > pair = Entry . < String > twice ( "Hello" );プリミティブ型の使用は許可されておらず、代わりにボックス化されたバージョンを使用する必要があります。ジェネリック型パラメータには参照型のみを格納できます。たとえば、はコンパイルに失敗し、代わりにプリミティブのラッパークラス(など)を使用する必要があります。Entry<int,int>pair;Entry<Integer,Integer>pair;
与えられたパラメータに基づいて汎用的なメソッドを作成することも可能です。
public < T > T [] toArray ( T ... elements ) { return elements ; }このような場合、プリミティブ型も使用できません。例:
Integer [] array = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );プリミティブ型は格納できませんが、プリミティブ型の配列(プリミティブ型自体ではなくオブジェクトであるため)は格納できます。
import java.util.ArrayList ; import java.util.List ;// List<int[]> と List<Integer[]> はどちらも同じように使用できますList < int []> listOfArrays = new ArrayList <> (); List < Integer []> myOtherList = listOfArrays ;型推論を使用することで、Java SE 7 以降では、プログラマーは空の山括弧 ( <>、"ダイヤモンド演算子" と呼ばれる) を、十分近いコンテキストが示唆する1 つ以上の型パラメータを含む山括弧のペアに置き換えることができます。[ 11 ]はしばしば<>「ダイヤモンド演算子」と呼ばれますが、演算子ではなく、単なる空の型パラメータ リストです。
したがって、上記のコード例はEntry次のように書き換えることができます。
Entry < String , String > grade = new Entry <> ( "Mike" , "A" ); Entry < String , Integer > mark = new Entry <> ( "Mike" , 100 );System.out.printf ( " grade : % s %n" , grade); System.out.printf ( " mark : % s % n " , mark ) ;Entry < Integer , Boolean > prime = new Entry <> ( 13 , true ); if ( prime . getValue ()) { System . out . println ( "%s は素数です。%n" , prime . getKey ()); } else { System . out . println ( "%s は素数ではありません。%n" , prime . getKey ()); }いわゆるダイヤモンド演算子は、C#やKotlinといったJava派生言語には存在しません。これらの言語は、冗長な型情報を回避する独自の方法を持っています。C ++には視覚的に類似したものが存在しますが、それは型パラメータの推論を示すものではなく、単にデフォルトの型パラメータを参照するものです。
template < typename T = int > class Box { // ... };Box <> v ; // Box<int> を参照しますパラメータ化された型の型引数は、具体的なクラスやインターフェースに限定されません。Javaでは、パラメータ化された型の型引数として「型ワイルドカード」を使用できます。ワイルドカードは「<?>」の形式の型引数で、オプションで上限または下限を指定できます。ワイルドカードが表す正確な型は不明であるため、パラメータ化された型を使用するオブジェクトで呼び出せるメソッドの型には制限があります。
java.util.Collection<E>以下は、要素aの要素型がワイルドカードによってパラメータ化されている例です。
import java.util.ArrayList ; import java.util.Collection ;Collection <?> c = new ArrayList < String > (); c . add ( new Object ()); // コンパイル時エラーc . add ( null ); // 許可の要素型が何を表しているのかわからないためc、オブジェクトを追加することはできません。 メソッドは、汎用インターフェースの要素型でadd()ある型の引数を取ります。実際の型引数が の場合、それは未知の型を表します。 メソッドに渡すメソッド引数の値は、この未知の型のサブタイプである必要があります。それがどのような型なのかわからないため、何も渡すことはできません。唯一の例外はnullです。これはすべての型のメンバーです。[ 12 ]ECollection<E>?add()
型ワイルドカードの上限を指定するには、キーワードを使用して、型引数が境界クラスのサブタイプであることを示します。[ 13 ]つまり、指定されたリストには、クラスを拡張する未知の型のオブジェクトが含まれていることを意味します。たとえば、リストはまたは である可能性があります。リストから要素を読み取ると、 が返されます。null 要素を追加することも、同様に許可されています。[ 14 ]extendsList<?extendsNumber>NumberList<Float>List<Number>Number
上記のワイルドカードの使用により柔軟性が向上します[ 13 ]。具体的な型を型引数とする2つのパラメータ化された型の間には継承関係がないためです。もも他方のサブタイプではありList<Number>ません。はのサブタイプですが。[ 13 ]したがって、をパラメータとして受け取るメソッドは、の引数を受け入れません。もし受け入れると、ではないを挿入することが可能になり、型安全性が損なわれます。がのサブタイプである場合に型安全性がどのように損なわれるかを示す例を以下に示します。List<Integer>java.lang.Integerjava.lang.NumberList<Number>List<Integer>java.lang.Numberjava.lang.IntegerList<Integer>List<Number>
import java.util.ArrayList ; import java.util.List ;List < Integer > intList = new ArrayList <> (); intList . add ( 2 ); List < Number > nums = intList ; // 置換規則に従って List<Integer> が List<Number> のサブタイプである場合は有効です。nums . add ( 3.14 ); Integer x = intList . get ( 1 ); // これで 3.14 が Integer 変数に代入されました!ワイルドカードを使った解決策が有効なのは、型安全性を損なうような操作を禁止するからである。
import java.util.List ;List <? extends Number > nums = ints ; // OK nums.add ( 3.14 ) ; // コンパイルエラーnums.add ( null ) ; //許可型ワイルドカードの下限クラスを指定するには、superキーワードを使用します。このキーワードは、型引数が境界クラスのスーパークラスであることを示します。したがって、はまたはを表すことができます。として定義されたリストから読み取ると、型の要素が返されます。このようなリストにを追加するには、型の要素、の任意のサブタイプ、またはnull (すべての型のメンバー) のいずれかが必要です。List<?superNumber>List<Number>List<Object>List<?superNumber>ObjectNumberNumber
Joshua Bloch著の『Effective Java 』に掲載されているPECS(Producer Extends, Consumer Super)というニーモニックは、Javaでワイルドカード(共変性と反変性に対応)をいつ使用するかを覚える簡単な方法を示しています。[ 13 ]
例外自体は汎用的なものにはなり得ませんが、throws句には型パラメータを含めることができます。
public < T extends Throwable > void throwMeConditional ( boolean conditional , T exception ) throws T { if ( conditional ) { throw exception ; } }この場合、型パラメータが によって下限が定められているためjava.lang.Throwable、 が句Tに現れることが保証されますthrows。
ジェネリクスはコンパイル時に型の正しさがチェックされます。[ 8 ]ジェネリクスの型情報は、型消去と呼ばれるプロセスで削除されます。[ 7 ]例えば、は非ジェネリクス型に変換されます。この型は通常、任意のオブジェクトを含みます。コンパイル時のチェックにより、結果として生成されるコードが正しい型を使用することが保証されます。[ 8 ]List<Integer>List
型消去のため、実行時に型パラメータを決定することはできません。[ 7 ]例えば、実行時に を調べても、型消去前に であったか でArrayListあったかを判断する一般的な方法はありません。多くの人がこの制限に不満を持っています。[ 15 ]部分的なアプローチがあります。たとえば、個々の要素を調べて、それが属する型を決定できます。たとえば、 に が含まれている場合、 はでパラメータ化されている可能性があります(ただし、や などの の任意の親でパラメータ化されている可能性もあります)。ArrayList<Integer>ArrayList<Float>java.util.ArrayList<E>java.lang.Integerjava.util.ArrayList<E>Integerjava.lang.Integerjava.lang.Numberjava.lang.Object
この点を実証するために、次のコードは「Equal」を出力します。
import java.util.ArrayList ; import java.util.List ;List <Integer> li = new ArrayList <> ( ) ; List <Float> lf = new ArrayList < > ( ); if ( li.getClass ( ) == lf.getClass ( ) ) { // true と評価されるSystem.out.println ( " Equal " ) ; }型消去のもう1つの効果は、ジェネリッククラスがjava.lang.Throwable直接的にも間接的にもクラスを拡張できないことです。[ 16 ]
// コンパイルエラーが発生しますpublic class GenericException < T > extends Exception { // ... }これがサポートされていない理由は、型消去によるものです。
try { throw new GenericException <Integer> ( ) ; } catch ( GenericException <Integer> e ) { System.err.println ( " Integer " ) ; } catch ( GenericException <String> e ) { System.err.println ( " String " ) ; }型消去のため、ランタイムはどの catch ブロックを実行すればよいか分からないため、コンパイラによってこれは禁止されます。
JavaのジェネリクスはC++のテンプレートとは異なります。Javaのジェネリクスは、使用されるパラメータ型の数に関係なく、ジェネリッククラスまたは関数のコンパイル済みバージョンを1つだけ生成します。さらに、Java実行環境は、どのパラメータ型が使用されているかを知る必要はありません。なぜなら、型情報はコンパイル時に検証され、コンパイル済みコードには含まれないからです。したがって、パラメータ型のJavaクラスをインスタンス化することは不可能です。インスタンス化にはコンストラクタの呼び出しが必要ですが、型が不明な場合はコンストラクタが利用できないためです。
例えば、以下のコードはコンパイルできません。
import java.util.List ;<T> T instantiateElementType ( List <T> arg ) { return new T ( ) ; //コンパイルエラーが発生する}実行時にはジェネリッククラスごとにコピーが1つしか存在しないため、静的変数は型パラメータに関係なく、クラスのすべてのインスタンス間で共有されます。したがって、静的変数の宣言や静的メソッドでは型パラメータを使用することはできません。
Java SE5以前に書かれたプログラムとの後方互換性を維持するために、Javaで型消去が実装されました。 [ 8 ]
Java の配列 (プリミティブ配列と配列の両方) とジェネリクスには、いくつかの重要な違いがありますjava.lang.Object。主な違いの 2 つは、バリアンスと具体化の観点からの違いです。
ジェネリクスは不変ですが、配列は共変です。[ 7 ]これは、配列などの非ジェネリックオブジェクトと比較した場合のジェネリックを使用する利点です。[ 7 ]具体的には、ジェネリクスはコンパイル時例外をスローして開発者にコードを修正させることで、実行時例外を防ぐのに役立ちます。
例えば、開発者がjava.lang.Object[]オブジェクトを宣言し、そのオブジェクトを新しいjava.lang.Long[]オブジェクトとしてインスタンス化しても、コンパイル時の例外はスローされません(配列は共変であるため)。[ 7 ]java.lang.Stringこれにより、コードが正しく記述されているという誤った印象を与える可能性があります。しかし、開発者がこのオブジェクトにを追加しようとするとjava.lang.Long[]、プログラムは をスローしますjava.lang.ArrayStoreException。[ 7 ]開発者がジェネリクスを使用すれば、この実行時例外を完全に回避できます。
開発者がCollection<Object>オブジェクトを宣言し、戻り値の型を持つこのオブジェクトの新しいインスタンスを作成するとArrayList<Long>、Java コンパイラは(正しく) コンパイル時例外をスローして、互換性のない型が存在することを示します (ジェネリクスは不変であるため)。[ 7 ]したがって、これにより実行時例外の可能性を回避できます。この問題は、代わりにオブジェクトをCollection<Object>使用してインスタンスを作成することで解決できますArrayList<Object>。Java SE7 以降のバージョンを使用するコードでは、ダイヤモンド演算子を使用してオブジェクトCollection<Object>でインスタンス化できます。ArrayList<>
配列は具体化されており、配列オブジェクトは実行時にその型情報を強制しますが、Java のジェネリクスは具体化されていません。[ 7 ]これは、配列が JVM 内の実際のコンパイラ生成クラスであるためです。各型に対してT、型が存在しますT[]。
より厳密に言えば、Java のジェネリック型を持つオブジェクトは非再化可能型です。[ 7 ]非再化可能型とは、実行時の表現がコンパイル時の表現よりも情報が少ない型のことです。[ 7 ]
Java のジェネリック型を持つオブジェクトは、型消去のため再化できません。[ 7 ] Java はコンパイル時にのみ型情報を強制します。コンパイル時に型情報が検証された後、型情報は破棄され、実行時には型情報は利用できなくなります。[ 7 ]
非再化可能な型の例としてはList<T>、 および がありList<String>、Tは一般的な形式パラメータである。[ 7 ]
プロジェクト Valhallaは、Java 10 以降の将来のバージョンに向けて、Java のジェネリクスと言語機能の改善を育成するための実験的なプロジェクトです。潜在的な機能強化には以下が含まれます。[ 17 ]
List<int>例えば、List<Integer>唯一の例外は null で、これはすべての型のメンバーです...