Javaジェネリクスの例チュートリアル – ジェネリックメソッド、クラス、インターフェース

Javaジェネリクスは、Java 5で導入された最も重要な機能の1つです。もしあなたがJavaコレクションで作業をしており、バージョン5以降を使っているならば、きっとそれを使用したことがあるでしょう。Javaコレクション Javaにおけるジェネリクスはコレクションクラスと非常に簡単ですが、コレクションの型を作成するだけでなく、それ以上の機能を提供します。この記事では、ジェネリクスの機能を学んでみます。時には専門用語にとらわれると理解が難しくなることがありますので、私はそれをシンプルで理解しやすいものにしようと思います。

以下のジェネリクスのトピックを見ていきます。

  1. Javaにおけるジェネリクス

  2. Javaジェネリッククラス

  3. Javaジェネリックインターフェース

  4. Javaジェネリック型

  5. Javaジェネリックメソッド

  6. Javaジェネリクス境界型パラメータ

  7. Javaのジェネリクスと継承

  8. Javaのジェネリッククラスとサブタイプ

  9. Javaのジェネリクスワイルドカード

  10. Javaのジェネリクス上限境界ワイルドカード

  11. Javaのジェネリクス未境界ワイルドカード

  12. Javaのジェネリクス下限境界ワイルドカード

  13. ジェネリクスワイルドカードを使用したサブタイプ

  14. Javaのジェネリクス型の消去

  15. ジェネリクスFAQ

1. Javaのジェネリクス

ジェネリクスは、Java 5で追加され、コンパイル時の型チェックを提供し、コレクションクラスを操作する際に一般的だったClassCastExceptionのリスクを取り除きました。コレクションフレームワーク全体が、型安全性のためにジェネリクスを使用するように書き直されました。ジェネリクスがコレクションクラスの安全な使用をどのように支援するかを見てみましょう。

List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK

for(Object obj : list){
	//型キャストにより、実行時にClassCastExceptionが発生します
    String str=(String) obj; 
}

上記のコードはコンパイルはされますが、実行時にClassCastExceptionがスローされます。なぜなら、リスト内のオブジェクトをStringにキャストしようとしているが、その中にはInteger型の要素があるからです。Java 5以降、以下のようにコレクションクラスを使用します。

List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>(); 
list1.add("abc");
//list1.add(new Integer(5)); //コンパイルエラー

for(String str : list1){
     //型キャストは不要で、ClassCastExceptionを回避します
}

リストを作成する際に、リスト内の要素の型がStringであることを指定していることに注意してください。したがって、リストに他の型のオブジェクトを追加しようとすると、プログラムはコンパイル時エラーをスローします。また、forループでリスト内の要素の型キャストが不要であることにも注意してください。これにより、実行時のClassCastExceptionが回避されます。

2. Javaのジェネリッククラス

自分たちのクラスをジェネリクス型で定義することができます。ジェネリクス型とは、型に対してパラメーター化されたクラスまたはインターフェースのことです。型パラメーターを指定するために、角かっこ(<>)を使用します。その利点を理解するために、次のような単純なクラスがあるとします:

package com.journaldev.generics;

public class GenericsTypeOld {

	private Object t;

	public Object get() {
		return t;
	}

	public void set(Object t) {
		this.t = t;
	}

        public static void main(String args[]){
		GenericsTypeOld type = new GenericsTypeOld();
		type.set("Pankaj"); 
		String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
	}
}

このクラスを使用する際に、型キャストを行う必要があり、実行時にClassCastExceptionを発生させる可能性があります。次に、同じクラスをJavaのジェネリッククラスを使用して書き直します。

package com.journaldev.generics;

public class GenericsType<T> {

	private T t;
	
	public T get(){
		return this.t;
	}
	
	public void set(T t1){
		this.t=t1;
	}
	
