banner
jzman

jzman

Coding、思考、自觉。
github

Javaシリーズのジェネリクス

JDK 1.5 からジェネリクスの概念が提供され、ジェネリクスにより開発者はより安全な型を定義できるようになり、強制型変換時に型変換例外が発生することを防ぐことができます。反省がない場合、Object を使用して異なる型のデータ間の操作を行うことができますが、強制型変換(ダウンキャスト)は具体的な型が不明な場合にエラーが発生します。ジェネリクスメカニズムの導入は、データ型が不明確な問題を解決するためのものです。

  1. ジェネリッククラスの定義
  2. ジェネリックインターフェースの定義
  3. ジェネリックメソッドの定義
  4. ジェネリック継承
  5. ジェネリック消去
  6. ジェネリックの高度な使用法
  7. ジェネリックの効果

ジェネリッククラスの定義#

ジェネリッククラスを定義するには、以下の構文を使用します:

//ジェネリッククラスの定義
class クラス名<T>{
    
}

ここで、T は型の名前を表し、T は他の名前にすることもできますが、一般的には T と書かれます。<> の中の型は複数指定でき、カンマで区切ります。以下はジェネリッククラスの具体例です:

/**
 * ジェネリッククラスの定義
 * @author jzman
 * @param <T>
 */
public class GenercityClass<T1,T2> {
	
	private T1 score;
	private T2 desc;
	
	public GenercityClass() {}
	
	public GenercityClass(T1 score, T2 desc) {
		this.score = score;
		this.desc = desc;
	}
	public T1 getScore() {
		return score;
	}
	public void setScore(T1 score) {
		this.score = score;
	}
	public T2 getDesc() {
		return desc;
	}
	public void setDesc(T2 desc) {
		this.desc = desc;
	}
	
	public static void main(String[] args) {
		//使用時に具体的な型を指定します。具体的な型は参照型のみで、基本型(intなど)は使用できません。
		GenercityClass<Integer, String> genercity = new GenercityClass<>(90,"A");
		int score = genercity.getScore();
		String desc = genercity.getDesc();
		System.out.println("score="+score+",desc="+desc);
	}
}

明らかに、ジェネリックで定義されたクラスは、使用時に異なる要求に応じて <T1,T2> が表す実際の型を指定できるため、型変換操作が発生せず、ClassCastException 例外が発生しません。コンパイラは事前に型が一致するかどうかをチェックします。以下のようにするとエラーが発生します:

GenercityClass<Integer, String> genercityClass = new GenercityClass<>();
//指定された具体的な型に合致しています
genercityClass.setScore(80);
//Stringの値をInteger型の変数に代入することはできません
genercityClass.setScore("A");

ジェネリックインターフェースの定義#

ジェネリックインターフェースを定義するには、以下の構文を使用します:

//ジェネリックインターフェースの定義
interface インターフェース名<T>{
    戻り値の型 メソッド名<T t>;
}

以下はジェネリックインターフェースの定義の具体例です:

/**
 * ジェネリックインターフェース:メソッド内でのみ指定されたジェネリックを使用できます
 * @author jzman
 */
public interface GenercityInterface<T> {
	//ジェネリックは静的プロパティの前で使用できません
//	T a;
	//メソッド内でジェネリックを使用
	void start(T t);
}

インターフェース内でジェネリックを使用する場合、インターフェースのメソッド内でのみジェネリックを使用でき、インターフェースのプロパティ上ではジェネリックを使用できません。なぜなら、Java インターフェース内のプロパティは public static final で修飾されているため、ジェネリックが表す具体的な型は使用時に決定され、コンパイル時にはまだジェネリックが表す具体的な型がわからないからです。

ジェネリックメソッドの定義#

ジェネリックメソッドを定義するには、以下の構文を使用します:

//ジェネリックメソッドの定義
修飾子 <T> 戻り値の型 メソッド名{
    
}

クラスを作成する具体例は以下の通りです:

package com.manu.genericity;

public class PersonBean {
	private String name;
	private int age;
    //Getter、Setterなどのメソッドは省略
    //...
}


以下はジェネリックメソッドの定義の具体例です:

/**
 * ジェネリックメソッド
 * @author jzman
 */
public class GenercityMethod {
	public static void main(String[] args) {
		PersonBean bean = new PersonBean("tom",10);
		printA("string");
		printA(bean);
		printB(bean);
	}
	//ジェネリックはそのスーパークラスを指定しない場合、型が不明なため、その情報にアクセスすることしかできません
	public static <T> void printA(T t) {
		System.out.println(t);
	}
	//ジェネリックはスーパークラスを指定することで、そのジェネリックが表す実体情報を変更できます
	public static <T extends PersonBean> void printB(T t) {
		t.setName("haha");
		System.out.println(t);
	}
}

出力結果は以下の通りです:

string
PersonBean [name=tom, age=10]
PersonBean [name=haha, age=10]

ジェネリックメソッド printA の具体的な型は不明であるため、そのジェネリック情報を変更することはできませんが、printB はそのスーパークラスを指定することで、ある程度ジェネリックが表す具体的な型の情報を変更できます。ジェネリックはメソッド内で定義でき、ジェネリックメソッドの有無はその所在するクラスに関係ありません

ジェネリック継承#

子クラスが親クラスを継承する際、ジェネリックはどのように処理されるのでしょうか。以下は、ジェネリックを持つ親クラスを継承する子クラスのいくつかのケースです:

/**
 * ジェネリック親クラス
 * @author jzman
 */
public abstract class GenercityEClass<T> {
	T name;
	public abstract void print(T t);
}

/**
 * 子クラスがジェネリッククラスで、型は使用時に決定されます
 * @author jzman
 * @param <T>
 */
class Child1<T> extends GenercityEClass<T>{
	T t; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(T t) {
		//親クラスのプロパティは親クラスによって決定されます
		T name = this.name;
	}
}

/**
 * 子クラスが具体的な型を指定します
 * @author jzman
 */
class Child2 extends GenercityEClass<String>{
	Integer t; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(String t) {
		//親クラスのプロパティは親クラスによって決定されます
		String name = this.name;
	}
}

/**
 * 親クラスのジェネリックの消去
 * 子クラスはジェネリッククラスで、親クラスは型を指定せず、ジェネリック消去はObjectを使用して代替されます
 * @author jzman
 * @param <T>
 */
class Child3<T/*,T1*/> extends GenercityEClass{
	T t;
	String t1; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(Object t) {
		//親クラスのプロパティは親クラスによって決定されます
		Object obj = this.name;
	}
}

/**
 * 子クラスと親クラスが同時にジェネリック消去を行います
 * @author jzman
 */
class Child4 extends GenercityEClass{
	//具体的な型のみ使用できます
	String str; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(Object t) {
		//親クラスのプロパティは親クラスによって決定されます
		Object obj = this.name;
	}
}

いくつかの結論が得られます:親クラスのプロパティは親クラスによって決定され、子クラスのプロパティは子クラスによって決定され、子クラスが親クラスを継承する際のメソッドのオーバーライドは、関連する型が親クラスによって決定されます。このようなジェネリックを伴う操作は、継承時にジェネリック消去が関与し、後で説明します。

ジェネリック消去#

ジェネリック継承の小節でジェネリック消去に触れたため、重要だと感じ、別途記録します。ジェネリック消去には 2 つのケースがあります。具体的には:

  1. 継承または実装(インターフェース)時のジェネリック消去;
  2. 具体的な使用時のジェネリック消去。

子クラスが親クラスを継承する際、ジェネリックの消去には 2 つのケースがあります。具体的には:

  1. 子クラスがジェネリックで、親クラスがジェネリック消去を行う
  2. 子クラスと親クラスが同時にジェネリック消去を行う

以下は一部のコードの具体例です:

/**
 * 親クラスのジェネリックの消去
 * 子クラスはジェネリッククラスで、親クラスは型を指定せず、ジェネリック消去はObjectを使用して代替されます
 * @author jzman
 * @param <T>
 */
class Child3<T/*,T1*/> extends GenercityEClass{
	T t;
	String t1; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(Object t) {
		//親クラスのプロパティは親クラスによって決定されます
		Object obj = this.name;
	}
}

/**
 * 子クラスと親クラスが同時にジェネリック消去を行います
 * @author jzman
 */
class Child4 extends GenercityEClass{
	//具体的な型のみ使用できます
	String str; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(Object t) {
		//親クラスのプロパティは親クラスによって決定されます
		Object obj = this.name;
	}
}
/**
 * 子クラスが消去され、親クラスがジェネリックを使用する(エラー)
 * @author jzman
 *
class Child5 extends GenercityEClass<T>{
	@Override
	public void print(T t) {
		
	}
}
 */

注意すべき点は、親クラスがジェネリックで、子クラスがジェネリック消去を行うことはできず、子クラスのジェネリックが親クラスのジェネリックの数以上である場合にのみジェネリック消去が行えるということです。クラスがジェネリックインターフェースを実装することは、クラス間の継承に似ており、詳しくは述べません。

以下は具体的な使用時のジェネリック消去の具体例です:

class Child1<T> extends GenercityEClass<T>{
	T t; //子クラスのプロパティは子クラスによって決定されます
	@Override
	public void print(T t) {
		//親クラスのプロパティは親クラスによって決定されます
		T name = this.name;
	}
	
	public static void main(String[] args) {
		//1.使用時のジェネリック消去
		Child1 child1 = new Child1();
		Object obj = child1.name;//プロパティnameは消去後の型Objectになります
		//2.使用時に型を指定
		Child1<String> child2 = new Child1();
		String name = child2.name;//プロパティnameは指定された型Stringになります
	}
}

ジェネリック消去についてはこれで終わります。

ジェネリックの高度な使用法#

