CodeGym /Java Blog /ランダム /Java ジェネリック: 実際に山括弧を使用する方法
John Squirrels
レベル 41
San Francisco

Java ジェネリック: 実際に山括弧を使用する方法

ランダム グループに公開済み

序章

JSE 5.0 以降、ジェネリックスが Java 言語の武器庫に追加されました。

Javaのジェネリックとは何ですか?

ジェネリックは、ジェネリック プログラミングを実装するための Java の特別なメカニズムです。これは、アルゴリズムの記述を変更せずに、さまざまなデータ型を操作できるようにする、データとアルゴリズムを記述する方法です。Oracle Web サイトには、ジェネリック専用の別のチュートリアル「レッスン」があります。ジェネリックを理解するには、まずジェネリックがなぜ必要なのか、そしてジェネリックが何をもたらすのかを理解する必要があります。チュートリアルの「なぜジェネリックスを使用するのか?」セクションでは、コンパイル時の型チェックを強化することと、明示的なキャストの必要性をなくすことが目的であると述べています。私たちのお気に入りのTutorialspointJava のジェネリックス: 実際に山括弧を使用する方法 - 1オンライン Java コンパイラーでいくつかのテストの準備をしましょう。次のコードがあるとします。

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
このコードは完全にうまく動作します。しかし、上司が私たちのところに来て、「こんにちは、世界!」と言ったらどうなるでしょうか。は使いすぎたフレーズで、「Hello」だけを返さなければならないのですか? 「,world!」を連結したコードを削除します。これは十分に無害だと思われますよね? しかし、実際にはコンパイル時にエラーが発生します。

error: incompatible types: Object cannot be converted to String
問題は、リストにオブジェクトが格納されていることです。StringはObjectの子孫です(すべての Java クラスが暗黙的にObjectを継承するため)。つまり、明示的なキャストが必要ですが、追加しませんでした。連結操作中に、オブジェクトを使用して静的なString.valueOf(obj)メソッドが呼び出されます。最終的には、 ObjectクラスのtoStringメソッドを呼び出します。言い換えれば、ListにはObject が含まれています。これは、特定の型 ( Objectではない) が必要な場合は常に、型変換を自分で行う必要があることを意味します。

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
ただし、この場合、List はオブジェクトを取るため、 StringだけでなくIntegerも格納できます。しかし、最悪のことは、コンパイラがここで何も問題を認識しないことです。そして、実行時にエラー (「実行時エラー」と呼ばれます) が発生します。エラーは次のようになります:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
これはあまり良くないことに同意する必要があります。これはすべて、コンパイラーがプログラマーの意図を常に正確に推測できる人工知能ではないためです。Java SE 5 ではジェネリックスが導入され、コンパイラーに意図、つまりどの型を使用するかを伝えることができるようになりました。コンパイラーに必要なものを伝えてコードを修正します。

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
ご覧のとおり、Stringへのキャストは必要なくなりました。さらに、型引数を山括弧で囲んでいます。これで、これはIntegerであるため、リストに 123 を追加する行を削除するまで、コンパイラーはクラスをコンパイルできなくなります。そしてそれは私たちにそう告げてくれるでしょう。多くの人はジェネリックを「構文糖衣」と呼んでいます。そして、ジェネリックがコンパイルされると、実際には同じ型変換になるため、それらは正しいです。コンパイルされたクラスのバイトコードを見てみましょう。1 つは明示的なキャストを使用し、もう 1 つはジェネリックを使用します。 Java のジェネリックス: 実際に山括弧を使用する方法 - 2コンパイル後、すべてのジェネリックは消去されます。これを「型消去」といいます。型消去とジェネリックは、古いバージョンの JDK と下位互換性があるように設計されていると同時に、コンパイラーが新しいバージョンの Java での型定義を支援できるようにします。

生のタイプ

ジェネリックスについて言えば、パラメーター化された型と生の型という 2 つのカテゴリが常にあります。生の型は、山括弧で囲まれた「型の明確化」を省略した型です。 Java のジェネリックス: 実際に山括弧を使用する方法 - 3一方、パラメーター化された型には、「明確化」が含まれています。ご覧のとおり、 Java のジェネリックス: 実際に山括弧を使用する方法 - 4スクリーンショットで矢印でマークされている、珍しい構成を使用しました。これは、Java SE 7 に追加された特別な構文です。「ダイヤモンド」と呼ばれます。なぜ?山括弧はひし形を形成します: <>。ダイヤモンド構文は「型推論」の概念に関連付けられていることも知っておく必要があります。結局のところ、コンパイラは<>を参照します。右側では、代入演算子の左側を調べて、値が割り当てられている変数の型を見つけます。この部分で見つかった内容に基づいて、右側の値のタイプを理解します。実際、ジェネリック型が左側に指定されていて右側に指定されていない場合、コンパイラーは型を推論できます。

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
ただし、これでは、ジェネリックを含む新しいスタイルとジェネリックを含まない古いスタイルが混在します。そして、これは非常に望ましくないことです。上記のコードをコンパイルすると、次のメッセージが表示されます。

Note: HelloWorld.java uses unchecked or unsafe operations
実際、ここにダイヤモンドを追加する必要がある理由は理解できないようです。しかし、ここに例があります:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
ArrayList にはコレクションを引数として受け取る 2 番目のコンストラクターがあること を思い出してください。そして、ここには何か邪悪な何かが隠されているのです。ダイヤモンド構文がないと、コンパイラーは騙されていることを認識できません。ダイヤモンド構文ではそれが可能です。したがって、ルール 1 は、パラメーター化された型では常にダイヤモンド構文を使用することです。そうしないと、生の型を使用している場所が見つからなくなる危険があります。「未チェックまたは安全でない操作を使用しています」という警告を排除するには、メソッドまたはクラスで@SuppressWarnings("unchecked")アノテーションを使用できます。しかし、なぜそれを使用しようと思ったのか考えてみましょう。ルールその 1 を思い出してください。場合によっては、型引数を追加する必要があるかもしれません。

