お久しぶりです.はまじです.
今日はJavaのGenerics(ジェネリックス)について,Effective Javaを読みつついろいろ試験コード書きながら紹介していこうと思います.
自分もGenericsについて完全に理解していなかったので,自分の勉強も兼ねてますw
そのまえに,前提として以下のような関係のクラスがあると想定しています.
class Card {
}
class Joker extends Card {
}初級編
Genericsについてあまり知らなくても,こんなコードを頻繁に見かけるのではないかと思います.
List<String> list = new ArrayList<String>();
この list には,String と,String を継承したクラスのみをリストに格納することができます.
これを型安全であると言います.そして,指定した String クラスのことを総称してパラメータと言います.
こうすることで,list には String(とその子クラス)しか入れられない,入っていないことが保証されるため,コンパイラが容易にエラーを検出することができます.
では型安全でないコードとはどういうものかというと,こんなコードです.
List list = new ArrayList();
このコードをコンパイルした時に,警告が出ると思います.
これは型チェックされていないことによる警告で,要素に対して呼び出すことのできるメソッドも限られる上,キャストに失敗して実行時エラーになることもありえます.
さきほどの List<String> をパラメータ化された型と呼ぶのに対して,List を原型と呼びます.
で,さきほどパラメータに指定したクラスと,そのクラスを継承したクラスが許可されれるということをお話ししました.
なので以下のコードは正常動作します.
List<Card> list = new ArrayList<Card>();
list.add( new Joker() );
ちなみに,Java7からインスタンス生成時の型省略が可能(ダイアモンドオペレータ)になりました.
つまり,以下の2行は同義です.
List<Card> list = new ArrayList<>();
List<Card> list = new ArrayList<Card>();
中級編
Genericsを用いてクラス・メソッドを実装する場合,それぞれどのようにすればいいのでしょうか?
Genericクラスを作る
Genericクラスはこんな感じで作れます.
class MyGene<E> {
}MyGeneクラスでは,内部で使用するためにEという仮のパラメータを宣言しておくことで,内部でひとつの型として使用することができます.
こうすることで,フィールドやメソッドの戻り値・引数として仮のパラメータを使用することができます.
class MyGene<E> {
private E elem;
public E getElem() {
return elem;
}
public void setElem(E elem) {
this.elem = elem;
}
}ここで注意しておくべき事項として,Genericクラス内では仮パラメータのインスタンスを生成できないことがあげられます.
また,今回詳しく触れませんが,仮パラメータの配列の生成も禁止されています.
GenericクラスではないクラスでGenericメソッドを作る
さて,Genericクラスではない普通のクラスのなかでも,仮パラメータを使用したメソッドを作ることができます.
ということで,Genericメソッドはこんな感じで作成します.
public <E> void map(List<E> list, E elem) {
//something
}メソッドの修飾子(public, private, protected, final, static)後と返り値の間に仮パラメータを宣言すると,そのメソッドの宣言部とメソッド実装内部で仮パラメータを使用することができます.
ちなみに,仮パラメータに対して制限をかけることもできます.
例えば,「Cardクラスとその子クラスだけを許可したい」という場合は,それぞれ下記のようになります.
class MyGene<E extends Card> {
// can use methods defined in Card class.
}
class NonGene {
public <E extends Card> E something(E elem) {
// can use methods defined in Card class.
}
}こうすることで,仮パラメータEは,Cardクラスかその子クラスであることが保証され,Cardクラスで宣言されたpublicなメソッドやフィールドを使用することができます.
なお,<E extends Card> のことを,境界型パラメータと呼びます.
上級編
さて,このコードは「正常動作」「実行時エラー(もしくは実行時意図しない結果)」「コンパイルエラー」のどれでしょう?
List<Card> list;
list = Arrays.asList(new Joker[5]);
答えは コンパイルエラー です.
何故か考えていきましょう.
list は List<Card>型のインスタンスです.
それに対して,Arrays#asList は(仮パラメータをTとして) T[]型を引数とし,List<T>型を返します.
つまり,Arrays.asList(new Joker[5]) は List<Joker> を返すわけです.
JokerクラスはCardクラスを継承してはいますが,List<Joker> は List<Card> を継承しているわけではないので代入できない,ということになります. ArrayList<Card> list = new ArrayList<Joker>(); が通らないのと一緒ですね.
これを回避する方法が次のようになります.
List<? extends Card> list;
list = Arrays.asList(new Joker[5]);
このワイルドカードの使用方法によって,list に代入できるのは List<Card> をはじめ,Cardクラスを継承したクラスをパラメータ型としたジェネリック型(ややこしい!)です.
なお,List<? extends Card> のことを 境界ワイルドカード型 と言います.
ちなみに逆(Collections#toArray() )はどうでしょうか?
List<Joker> list = new ArrayList<>();
Card[] cards = list.toArray(new Joker[0]);
これは問題なく通ります. Card[] cards = new Joker[5]; が通るのと同じ原理です.
しかしながら,以下のコードはコンパイルエラーを検出してくれません・・・.
実行時エラー(ArrayStoreException)で落ちます.
List<String> list = new ArrayList<>();
Card[] cards = list.toArray(new Card[0]);
toArray()の内部で(厳密には違いますが )引数の配列の要素に対して代入をしているわけです.
最後に,ワイルドカードを使用した問題を紹介します.
どうなるでしょうか?
List<? extends Card> list = new List<>();
list.add( new Card() );
答えは コンパイルエラー です.
ワイルドカードのパラメータに対しては,null以外のいかなる要素も代入することができません
(参照:Effective Java P.111).
これはコンパイラが防いでいるらしいです.
Eclipseのコンテンツアシスト等の補完機能で確認すると,null型しか代入できないような補完内容になると思います.
最後に
アプリ実装程度なら,中級以上のテクニックはあまり使わないとは想いますが,知っておくことで実装の幅が広がるのではないでしょうか?
今回,ブログの記事を書きながらEffective Javaを読みつつ,コードを書いて確かめることによって,とても勉強になりましたし,Generics使ってライブラリ作ってみたくなりました.
おまけ
SyntaxHighlightがしょぼいので、ちょっとずつ変えていきます・・・。