クラスで型引数を使用する

Effective Javaを読みましたが、自分でジェネリクスなクラスを作る場合の書き方と利点が分からなかったので調べました。

型引数とは

型引数はクラス名・インタフェース名に続いて指定します。型引数は<>で囲み、その中に1つ以上の型変数を定義します。型引数を複数個指定する場合にはカンマで区切ります。

public interface Map<K, V> {
    //...
}

KとVが型変数です。型変数の命名規則は変数の命名規則と同じですが、英大文字で1字が推奨されています。定義した型変数は、implements句やextends句、メソッドの引数、返り値だけでなく、インスタンス変数やメソッド内部で用いることもできます。

public class Value<V> {
    private V value = null;
    public Value(V value){
        this.value=value;
    }
    public V getValue(){
        return this.value;
    }
    public String toString(){
        return value.toString();
    }
}

型変数Vで定義されるvalue変数の型は、このクラスの作成時には決まっていませんが、java.lang.Objectのメソッドだけは呼び出すことが可能です。これは全てのクラスが暗黙的にjava.lang.Objectを継承しているからです。

この理屈が分からない場合はClass Hierarchy (Java Platform SE 8 )を見てください。全てのクラスはjava.lang.Objectを継承していることが分かります。

型引数の継承

もし次のようにextendsで型引数を継承した場合、java.lang.Object以外のクラスのメソッドを呼び出すことも可能です。

import java.util.Date;
public class Term<S extends Date, E extends Date> {
    private S date1 = null;
    private E date2 = null;
    public Term(S date1, E date2){
        this.date1 = date1;
        this.date2 = date2;
    }
    public long calculateSpan(){
        return date1.getTime() - date2.getTime();
    }
}

この例では、型変数SとEはjava.util.Dateのサブクラスに指定しています。つまり、型変数SとEはDateクラスの機能を含むことになります。従って、java.util.Dateのメソッドも呼び出すことができるようになります。

呼び出し方

型引数を使用したクラスを呼び出すには以下のように指定します。

Term<Time, Time> period = new Term<Time, Time>(new Time(), new Time(5000));

「型引数の継承」の項で作成したTermクラスを使用します。Termクラスの型引数SとEにTime型を渡します。Time型はDate型のサブクラスです。

ワイルドカード

型引数にはワイルドカードを使用することができます。これを利用することで型変数を特定しない操作ができます。ワイルドカードは?で表し、メソッド宣言に使用します。

class Value<T> {
    private T value;
    
    public Value(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    
    public void setValue(T value) {
        this.value = value;
    }
}

class Test {
    public void printValue(Value<?> obj) {
        System.out.println(obj.getValue());
    }
    
    public static void main(String args[]) {
        printValue(new Value<String>("dog house"));
        printValue(new Value<Integer>(new Integer(10)));
    }
}

printValueメソッドの引数に注目してください。Value<?> objはValueクラスの型引数に指定する型は何でも構わないという意味になります。printValueメソッドでは、T型の実装に依存するような振る舞いをすることはできません。

注意点として、ワイルドカードの型変数に対して代入する操作はコンパイラによって禁止されています。次のコードはコンパイルエラーになります。

public static void main(String args[]) {
    Value<?> obj = new Value<String>("dog house");\
    obj.setValue("cat house");
}

境界ワイルドカード

単にワイルドカードを使用するだけでは本来ジェネリクスが持っている「不変」という性質の利点を捨ててしまうことになります。

そこで、「上限」と「下限」という一定の制限をワイルドカードに与えることで、特定クラスのサブクラスであることを強要、またはスーパークラスであることを強要することができます。

この「上限」と「下限」が与えられたワイルドカードを「境界ワイルドカード」と呼びます。

上限(サブクラスであることを強要する)

ワイルドカードがサブクラスであることを強要します。?extendsを使用します。

例えば、Number型を上限として設定したい場合は次のようになります。

class Value<T> {
    private T value;
    
    public Value(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    
    public void setValue(T value) {
        this.value = value;
    }
}

static void printValue(Value<? extends Number> obj) {
    System.out.println("type = " + obj.getValue().getClass());
    System.out.println("int value = " + obj.getValue().intValue());
    System.out.println("double value = " + obj.getValue().doubleValue());
}

public static void main(String args[]) {
    printValue(new Value<Integer>(100));
    printValue(new Value<Double>(1.23456789));
    
    //以下はエラーになる
    //printValue(new Value<String>("Kitty"));
}

Valueクラスの型変数Tは不明な型ですが、Numberクラス、またはNumberクラスのサブクラスでなければならないという境界が設定されます。そのため、型変数TがString型である場合はコンパイルエラーとなります。

下限(スーパークラスであることを強要する)

ワイルドカードがスーパークラスであることを強要します。?superを使用します。

例えば、String型を下限として設定したい場合は次のようになります。

public class GenericsWildcard {

	static void printValue(Value<? super String> obj) {
	    System.out.println("type = " + obj.getValue().getClass());
	    System.out.println("int value = " + obj.getValue() + "\n");
	}

	public static void main(String args[]) {
	    printValue(new Value<String>("dog"));
	    printValue(new Value<Object>(new Integer(10)));

	    //以下はエラーになる
	    //printValue(new Value<Integer>(new Integer(10)));
	}
}

class Value<T> {
    private T value;

    public Value(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Valueクラスの型変数Tは不明な型ですが、Stringクラス、またはStringクラスの親クラスでなければならないという境界が設定されます。そのため、TにString型のサブクラスや、他の関係のないクラスを指定することはできません。

境界ワイルドカードの使い分け

Java言語は、境界ワイルドカードとして「上限」(? extends クラス名)と「下限」(? super クラス名)の両方をサポートしています。どちらを使うのか、そしてそれをいつ使うのかを、どのようにして判断すれば良いのでしょうか。

これに関してはgetとputの原則(get-put principle)という単純なルールがあり、このルールによって、どちらの種類のワイルドカードを使用するべきか判断することができます。

  • 構造から値を取得する(get)だけの場合には「上限」を使用します。
  • 構造の中に値を格納する(put)だけの場合には「下限」を使用します。
  • 両方を行いたい場合にはワイルドカードを使用してはいけません。

参考資料
書籍 Effective Java
1. ジェネリクス (3) | TECHSCORE(テックスコア)
ワイルドカード
Java の理論と実践: Generics のワイルドカードを使いこなす、第 2 回