The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目27:ジェネリックメソッドを利用する

ポイント

ジェネリックメソッド

前回までは主にクラスのジェネリクス化について書いてきました。 今回はメソッドジェネリックス化についての話をします。

例として2つのセットを引数としてとり、それらの和集合を返すメソッドを考えます。

下記のコードはオリジナルのソースコードです。 原型を利用しているためコンパイル警告が発生します。

public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}
// => 原型を利用しているため各所からコンパイル警告が発生する

このメソッドジェネリック化するとこのようになります。 型パラメータのリストは、メソッドの修飾子(public static)とメソッドの戻り値型(Set<E>)の間に入ります。

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return resutl;
}

こうすることで、コンパイル警告が発生しなくなります。 すなわち、ジェネリックスによって、union() 内の Set の要素は同じ型 E であることが保障されるのです。

このような static ユティリティメソッドは特にジェネリック化に向いています。 実際、java.util の Collections のアルゴリズム系のユティリティメソッドはすべてジェネリック化されています。

この例では登場する3つの Set の型パラメータはすべて同じである必要がありました。 しかし、境界ワイルドカード型を利用すると、この制限を緩めることができます。 境界ワイルドカード型についてはは項目28で解説する予定です。

型推論

ではさきほど定義したジェネリックス化された union を使ってみましょう。

Set<String> guys = new HashSet<String>(Arrays.asList("Tom", "Dick", "Harry"));
Set<String> stooges = new HashSet<String>(Arrays.asList("Larry", "Moe", "Curly"));

Set<String> aflCio = union(guys, stooges);
// => union() には実型パラメータを与える必要がない

このように、union メソッドでは型パラメータを明示的に指定する必要はありません。 これはコンパイラ型推論を行い、自動的に実型パラメータを決定して検証を行っているためです。

一方、ジェネリックスクラスのコンストラクタは、呼び出す際に型パラメータの値を明示的に指定する必要がありました。

Map<String, List<String>> map = new HashMap<String, List<String>>();
// => コンストラクタに明示的に実型パラメータを与えている

このように毎回コンストラクタに実型パラメータを記述するのは手間です。 実は、ジェネリックメソッド型推論を行う性質を利用して、実型パラメータを指定せずにインスタンスを生成する方法があります。

これをジェネリック static ファクトリーメソッドと呼びます。

// ジェネリック static ファクトリーメソッド
public static <K, V> HashMap<K, V> newHashMap() {
    return new HashMap<K, V>();
}

このファクトリーメソッドを使えば、明示的に型パラメータを書く必要はありません。 コンパイラが、ジェネリックメソッドで有効な型推論を行ってくれるためです。

Map<String, List<String>> map = newHahMap();
// => newHashMap() には実型パラメータを書かなくて良い

ダイヤモンド演算子

実は、上記のジェネリック static ファクトリーメソッドは、現在において有用ではありません。

Java 7 から、コンストラクタの型パラメータは省略可能になったためです。 これはダイヤモンド演算子と呼ばれ、コンパイラ型推論を行って自動で型を検証します。

Map<String, List<String>> map = new HashMap<>();
// => ダイヤモンド演算子。コンストラクタの実型パラメータは省略可能

2006 年に書かれた Effective Java ではこのように書かれています。

ジェネリックメソッドが呼び出された時に行う型推論を、ジェネリック型のコンストラクタ呼び出しの際に言語が行うと良いのですが。

これは Java 7 によって現実になりました。

ジェネリックシングルトンファクトリー

ジェネリックスはイレイジャによって実装されているため、実行時に型パラメータに関する情報は存在しません。

この性質を利用して『要求された個々の型パラメータに対して、単一のオブジェクトを繰り返し利用する』というパターンを実現することができます。 これをジェネリックシングルトンファクトリーと呼びます。

例えば、何らかの型 T を受け取って、その型の値を返す関数を記述している次のインタフェースがあります。

public interface UnaryFunction<T> {
    T apply(T arg);
}

この関数の実装は、単に引数で渡ってきたオブジェクトをそのまま返すだけなので状態は持ちえません。 そこで、型パラメータごとに UnaryFunction 生成するのではなく、一つだけの関数オブジェクトを作り、常にそれを返すという実装が可能です。

繰り返しになりますが、ジェネリックスはイレイジャで実装されているため、実行時には型情報は消えてしまいます。 一つのオブジェクトを用意しておき、使い回しても問題ないのです。

private static UnaryFunction<Object> IDENTITY_FUNCTION =
    new UnaryFunction<Object>() {
        @Override public Object apply(Object arg) { return arg; }
    };

// IDENTITY_FUNCTION は状態を持たず、型パラメータは非境界なので、
// 1つのインスタンスを共有することは安全である。
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
    return (UnaryFunction<T>) IDENTITY_FUNCTION;
}

この実装はコンパイルすると無検査キャスト警告が発生します。 ただし、IDENTYTY_FUNCITON はどのような T がきても、単に引数を返すだけなので安全です。 項目24の提言に従って、無検査警告を抑制します。

このシングルトンは以下のように利用します。

String[] strings = {"jute", "hemp", "nylon"};
UnaryFunction<String> sameString = identityFunction();
for (String s : strings) {
    sameString.apply(s);
}

Number[] numbers = {1, 2, 3};
UnaryFunction<Number> sameNumber = identityFunction();
for (Number number : numbers) {
    sameNumber.apply(number);
}

UnaryFunction someString と UnaryFunction sameNumber で、同じオブジェクトが使い回されていることがポイントです。

実際、ジェネリックシングルトンファクトリーは Collections.reverseOrder で使われています。

再帰型境界

型パラメータが、その型パラメータ自身と関係する、何らかの制約で制限されていることがあります。 これを再帰型境界と呼びます。

最もよく知られた再帰型境界は Comparable インタフェースです。

public interface Comparable<T> {
    int compareTo(T o);
}

Comparable インタフェースは何らかの型 T と比較できることを示すインタフェースです。 多くの場合、このインタフェースを実装する型は、自分自身の型と比較可能であることを示します。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
   // => String 型 は String 型と比較可能
   ...
}

public final class Integer extends Number implements Comparable<Integer> {
   // => Integer 型 は Integer 型と比較可能
   ...
}

リストをソートしたり、検索したり、最小値・最大値を計算するためには、リスト内の要素同士を比較できることが必要です。 これを相互比較可能であると言います。

この相互比較可能であることを示すジェネリックスは以下の通りです。

public static <T extends Comparable<T>> T max(List<T> list) {
}

これは

リストの要素 T は Comparable<T> のサブタイプであること

を要求しています。すなわち、

リストの要素 T は自身の要素 T と比較可能であること

という相互比較可能性を要求していることになるのです。

再帰型境界はどんどん複雑になっていく可能性があります。 ただし、実際には複雑なケースはそれほど多くありません。

この後の項目で学習するワイルドカードの変形形式を学ぶと、多くの再帰型境界を扱うことができるようになります。

感想

ジェネリック static メソッドはさすがにやりすぎ感がある(もちろん、今はダイヤモンド演算子を使うわけだが)。

(c) The King's Museum