Javaジェネリクス入門、その2

2018年8月3日

これは、Javaジェネリクス入門、その1の続編である。

前回のおさらい

ざっと前回をおさらいする。

  • ジェネリクス以前のJavaのリストには、いわば生物を格納する機能しかなく、ライオンも魚も人間も何でも格納できた。
  • リストを名簿として使いたい場合にも、人間に限定することができなかった。そこで、魚を入れてしまうという間違いがおきかねない。
  • さらに、リストから要素を取り出すときには「人間」にキャストしなければならなかった。

なぜキャストが必要かといえば、

  • Java処理系では常に型を明確にしなければならないが、それは型システムの強大なパワーを得るためである。型の扱いが曖昧なスクリプト言語ではこれは決して得られない。型を明確にするという面倒さというデメリットと引き換えに、大きなメリットが得られるのである。

ジェネリクス以前の解決策としては、

  • 我慢してリストをこのまま使うか、あるいはリストをラップした新しいクラスをいちいち作るしかなかった。

ということである。そして、ジェネリクス以後のリストは以下のように使用できるようになったのだ。

List<String>list = new ArrayList<>();
list.add("test");
String a = list.get(0);

ジェネリクスの意味

つまり、ジェネリクスの意味としては、こんな風にかけるだろう。

  • あらかじめ一般的な機能を実現する物を作っておき、使うときにそれを何に使うかを限定する。

これを、具体的に例えを用いて言えばこうなる。

  • あらかじめ生物が何でも入るリストという機能を作っておき、使うときには人間のリスト、魚のリストと言ったように限定して使う。

ということである。こうしておけば、わざわざ個別に「人間のリスト」「魚のリスト」と言ったものを作成する必要はなくなる。

簡単なジェネリクスのプログラム

最も簡単な例を考えてみる。以下のプログラムは、ただ一つだけ要素を格納するクラスであり、それを「文字列を格納するもの」「整数を格納するもの」として使用している。機能を明確にするために、記述は少々冗長になっている。

public class Sample {

  static class General<T> {
    T object;
    void put(T object) { this.object = object; }
    T get()            { return object; }
  }

  public static void main(String[]args) {
    // 文字列を格納するものとして使う
    General<String>foo = new General<>();
    foo.put("this is test");
    String strResult = foo.get();
    System.out.println(strResult);

    // 整数を格納するものとして使う
    General<Integer>bar = new General<>();
    bar.put(123);
    int intResult = bar.get();
    System.out.println(intResult);
  }
}

さて、「static class General<T>」のTの部分を型パラメータと言い、この部分が「使用されるときの対象の型」となるわけだ。型パラメータのついたクラスを「ジェネリックなクラス」と言うらしい(らしい、というのは何百・何千とこんなクラスを書いていても名前など意識したことが無いからである)。

そして、Generalというクラスの定義の中で、このTを「実際に使用されたときのクラス」として用いることができる。つまり、以下の部分である。

    T object;
    void put(T object) { this.object = object; }
    T get()            { return object; }

想像できるように、もしTがString(文字列)の場合、つまり「General<String>」とした場合には、以下のように「変身する」わけである。

    String object;
    void put(String object) { this.object = object; }
    String get()            { return object; }

これが型パラメータの威力である。単にTと記述しておき、使うときにそこに具体的なクラスを記述してやる。すると、Generalの中身が一挙に変身するというわけだ。

別にTでなくてもいい

上の例のように型パラメータが一つの場合には(複数の場合もある)、パラメータ名としてTを使うのが習わしだが、別に何でもよい。こんな風に書いても一向に構わない。

  static class General<Youso> {
    Youso object;
    void put(Youso object) { this.object = object; }
    Youso get()            { return object; }
  }

型パラメータの名前が何でろうが、機能的には何の代わりもない。これを使う方のプログラムとしては全く同じである。

  public static void main(String[]args) {
    // 文字列を格納するものとして使う
    General<String>foo = new General<>();
    foo.put("this is test");
    String strResult = foo.get();
    System.out.println(strResult);

    // 整数を格納するものとして使う
    General<Integer>bar = new General<>();
    bar.put(123);
    int intResult = bar.get();
    System.out.println(intResult);
  }

この回のまとめ

「なんだ、これだけかよ」と思うなかれ、これは本当に基礎なのである。ジェネリクスの本当のパワーを得るには、こんなものでは済まない(そして、私自身も説明しきれるかわからない)。ともあれ、今回のまとめとしては以下である。

定義としては、

  • 型パラメータ付のクラスを定義する。これをジェネリッククラスと言う。
  • その型パラメータ名称を、実際に使われる際の型として、クラス内のあちこちで使う。

使用時には、

  • 実際に使うときは、型パラメータの代わりに実際の型を指定する。
  • すると、先の型パラメータが、その型に変身したかのような効果を持つ。

ということである。