【Java】ジェネリクスとは?メリットと使い方【コード例付き】
ジェネリクスとは
ジェネリクスとは「クラス・メソッド・インターフェースの中で利用される値の型を後から設定できる仕組み」です。
英語で書くとgenerics。意味は「一般的な」や「総称型の」になります。似たような単語のgeneralからも想定できますね。
ジェネリクスを利用したクラスやメソッドはまだ完全体ではなく、利用時に型を宣言して初めてどのようなクラス・メソッドなのかが確定します。
定義の中で特に重要なのが「利用される値」であることです。これはつまりジェネリクスで指定した型が利用されるのは戻り値や引数だけに限定されないということ。クラス・メソッド・インターフェースの中で使われるものであれば何にでも設定できます。
具体的なイメージを持てるように次の章でコード例を用いつつ使い方を説明します。
使い方
クラス、メソッド、インターフェースで定義の仕方が異なるので、順番に使い方を説明します。それぞれ定義の仕方と呼び出し方に分けてお伝えします。
クラス
定義の仕方
クラス名の後にジェネリクスを配置します。(1行目の<T>)
public class Sample<T> {
private T variable1;
private T variable2;
public T method(T argument) {
this.variable1 = argument;
return this.variable2;
}
}
クラス名の後に指定されたジェネリクスT型はクラスの中の至る所で使っています。
今回の例の場合、以下の箇所で利用しています。
- インスタンス変数の型
- メソッドの戻り値の型
- メソッドの引数の型
このようにクラス定義内の至る所でジェネリクスで定義した型が利用可能です。
呼び出し方
ジェネリクスを利用したクラスは定義した時と同じようにクラス名の後に型を設定して呼び出します。
Sample<Integer> sample = new Sample<Integer>();
ちなみにJava7以降だと、右辺のジェネリクスの型は左辺の型をもとに推論をしてくれるので以下のようなコードでも呼び出し可能です。これは型推論と呼ばれ、私がこれまで読んだ大半のコードでは型推論を使っていました。
Sample<Integer> sample = new Sample<>();
インターフェース
定義の仕方
インターフェースでのジェネリクスの利用方法はクラスと同じです。インターフェース名の後にジェネリクスをつけます。(1行目の<T>)
public interface Sample<T> {
T view(T item);
}
クラスの時と一つ明確に異なる点があるのでお伝えしておきます。
インターフェースの場合、変数を設定すると自動でpublic, static, finalの属性が付きます。つまり変わることがない値として設定されます。ジェネリクスは後から型を設定するという性質上、不変であることが前提のインターフェースの変数の型には利用できません。ジェネリクスというよりもインターフェースの変数の仕様に関する話ですが覚えておいてください。
呼び出し方
インターフェースを実装するクラスの定義方法を紹介します。分かりやすくなるようにインターフェースの定義も合わせて記載しています。
// インターフェース定義
public interface Container<T> {
void set(T item);
T get();
}
// このクラスで利用
public class StringContainer implements Container<String> {
private String item;
@Override
public void set(String item) {
this.item = item;
}
@Override
public String get() {
return item;
}
}
実装する際にクラスと同じくインターフェース名の直後に型を設定します。
その後、インターフェース内でジェネリクスを利用すると設定していた箇所には、直接型を記載してクラスを定義します。
ちなみにクラス定義にもジェネリクスを設定して以下のようにすることも可能です。
public interface Container<T> {
void set(T item);
T get();
}
public class StringContainer<T> implements Container<T> {
private T item;
@Override
public void set(T item) {
this.item = item;
}
@Override
public T get() {
return item;
}
}
メソッド
定義の仕方
メソッドの場合はクラス・インターフェースとはジェネリクスの設定箇所が異なるので注意です。(3行目の<T>)
public class Sample {
public <T> void printArray(T[] arguments) {
for(T argument: arguments) {
System.out.println(argument);
}
}
}
戻り値の型の直前にジェネリクスを設定します。
ジェネリクスを通してもらった型の情報はメソッド内のいたる所で利用可能です。
呼び出し方
“定義の仕方"のコード例で作ったメソッドを呼び出してみます。
Sample sample = new Sample();
Integer[] intArray = {1, 2, 3;}
String[] stringArray = {"ringo", "mikan"};
sample.printArray(intArray); // 1, 2, 3が改行されて出力
sample.printArray(stringArray); // ringoとmikanが改行されて出力
この例から分かるように、クラスやインターフェースとは異なり呼び出し時にジェネリクスに対応する型を指定していません。
メソッドでジェネリクスを使う場合にはメソッド定義のタイミングで引数の型にジェネリクスを設定する必要があり、メソッド実行時に渡されたパラメータの型を持ってジェネリクスの型が決まります。
この点はクラスやインターフェースと大きく異なる点なのでよく覚えておいてください。
メリット
ジェネリクスのメリットは「コード量を減らしつつ安全性も保持できる」ことです。
ほとんど同じような処理だけど引数で受け取る値の型もしくは戻り値の型だけが異なといった状況の時、ジェネリクスを使用しない場合はほとんど同じコードを、想定する型の数だけ書くことになります。もし修正が必要になったら全ての箇所を修正する必要があります。しかしジェネリクスを使うと処理が同じなのであれば一つにまとめることができます。
またジェネリクスを呼び出す際には型が確定するので、コードを書いているときに本当に利用できるメソッドを利用しているのかなどのバリデーションが効きます。その結果プログラミングをしている最中にミスに気付くことができ、実行時エラーを防ぐことができます。
このように型が決まっていることで利用可能なバリデーションによる安全性とコード量を減らすことを両立できることがジェネリクスの大きなメリットです。
デメリット
そんな便利なジェネリクスですが、実はとても大きなデメリットがあります。それが可読性が下がることです。
コードを読む際に型が確定していれば読みやすいのですが、ジェネリクスを利用しているクラスやメソッドはどのような型の値が入るのか想像しながら読むことになります。この結果、脳の領域を少なからず持っていかれるので読みにくくなります。
またジェネリクスが存在しない言語も複数あるので、Java新参者にとっては恐怖です。
個人的な考えではありますが、良いコードを書く上で「理解しやすい」ことはトップレベルに重要だと考えています。
便利だとしてもやたらに使うのではなく、使い所をちゃんと吟味しましょう。
発展形
ただでさえややこしいジェネリクスですが、発展形の使い方があります。この章では簡単に説明するのでそんな使い方もあるんだなぁくらいに思ってもらえれば大丈夫です。
上限境界ワイルドカード型
ジェネリクスを使った場合、基本的にはどんな型も指定できます。Stringにも設定できるし、Integerにも設定できます。ただし、設定できる型をある程度制限したい、そんなときに利用するのが境界ワイルドカード型です。
上限境界ワイルドカード型では、設定できる型を特定のクラスもしくはインターフェースのサブクラスに制限できます。
設定方法は以下です。
<? extends Number>
この例の場合、ジェネリクスにはNumberのサブクラスしか設定できません。
対象(コード例だとNumberの箇所)がインターフェースであってもextendsのままであることに注意です。
下限境界ワイルドカード型
下限制約ワイルドカード型では、設定できる型を特定のクラスのスーパークラスに制限できます。
設定方法は以下です。
<? super Integer>
ほとんど利用されることはないのであまり覚える必要はないです。
非境界ワイルドカード型
無制約とある通り、何も制限しません。
これだとただのジェネリクスと変わらないような気がしますが、明確な違いがあります。
通常のジェネリクスは処理の中で型が決まっていることを前提に処理が進むことに対し、無制約ワイルドカード型の場合は値が定まっていない状態で処理が行われます。
無制約ワイルドカードが設定されたクラスやインターフェースを呼び出す際には、通常のジェネリクス型と異なり型を設定しません。そのためコンピュータは無制限ワイルドカード型の場合はほわほわした抽象的なものとして扱います。
設定方法は以下です。
<?>
おまけ
ジェネリクスが利用されたコードを読んでいるとTだけでなく、EやVといった文字が使われていることに気づくかもしれません。
これらは動作自体はTとした時とは変わりませんが、コードを読む人に対して"何の型を定義するのか"を分かりやすくする役割を持っています。
例えばEはElementの意味で、リストの要素の型をジェネリクスで定義することをコードを読む人に対して表現しています。
同じような形で、Kならキーの型を、Vならバリューの型にジェネリクスを利用することを表現しています。
文字が変わって驚くかもしれませんんが、動作自体はTを利用した時と全く変わらないのでご安心ください。
まとめ
プログラミング初心者、Java新参者を悩ませるジェネリクスについて説明しました。ジェネリクスはプロダクトコードでよく利用されるとても重要な記法です。
ややこしい内容ではあるのですが、よく理解してJava中級者を名乗れる状態になりましょう。