ジェネリックな設計 基礎編

11/10に開催されたJJUG CCC 2012 Fallジェネリクスについてセッションを行いました。

このエントリはセッション内容を補足するものです。本セッションはジェネリックなクラスの設計を行えるようになって欲しいという狙いで話をしました。ジェネリックなクラスを利用できるというのは前提条件として書いてます。入門的な内容であれば

Javaジェネリクス再入門 - プログラマーの脳みそ

を参考にしてください。

セッション資料はこちら ジェネリクスの基礎と応用 JJUG CCC 2012 Fall

ジェネリクスのスコープ

まずジェネリクスのスコープの話から入ります。Javaジェネリクスには2つのスコープがあります。

  • メソッドをスコープとした型変数
  • インスタンスをスコープとした型変数

後者はおなじみの

public interface List<E>

などの型変数です。

これに対して

public static <T> void hoge(T t) {}

というようにメソッドだけを範囲とする型変数を宣言できます。このようなメソッドをジェネリックメソッドと呼びます。

ジェネリックメソッドのI/O

綺麗に設計されたstaticメソッドは、引数を渡し、戻り値が返るというシンプルな構造をしています。メソッドの内部はブラックボックスになっていて、ただ入力をすれば出力が得られるというものです。それ以外への影響を及ぼしません。

ジェネリックメソッドでの型変数というのはこのようなメソッドに対してInとOutのデータの型の関連性を表現します。

  • 複数ある引数間で型の関連性を示す
  • 引数と戻り値の間で型の関連性を示す

ということになります。

実例を見てみましょう。java.util.Collectionsにあるstaticメソッド群から例を挙げます。

なお、Javaの標準APIソースコードJDKのインストールフォルダにあるsrc.zipに含まれています。Windows環境のEclipseからだとCtrl+TでOpen Typeダイアログを開き、java.util.Collectionsと入力し、JREのCollectionsクラスを選択します。Class File EditorでCollectionsクラスが開かれたらAttache Source... ボタンを押し、External locationを選択、External Fileボタンを押してJDKのsrc.zipを選択しましょう。ソースコードが見れるようになると思います。

リスト内に出現する指定された値をすべてほかの値に置き換えます。

Collections (Java Platform SE 6)
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)

replaceAllは戻り値がなく、引数が3つあるメソッドです。型変数Tを使用して引数の型がList型とT型であるという関連性が表現されています。

指定されたオブジェクトだけを格納している不変のセットを返します。

Collections (Java Platform SE 6)
public static <T> Set<T> singleton(T o)

singletonでは引数の型と戻り値の型の関連性を表現しています。これらに対して

デフォルトの乱数発生の元を使用して、指定されたリストの順序を無作為に入れ替えます。

Collections (Java Platform SE 6)
public static void shuffle(List<?> list)

shuffleメソッドは引数をひとつしか持たないため、引数間や戻り値との間の肩の関連性を示す必要がありません。このようなケースでは型変数を用意する意味がないと言えます。

インスタンスの型変数

さて、まずは簡素なジェネリックメソッドでジェネリックな設計のイメージを掴んでもらいました。Javaのコレクションフレームワーク(java.utilパッケージにあるListやMapなど)でお馴染みのジェネリックなクラスに思考を拡張してみましょう。

ジェネリックメソッドでは

  • メソッドの引数間
  • メソッドの戻り値

で型の関連性を示すのでした。インスタンスの型変数は更に

  • 複数のメソッド間
  • 公開されているフィールド
  • 内部クラス

といったものの間で型の関連性を示すことになります。実例としてお馴染みのArrayListから抜粋してみましょう。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, 略 {
    public E get(int index) { 略 }
    public boolean add(E e) { 略 }

    public Iterator<E> iterator() {
        return new Itr();
    }
    private class Itr implements Iterator<E> {
        public E next() { 略 }
    }
}

getの戻り値の型とaddの引数の型が同じであることが表現されています。また、iterator() メソッドではprivateな内部クラスItrのインスタンスを返していることがわかります。このItrのクラス宣言では型変数の宣言が行われていないことに注目してください。 next()で返すE型というのは内部クラスItrのものではなく、外部クラスであるArrayListによって宣言された型変数Eなのです。

