読者です 読者をやめる 読者になる 読者になる

Genericsのワイルドカードを使ったコピー・ファクトリ・メソッド

JAVALOBBYに、「Java Generics Wildcard Capture - A Useful Thing to Know」という記事が掲載されています。コピー・ファクトリ・メソッドやコピー・コンストラクタを作るときに、Genericsワイルドカードを使うテクニックを紹介したものです。コピー・ファクトリ・メソッドというのは、既存のオブジェクトのパラメータをコピーした新しいオブジェクトを生成するメソッドということのようです。

コードを簡略化して書くと、

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

こういうクラスがあったときに、

public class FieldUtil {
    public static Field<?> copy(Field<?> field) {
        Field<?> objField = new Field<?>();
        objField.setValue(field.getValue());
        return objField;
    }
}

こういうメソッドを作りたいけど、このままじゃコンパイルできないから、どうしたらいいだろう、ということです。解決策としてはヘルパー・メソッドを使う方法が紹介されているのですが、それはひとまず置いておいて、なぜこれがダメなのかを考えてみます。

肝となっているのは

objField.setValue(field.getValue());

この部分です。これが返すエラーが「FieldのsetValue(capture#3-of ?) の引数にjava.lang.Objectは適用できないよ」というような感じのものです(記事中ではcapture#4-of ?となっていますが、おそらく普通はObjectになると思います)。これは一体何でしょうか。

copy()メソッドの引数であるfieldの型はワイルドカードを持っています。この場合、コンパイラはfieldがFieldとなるようなTを推測し、キャプチャと呼ばれるそのTの型ためのプレースフォルダを作成します。「capture#3-of ?」というのは、fieldの型に含まれるワイルドカードのキャプチャに対して割り当てられた名前を指しています。この時点ではsetValue()に渡すべき正式な引数の型がまだわかっていないので、キャプチャが使われているわけです。

一方でfield.getValue()の方ですが、コンパイラはこの戻り値の?を未知のTという型であると推測します。?は「? extends Object」のことなので、コンパイラはfield.getValue()の戻り値はObjectであると判断します。つまり「capture#3-of ?」ではないし、「capture#3-of ?」にObjectが適用できるかも検証できないことから、前述のエラーメッセージになるというわけです。

このエラーの回避方法として紹介されているのが、次のようなヘルパー・メソッドを用意することです。これはGenericsワイルドカード使用における「キャプチャー・ヘルパー」として知られるイディオムです。

public class FieldUtil {
    public static Field<?> copy(Field<?> field) {
        return copyHelper(field);
    }
    private static <T> Field<T> copyHelper(Field<T> field) {
        Field<T> objField = new Field<T>();
        objField.setValue(field.getValue());
        return objField;
    }
}

copyHelper()はジェネリック・メソッドなので、その引数であるFieldのTは、任意の未知の型を表すことができます。copy()メソッドのfieldの型は未知のXを持ったFiledであることは、コンパイラによる型推論によって確定しています。したがってcopy()からcopyHelper()を呼び出すことが可能です。copyHelper()中のfield.getValue()の戻り値の型は、ObjectではなくてTなので、前の例のようなコンパイラの制約には引っかからないというわけです。