Javaの汎用メソッド

ジェネリックを使用すると、パラメーターの型と戻り値の型がパラメーター化されたメソッドを作成できます。Oracle チュートリアルの別のセクション「ジェネリック メソッド」でこの機能について説明します。このチュートリアルで説明する構文を覚えておくことが重要です。
  • 山かっこ内の型パラメータのリストが含まれます。
  • 型パラメータのリストは、メソッドの戻り値の型の前に置かれます。
例を見てみましょう:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Utilクラス を見ると、2 つのジェネリック メソッドがあることがわかります。型推論の可能性のおかげで、型をコンパイラーに直接示すことも、自分で指定することもできます。この例では両方のオプションが示されています。ちなみに、この構文はよく考えてみると非常に理にかなっています。ジェネリック メソッドを宣言するときは、メソッドの前に型パラメータを指定します。メソッドの後に型パラメータを宣言すると、JVM はどの型を使用するかを判断できなくなるからです。したがって、最初にT型パラメータを使用することを宣言し、次にこの型を返すと宣言します。当然のことながら、Util.<Integer>getValue(element, String.class) はエラーで失敗します。互換性のない型: Class<String> を Class<Integer> に変換できません。汎用メソッドを使用するときは、型の消去を常に覚えておく必要があります。例を見てみましょう:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
これは問題なく動作します。ただし、呼び出されるメソッドの戻り値の型がIntegerであることをコンパイラが理解している場合に限ります。コンソール出力ステートメントを次の行に置き換えます。

System.out.println(Util.getValue(element) + 1);
エラーが発生します:

bad operand types for binary operator '+', first type: Object, second type: int.
つまり、型の消去が発生しました。コンパイラは、その型が誰も指定されていないと判断するため、その型はObjectとして示され、メソッドはエラーで失敗します。

汎用クラス

パラメータ化できるのはメソッドだけではありません。授業でも同様にできます。Oracle のチュートリアルの「ジェネリック タイプ」セクションでは、これについて説明します。例を考えてみましょう。

public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
ここではすべてがシンプルです。ジェネリック クラスを使用する場合、型パラメータはクラス名の後に示されます。次に、メインメソッドでこのクラスのインスタンスを作成しましょう。

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
このコードはうまく動作します。コンパイラは、数値のリスト文字列コレクションがあることを認識します。しかし、type パラメータを削除してこれを実行するとどうなるでしょうか。

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
エラーが発生します:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
繰り返しますが、これは型の消去です。クラスでは型パラメーターが使用されなくなったため、コンパイラーはListを渡したため、 List<Integer>を含むメソッドが最も適切であると判断します。そしてエラーが出て失敗します。したがって、ルール 2 があります。ジェネリック クラスがある場合は、常に型パラメーターを指定します。

制限

ジェネリック メソッドとクラスで指定される型を制限できます。たとえば、コンテナが型引数として数値のみを受け入れるようにしたいとします。この機能については、Oracle のチュートリアルの「境界型パラメータ」セクションで説明されています。例を見てみましょう:

import java.util.*;
public class HelloWorld {
	
    public static class NumberContainer<T extends Number> {
        private T number;
    
        public NumberContainer(T number) { this.number = number; }
    
        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
ご覧のとおり、type パラメーターをNumberクラス/インターフェイスまたはその子孫に制限しました。クラスだけでなくインターフェースも指定できることに注意してください。例えば:

public static class NumberContainer<T extends Number & Comparable> {
ジェネリックはワイルドカードもサポートしています 。ジェネリックは 3 つのタイプに分類されます。 ワイルドカードの使用は、 Get-Put 原則に従う必要があります。それは次のように表現できます。
  • 構造体から値のみを取得する場合は、拡張ワイルドカードを使用します。
  • 構造体に値を入れるだけの場合は、スーパーワイルドカードを使用します。
  • また、構造体への取得と構造体への配置の両方を行う場合は、ワイルドカードを使用しないでください。
この原則は、Producer Extends Consumer Super (PECS) 原則とも呼ばれます。Java のCollections.copyメソッドのソース コードからの小さな例を次に示します。 Java のジェネリックス: 実際に山括弧を使用する方法 - 5また、何が機能しないのかについての小さな例を次に示します。

public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
ただし、 extends をsuperに 置き換えれば、すべて問題ありません。リストの内容を表示する前にリストに値を設定するため、これはConsumerです。したがって、スーパーを使用します。

継承

ジェネリックには、継承という別の興味深い機能があります。ジェネリックの継承がどのように機能するかについては、Oracle のチュートリアルの「ジェネリック、継承、およびサブタイプ」で説明されています。重要なことは、次のことを覚えて認識することです。これはできません:

List<CharSequence> list1 = new ArrayList<String>();
ジェネリックスでは継承の動作が異なるため、 Java のジェネリックス: 実際に山括弧を使用する方法 - 6エラーで失敗する別の良い例を次に示します。

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
繰り返しますが、ここではすべてがシンプルです。String はObjectの子孫であっても、List<String>List<Object>の子孫ではありません。 学んだことをさらに強化するには、Java コースのビデオ レッスンを視聴することをお勧めします。

結論

そこで、ジェネリック医薬品に関する記憶を新たにしました。それらの機能を最大限に活用することがほとんどない場合、詳細の一部があいまいになります。この短いレビューがあなたの記憶を呼び起こすのに役立つことを願っています。さらに良い結果を得るために、次の内容をよく理解しておくことを強くお勧めします。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION