疲れたらやすむ

Javaを学ぶ上でハマったところを書いていきます。iPhoneアプリ開発や日常ネタもあるかも。

【Java】ジェネリクスについて

今回はジェネリクスに関する記事になります。

クラスやメソッド宣言に、<T>などと記述されていることがあります。
それがジェネリクスと呼ばれるものです。

ジェネリクスとは

Javaにおいて、ジェネリクスとは「型の安全性を高め、さらに汎用性を持たせる」ことが出来る仕組みです。
普段よく使用するものでは、ListやMapはジェネリクスを使用しています。

自作のジェネリクスクラスは以下の様に定義します。

GenericsSample.java

public class GenericsSample<T> {

	private T t;

	public GenericsSample(T t) {
		this.t = t;
	}

	public T getT() {
		return this.t;
	}
}

これがジェネリクスを使用したクラスで、総称型と呼ばれるクラスになります。
ジェネリクスはクラスやインターフェース、メソッドなどに記述出来ます。

クラス名の右側に<T>と記述されていますが、これを型引数と言います。
<>内の文字は何でもOKですが基本的には以下の通り。

変数名 指針
E コレクションに格納される要素
K, V キーと値
T 上記以外の一般的な要素
R 戻り値

このGenericsSampleを実際に使用する場合はこんな感じになります。

Main.java

public class Main {
	public static void main(String[] args) {

		GenericsSample<String> generics = new GenericsSample<String>("Hello");
		System.out.println(generics.getT());
	}
}

実行結果

Hello

使用には総称型の型引数<T>の部分をパラメータ化する必要があり、今回はString型でパラメータ化しています。
パラメータ化は、後に触れる境界を指定していない場合はどんな型でもOKです。
StringやInteger、独自クラスでもなんでも。
ListやMapなどでも型を指定すると思いますが、それと同じです。

今回のGenericsSampleのソースでは、T型のフィールドtをprivateで宣言しています。
この時点ではTが何型であるのか決まっていません。
ただし実際にはパラメータ化(型の指定)をする必要があるため、実行後は型が決まります。


ジェネリクスをメソッドで使用する場合は以下の様に記述します。

public <T> void doSomething() {
	T t;
}

安全性と汎用性

ジェネリクスを使用することで、予想外の例外を未然に防げたりクラスやメソッドなどの汎用性を高めることが可能です。

まずは安全性について。

少し極端な例ですが、下記のコードがあったとします。

List list = new ArrayList<>();
list.add("Hello");
list.add(new Integer(1));

for (Object o : list) {
	String s = (String) o;
}

Listは総称型であるため、基本的に使用にはそのListは何の型を扱うのかをパラメータ化することが推奨されています。
普段であれば、Listを定義する際にそのListが何型なのかを<>の中に記述するはずです。
しかし記述しない場合でもコンパイルエラーとはならず実行出来てしまいます。

その結果、意図せず複数の型の値が格納されたListが完成してしまいました。

ではfor文で取り出してみましょう。
String型の値しか入っていないと思っていたListの中にInteger型の値の混ざっていました。
その結果、実行時にClassCastExceptionが発生。

つまり、パラメータ化しないと実行するまでバグであることがわからないのです。


そしてパラメータ化した場合。

List<String> list = new ArrayList<String>();
list.add("Hello");
list.add(new Integer(1));

Integer型の値をaddした時点でコンパイルエラーとなります。
バグを未然に防いでくれ、これが型の安全性を高めます。


そして汎用性とは、例えばListでは複数の型でパラメータ化することにより様々な型のListが定義出来ます。

List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();

ジェネリクスを使用しない場合、String型やInteger型用のListを定義する必要があり、非常に冗長ですよね。
あまり気にしないで普段使用していますが、意外と身近にジェネリクスの便利な部分が転がっています。

制約がある

ジェネリクスを使用したクラスにはいくつか制約があります。

予めお伝えしておくと、以下のコードは各箇所でコンパイルエラーとなります。

GenericsSample.java

public class GenericsSample<T> {

	static T t1; // 1.

	T t2 = new T(); // 2.

	T[] tArray = new T[1]; // 3.

	public void doSomething() {
		Class<T> cls = T.class; // 4

		T t3;
		boolean b = t3 instanceof T); // 5
	}
}

1.static修飾子の型変数は定義出来ない

2.型変数のインスタンス生成は出来ない

3.型変数が要素の配列は生成出来ない

4.型変数のclass参照は出来ない

5.instanceofによる型の判定は出来ない

型境界

ジェネリクスには型の境界を設けることが可能です。
型境界を指定すると、パラメータ化する型を限定することが出来ます。
たぶん初見でジェネリクスに苦手意識を持ってしまうのはこのあたりのせいだと思います。

この内容では継承が絡んできます。
まずは例として継承関係にあるクラスを作成します。

A.java

public class A {

}

B.java

public class B extends A {

}

C.java

public class C extends B {

}

これらのクラスの関係としては、Aが親でBが子、さらにBが親のCが子となります。

この継承関係で、型境界をBに指定してみます。
型境界は、<>内の型名の後ろにextendsを付けて境界にする型を記述します。

GenericsSample.java

public class GenericsSample<T extends B> {
	
}

このジェネリクスのクラスを実際に使用してみます。

Main.java

public class Main {
	public static void main(String[] args) {

		// コンパイルエラー
		GenericsSample<A> genericsA = new GenericsSample<A>();

		// OK
		GenericsSample<B> genericsB = new GenericsSample<B>();

		// OK
		GenericsSample<C> genericsC = new GenericsSample<C>();

		// コンパイルエラー
		GenericsSample<String> genericsString = new GenericsSample<String>();
	}
}

今回は境界にBを指定しているため、Bとして扱える型であれば許容されます。
つまり、境界に指定した型とis-a関係であれば良いということになります。

AはBを子に持つがBではないためコンパイルエラー。
BはBであるためOK。
Cは親にBを持ち、Bとして扱うことが可能であるためOK。
StringはBとは無関係のためコンパイルエラー。

ワイルドカードと上下限の境界

ジェネリクスをパラメータ化する際に、ワイルドカード(<?>)を使用することが出来ます。
ワイルドカードとは、何らかの型が指定されることを意味しており、つまりどんな型でも許容します。

GenericsSample.java

public class GenericsSample<T> {

}

Main.java

public class Main {
	public static void main(String[] args) {

		// 変数宣言でのワイルドカード(下限境界)
		GenericsSample<? super B> generics = new GenericsSample<>();
	}

	// メソッドの引数でのワイルドカード
	public void doSomething(GenericsSample<?> generics) {

	}
}

この様に、ジェネリクスクラスの変数宣言にワイルドカードを使用します。

<?>を使用した場合は型が決まっておらず、言わばObject型のような感覚です。
実際、<?>は<? extends Object>を省略したものとされています。

そして、型境界でも出てきたextendsと、もう1つsuperを使用し境界を指定出来ます。
superはextendsの逆という解釈でOKです。

先ほどの型境界で登場したA、B、Cそれぞれのクラスが存在した場合。

Main.java

public class Main {
	public static void main(String[] args) {

		// OK
		doSomething(new GenericsSample<A>());

		// OK
		doSomething(new GenericsSample<B>());

		// コンパイルエラー
		doSomething(new GenericsSample<C>());
	}

	static public void doSomething(GenericsSample<? super B> generics) {

	}
}

Bか、Bのスーパータイプを許容するのが<? super B>です。

ワイルドカードやsuperは、型引数には使用出来ないので混同しない様に注意が必要です。