JavaRush /Java Blog /Random-JA /Java のジェネリックスの理論または実際に括弧を付ける方法
Viacheslav
レベル 3

Java のジェネリックスの理論または実際に括弧を付ける方法

Random-JA グループに公開済み

導入

JSE 5.0 以降、ジェネリックスが Java 言語の武器庫に追加されました。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 1

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

ジェネリック(一般化) は、一般化されたプログラミングを実装するための Java 言語の特別な手段です。つまり、データとアルゴリズムを記述するための特別なアプローチであり、その記述を変更せずにさまざまなタイプのデータを操作できるようになります。Oracle の Web サイトでは、ジェネリックス専用の別のチュートリアル「レッスン: ジェネリックス」が提供されています。

まず、ジェネリックを理解するには、ジェネリックがなぜ必要なのか、そしてジェネリックが何を提供するのかを理解する必要があります。チュートリアルの「ジェネリックを使用する理由」セクション 目的の1つは、コンパイル時の型チェックを強化し、明示的なキャストの必要性を排除することだと言われています。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 2
実験用にお気に入りのチュートリアルポイントのオンライン 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!」というフレーズを言ったらどうなるでしょう。殴られた場合は「こんにちは」と返すことしかできません。コードから文字列との連結を削除しましょう", world!"。もっと無害なものは何でしょうか?しかし実際には、 DURING COMPILATION というエラーが発生します。 error: incompatible types: Object cannot be converted to String 問題は、この場合、 List には Object 型のオブジェクトのリストが格納されているということです。String は Object の子孫であるため (Java ではすべてのクラスが Object から暗黙的に継承されるため)、明示的なキャストが必要ですが、これは実行しませんでした。そして、連結するときに、オブジェクトに対して静的メソッド String.valueOf(obj) が呼び出され、最終的にオブジェクトに対して toString メソッドが呼び出されます。つまり、リストにはオブジェクトが含まれています。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 はオブジェクトのリストを受け入れ、文字列だけでなく整数も格納します。しかし、最悪のことは、この場合、コンパイラは何も問題を認識しないことです。そして、ここでは実行中にエラーを受け取ります(エラーは「実行時」に受け取られたとも言われています)。エラーは次のようになります。「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 へのキャストは必要なくなりました。さらに、ジェネリックを構成する山括弧が追加されました。現在、コンパイラは、リストへの 123 の追加を削除するまでクラスのコンパイルを許可しません。これは整数です。彼は私たちにそう言ってくれるでしょう。多くの人はジェネリックを「構文糖衣」と呼んでいます。そして、ジェネリックはコンパイル時に実際にそれらと同じカーストになるため、それらは正しいです。手動キャストとジェネリックを使用して、コンパイルされたクラスのバイトコードを見てみましょう。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 3
コンパイル後、ジェネリックに関する情報はすべて消去されます。これを「型消去」または「型消去」と呼びます。型消去とジェネリックは、古いバージョンの JDK との下位互換性を提供しながら、コンパイラーが新しいバージョンの Java での型推論を支援できるように設計されています。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 4

生のタイプまたは生のタイプ

ジェネリックスについて話すときは、常に 2 つのカテゴリがあります。型付き型 (ジェネリック型) と「生の」型 (未加工型) です。 生の型は、山かっこで「修飾」を指定しない型です。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 5
型付き型はその逆で、「明確化」を示します。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 6
ご覧のとおり、スクリーンショットでは矢印が付いている珍しいデザインが使用されています。これは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") Suppress は抑制と翻訳され、文字通り警告を抑制します。しかし、なぜそれを表示することにしたのか考えてみてください。ルール 1 を思い出してください。入力を追加する必要があるかもしれません。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 7

一般的なメソッド

ジェネリックを使用すると、メソッドを入力できます。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 つの型付きメソッドが含まれていることがわかります。型推論を使用すると、型定義をコンパイラに直接提供することも、自分で指定することもできます。この例では両方のオプションが示されています。ところで、この構文はよく考えてみると非常に論理的です。メソッドを入力するときは、メソッドの前にジェネリックを指定します。メソッドの後にジェネリックを使用すると、Java はどの型を使用するかを判断できなくなるからです。したがって、最初にジェネリック T を使用することを発表し、次にこのジェネリックを返すと言います。当然、Util.<Integer>getValue(element, String.class)エラーで失敗しますincompatible types: Class<String> cannot be converted to 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); そして、エラーが表示されます: 二項演算子 '+' のオペランド タイプが正しくありません、最初のタイプ: Object 、2 番目のタイプ: int つまり、型が消去されました。コンパイラは、型が指定されていないこと、型が Object として指定されていることを認識し、コードの実行はエラーで失敗します。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 8

ジェネリックタイプ

メソッドだけでなくクラス自体も入力できます。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);
		}
	}
}
ここではすべてがシンプルです。クラスを使用する場合、ジェネリックはクラス名の後にリストされます。次に、main メソッドでこのクラスのインスタンスを作成しましょう。
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
うまくいきます。コンパイラは、数値のリストと文字列型のコレクションがあることを認識します。しかし、ジェネリックを消去して次のようにするとどうなるでしょうか。
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 Erasure を再度入力してください。クラスにはジェネリックがなくなったため、コンパイラは List を渡したため、List<Integer> を含むメソッドの方が適切であると判断します。そして私たちは間違いで落ちます。したがって、ルール #2: クラスが型指定されている場合は、常にジェネリックで型を指定します

制限

ジェネリックで指定された型に制限を適用できます。たとえば、コンテナが入力として Number のみを受け入れるようにしたいとします。この機能については、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");
    }
}
ご覧のとおり、ジェネリック型を Number クラス/インターフェイスとその子孫に制限しました。興味深いことに、クラスだけでなくインターフェイスも指定できます。例: public static class NumberContainer<T extends Number & Comparable> { ジェネリックにはワイルドカードの概念もあります https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html これらは、次に 3 つのタイプに分類されます。 いわゆるGet Put 原則がワイルドカードに適用されます。それらは次の形式で表現できます。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 9
この原則は、PECS (Producer Extends Consumer Super) 原則とも呼ばれます。Habré について詳しくは、「 Java API の使いやすさを向上させるための汎用ワイルドカードの使用」という記事や、スタックオーバーフローに関する優れた議論「汎用 Java でのワイルドカードの使用」を参照してください。Java ソースからの小さな例、Collections.copy メソッドを次に示します。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 10
さて、それがどのように機能しないかを示す小さな例:
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 に置き換えると、すべてがうまくいきます。リストを出力する前に値を入力するので、それは私たちにとってコンシューマ、つまりコンシューマです。したがって、スーパーを使用します。

継承

ジェネリックには、継承という別の珍しい特徴があります。ジェネリックの継承については、Oracle チュートリアルの「ジェネリック、継承、およびサブタイプ」セクションで説明されています。重要なことは、次のことを覚えて認識することです。これはできません:
List<CharSequence> list1 = new ArrayList<String>();
ジェネリックでは継承の動作が異なるため、次のようになります。
Java のジェネリックスの理論または実際に括弧を付ける方法 - 11
そして、これはエラーで失敗する別の良い例です。
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
ここでもすべてがシンプルです。String は Object の子孫ですが、List<String> は List<Object> の子孫ではありません。

最後の

そこで私たちはジェネリック医薬品についての記憶を新たにしました。最大限に活用することがほとんどない場合、詳細の一部が記憶から抜け落ちてしまいます。この短いレビューがあなたの記憶を新たにするのに役立つことを願っています。より大きな結果を得るには、次の資料を読むことを強くお勧めします。 #ヴィアチェスラフ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION