1. ホーム
  2. Java

Javaジェネリックの深い理解

2022-02-19 22:59:35
<パス <ブロッククオート

まず、質問です。
Javaジェネリックの役割とは何ですか?ジェネリック消去とは何ですか?ジェネリックは一般的にどのようなシナリオで使用されますか?

もし、この質問に答えられないのであれば、この記事はあなたにとって何らかの価値があるかもしれません。

この記事を読んだ後、あなたは以下のことを学びます。

ジェネリックとは

ジェネリックは、Java SE 1.5 の新機能で、Java Core Technologies で次のように定義されています。
>

Generic"は、書かれたコードが異なるタイプのオブジェクトで再利用できることを意味します。

ジェネリックが導入されたのは、より再利用性の高いコードを書くためだということがわかりますね。

ジェネリックタイプの本質は パラメータ化された型 というのは、操作されるデータの種類がパラメータとして指定されていることを意味します。
例えば、一般的なコレクションクラスであるLinkedList。

public class LinkedList<E> extends AbstractSequentialList<E> implements
    List<E>, Deque<E>, Queue<E>, Cloneable, Serializable {
//...

transient Link<E> voidLink;

//...
}


ご覧の通り LinkedList<E> クラス名とそれを実装したインターフェイス名の後に特殊な部分 "" があり、そのメンバの型は Link<E> これは、実行時にLinkedListを作成する際に、new LinkedListのような異なる型を渡すことで、そのメンバーもStringとして格納できるようにするための型パラメータである。

ジェネリックを導入する理由

ジェネリックを導入する前に、異なる型を扱えるジェネリックメソッドを実装するためには、以下のように、プロパティやメソッドのパラメータとしてObjectを使用する必要があります。

public class Generic {
    private Object[] mData;

    public Generic(int capacity) {
        mData = new Object[capacity];
    }

    public Object getData(int index) {
        //...
        return mData[index];
    }

    public void add(int index, Object item) {
        //...
        mData[index] = item;
    }
}


を使用しています。 Object の配列でデータを保持し、使用するたびに異なるタイプのオブジェクトを追加できるようにします。

    Generic generic = new Generic(10);
    generic.add(0,"shixin");
    generic.add(1, 23);


しかし Object はすべてのクラスの親なので、すべてのクラスを上記のクラスのメンバとして追加することができます。必要なときには強制変換を行わなければならず、この強制変換は変換例外が発生する可能性が高いです:。

    String item1 = (String) generic.getData(0);
    String item2 = (String) generic.getData(1);


上記のコードの2行目は、IntegerをStringに強制変換して、エラーで実行します。

ClassCastException: java.lang.Integer cannot be cast to java.lang.String

at net.sxkeji.shixinandroiddemo2.test.generic.GenericTest.getData(GenericTest.java:46)


このように、Objectを使って汎用的な異なる種類の処理を実装することは、この2つの欠点があります。

  1. 使うたびに目的の型に強制的に変換する必要がある
  2. コンパイラはコンパイル時に型変換がうまくいっているかどうかわからず、実行時にわかるので、安全ではない

Java Programming Ideasの記述によると、ジェネリック型が登場した動機は以下の通りです。

ジェネリックが登場した理由はいろいろありますが、中でも注目すべきはコンテナ・クラスを作るためです。

実際、JDK1.5でジェネリックが登場して以来、多くのコレクションクラスがジェネリックを利用して、Collectionのように異なる型の要素を保持するようになった。

public interface Collection<E> extends Iterable<E> {

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);
    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<? > c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<? > c);
}   


実際にジェネリックを導入する主な目的は以下の通りです。

  • 型の安全性
    • 汎化の主な目的は、Javaプログラムの型安全性を向上させることである
    • 不正なJava型によるClassCastExceptionsをコンパイル時にチェックすることができます。
    • エラーの発生が早ければ早いほどコストがかからないという原則に準拠
  • 強制的な型変換の排除
    • ジェネリックの副次的な利点は、使用時にターゲットの型を直接取得できるため、多くの強制的な型変換が不要になることです。
    • 必要なものを得ることができるため、コードが読みやすくなり、エラーの可能性も低くなる
  • 潜在的なパフォーマンス向上
    • ジェネリックの実装方法によって、ジェネリックをサポートするためにJVMやクラスファイルの変更は(ほとんど)必要ありません。
    • すべての作業はコンパイラで行われます
    • コンパイラが生成するコードは、より型安全であることを除けば、ジェネリックス(と強制型変換)を使わずに書かれたコードとほとんど同じである

