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

2018年6月23日

※このシリーズは/tag/ジェネリクスに投稿一覧があるので参照してほしい。

Javaジェネリクスで検索してみると、やはりあまりに一方的な解説、わけのわからない解説が多いので、ここでは初心者向けにわかりやすく解説してみることにする。ただ、全くの前提無しというわけにはいかない。ある程度、Javaプログラムが書けて、Eclipse-IDE等を使える者を対象とする。

ジェネリクスの導入と複雑さ

Javaにジェネリクスが導入されたのは、たしか1.5あたりだったと思う。10年位前だろうか?それ以前のJavaにはジェネリクスの仕組みがなかった。ちなみに、今資料を見つけ出せないのだが、ジェネリクスの理論を作り出したのは、たしか日本の計算機科学研究者だったと思う。

というと、非常に難しいもののように思うが、たしかに深く掘り下げるとかなり難しいものではある。しかし、基本的なことを押さえておけば、非常に便利なものであることがわかるはずだ。

ジェネリクス以前は何が不便だったのか?

さて、ジェネリクス以前は何が不便だったのかがわからなければ、その便利さもわからないだろう。簡単な例としては、よくあるリストである。

List list = new ArrayList();
list.add("test");
list.add(new Integer(125));
list.add(new Character('あ'));

ジェネリクス以前のリストには、何でも格納できた。文字列だろうが、数値だろうが、キャラクタだろうが。そして、このリストから何かを取り出して使うには、キャストしなければならない。

String a = (String)list.get(0);

まず、このプログラムの問題は二つある。

  • リストを作った人はそこに文字列のみを格納するつもりだったのかもしれないが、何でも格納できてしまう
  • さらに、リストから取り出して要素を使うには、いちいちキャストしなければならない。

なぜなら、ジェネリクス以前には、リスト(この場合にはArrayListあるいはList)にはObjectが格納できてしまうからである。Objectはすべてのクラスの最上位にあり、Java内のすべてのクラスはObjectであるので、結局すべてのものがObjectとして扱えることになる。

クラスの階層構造

さて、ここがわからないとこの先の話に進めないので、簡単におさらいをしておく。Objectがすべてのオブジェクトの上位にあるというのはいかなる意味か?例えて言えば、次のような意味だ。

  • 仮にいま、生物がObjectであるとする。
  • Animal(動物)はObjectの下の階層にある。
  • Fish(魚)はObjectの下の階層にある。
  • Human(人間)はAnimalの下の階層にある。

といったように、Javaのクラス階層はObjectを頂点とする階層構造になっている。つまり、クラスとは「種」のことを意味しており、種は階層構造になっている。そして、例えばHumanという種に属する私やあなたのような実体をインスタンス、あるいは単にオブジェクトと呼ぶことがある。

つまり、クラスとは抽象的な概念あるいは型紙であり、インスタンスとはその実体と思えばよい。「種」という抽象的な概念があり、私やあなたのような「実体」があるわけだ。そして、「種」は生物>動物>人間といったように階層構造をなしている。

生物リストに格納できるもの

これを先のリストに当てはめてみれば、以下と解釈できる。

  • ジェネリクス以前のJavaのリストには、「生物」が格納できた。
  • だから、魚もLionもTigerもHumanも何でも格納できた。
  • しかし、リストを作った人の意図としては、これが名簿だったかもしれない。つまり人間だけ格納したい。しかし、その意図とは異なり、何でも格納できてしまう。
  • そして、リストから要素を取り出すと、それは「生物」になっているため、「人間」というキャストをつけなければならない。

というわけだ。

なぜ「人間」にキャストしなければならないのか?

ここでスクリプト言語をやっている者であれば疑問に思うだろう。要素を取り出した後で、なぜそれを「人間」とキャストしなければならないのか?スクリプト言語であれば、キャストなどせずに、人間固有のメソッドを呼び出せるのにと。例えば以下のように、人間の名前を取得するには以下のように書けばよい。

list.get(0).getName()

ここは、Javaのような強い型付けの言語と、いい加減なスクリプト言語の多いなる違いである。「いい加減」と書くと立腹する向きもあろうが、このようにしか書きようがない。Javaでは常にオブジェクトの型が明示されていなければならないのである。

この件については、スクリプト言語とはを参照されたい。

Javaでは常に型が必要

ともあれ、Javaでは処理系(コンパイラやIDE)が常に型を把握している必要がある。これが、スクリプト言語などには望むべくも無い大きなパワーをもたらす。例えば、

  • 存在しないメソッドの呼び出しを記述すると、コンパイル時点でエラーとなり、実行せずともエラーが判明する。
  • しかし、それ以前に、現代のIDE環境ではコンパイル以前にIDEでのプログラム記述時点でエラーが判明する。
  • さらにIDEでは、その型について呼び出せるメソッドの候補を自動的に表示してくれる。

これらはスクリプト言語では真似のできない芸当である。強い型付けの言語であることのメリットは他にも膨大にあるが、Javaのような言語で常に型を明確にしなければならない(面倒なことをしなければならない)のは、これらの強大なパワーを得るためなのである。

このあたりは、スクリプト言語使いには理解されないところである。つまり、もしあなたがスクリプト言語から来ており、スクリプト言語のやり方に慣れているのであれば、発想を転換しなければならない。さもなければ、ジェネリクスはおろかJavaらしいプログラムを書くことさえできないだろう。

以前のJavaでの解決策

ともあれ、ジェネリクス以前のJavaにおいては、用意されているリストに何でもオブジェクトが格納できてしまう。これでは非常に具合がよろしくない。この解決策としては、専用のリストクラスを作ることしかなかった。

例えば、ArrayListを内部に持った文字列専用のクラスを作成する。

class StringList {
  List list = new ArrayList();
  void add(String a) { list.add(a); }
  String get(int index) { return (String)list.get(index); }
}

などとするわけだ。これを使えば、入れるときも文字列だけ、出すときも文字列として出てくる。

StringList list = new StringList();
list.add("test");
...
String a = list.get(0);

しかし、少し考えれば分かる通り、リストとして管理したいクラスの種類が増えるたびに、こういったクラスを作成しなければならない。これはいかにも面倒だ。つまり、ここでの悩みは、

  • 関係無いものが放り込まれる危険があり、出す時のキャストをしても、そのままArrayListを使い続けるか。あるいは、
  • 面倒だが、対象とする要素クラスごとに専用のリストクラスを作成するか。

という二つに一つの選択肢しか無かったのである。

ジェネリクスの解決策

そこで登場したのがジェネリクスだ。ジェネリクスでは「型パラメータ」というものを使って、この場合ならリストに格納できるクラスを指定できる。

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

これも最近のJavaではArrayListの方の型は省略できるようになっている。つまり、

List<String>list = new ArrayList<>();

と記述することができる。

ともあれ、これでリストが文字列専用として「変化」するのである。ジェネリクスが登場した時のことを今でも思い出すのだが、Javaプログラマにとっては夢のような機能であった。