この記事を書いた動機
先日、社内の後輩と話していて、「Java の Generics の何がうれしいのかよくわからない」と言われました。自分にとってはあれは神機能なんだけど、Generics が無い時代を未経験の人からすれば、何のための機能なのか、どういう背景で作られた機能なのかがわからないんだろうなと思ったのでQiitaの記事として書き残してみる気になりました。
なのでこの記事は、「いつもなんとなく Generics を使っているけど、これの何がうれしいのかがよくわからない。」という人向けの記事です。
Genericsって?
そもそも Generics って何?っていう人のために一応書いておきます。
import java.util.ArrayList;
import java.util.List;
public class Generics {
public static void main(String... args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for (Iterator<String> itr = list.iterator(); itr.hasNext(); ) {
String str = itr.next(); // 説明のためにあえて拡張forは使わない
System.out.println(str);
}
}
}
上記コードの<String>
と<>
の部分がGenericsです。これで List に格納できる型を制限しています。
(Generics は List だけではなく Map や Set、自作のクラスやメソッドなどでも使えますが、今回は List で説明させていただきます)
2004/9/30 に公開された Java 5 から導入された機能です。右辺の<>
はダイヤモンド演算子と呼ばれており、左辺の<String>
で型が明示されているので、右辺からは省略できるというものです。これは Java 7 からの機能なので、それまでは左辺と右辺の両方に<String>
と書く必要がありました。 面倒くせぇ・・・
ちなみに、ダイヤモンド演算子という名前は公式なものではありませんが、一般的に定着している名前なのでこれで通じます。
Genericsがなかった頃
Genericsがなかった頃は以下のようなプログラムを書いていました。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class NoGenerics1 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
for (Iterator itr = list.iterator(); itr.hasNext(); ) {
String str = (String) itr.next(); // 要素取得するときにキャストする必要があった!
System.out.println(str);
}
}
}
<String>
や<>
はなく、ループの中で要素を取得する際、キャストする必要がありました((String) itr.next()の部分)。このキャストがかなり厄介で、ClassCastException の温床だったんです。
どういうことか。
以下のようなプログラムがあったとします。Generics がないと1つの List に複数の型を add することができてしまいます(以下の例では String と int)。その後、ループしながら String にキャストしています。当然、2番目の要素である int の1を取得する際にも String にキャストしようとします。その結果、型が違うので ClassCastException が発生します(int → String へのキャスト)。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class NoGenerics2 {
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add(1); // Genericsで型を指定していないので、同じListに別の型をaddすることができた。
list.add("c");
for (Iterator itr = list.iterator(); itr.hasNext(); ) {
String str = (String) itr.next(); // ここでClassCastException が起こる。
System.out.println(str);
}
}
}
a
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at NoGenerics2.main(NoGenerics2.java:13)
実行結果を見ると、1番目の要素である String の"a"は問題なく取得することができ、無事に System.out されています。
2番目の要素である int の 1 を取得するタイミングで ClassCastException が発生していることがわかります。
今回程度のプログラムであれば原因はすぐわかるでしょうし、修正するのも簡単です。しかし List はしょっちゅう使われる機能であり、メソッドの引数やコンストラクタを通じていろいろなクラスをあっちこっち移動します。規模の大きいシステムであればあるほど移動範囲は広がります。そして呼び出し元に戻ったときには変な要素が追加されていたりして、それに気づかずにループして ClassCastException が発生する・・・ということが本当によく起きていたんです。今では Generics のおかげで ClassCastException を目撃する頻度はかなり減りましたが、昔はよく見るエラーの一つが ClassCastException だったぐらいです(個人的見解)。
Genericsを使うと ClassCastException を事前検知できる
上記のNoGenerics2.java
を Generics を使用するように修正するとどうなるか。
list.add(1); のところでコンパイルエラーになります。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Generics2 {
public static void main(String... args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add(1); // ここでコンパイルエラーになる!
list.add("c");
for (Iterator<String> itr = list.iterator(); itr.hasNext(); ) {
String str = itr.next(); // 説明のためにあえて拡張forは使わない
System.out.println(str);
}
}
}
このコンパイルエラーで教えてくれるのが素晴らしくて、Generics 導入前はプログラマが好きに要素を追加することができてしまっていたから ClassCastException の温床になっていました。ClassCastException は実行時例外なのでテストするまで(実際に動かすまで)バグに気づくことはできません。
それがコンパイルの段階で検知できるようになったのです!すばらしい!
一応今でも・・・
今でも Generics を使わずにプログラムを書くことはできますが、ワーニングが発生します。誰も得しないのでやめましょう。
以下のクラスの List list = new ArrayList();
から for (Iterator itr = list.iterator(); itr.hasNext(); ) {
までワーニングだらけです。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Generics3 {
public static void main(String... args) {
List list = new ArrayList();
list.add("a");
list.add(1);
list.add("c");
for (Iterator itr = list.iterator(); itr.hasNext(); ) {
String str = (String) itr.next(); // 説明のためにあえて拡張forは使わない
System.out.println(str);
}
}
}
さらに @SuppressWarnings
アノテーションを使うことでワーニングを抑止することも一応できます。しかし本当に誰も得しないのでやめましょう。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Generics4 {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String... args) {
List list = new ArrayList();
list.add("a");
list.add(1);
list.add("c");
for (Iterator itr = list.iterator(); itr.hasNext(); ) {
String str = (String) itr.next(); // 説明のためにあえて拡張forは使わない
System.out.println(str);
}
}
}
結論(?)
こんな昔話みたいなことをしていると自分も年取ったもんだなと思います。でも、こういった背景を知らずにとにかくおまじないのように<String>
とか<>
を書いている人もいるのか・・・と思うと、こういう知識や経験も積極的に発信していかないと失われてしまうんだなと思ったのでこの記事を書いてみました。
この記事を見て一人でも多くの人が、ちゃんと背景を理解したうえで Generics を使ってくれるようになってくれればいいなと思います。
以上。