ジェネリクスなクラスについて

相変わらずEffective Javaを読んでいる。自分でジェネリクスなクラスを作る場合の書式と利点が分からなかったので調べた。後から見返して分かり辛いと思ったところは加筆する予定。
 
ちなみに基本的なジェネリクスの使用方法である、リスト型 + ジェネリクスの使い方は過去の記事でまとめたので、そちらを参照してほしい。
Javaでは配列よりリスト(ジェネリクス)を選ぶ
 

型引数とは

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

public interface Map<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();
    }
}

 
この例ではインスタンス変数として、型引数で指定された型のオブジェクトvalueを指定している。またtoString()メソッド内で、valueのメソッドを呼び出している。valueの型はこのクラス作成時には一意には決まっていないが、必ずjava.lang.Objectのインスタンスであるので、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();
    }
}

 
extends句を用いて、型引数としてjava.util.Dateのサブクラスしか認めないようにしている。この場合には、インスタンス変数date1もdate2もjava.util.Dateのインスタンスになる。従ってjava.util.Dateのメソッドも呼び出すことができる。
 
次のように指定することで、上記のジェネリクスなクラスを使用することが出来る。

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

 
このようにすると、Termクラスの型引数S, EにDate型のサブクラスであるTime型を渡すことができ、渡されたTime型はTermクラスの内部で使用される。
 

ワイルドカードで柔軟な型引数を作る

ジェネリクスのワイルドカードは?記号で表し、メソッドの呼び出し時に型引数として利用することができる。ワイルドカードを指定することで、型変数を特定しない操作が出来るようになる。
ジェネリクスのワイルドカードは次のように利用する。

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メソッドの引数に注目してほしい。引数objはワイルドカードを用いたValue型として宣言されている。この場合、Valueクラスの型引数に指定する型は問わないという意味になる。
 
printValue()メソッドでは、T型の実装に依存するような振る舞いがないということにも注目してほしい。T型の実体は少なくともObject型を継承していることは保障されているため、obj.getValue()メソッドを呼び出す操作は認められている。
 
しかし、一方でワイルドカードの型変数に対して代入する行為はコンパイラによって禁止されている。例えば、次のコードはコンパイルエラーになる。

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

 

上限と下限を設定する

しかし、単にワイルドカードを使用するだけでは本来ジェネリクスが持っている不変という性質の利点を捨ててしまうことになる。そこで、ワイルドカードに「上限」と「下限」を設定し、一定の範囲をワイルドカードに与えることで、特定のクラスのサブクラスであることを強要、またはスーパークラスであることを強要することができる。この「上限」と「下限」が与えられたワイルドカードを「境界ワイルドカード」と呼ぶ。
 

上限境界ワイルドカードを設定する

境界ワイルドカードのうち、指定した型と同じかその型を継承するサブクラス(サブインタフェース)であることを強要するものを「上限境界ワイルドカード」と呼ぶ。
ワイルドカードに対して上限を設定するには、extendsキーワードを使用する。例えば、Number型を上限として設定したい場合は次のようになる。
 

Value <? extends Number>

 
実際にコードに当てはめると次のようになる。

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型を下限として設定したい場合は次のようになる。
 

Value <? super String>

 
実際にコードに当てはめると次のようになる。

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)));
}

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

上限と下限どちらを使うか

Java言語は、上限境界ワイルドカードと下限境界ワイルドカードの両方をサポートしているため、どちらを使うのか、そしてそれをいつ使うのかを、どのようにして知ればよいだろうか。これに関してはgetとputの原則(get-put principle)という単純なルールがあり、このルールによって、どちらの種類のワイルドカードを使うのかを判断することができる。
構造から値を取得する (get) だけの場合には 上限境界(extends)ワイルドカードを使い、構造の中に値を格納する(put)だけの場合には下限境界(super)ワイルドカードを使い、そして両方を行う場合にはワイルドカードを使ってはならない。
 
 
以上
 
 
参考
書籍 Effective Java
1. ジェネリクス (3) | TECHSCORE(テックスコア)
ワイルドカード
Java の理論と実践: Generics のワイルドカードを使いこなす、第 2 回

Article written by

Comments are closed, but trackbacks and pingbacks are open.