なぜ Java でジェネリックスが必要なのか?

なぜジェネリックスなのか?

ジェネリックスはどのような問題を解決するものなのでしょうか?

それを理解するために、次のような箱 (Box) クラスを考えましょう。

public class Box {
  Object o;

  public Box(Object o) {
    this.o = o;
  }

  public Object get() {
    return o;
  }
}

これは単なる箱 (Box) を表すクラスです。コンストラクタで何かオブジェクトを受け取ったら、それを内部的に Object 型の o に保持しておき、get メソッドが呼ばれたときにそれを返すだけです。

これを利用するプログラムを考えましょう。

Box b = new Box(123);
Integer i = (Integer) b.get();
System.out.println(i); // 123

Box のコンストラクタで 123 という数字を渡し、それを get メソッドで取り出し、Integer にキャストして Integer 型の変数にセットし出力しています。

この実行結果は確かに 123 という数字が出力されます。

では、もう一つの利用例をみてみましょう。

Box b = new Box("Hello");
String s = (String) b.get();
System.out.println(s); // Hello

この例では Box のコンストラクタで "Hello" という文字列を渡し、それを get メソッドで取り出すときに String にキャストして変数にセットし、それを出力しています。 これを実行すると確かに Hello と出力されます。

最初の例では Box クラスは 123 という整数を扱い、二つ目の例では "Hello" という文字列を扱うことができています。 この箱 (Box) クラスは、いろいろな型のオブジェクトを保存するための「箱」として使うことができています。

ここまでは問題ないように見えます。

では、次の例をみてください。

Box b = new Box(123);
String s = (String) b.get();
System.out.println(s);

この例ではコンストラクタで 123 を渡していますが、それを取り出すときに String にキャストしています。

コンパイルは成功しましたので、実行してみましょう。すると次のような例外が投げられてしまいました。

Exception in thread "main" java.lang.ClassCastException: 
class java.lang.Integer cannot be cast to class java.lang.String 
(java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at com.keicode.java.test.TestApp.main(TestApp.java:6)

Integer オブジェクトはそのまま直接キャストして String にはなりませんから、このような例外が発生したのです。

さて、何が問題だったのでしょうか。

不具合があってもコンパイルでき、実行時に失敗する

まず、上記のコードは int を直接 String にキャストするという誤ったコードを書いているわけですから、 これはコンパイル時に誤りを検出できたほうがいいはずです。

一方、必ず存在するはずのファイルが実行時には存在しなかったなど、実行時にしかわからないような処理の失敗は、当然ながらコンパイル時にわかるものではありません。 そうした問題は実行時の例外発生で致し方ありません。

コードに明らかな不具合があるのにコンパイルはでき、実行時に例外が発生するのでは安定したコードを書くのは非常に難しくなります。

従来の方法では型毎に別々のメソッドを実装する必要がある

結局この箱クラスの get メソッドは、コンストラクタでセットした型のオブジェクトを返さなければならなかったのです。例えば、コンストラクタで Integer 型なら Integer が返る。 String をセットすれば String が返るという動きです。

では、Integer 用の箱 (Box) クラスとして IntegerBox クラス、String 用の箱クラスとして StringBox クラス、Long 用の箱として LongBox ... などと定義していくのでは大変です。

ジェネリックスの登場

そこで登場するのがジェネリックスです。

ジェネリックスでは型を汎用化します。

これがどういうことか、ジェネリックス (Generics) を用いて、上の Box クラスを書きなおしてみましょう。

public class Box<T> {
  T o;

  public Box(T o) {
    this.o = o;
  }

  public T get() {
    return o;
  }
}

今まで Object 型としていた箇所が T で置き換えられ、クラス名に <T> という指定が追加されています。

class Box<T> とすることで、「この Box クラスは型 T に対するクラスですよ」と定義していることになります。 このようにジェネリックスを使って Box を定義しておけば、Box<Integer>、Box<String> のようにそれぞれの型用の Box クラスを用意することが可能となります。

逆にジェネリックスがもし使えなければ、Integer 用の IntegerBox、String 用の StringBox、のように、型違いで同様のクラスをたくさん定義する必要があります。

これを使う側は次のようなコードになります。

Box<Integer> b = new Box(new Integer(123));
Integer i = b.get();
System.out.println(i);

型の指定で <型> の形式で、実際に利用する型を指定しています。

この Box はそれぞれの型専用の Box になりますから、キャストをする必要もなくなりますし、さらに Integer の Box に String を入れてしまうというような誤りはコンパイル時に検出できることになり、一石二鳥です。

以上、ジェネリックスが解決する問題と、基本的な利用方法について説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Java 入門