文法の混乱を解く鍵 3種類の山括弧

さて、設計の概要に触れたところでJavaジェネリクスの構文について理解を深めましょう。Javaジェネリクス再入門 - プログラマーの脳みそでも触れていますが、山括弧<>には使用する場所によって3種類あり、その3種類で書けるものが異なります。

  • 型変数の宣言
  • 型変数のバインド
  • パラメータ化された型(parameterized type)

型変数の宣言

型変数の宣言はクラス宣言時に型変数を宣言するケースと

calss Hoge<T> { }

ジェネリックなメソッドで型変数を宣言するケースがあります。

public <T> void hoge(T param) { }

これら、型変数の宣言では境界を宣言するためにextendsキーワードを用いることがあります。

calss Hoge<T extends Piyo> { }

型変数のバインド

型変数に具体的にどの型であるかを指定することをバインドと言います。型変数のバインドには3種類あって

  • new ArrayList(); など new時に行うもの
  • HogeClass.hoge(); などジェネリックメソッドの型変数に対してメソッド呼び出し時に行うもの
  • class StringList extends ArrayList といったようにクラスの継承時に行うもの

があります。型のバインドでは具象型を指定するか、すでに宣言されている有効な型変数を使うことができます。

public class BindTest<T> {
	List<T> list = new ArrayList<T>();
}

この例はクラスBindTestで宣言した型変数Tでnew ArrayList();というように型のバインドを行なっています。

ArrayListのクラス宣言も例に挙げてみましょう。

public class ArrayList<E> extends AbstractList<E>

この場合、class ArrayListは型変数の宣言で、extends AbstractListは、先程宣言された型変数EをAbstractListにバインドしますよ、という意味になります。

継承時に具象型でバインドすると以下のようになります。

public class StringList extends ArrayList<String> {}

このように、継承時に固定の型でバインドすることでクラスStringListは型変数を持たないクラスとなりました。

ListのListのMapといったような複雑なジェネリクス宣言をしてしまっている場合、意味のあるまとまりでクラスを作ってやることでソースコードの見通しがとてもよくなることがあります。クラス作成をサボらないほうが幸せになれます。

パラメータ化された型(parameterized type)

パラメータ化された型の例は

List<String> list = null;

といった場合のListのことです。

このパラメータ化された型ではワイルドカードを使用することができます。

List<? extends Hoge> exHogeList = new ArrayList<>();
List<? super Hoge> superHogeList = new ArrayList<>();

ここでは型変数の宣言はできません。型変数の境界の宣言はといったように型変数に対して extends を続けますので、ワイルドカードと極めて似た形になりますが別物です。

また、ワイルドカードでは<? super Hoge>といったsuperの指定ができますが、型変数の境界の宣言ではsuperはつかえません。

ややこしい例として

public class Hoge<T extends Piyo<? extends B>>

といったクラス宣言があったとしましょう。このとき、Piyoがパラメータ化された型であってひとまとまりです。一見して型変数を宣言する<>の中にワイルドカードである?が存在するように見えます。

このように型を指定する箇所というのはパラメータ化された型を書くことができてしまう。これにより、ジェネリクスの構文は見るものに相当な混乱を与えているとおもいます。

基礎編のまとめ

ジェネリクスの概要として、ブラックボックスをまず用意して、そのブラックボックスのIn/Outに対して型変数を使って型の関連性を示す、ということを言いました。まずはうまくブラックボックスを作らないと始まりません。そういう意味で、まずはオブジェクト指向で綺麗な設計ができないことには型変数の導入はうまくいきません。

ジェネリクスの構文については似て非なる3種の山括弧があり、しかもそれらが入れ子になって現れたりするため、なれるまでは構文解析がなかなか行えません。

これに関しては、3種の山括弧があるのだということを認識した上で、ある程度数をこなすより仕方がないのではないかと思います。

ジェネリックなクラス設計というのはオブジェクト指向によるクラス設計の延長線上にあります。設計が好きな人にはぜひとも活用していただきたいと思います。

次回予告

応用編についての解説を行いたいと思います。