	public static void main(String args[]){
		GenericsType<String> type = new GenericsType<>();
		type.set("Pankaj"); //valid
		
		GenericsType type1 = new GenericsType(); //raw type
		type1.set("Pankaj"); //valid
		type1.set(10); //valid and autoboxing support
	}
}

mainメソッドでGenericsTypeクラスを使用していることに注目してください。型キャストを行う必要はなく、実行時のClassCastExceptionを回避できます。作成時に型を指定しない場合、コンパイラは「GenericsTypeはraw型です。ジェネリック型GenericsType<T>への参照はパラメーター化する必要があります」という警告を生成します。型を指定しない場合、型はObjectになり、したがってStringおよびIntegerオブジェクトの両方を許可します。ただし、これを避けることを常に心がける必要があります。なぜなら、raw型で作業する際に型キャストを行う必要があり、これが実行時エラーを発生させる可能性があるからです。

ヒント: コンパイラの警告を抑制するために@SuppressWarnings("rawtypes")アノテーションを使用できます。詳細は、Javaアノテーションチュートリアルを参照してください。

また、それがJavaのオートボクシングをサポートしていることにも注意してください。

3. Javaのジェネリックインターフェイス

Comparableインターフェイスは、インターフェイス内のジェネリクスの優れた例であり、次のように記述されます:

package java.lang;
import java.util.*;

public interface Comparable<T> {
    public int compareTo(T o);
}

同様の方法で、Javaでジェネリックインターフェイスを作成できます。Mapインターフェイスのように、複数の型パラメーターを持つこともできます。また、パラメーター化された型にパラメーター化された値を提供することもできます。例えば、new HashMap<String, List<String>>();は有効です。

4. Javaのジェネリックタイプ

Javaのジェネリックタイプの命名規則は、コードを理解するのに役立ち、命名規則を持つことはJavaプログラミング言語のベストプラクティスの一つです。したがって、ジェネリクスも独自の命名規則を持っています。通常、型パラメーターの名前は、Javaの変数と明確に区別するために単一の大文字の文字です。最も一般的に使用される型パラメーターの名前は次のとおりです:

  • E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
  • K – Key (Used in Map)
  • N – Number
  • T – Type
  • V – Value (Used in Map)
  • S,U,V etc. – 2nd, 3rd, 4th types

5. Javaのジェネリックメソッド

時々、クラス全体をパラメータ化したくない場合があります。その場合は、Javaのジェネリクスメソッドを作成できます。constructorは特別な種類のメソッドですので、コンストラクタでもジェネリクス型を使用できます。以下は、Javaのジェネリックメソッドの例を示すクラスです。

package com.journaldev.generics;

public class GenericsMethods {

	//Javaジェネリックメソッド
	public static  boolean isEqual(GenericsType g1, GenericsType g2){
		return g1.get().equals(g2.get());
	}
	
	public static void main(String args[]){
		GenericsType g1 = new GenericsType<>();
		g1.set("Pankaj");
		
		GenericsType g2 = new GenericsType<>();
		g2.set("Pankaj");
		
		boolean isEqual = GenericsMethods.isEqual(g1, g2);
		//上記の文は単純に以下のように書くことができます
		isEqual = GenericsMethods.isEqual(g1, g2);
		//この機能は、型推論として知られており、角かっこ内に型を指定せずにジェネリックメソッドを通常のメソッドとして呼び出すことができます。
		//コンパイラは必要な型を推論します
	}
}

ジェネリック型をメソッド内で使用する構文を示すisEqualメソッドのシグネチャに注目してください。また、これらのメソッドをJavaプログラムで使用する方法に注目してください。これらのメソッドを呼び出す際に型を指定することもできますし、通常のメソッドのように呼び出すこともできます。Javaコンパイラは、使用する変数の型を決定するために十分に賢いです。この機能は型推論と呼ばれます。

6. Javaジェネリクスの境界型パラメータ