ジェネリック型の使用方法

ジェネリックタイプのエッセンスは パラメータ化された型 これは、操作されるデータの種類がパラメータとして指定されていることを意味します。

型パラメータのポイントは、このコレクションに格納されるインスタンスの型をコンパイラに伝えることで、他の型が追加されたときにそれを促し、コンパイル時の型安全性を保証することである。

このパラメータ型は、クラス、インタフェース、メソッドの作成に使用することができ、それぞれジェネリッククラス、ジェネリックインタフェース、ジェネリックメソッドと呼ばれる。

/**
 * <header>
 * Description: A generic class
 * </header>
 * <p>
 * Author: shixinzhang
 */
public class GenericClass<F> {
    private F mContent;

    public GenericClass(F content){
        mContent = content;
    }

    /**
     * Generalized methods
     * @return
     */
    public F getContent() {
        return mContent;
    }

    public void setContent(F content) {
        mContent = content;
    }

    /**
     * Generic interface
     * @param <T>
     */
    public interface GenericInterface<T>{
        void doSomething(T t);
    }
}



一般化されたクラス

汎用クラスと通常のクラスの違いは、クラス名の後に型パラメーターのリストが続くことです。 <E> リストというからには、もちろん複数の型パラメータがあってもいいわけで、たとえば public class HashMap<K, V> パラメータの名前は、開発者が自由に決めることができます。

クラス名でパラメータタイプを宣言すると、内部のメンバやメソッドはこのパラメータタイプを使用できるようになり、例えば上記の GenericClass<F> はクラス名の後に型 F を宣言した汎用クラスで、そのメンバやメソッドは F を使ってメンバ型やメソッドのパラメータ/戻り値の型が F であることを示すことができます。

ジェネリック・クラスの最も一般的な使い方は、Javaのコレクション・コンテナ・クラスのように、異なるタイプのデータのためのコンテナ・クラスとして使用することです。

一般的なインターフェース

ジェネリッククラスと同様に、ジェネリックインターフェースもインターフェース名の後に型パラメータを追加します。 GenericInterface<T> インターフェイスが型を宣言すると、インターフェイスのメソッドはその型を直接使用できるようになります。

実装クラスは、ジェネリック・インターフェースを実装する場合、特定のパラメータ型を指定する必要があります。そうしないと、デフォルトの型がObjectになり、ジェネリック・インターフェースの目的を達成できません。

型を指定しない実装クラスは、デフォルトでObject型になります。

public class Generic implements GenericInterface{

    @Override
    public void doSomething(Object o) {
        //...
    }
}


型の実装を指定する。

public class Generic implements GenericInterface<String>{
    @Override
    public void doSomething(String s) {
        //...
    }
}


汎用インターフェースのより実用的な使用例は、次のようなポリシーパターンのパブリックポリシーとして使用することです。 Javaディミスティファイド。コンパレータとコンパラブルの違い で汎用インタフェースとして紹介されたComparatorは、その名の通り「比較器」です。

public interface Comparator<T> {

    public int compare(T lhs, T rhs);

    public boolean equals(Object object);
}


汎用インターフェースは基本的なルールを定義し、それをクライアントへの参照として渡すことで、実行時に異なるポリシーの実装クラスを渡すことができるようにします。

一般的なメソッド

ジェネリックメソッドとは、ジェネリック型を使用するメソッドのことで、そのメソッドが属するクラスがジェネリッククラスであれば、そのクラスが宣言するパラメータを使用すればよいので簡単ですね。

もしメソッドがジェネリッククラスでないクラスにある場合や、ジェネリッククラスで宣言されている型とは異なる型のデータを扱いたい場合は、例によって独自の型を宣言する必要があります。

/**
 * A traditional method would have unchecked ... raw type warning
 * @param s1
 * @param s2
 * @return
 */
