はじめに
Javaを触っていると、
ジェネリクスを使って抽象的な処理を記載しているメソッドによく出会います。
ジェネリクスについては研修のときに少し触れたくらいできちんと知識整理できていなかったため、
今回の記事執筆を機に整理してみました。
自分と同様、
「コードにTとか?とか出てくることあるけど、あんまり理解できてない」
くらいの理解度の方向けの記事になります。
ジェネリクス(Generics)とは?
ジェネリクスとは、クラスやメソッドの「型」をパラメータ化し、汎用的に扱えるようにする仕組み。
ジェネリクスを使ったコードの例は以下。
ジェネリクスを使ったコード
public static <T> void printListElement(List<T> list) {
for (T element : list) {
System.out.print(element);
}
}
Tは何なのか?
Tは、「実際に使われる型のプレースホルダ(置き換え用文字列)」のようなもの。
ジェネリクスは慣例として
T(Typeの意)
E(Elementの意)
K, V(Key, Valueの意)
R(Return typeの意)
等を使って、クラスやメソッドの型を置き換える。
上記コードのメソッド宣言部分を例にすると、
public static <T> void printListElement(List<T> list) {
→引数listの要素の型をTで置き換えている。
※static <T> voidの<T>は、「Tで置き換えるよ~」の宣言。
また、慣習と異なるため避けた方がよいが、
置き換える文字列を以下のようにしても動く。
public <Generics> Generics genericMethod(Generics param) {// Genericsで置き換えるよ~の宣言
return param; // Generics型の引数を受け取って、そのまま返す
}
→<Generics>で"Generics"という文字列を型のプレースホルダにすることを宣言しているため、
問題なく動作する。
型宣言の作法
「この文字で置き換えるよ~」という宣言をするときは、
<>で置き換え文字列を囲う。
①クラス宣言でジェネリクスを使うことを宣言するとき
つまり、メンバ変数(フィールド)でジェネリクスを使いたい場合。
この場合は、
ClassName<T>
の形で宣言する。
(例)
class HogeClass<T> { // Tで置き換えるよ~の宣言
private T value; // T型のフィールド
public HogeClass(T value) {
this.value = value; // コンストラクタでT型のインスタンスを受け取り、フィールドにセット
}
public T getValue() {
return value; // T型のvalueを返す
}
}
②メソッド宣言でジェネリクスを使うことを宣言するとき
つまり、メソッドの引数やメソッド内で定義する変数でジェネリクスを使いたい場合。
この場合は、
<T> methodName()
の形で宣言する。
(例)
public <T> T genericMethod(T param) {// Tで置き換えるよ~の宣言
return param; // T型の引数を受け取って、そのまま返す
}
上記のように、戻り値の型にさっそく置き換えたTを使うこともあるので、
「この文字で置き換えるよ~」の宣言は戻り値の型の宣言より左で行う、
と覚えておくと覚えやすい。
応用編
ジェネリクスに制約を付与する
下記のコードはエラーになる
public static <E> void printList(List<E> list) {
for (E element : list) {
System.out.print(element);
element.someMethod(); // エラー
}
}
E型のインスタンスがsomeMethod()というメソッドを持っているかどうかは分からないため、エラーになる。
このような実装をしたい場合は、型宣言のときに、
someMethod()というメソッドを持つクラスを継承していることを条件にすればOK。
(例)
※クラスClassWithSomeMethodにはsomeMethod()が実装されているものとする。
public static <E extends ClassWithSomeMethod> void printList(List<E> list) {
for (E element : list) {
System.out.print(element);
element.someMethod(); //elementは確実にsomeMethod()を持っているため問題ない
}
}
ワイルドカード "?" との違い
ワイルドカード
似たような役割をするものに、ワイルドカード(?)がある。
ワイルドカードを用いたコードの例は以下。
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
?を使った場合、?にはどんな型が来るのか予想がつかない。
そのため、全てのクラスの親クラスであるObject型でしか受け取ることができない。
メソッドも、Object型で定義されているメソッド(toString()やhashCode())しか使うことができない。
public static void printList(List<?> list) {
for (Object obj : list) {
obj.toString() //これはできる
obj.length() //これはできない:Object型にはlength()メソッドはない
}
}
プレースホルダとワイルドカード ? の違い
プレースホルダTを使って書いた以下のコードを見てほしい。
public static <T> void addElement(List<T> list, T element) {
list.add(element); // 引数 element の型Tとリストの型Tが一致しているので追加可能
}
これをワイルドカードを使って書くことはできない。
public static void addElement(List<?> list, ? element) {
list.add(element); //エラー:listの要素の型はelementの要素の型と一致するとは限らない
}
これがTやEなどのプレースホルダとワイルドカード?の違い。
▷TやEなどのプレースホルダ
クラスやメソッドで、「この文字をプレースホルダとして使うよ~」と宣言してから使う。
宣言してから使うということは、複数回使える、つまり一貫性があるということ。
下記のコードで、Tにはどんな型が入るかは分からないが、
フィールドのvalueの型とコンストラクタの引数valueの型が同じ
であることは保証される。
class HogeClass<T> {
private T value;
public HogeClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
▷ワイルドカード:?
反対に、ワイルドカードはプレースホルダを複数回使う必要がないときに使う。
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj);
}
}
?で置き換えた型はコードの中で再利用していない。
同じコードをTを使って書くこともできるが、せっかくTで置き換えても、
Tが1か所でしか使われていないため、あまりうま味がない。
public static <T> void printList(List<T> list) {
for (T obj : list) {
System.out.print(obj);
}
}