ジェネリックの高度な使用法は以下の通りです:

  1. ジェネリックの使用可能な型を制限する
  2. 型ワイルドカードを使用する

ジェネリックの使用可能な型を制限する#

デフォルトでは、任意の型を使用してジェネリッククラスオブジェクトをインスタンス化できますが、ジェネリッククラスインスタンスの型に制限を加えることもできます。具体的な構文は以下の通りです:

//ジェネリックの使用可能な型を制限する
class <T extends AnyClass>{
    
}

上記の AnyClass はクラスでもインターフェースでもかまいません。つまり、ジェネリックの型は AnyClass クラスを継承するか、AnyClass インターフェースを実装する必要があります。クラスでもインターフェースでも、型制限を行う際は extends キーワードを使用します。以下は具体例です:

/**
 * ジェネリックの使用可能な型制限
 * @author jzman
 */
public class LimitGenercityClass<T extends List> {
	public static void main(String[] args) {
		// T extends:<上限、Tは上限の子クラスまたはその本体であることができます
		LimitGenercityClass<ArrayList<String>> limitClass1 = new LimitGenercityClass<>();
		LimitGenercityClass<LinkedList<String>> limitClass2 = new LimitGenercityClass<>();
		//HashMapはListインターフェースを実装していないため、HashMapはジェネリッククラスインスタンスの型をインスタンス化できません
//		LimitGenercityClass<HashMap<String,String>> limitClass3 = new LimitGenercityClass<>();	
	}
}

上記のコードでは、ジェネリック T に制限を加えています。具体的なジェネリック型は List インターフェースを実装する必要があります。ArrayList と LinkedList は List インターフェースを実装していますが、HashMap は List インターフェースを実装していないため、HashMap は対応するジェネリックの具体的な型をインスタンス化できません。

型ワイルドカードを使用する#

ジェネリックメカニズムでは、型ワイルドカードが提供されており、主にジェネリッククラスオブジェクトを作成する際に、そのジェネリッククラスの型を特定のクラスまたはインターフェースを継承または実装するように制限するために使用されます。?ワイルドカードを使用して実現できます。また、extends、super キーワードを使用してジェネリックに制限を加えることもできます。型ワイルドカードの具体的な構文は以下の通りです:

//型ワイルドカードを使用する
ジェネリッククラス名<? extends AnyClass> a ;

以下は型ワイルドカードの使用の具体例です:

/**
 * ワイルドカードの使用
 * ? extends AnyClass:ジェネリックの具体的な型はAnyClassの子クラスのみ
 * ? super AnyClass:ジェネリックの具体的な型はAnyClassのスーパークラスのみ
 * @author jzman
 */
public class CommonCharGenercityClass<T> {
	public static void main(String[] args) {
		//1.型ワイルドカードを型宣言に使用
		CommonCharGenercityClass<? extends List> commA = null;
		//ワイルドカード内でextendsキーワードを使用して、ジェネリックはListの子クラスのみであることを制限しました
		commA = new CommonCharGenercityClass<ArrayList<String>>();
		commA = new CommonCharGenercityClass<LinkedList<String>>();
//		commA = new CommonCharGenercityClass<HashMap<String>>();
		
		CommonCharGenercityClass<? super List> commB = null;
		//エラー、ワイルドカード内でsuperキーワードを使用して、ジェネリックはListのスーパークラスのみであることを制限しています。例えばObject
//		commB = new CommonCharGenercityClass<ArrayList<String>>();
		commB = new CommonCharGenercityClass<Object>();
		
		List<String> listA = new ArrayList<>();
		listA.add("tom");
		
		List<?> listB = listA;
		//ワイルドカードを使用した場合、具体的な型が不明なため、データの取得と削除しかできず、データの追加はできません
//		listB.add("new");
		listB.remove(0);
		System.out.println(listB);	
	}
	
	//2.メソッドパラメータにワイルドカードを使用
	public static void test(CommonCharGenercityClass<? extends List> list) {
		//...
	}
}

型ワイルドカード ? は、extends および super キーワードと組み合わせて、ジェネリックの具体的な型に制限を加えることができます。extends は、ジェネリックの具体的な型がターゲット型の子クラスであるべきことを制限し、super は、ジェネリックの具体的な型がターゲット型のスーパークラスであるべきことを制限します。さらに、型ワイルドカードはクラスの宣言には使用できません。

ジェネリックの効果#

  1. コンパイル時に型安全性をチェックし、エラーを事前に発見します。
  2. ジェネリック内の型の強制変換は自動かつ暗黙的であり、コードの再利用率を向上させます。

まとめ:ジェネリックの型は参照型でなければならず、基本型ではありません。ジェネリックの数は複数指定でき、?を使用してオブジェクト作成時のジェネリック型やメソッドパラメータ型に制限を加えることができます。例えば、extends および super キーワードを使用してジェネリックの具体的な型に対して下限または上限の制限を行います。最後に、ジェネリック配列を宣言できますが、ジェネリック配列のインスタンスを作成することはできません。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。