メソッドで2つのオブジェクトを比較するメソッドで、受け入れるオブジェクトがComparableであることを確認したい場合、パラメータ化された型で使用できるオブジェクトのタイプを制限したいとします。境界型パラメータを宣言するには、型パラメータの名前をリストアップし、その後に上限を示すextendsキーワード、そして以下のようなメソッドのように、その上限を示します。

public static <T extends Comparable<T>> int compare(T t1, T t2){
		return t1.compareTo(t2);
	}

これらのメソッドの呼び出しは、無境界メソッドと類似していますが、Comparableでないクラスを使用しようとすると、コンパイル時エラーが発生します。境界型パラメータは、メソッドだけでなく、クラスやインターフェースでも使用できます。Java Genericsは複数の境界もサポートしており、つまり<T extends A & B & C>です。この場合、Aはインターフェースまたはクラスであることができます。Aがクラスである場合、BとCはインターフェースである必要があります。複数の境界には1つのクラスしか持てません。

7. Java Generics and Inheritance

私たちは、Javaの継承がAがBのサブクラスである場合、変数Aを変数Bに割り当てることを可能にすることを知っています。したがって、Aのジェネリック型はBのジェネリック型に割り当てることができると思うかもしれませんが、それはそうではありません。これを簡単なプログラムで確認しましょう。

package com.journaldev.generics;

public class GenericsInheritance {

	public static void main(String[] args) {
		String str = "abc";
		Object obj = new Object();
		obj=str; // works because String is-a Object, inheritance in java
		
		MyClass myClass1 = new MyClass();
		MyClass myClass2 = new MyClass();
		//myClass2=myClass1; // MyClassはMyClassではないため、コンパイルエラー
		obj = myClass1; // MyClass parent is Object
	}
	
	public static class MyClass{}

}

MyClass変数をMyClass変数に割り当てることは許可されていません。なぜなら、それらは関連していないからです。実際、MyClassの親はObjectです。

8. Javaジェネリッククラスとサブタイプ

ジェネリッククラスまたはインターフェースを拡張または実装することで、そのサブタイプを作成できます。1つのクラスまたはインターフェースの型パラメーターと別のクラスまたはインターフェースの型パラメーターとの関係は、extendsおよびimplements句によって決定されます。例えば、ArrayListはListを実装し、ListはCollectionを拡張するので、ArrayListはListのサブタイプであり、ListはCollectionのサブタイプです。型引数を変更しない限り、サブタイプの関係は維持されます。以下は複数の型パラメーターの例を示しています。

interface MyList<E,T> extends List<E>{
}

ListのサブタイプにはMyList、MyListなどがあります。

9. Javaジェネリックワイルドカード

疑問符(?)はジェネリクスにおけるワイルドカードであり、未知の型を表します。ワイルドカードは、パラメータ、フィールド、またはローカル変数の型として使用することができ、場合によっては戻り値の型としても使用できます。ジェネリックメソッドを呼び出したり、ジェネリッククラスをインスタンス化する際にはワイルドカードを使用できません。以下では、上限境界ワイルドカード、下限境界ワイルドカード、およびワイルドカードのキャプチャについて学びます。

9.1) Java Generics 上限境界ワイルドカード

上限境界ワイルドカードは、メソッド内の変数の型に対する制限を緩和するために使用されます。リスト内の数値の合計を返すメソッドを書きたいとします。その場合、実装は次のようになります。

public static double sum(List<Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}

上記の実装の問題点は、ListやListといったリストとは関連がないため、動作しないことです。このとき、上限境界ワイルドカードが役立ちます。ジェネリクスワイルドカードをextendsキーワードと共に使用し、上限境界またはそのサブクラスの型の引数を渡すことができます。上記の実装は、以下のように修正できます。

package com.journaldev.generics;

import java.util.ArrayList;
import java.util.List;

public class GenericsWildcards {