public Set union(Set s1, Set s2){
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

/**
 * Generic method, called between method modifier and return value List of type parameters <A,V,F,E... > (can have more than one)
 * The type parameter list specifies the type range of generic parameters in parameters and return values.
 * @param s1
 * @param s2
 * @param <E>
 * @return
 */
public <E> Set<E> union2(Set<E> s1, Set<E> s2){
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}


なお、上記のコードには <E> これはクラス名の後の型パラメータのリストと同じで、このメソッドにおける型パラメータの意味、およびスコープを指定するものです。

汎用型のワイルドカード

ある特定の操作を実行できるように、特定のスコープを持つ型を渡すことが望ましい場合があり、このような場合にワイルドカード境界が使用されます。

ジェネリックスには3つのワイルドカードがあります。

<? > unrestricted wildcard
<? extends E> The extends keyword declares an upper bound on the type, indicating that the parameterized type may be the specified type or a subclass of this type
<? super E> The super keyword declares the lower bound of the type, indicating that the type parameterized may be the specified type, or a parent class of this type


次に、個々のワイルドカード文字について説明する。

無制限のワイルドカード < ? >

一般的な型を使用したいが、実際に操作する型がわからない、または心配な場合は、無制限のワイルドカード(括弧内のクエスチョンマーク、つまり、次のようなもの)を使用できます。 <? > のように、どのような型でも保持できることを示します。

ほとんどの場合、この制限は良いのですが、2つの要素の位置を入れ替えるなど、本来は正しいはずの基本的な操作ができなくなってしまいます。

private void swap(List<? > list, int i, int j){
    Object o = list.get(i);
    list.set(j, o);
}


このコードは正しいように見えますが、Javaコンパイラはコンパイルエラーとset文が不正であることを示唆しています。コンパイラは List<? > から List<Object> ちょうどいい、なぜだろう? ? そして Object は同じではないのですか?

確かに ? Object List<? > は未知の型のリストを表し、一方 List<Object> は任意の型のリストを示す。

例えば List<String> の場合、リストの要素タイプは String に新しい要素を追加したい場合。 List を追加して Object というのは、もちろん許されない。

この問題は、型パラメーターを持つジェネリックメソッドを使えば、次のように解決できる。

 private <E> void swapInternal(List<E> list, int i, int j) {
    //...
    list.set(i, list.set(j, list.get(i))));
}

private void swap(List<? > list, int i, int j){
    swapInternal(list, i, j);
}

swap

swapInternal を呼び出すことができます。 swapInternal を型パラメータで指定し /** * The restricted wildcard extends (with an upper limit), indicating that the argument type must be BookBean and its subclasses, for more flexibility * @param arg1 * @param arg2 * @param <E> * @return */ private <K extends ChildBookBean, E extends BookBean> E test2(K arg1, E arg2){ E result = arg2; arg2.compareTo(arg1); //..... return result; } Javaコンテナクラスにも同様の使い方があり、public APIがワイルドカード形式になっているので、よりシンプルですが、内部的には型引数を持つメソッドを呼び出しています。

(この例は引用元です。 http://mp.weixin.qq.com/s/te9K3alu8P8jRUUU2AkO3g )

上限のワイルドカード < ?は E> を拡張します。

型パラメータでextendsを使用すると、このジェネリックのパラメータがEまたはEのサブクラスでなければならないことを示します。これには2つの利点があります。

  • 渡された型がEまたはEのサブクラスでない場合、編集は機能しない
  • Eのメソッドをジェネリックタイプで使用することができます。そうでない場合は、Eに変換しないと使用できません

一例として


private <E> void add(List<? super E> dst, List<E> src){
    for (E e : src) {
        dst.add(e);
    }
}


ご覧のように、タイプ・パラメータのキャップが複数ある場合は、カンマで区切ってリストアップします。

下界のワイルドカード < ? super E>

型パラメータにsuperを使用することは、このジェネリックのパラメータがEまたはEの親でなければならないことを示す。

コードに従って導入する。

    private <E extends Comparable<? super E>> E max(List<? extends E> e1){
        if (e1 == null){
            return null;
        }
        // the element returned by the iterator belongs to a subtype of E
        Iterator<? extends E> iterator = e1.iterator();
        E result = iterator.next();
        while (iterator.hasNext()){
            E next = iterator.next();
            if (next.compareTo(result) > 0){
                result = next;
            }
        }
        return result;
    }


ここで、quot;greater than or equal to"は、dstがsrcよりも大きな範囲を表すことを意味し、dstを格納できるコンテナはsrcも格納できることがわかります。

ワイルドカードによる比較

上記の例からわかるように、無制限のワイルドカード < ? > は Object とやや似ており、無制限または不定の範囲シナリオを表現するために使用されます。

2つの制限付きワイルドカード形式 < ? super E> と < ? extends E> もより紛らわしいので、もう一度比較してみましょう。

どちらも、メソッド・インターフェースをより柔軟にし、より広い範囲の型を受け入れることを目的としています。

  • < ? super E> for flexible 書き込みや比較 これは、親型のコンテナにオブジェクトを書き込むことができ、親型の比較メソッドをサブクラスのオブジェクトに適用することができるようにするものです。
  • < ?はE>を拡張し、柔軟性を持たせています。 読む は、E または E の任意のサブタイプのコンテナオブジェクトを読み取るメソッドにします。

理解を深めるために、Effective Javaのフレーズを使ってみましょう。

<ブロッククオート

プロデューサーには上限が、コンシューマーには下限があるというルールで、プロデューサーやコンシューマーを表す入力パラメーターにワイルドカードを使用すると、柔軟性が最大限に高まります。

PECS: プロデューサー・エクステンド、コスチューム・スーパー

つまり、ワイルドカードを使う基本的な原理ですね。

  • パラメータ化された型がTのプロデューサを表す場合、< ? extends T>を使用します。
  • Tのコンシューマを表す場合は、< ? super T>を使用します。
  • プロダクションとコンシューマーの両方である場合、正確なパラメータ・タイプが必要なので、ワイルドカードを使用する意味はあまりありません。

ちょっとしたまとめ。

  • producer of Tは、結果がTを返すことを意味します。これは、特定の型が返される必要があり、十分に具体的であるようにキャップされなければなりません。
  • TのコンシューマはTを操作することを意味し、操作のコンテナが十分に大きいことが必要なので、コンテナはTの親、すなわちスーパーTである必要があります。

一例として

<E extends Comparable<? super E>>

上記のコードでタイプパラメータEのスコープは Object で、順を追って見ていくことができます。

  1. 比較するためには、Eは比較可能なクラスである必要があるので、extends Comparable<...> とする必要があります(継承のためのextendsと混同しないように注意してください。)
  2. Comparable< ? super E> Eを比較するためには、Eの消費者であるため、superを使用する必要がある
  3. また、パラメータ List< ? extends E> は、操作するデータが E のサブクラスのリストであることを意味し、コンテナが十分に大きくなるように上限を指定する。

汎用型に対する型消去

Java のジェネリックと C++ のテンプレートには、1 つの大きな違いがあります。

  • C++でテンプレートをインスタンス化すると、型ごとに異なるコードが生成され、これをコードの肥大化と呼びます。
  • この問題は、Javaでは発生しません。仮想マシンには一般型オブジェクトは存在せず、すべてのオブジェクトは通常のクラスです。

(より抜粋)。 http://blog.csdn.net/fw0124/article/details/42295463 )

Javaでは、ジェネリックスはJavaコンパイラの概念であり、ジェネリックスで書かれたJavaプログラムは、パラメータ化された型が多く、型変換が少ないことを除けば、基本的に通常のJavaプログラムと同じである。

実は、ジェネリックプログラムは、まず、ジェネリックでない通常のJavaプログラムに変換されて処理されます。コンパイラはジェネリックJavaから通常のJavaに自動的に変換し、Java仮想マシンは基本的にジェネリックのことを知らずに動作します。

コンパイラは、ジェネリックスを含むJavaコードをコンパイルするとき、次のように実行します。 型チェック 型推論 と呼ばれる、通常のJava仮想マシンが受信して実行できるジェネリックのない普通のバイトコードを生成します。 タイプ消去

実は、ジェネリックスを使おうが使うまいが、コレクションフレームワークでオブジェクトを保持するデータ型は List<String> strings = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); System.out.println(strings.getClass() == integers.getClass());//true これはソースコードだけでなく、リフレクションでも確認することができます。

/**
 * Both are not method overloads. They are both the same method after erasure, so compilation will not pass.
 * After erasure.
 * 
 * void m(List numbers){}
 * void m(List strings){} // Compilation does not pass, the same method signature already exists
 */
void method(List<Object> numbers) {

}

void method(List<String> strings) {

}


上記のコードの出力は予想通りfalseではなくtrueになった。この理由はgeneric型の消去である。

消去の実装方法

Javaコンパイラがコンパイル時にジェネリック情報を消去するとき、コンパイル時に追加・削除された型が消去前に宣言されたものであることをどうやって確認できるのか、いつも不思議に思っています。

より この記事 ジェネリックは単なる構文糖であることを学んだので、数段落で理解することにする。

<ブロッククオート

このキーワードは、quot;Type Erasure"つまり、私たちが学校で字や絵の間違いを消したのと同じことです。)

同じことがJavaコンパイラでも行われていて、Genericsを使って書かれたコードを見ると、そのコードを完全に消去して生の型、つまりGenericsを使わないコードに変換してしまうのです。型に関連する情報はすべて消去される。そのため、ArrayListはJDK1.5以前の古いArrayListになり、正式な型パラメータ、例えば < K, V> や < E> はオブジェクトかその型のスーパークラスで置き換えられるのです。

また、翻訳されたコードが正しい型を持っていない場合、コンパイラは型キャスト演算子を挿入します。Javaコンパイラは型安全性を保証し、コンパイル時に型安全性に関連するエラーがあればフラグを立てるので、心配する必要はない。

要するに、Javaのジェネリックスは構文上の砂糖であり、実行時に型に関連する情報を一切保存しないのです。すべての型関連情報は、Type Erasureによって消去されます。これは、Genericsなしで書かれたすべてのJavaコードを再利用するために、Generics機能を開発する際の主な要件でした。

という意味だと思われます。

Javaエディタはジェネリックコードの型を完全に消し、プリミティブにします。

もちろん、この時点ではまだ欲しい型とは程遠いコードなので、Javaコンパイラはそのコードに型変換を加えて生の型を目的の型に変換するのです。これらの操作はコンパイラがバックグラウンドで行うので、型安全です。

つまり、汎用型は、実行時に型情報を保存しない構文上の糖分である。

消去による一般的な不変性

ジェネリック型には論理的な親子関係はありません。消去後はどちらもListになるため、以下のような形のコードはコンパイラからエラーとして報告されるでしょう。

public class GenericErasure {
    interface Game {
        void play();
    }
    interface Program{
        void code();
    }

    public static class People<T extends Program & Game>{
        private T mPeople;

        public People(T people){
            mPeople = people;
        }

        public void habit(){
            mPeople.code();
            mPeople.play();
        }
    }
}


このようなジェネリックの場合を不変性と呼び、対応する概念として共分散、反転がある。

  • 共変量。AがBの親で、Aのコンテナ(例えばList< A>)がBのコンテナ(List< B>)の親でもある場合、covariant(親子関係が一貫している)と呼ばれます。
  • 反転:AがBの親であっても、AのコンテナがBのコンテナのサブクラスである場合、反転と呼ぶ(コンテナの配置が位置を簒奪する)
  • immutable: AとBの関係に関わらず、AのコンテナとBのコンテナは親子関係を持たず、immutableと呼ばれる

Javaでは配列は共変であり、ジェネリックはイミュータブルである。

ジェネリッククラスを共変させたい場合は、boundsを使用する必要があります。

消去の救世主:バウンズ

汎用型は実行時に元の型に消去されるため、多くの操作が不可能になることが分かっています。

boundsが指定されていない場合、typeパラメータはObjectに消去されます。

パラメータに境界を保持させたい場合、パラメータに境界を設定すると、汎用パラメータはその最初の境界(複数の境界が可能)まで消去されるので、実行時の消去後でもスコープが確保されます。

例えば

private <E> void swap(List<E> list, int i, int j) {
    //...
}


上記のコードでは、Peopleの型パラメータTは2つの境界を持ち、コンパイラは実際に型パラメータをその最初の境界の型に置き換えます。

一般化のためのルール

  • 汎用型のパラメータ型は、単純型ではなく、クラス(カスタムクラスを含む)のみとすることができます。
  • 同じジェネリックタイプは複数のバージョンに対応でき(パラメータタイプが不定なため)、ジェネリッククラスインスタンスの異なるバージョンは非互換です。
  • ジェネリックタイプのタイプパラメータは、複数のパラメータを持つことができます。
  • 汎用型のパラメータ型はextends文を使用することができ、従来はquot;bounded type"と呼ばれていました。
  • ジェネリック型のパラメータ型は、Classのようなワイルドカード型にすることもできます。

ジェネリックの利用シーン

クラス内で操作する参照データ型が不明な場合、従来はObjectで拡張を完結させていた。JDK1.5以降では、セキュリティを確保しつつ拡張を完結させるためにジェネリックを使用することが推奨されています。

概要

1. 再利用を実現するためにObjectを使うと、汎用型の安全性や直感的な表現力が失われることは前述しましたが、それでもArrayListなどのソースコードでObjectを型として使っているのはなぜでしょうか。

ここには、Effective Javaによると、"portability compatibility"が関係しており、以下のように記載されています。

ジェネリックが登場したとき、Javaプラットフォームは第2の10年に入ろうとしていましたが、それ以前にすでにジェネリックを使用しないJavaコードが大量に存在していました。このコードすべてを合法的に維持し、ジェネリックスを使用する新しいコードと相互運用できるようにすることが重要だと考えられていたのです。

これは互換性の問題であり、新しいコードでプリミティブ型の代わりにジェネリックスを使用することです。

2. ジェネリックは消去法で実装されています。つまり、汎用型はコンパイル時にのみ型情報を強化し、実行時にその要素の型情報を破棄(消去)します。消去により、ジェネリックを使用するコードは、ジェネリックを使用しないコードと自由に相互運用することができる。

3. 型パラメータがメソッド宣言に一度だけ現れる場合、ワイルドカードに置き換えることができます。

例えば、以下のswapメソッドは、与えられたリスト内の2つの位置の要素を入れ替えるために使用されます。

private void swap(List<? > list, int i, int j){
    //...
}


一度だけ表示される type パラメータは宣言する必要がなく、ワイルドカードで完全に置き換えることができます:。

List<Object>

それと対照的に、2番目のものはよりシンプルで明確ですよね?

4. 配列でジェネリックを使うことはできない

これはおそらくJavaの汎化に関するインタビューの質問の中で最も簡単なものですが、Arrayが実際には汎化をサポートしていないことを知っている場合のみです。これが、Joshua BlochがEffective JavaでArrayではなくListを使うことを推奨する理由です。

5. Javaで List と元の型である List とはどのような違いがあるのでしょうか?

プリミティブ型とパラメータ付き型の主な違いは、次のとおりです。

  • コンパイラは、コンパイル時に元の型のタイプセーフを行わないが、パラメータ付きの型はチェックする
  • 型として Object を使用することで、そのメソッドが String や Integer などの任意の型の Object を受け入れることができることをコンパイラに伝えることができます。
  • 元の型Listに引数で任意の型を渡すことはできますが、List< Object>を受け付けるメソッドには、汎用型の不変性からList< String>を渡せず、コンパイルエラーが発生します。

ジェネリックスにおけるプリミティブ型の正しい理解についての質問です。

ありがとうございます

https://docs.oracle.com/javase/tutorial/java/generics/index.html

http://www.journaldev.com/1663/java-generics-example-method-class-interface#java-generics-bounded-type-parameters

http://blog.csdn.net/crave_shy/article/details/50822354

http://mp.weixin.qq.com/s/te9K3alu8P8jRUUU2AkO3g

http://mp.weixin.qq.com/s/TNxaOK2hBDzRtlNUlGxHcQ

http://www.infoq.com/cn/articles/cf-java-generics

上記の2つのインタビュー質問は http://javarevisited.blogspot.com/2012/06/10-interview-questions-on-java-generics.html

http://www.jianshu.com/p/4caf2567f91d