	public static void main(String[] args) {
		List<Integer> ints = new ArrayList<>();
		ints.add(3); ints.add(5); ints.add(10);
		double sum = sum(ints);
		System.out.println("Sum of ints="+sum);
	}

	public static double sum(List<? extends Number> list){
		double sum = 0;
		for(Number n : list){
			sum += n.doubleValue();
		}
		return sum;
	}
}

インターフェイスの観点からコードを書くのと似ています。 上記の方法では、上限クラスNumberのすべてのメソッドを使用できます。 上限付きリストでは、null以外のオブジェクトをリストに追加することはできません。 sumメソッド内でリストに要素を追加しようとすると、プログラムはコンパイルされません。

9.2) Javaジェネリクスの非境界ワイルドカード

時々、すべてのタイプでジェネリックメソッドを使用したい場合があります。 この場合、非境界ワイルドカードを使用できます。 これは、を使用するのと同じです。

public static void printData(List<?> list){
		for(Object obj : list){
			System.out.print(obj + "::");
		}
	}

ListまたはListまたはその他のタイプのオブジェクトリスト引数をprintDataメソッドに提供できます。 上限付きリストと同様に、リストに何も追加することはできません。

9.3) Javaジェネリクスの下限付きワイルドカード

整数を整数のリストに追加したい場合、引数の型をListに保持することができますが、それではIntegerに結びつきます。ListやListも整数を保持できるため、これを達成するために下限境界ワイルドカードを使用できます。ジェネリックスのワイルドカード(?)とsuperキーワード、および下限境界クラスを使用してこれを実現します。下限境界または下限境界のスーパータイプを引数として渡すことができ、この場合、Javaコンパイラは下限境界オブジェクト型をリストに追加することを許可します。ジェネリックスのワイルドカードを使用したサブタイプ化

public static void addIntegers(List<? super Integer> list){
		list.add(new Integer(50));
	}

10. Generics Wildcardを使用したサブタイプ化

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

11. Java Generics型の消去

Javaのジェネリックスは、コンパイル時の型チェックを提供するために追加されましたが、実行時には使用されず、そのため、Javaコンパイラはジェネリックスの型チェックコードをバイトコードからすべて削除し、必要に応じて型キャストを挿入する型消去機能を使用します。型消去は、パラメータ化された型のために新しいクラスが作成されないことを保証します。その結果、ジェネリックスには実行時のオーバーヘッドが発生しません。たとえば、次のようなジェネリッククラスがある場合、

public class Test<T extends Comparable<T>> {

    private T data;
    private Test<T> next;

    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }

    public T getData() { return this.data; }
}

Javaコンパイラは、下記のコードのように、境界型パラメータTを最初の境界インターフェース、Comparableで置き換えます。

public class Test {

    private Comparable data;
    private Test next;

    public Node(Comparable d, Test n) {
        this.data = d;
        this.next = n;
    }

    public Comparable getData() { return data; }
}

12. ジェネリクスFAQ

12.1) Javaでジェネリクスを使用する理由は何ですか?

ジェネリクスは強力なコンパイル時の型チェックを提供し、ClassCastExceptionのリスクを減らし、オブジェクトの明示的なキャストを減らします。

12.2) ジェネリクスのTとは何ですか?

私たちはジェネリッククラス、インターフェース、およびメソッドを作成するために<T>を使用します。 Tは使用時に実際の型で置換されます。

12.3) Javaでのジェネリクスの動作原理はどのようになっていますか?

ジェネリックコードは型安全性を確保します。コンパイラは実行時の過負荷を減らすために、コンパイル時にすべての型パラメータを削除する型消去を使用します。

13. Javaにおけるジェネリックス – さらなる読み物

これでJavaにおけるジェネリックスに関するすべてです。Javaのジェネリックスは非常に広範であり、理解して効果的に使用するには多くの時間が必要です。この記事は、ジェネリックスの基本的な詳細と、型安全性を拡張するためにそれをどのように使用できるかを提供する試みです。

Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface