Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

Generics

 
 
Tiger Genrics とはナンだろう
 
 

Generics はすでに雑誌などで解説記事が書かれているのでご存知のかたも多いかと思います。

Generics というのは C++ のテンプレート機能のような機能で、ひとことでいえばクラス (インタフェース)、メソッドのパラメータ化ということができます。

といってもよく分からないですね。

それでは実際に例を示しましょう。

コレクションは要素を Object クラスのオブジェクトとして保持するために、使うときにはいちいちキャストを行わなくてはいけません。

    List list = new ArrayList();
 
    list.add(new Integer(10));
 
    int x = ((Integer)list.get(0)).intValue();  // キャストが必要

最後の行は非常に醜くなってしまいます。

Generics を使うとつぎのようになります。

    List<Integer> list = new ArrayList<Integer>();
 
    list.add(new Integer(10));
 
    int x = list.get(0).intValue();  // キャストが不必要

とてもすっきりしました。

1 行目の <Integer> というのが Generics の表記です。これはどういう意味かというと、<Integer> で修飾された List オブジェクトは保持する要素がすべて Integer オブジェクトになるということを示しています。このためキャストがいらなくなっているのです。

見ためにはキャストがいらなくなっただけですが、プログラムの保守という観点からするともっと重要な点があります。

たとえば次のコードは問題なくコンパイルできます。

アプリケーションのソース CastTest.java

 

import java.util.ArrayList;
import java.util.List;
 
public class CastTest {
    public static void main(String[] args) {
        List list = new ArrayList();
 
        list.add(new Integer(10));
  
        String str = (String)list.get(0);
    }
}

しかし、これを実行すると当たり前ですが ClassCastException が発生してしまいます。

C:\JSR014>javac CastTest.java
Exception in thread "main" java.lang.ClassCastException
        at CastTest.main(CastTest.java:10)

ようするにこの間違いはプログラムを実行しない限り発見されないので、デバッグが面倒になります。コンパイル時にチェックできれば、あっというまに修正できるのですが...

これを Generics を使用すればつぎのように書くことができます。

アプリケーションのソース CastTest2.java

 

import java.util.ArrayList;
import java.util.List;
 
public class CastTest2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
 
        list.add(new Integer(10));
  
        String str = list.get(0);
    }
}

これをコンパイルします。

C:\JSR014>javac CastTest.java
CastTest2.java:8: シンボルを解決できません。
シンボル: メソッド add (java.lang.Integer)
場所    : インタフェース の
        list.add(new Integer(10));
            ^
エラー 1 個

C:\JSR014>

コンパイル時にコンパイルエラーが出るため、まちがった要素を保持させようとしていたことがすぐわかります。ちょっとした違いのような気もしますが、これが保守性には大きな違いになるのです。

Generics は JCP で標準が策定されています。JSR-014 なのでずいぶん早い時期に JCP に登録されたのですが、なかなかまとまりませんでした。やっと Tiger でコアにとりこまれます。

Generics の仕様は JCP からダウンロードできます。

JSR-014 Add Generic Types To The Java Programming Language http://jcp.org/en/jsr/detail?id=14

なるべく、最新の情報を追いかけていこうとは思っていますが、もし違っている部分がありましたらメールください。

 

 
 
Tiger どんどん使おう クラス編
 
 

Generics の使い方が簡単だということは一番はじめの例から分かると思います。ここでは、いくつか例を示しながら、もうちょっと使い方を見ていきましょう。

まずはコレクション関連で使ってみましょう。コレクションは Generics によってもっとも変化した部分なので、ここが抑えられればもう大丈夫。

アプリケーションのソース
Generics 未使用
Genrics 使用

 

List

はじめの例は List インタフェースと ArrayList クラスの例です。単に文字列の連結をやっています。今までの書き方では次のようになります。ちなみにクラス名の WO は without のことです。

import java.util.ArrayList;
import java.util.List;
 
public class WOGenericsSample1 {
    public static void main(String[] args) {
        List list = new ArrayList();
        
        for(int i = 0; i < args.length; i++) {
            list.add(args[i]);  // 型チェックが行われない
        }
 
        StringBuffer buffer = new StringBuffer();
        for(int i = 0; i < list.size(); i++) {
            String tmp = (String)list.get(i); // キャストが必要
            buffer.append(tmp);
        }
 
        System.out.println(buffer);
    }
}

これを Generics で書くと次のようになります。赤の部分が Generics の宣言部分です。

import java.util.ArrayList;
import java.util.List;
 
public class GenericsSample1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        
        for(int i = 0; i < args.length; i++) {
            list.add(args[i]);   // 型チェックが行われる
        }
 
        StringBuffer buffer = new StringBuffer();
        for(int i = 0; i < list.size(); i++) {
            String tmp = list.get(i); // キャストはいらない
            buffer.append(tmp);
        }
 
        System.out.println(buffer);
    }
}

リストに要素を追加するときにはパラメータの型 (ここでは String) であるかどうかチェックされます。逆に、Generics が使えるときに、Generics を使わないでコンパイルすると

C:\JSR014\examples>javac WOGenericsSample1.java
注: WOGenericsSample1.java の操作は、未チェックまたは安全ではありません。
注: 詳細については、-Xlint:unchecked オプションを指定して再コンパイルしてくださ
い。

C:\JSR014\examples>

と表示されます。-warnunchecked オプションをつけてコンパイルすると

C:\JSR014\examples>javac -Xlint:unchecked WOGenericsSample1.java
WOGenericsSample1.java:9: 警告: raw 型 java.util.List のメンバとしての add(E) の
呼び出しは未確認です。
            list.add(args[i]);  // 型チェックが行われない
                ^
警告 1 個

C:\JSR014\examples>

と表示されて、型チェックされないことが警告されます。

get するときにキャストがいらないのは、前述の通り。

 

Iterator

次は Iterator インタフェースを使用した例です。今までの記述方だと次のようになります。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
 
public class WOGenericsSample2 {
    public static void main(String[] args) {
        List list = new ArrayList();
         
        for(int i = 0; i < args.length; i++) {
            list.add(args[i]);  // 型チェックが行われない
        }
 
        StringBuffer buffer = new StringBuffer();
        Iterator it = list.iterator();
        while(it.hasNext()) {
            String tmp = (String)it.next(); // キャストが必要
            buffer.append(it.next());
        }
 
        System.out.println(buffer);
    }
}

Collection#iterator メソッドは戻り値がパラメータ化された Iterator オブジェクトになります。Generics で書きなおすと

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
 
public class GenericsSample2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        
        for(int i = 0; i < args.length; i++) {
            list.add(args[i]);  // 型チェックが行われる
        }
 
        StringBuffer buffer = new StringBuffer();
        Iterator<String> it = list.iterator();
		
        while(it.hasNext()) {
            buffer.append(it.next()); // キャストがいらない
        }
 
        System.out.println(buffer);
    }
}

キャストがいらないというのは、ずいぶん楽になります。ほんとに。

 

入れ子パラメータ

さて、次は 2 重リストを作ってみましょう。リストの要素がリストというリストです。次のプログラムはトランプのカードを作るという例です。

import java.util.ArrayList;
import java.util.List;
 
public class WOGenericsSample3 {
    public final static String[] suits = {"S", "C", "H", "D"};
 
    public static void main(String[] args) {
        List deck = new ArrayList();
 
        for (int i = 0; i < suits.length; i++) {
            List row = new ArrayList();
            for (int j = 1; j <= 13; j++) {
                if (j == 1) {
                    row.add(suits[i] + "A"); // 型チェックが行われない
                } else {
                    row.add(suits[i] + j);   // 型チェックが行われない
                }
            }
            deck.add(row);  // 型チェックが行われない
        }
         
        for (int i = 0; i < suits.length; i++) {
            for (int j = 0; j < 13; j++) {
                String card = (String)((List)deck.get(i)).get(j);
                                                     // とっても面倒
} System.out.println(); } } }

要素を取り出すのに 2 回もキャストする必要があります。面倒くさいですし、分かりにくいですね。

これを Generics で書くと次のようになります。2 重リストなので、パラメータの中にパラメータ化したクラスが入るという入れ子構造になっています。

import java.util.ArrayList;
import java.util.List;
 
public class GenericsSample3 {
    public final static String[] suits = {"S", "C", "H", "D"};
 
    public static void main(String[] args) {
        List<List<String>> deck = new ArrayList<List<String>>();
 
        for (int i = 0; i < suits.length; i++) {
            List<String> row = new ArrayList<String>();
            for (int j = 1; j <= 13; j++) {
                if (j == 1) {
                    row.add(suits[i] + "A"); // 型チェックが行われる
                } else {
                    row.add(suits[i] + j);   // ここも型チェック
                }
            }
            deck.add(row);  // もちろん、ここでも型チェックが行われる
        }
         
        for (int i = 0; i < suits.length; i++) {
            for (int j = 0; j < 13; j++) {
                System.out.print(deck.get(i).get(j) + " "); // 簡単
            }
            System.out.println();
        }
    }
}

Generics を使った定義の部分がちょっと見にくいかもしれません。慣れればナンでもありません ^^;; 宣言は 1 回だけですが、get するのは何度もあるかもしれないのです。

get の部分の簡潔さは感動的です。

 

複数パラメータ

パラメータの入れ子ができることは分かりましたので、複数のパラメータの例を次に示しましょう。

パラメータを 2 つ使うものにマップがあります。マップはご存知のようにキーと値のペアになりますが、この両方ともパラメータとすることができます。

まずは今までの書き方。

import java.util.HashMap;
import java.util.Map;
 
public class WOGenericsSample4 {
    public final static String[] num = {"zero",
                                        "one",
                                        "two",
                                        "three",
                                        "four",
                                        "five"};
  
    public static void main(String[] args) {
        Map map = new HashMap();
 
        for (int i = 0; i < num.length; i++) {
            map.put(num[i], new Integer(i)); // 型チェックは行われない
        }
         
        for (int i = 0; i < num.length; i++) {
            // キャストが必要 & 型チェックなし
            Integer v = (Integer)map.get(num[i]); 
            System.out.println(num[i] + " : " + v);
        }
    }
}

Generics で書きかえたものが次です。複数のパラメータがある場合はカンマで区切って書きます。

import java.util.HashMap;
import java.util.Map;
 
public class GenericsSample4 {
    public final static String[] num = {"zero",
                                        "one",
                                        "two",
                                        "three",
                                        "four",
                                        "five"};
 
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();
 
        for (int i = 0; i < num.length; i++) {
            map.put(num[i], new Integer(i)); // 型チェックが行われる
        }
         
        for (int i = 0; i < num.length; i++) {
            // キャストはいらない & 型チェックあり
            Integer v = map.get(num[i]); 
            System.out.println(num[i] + " : " + v);
        }
    }
}

Generics を使うと put メソッドで型チェックが行われます。キーと値の両方ともチェックされます。

また、get するときもキーの型チェックが行われます。

どうですか、Generics を使うのもそんなに面倒ではないことがお分かりだと思います。次は少し文法的なことを調べてみましょう。

 

 
 
Tiger こんなことできるかな ?
 
 

今まではあまり考えることなく Generics を使ってきましたが、いろいろと気になるところもあります。それらをチェックしてみましょう。

 

なにをパラメータとできるか

今までの例はみなパラメータとしてクラスもしくはインタフェースを使用してきました。それじゃ、こんなのは

    List<int>    // NG

パラメータに使用できるのはあくまでも、クラスかインタフェースだけでプリミティブ型は使用できません。もちろん、ラッパークラスを使用することはできます。

もちろん、勝手にパラメータの数を増やすこともできませんList インタフェースのパラメータは 1 つだけなので、次の例は間違いです。

    List<String, Integer>    // NG

 

パラメータ化されたクラスは元のクラスと区別されるのか

C++ のテンプレートはパラメータ化されると、違うクラスとして認識されます。Java ではどうでしょう。

次の例で確かめてみましょう。

import java.util.ArrayList;
 
public class ClassDiffTest1 {
    public static void main(String[] args) {
        ArrayList<String> clist = new ArrayList<String>();
        ArrayList<Integer> ilist = new ArrayList<Integer>();
 
        if (clist.getClass().equals(ilist.getClass())) {
            System.out.println("Classes are Same: "
                               + clist.getClass());
        } else {
            System.out.println("Classes are Different: "
                               + clist.getClass() + " " 
                               + ilist.getClass());
        }
    }
}

さっそく実行。

C:\JSR014\examples>java ClassDiffTest1
Classes are Same: class java.util.ArrayList
C:\JSR014\examples>

あら、一緒だ。

それじゃ、こんなこともできるのでしょうか。

import java.util.ArrayList;
 
public class ClassDiffTest2 {
    // クラスが同じなら代入はできるのだろうか
    public static void main(String[] args) {
        ArrayList<String> clist = new ArrayList<String>();
        ArrayList<Integer> ilist = new ArrayList<Integer>();
 
        clist = ilist;
    }
}

これをコンパイルすると、やっぱりダメでした ^^;;

C:\JSR014\examples>javac ClassDiffTest2.java
ClassDiffTest2.java:9: 互換性のない型
検出値  : java.util.ArrayList<java.lang.Integer>
期待値  : java.util.ArrayList<java.lang.String>
        clist = ilist;
                ^
エラー 1 個

C:\JSR014\examples>

クラスは同じなのに代入できないというのも変な感じですが、まあ当たり前でしょう。

それじゃ、パラメータ化していないオブジェクトに代入してみましょう。ちなみにパラメータ化していないものを Raw タイプというそうです。

import java.util.ArrayList;
 
public class ClassDiffTest3 {
 
    // パラメータ化していないオブジェクトに代入できるか
    // また、型チェックは行われるのだろうか
    public static void test1() {
        ArrayList list = new ArrayList();
        ArrayList<String> clist = new ArrayList<String>();
 
        list = clist;
 
        clist.add("A");
        String str = clist.get(0);
 
        clist.add(new Integer(1));
        Integer i = clist.get(1);
    }

    // パラメータ化していないオブジェクトを
    // パラメータ化したオブジェクトに代入できるか
    // 型チェックは
    public static void test2() {
        ArrayList list = new ArrayList();
        ArrayList<String> clist = new ArrayList<String>();
 
        clist = list;
 
        clist.add("A");
        String str = clist.get(0);
 
        clist.add(new Integer(1));
        Integer i = clist.get(1);
    }
 
    public static void main(String[] args) {
        test1();
        test2();
    }
}

これをコンパイルすると

C:\JSR014\examples>javac -Xlint:unchecked ClassDiffTest3.java
ClassDiffTest3.java:16: シンボルを見つけられません。
シンボル: メソッド add (java.lang.Integer)
場所    : java.util.ArrayList<java.lang.String> の クラス
        clist.add(new Integer(1));
             ^
ClassDiffTest3.java:17: 互換性のない型
検出値  : java.lang.String
期待値  : java.lang.Integer
        Integer i = clist.get(1);
                             ^
ClassDiffTest3.java:27: 警告: java.util.ArrayList から java.util.ArrayList<java.
lang.String> の代入は未確認です。
        clist = list;
                ^
ClassDiffTest3.java:32: シンボルを見つけられません。
シンボル: メソッド add (java.lang.Integer)
場所    : java.util.ArrayList<java.lang.String> の クラス
        clist.add(new Integer(1));
             ^
ClassDiffTest3.java:33: 互換性のない型
検出値  : java.lang.String
期待値  : java.lang.Integer
        Integer i = clist.get(1);
                             ^
エラー 4 個
警告 1 個

C:\JSR014\examples>

パラメータ化されたオブジェクトをパラメータ化していないオブジェクトに代入することは OK のようです。逆は警告が出ているので、いまいちお勧めできません。

パラメータ化していないオブジェクトに代入しても型チェックはちゃんと行われているようです。また、キャストも使う必要がないようです。

まぁ、紛らわしいのでこういう使い方はやめておいた方が懸命だとは思いますが。

 

キャストはできるか

代入ができたので、キャストも試してみましょう。

import java.util.List;
import java.util.ArrayList;
 
public class ClassCastTest1 {
    public static void main(String[] args) {
        ArrayList<Integer> ilist = new ArrayList<Integer>();
 
        List list = (List)ilist;
        list = (List<Integer>)ilist;
        list = (List<String>)ilist;
    }
}

多分、最後の List<String> はダメだと思いますが、List へのキャストはどうでしょうか。さっそく、コンパイル。

C:\JSR014\examples>javac ClassCastTest1.java
ClassCastTest1.java:10: 変換できない型
検出値  : java.util.ArrayList<java.lang.Integer>
期待値  : java.util.List<java.lang.String>
        list = (List<String>)ilist;
                             ^
エラー 1 個

C:\JSR014\examples>

List へのキャストは大方の予想通り大丈夫です。List<String> へのキャストはやっぱりダメですね。

 

パラメータ化されたクラスを派生できるか

パラメータ化されたとしても、クラスとして扱われるようなので、こんなことができるかどうか試してみたくなりました。

import java.util.ArrayList;
 
public class ClassExtendsTest1 {
    public static void main(String[] args) {
        A a = new A();
 
        a.add("abc");
        String str = a.get(0);
 
        a.add(new Integer(10));
        Integer i = (Integer)a.get(1);
    }
}

class A extends ArrayList<String> {}

ようするにパラメータ化されたクラスを派生できるか、もしくはパラメータ化されたインタフェースをインプリメントできるかということです。

コンパイルしてみると、次のようになりました。

C:\JSR014\examples>javac ClassExtendsTest1.java
ClassExtendsTest1.java:10: シンボルを見つけられません。
シンボル: メソッド add (java.lang.Integer)
場所    : A のクラス
        a.add(new Integer(10));
         ^
ClassExtendsTest1.java:11: 変換できない型
検出値  : java.lang.String
期待値  : java.lang.Integer
        Integer i = (Integer)a.get(1);
                                  ^
エラー 2 個

C:\JSR014\examples>

派生させた部分に関してはコンパイルエラーは出ていないようです。A は普通のクラスのように見えますが、ちゃんと型チェックやキャストが不要になっています。10 行目と、11 行目をコメントアウトして実行してみるとちゃんと実行できました。

インタフェースでもやってみました。よく分からないのが next メソッドの戻り値です。元々の Iterator インタフェースの場合の戻り値は Object 型です。でも、パラメータ化したらパラメータの型になるはずだからです。

import java.util.ArrayList;
import java.util.Iterator;
 
public class ClassExtendsTest2 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("abc");
        list.add("efg");
        list.add("hij");
 
        B b = new B(list);
 
        while(b.hasNext()) {
            String str = b.next();
            System.out.println(str);
        }
    }
}

class B implements Iterator<String> {
    private Iterator<String> it;
 
    public B(ArrayList<String> list) {
        it = list.iterator();
    }
 
    public boolean hasNext() {
        return it.hasNext();
    }
 
//    public String next() {
    public Object next() {
        return it.next();
    }
 
    public void remove() {
        it.remove();
    }
}

とりあえず、このままでコンパイルしてみましょう。

C:\JSR014\examples>javac ClassExtendsTest2.java
ClassExtendsTest2.java:14: 互換性のない型
検出値  : java.lang.Object
期待値  : java.lang.String
            String str = b.next();
                               ^
ClassExtendsTest2.java:20: B は abstract でなく、java.util.Iterator 内の abstrac
t メソッド next() をオーバーライドしません。
class B implements Iterator {
^
ClassExtendsTest2.java:32: B の next() は java.util.Iterator の next() を実装で
きません。互換性のない戻り値の型を使おうとしました。
検出値  : java.lang.Object
期待値  : java.lang.String
    public Object next() {
                  ^
エラー 3 個
 
C:\JSR014\examples>

やはり、next メソッドの戻り値はパラメータの String になるようです。これを変更して実行してみると

C:\JSR014\examples>java ClassExtendsTest2
abc
efg
hij
C:\JSR014\examples>

とちゃんと実行できました。

ただし、このようにパラメータ化されたクラスやインタフェースを派生・インプリメントしてもあまり意味がありません。やるんだったらパラメータ化したまま定義したほうがいいと思います。というわけで、次からは実際に自分でパラメータ化したクラスを作ってみましょう。

 

 
 
Tiger Generics なクラス・メソッドを自作する
 
 

今までは使い方ばかりだったのですが、ここからは Generics を利用してクラス、インタフェース、メソッドを作ってみましょう。

 

クラスのパラメータ化

まずはクラス、インタフェースからです。クラス、インタフェースの場合、クラスの定義の部分でクラス名、インタフェース名の直後に <T> のようにパラメータをつけて宣言します。別にパラメターの型名は T でなくてナンでもいいのですが、慣例的にはアルファベットの大文字 1 文字を使用することが多いです。

public class Something<T> {
    protected T t;
 
    public Something(T t) {
        this.t = t;
    }
 
    public T get() {
        return t;
    }

}

クラスの中ではパラメータの型は普通の型として使用することができます。そのため、プロパティやメソッドの引数、メソッドの戻り値、テンポラリ変数などに使用することができます。

インタフェースでも同様です。

public interface IAny<S> {
    public S get();
}

派生クラスやインタフェースのインプリメントもできます。

public class Anything<T> extends Something<T> implements IAny<T> {
    public Anything(T t) {
        super(t);
    }
 
    public T get() {
        return t;
    }

    public void set(T t) {
        this.t = t;
    }
}

簡単でしょ。

実をいうと驚いたことにワイルドカードがつかえるのです。ワイルドカードは ? で表わします。

import java.util.List;
 
public class Everything<T> {
    private List<T> list;
  
    public Everything(List<T> list) {
        this.list = list;
    }
 
    public void set(List<? extends T> s) {
        list = (List<T>)s;
    }
 
    public void dainyuu(List<? super T> s) {
        s = list;
    }
}

<? extends T> というのは「T の派生クラスだったらなんでもいい」ということです。<? super T> という書き方は T のスーパクラスだったらなんでもいいということです。両方ともインタフェースも使うことができます。

とはいうものの List<? extends T> を List<T> にキャストするとコンパイル時に警告されます。

また、単に <?> と書いてすべてのクラスという書き方もできますが、これはパラメータ化したメソッドで使われることが多いので、また後で説明します。

ちょっと試してみましょう。

import java.util.ArrayList;

public class WildcardSample {
    public static void main(String[] args) {
        Everything<B> e = new Everything<B>(new ArrayList<B>());
 
        e.set(new ArrayList<A>));
        e.set(new ArrayList<B>));
        e.set(new ArrayList<C>));
 
        e.dainyuu(new ArrayList<A>));
        e.dainyuu(new ArrayList<B>));
        e.dainyuu(new ArrayList<C>));
    }
}
 
class A {}
class B extends A {}
class C extends B {}

A クラスの派生クラスの B。その派生クラスの C を用意して、Everything クラスの get/set メソッドをコールしてみるという例です。

これをコンパイルすると

C:\JSR014\examples>javac -Xlint:unchecked WildcatdSample.java
WildcardSample.java:7: set(java.util.List<? extends B>) (Everything<B> 内) を (j
ava.util.ArrayList<A>) に適用できません
        e.set(new ArrayList<A>());
         ^
WildcardSample.java:13: dainyuu(java.util.List<? super B>) (Everything<B> 内) を
 (java.util.ArrayList<C>) に適用できません
        e.dainyuu(new ArrayList<C>());
         ^
.\Everything.java:11: 警告: 型 java.util.List<T> へのキャストは未確認です。
        list = (List<T>)s;
                        ^
エラー 2 個
警告 1 個
 
C:\JSR014\examples>

エラーは 2 つで、警告が 1 つ。警告は前述したように List<? extends T> を List<T> にキャストしたことによります。

エラーは List<? extends B> が期待されていたところに List<A> だったというのが 1 つ。A は B のスーパクラスなのでこれはだめです。

次が List<? super B> が期待されていたところに List<C> だったということです。C は B の派生クラスなのでやっぱりダメですね。

 

メソッドのパラメータ化

さて、次はメソッドの方です。クラスがパラメータ化されていなくても、メソッドだけをパラメータ化することができます。

メソッドをパラメータ化するには public や private や static などの後、戻り値の前に <パラメータ名> を記述します。 クラスの場合はクラス名の後だったのに対し、メソッドではメソッド名の前になります。

次の例は java.util.Collections クラスの fill メソッドの定義です。

    public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal);

T がパラメータとなる型です。メソッドの中では T は普通の型のように使えるのはクラスのときと同様です。

java.util.Collections クラスや java.util.Arrays クラスのように static メソッドばかりのクラスはインスタン化しないので、このようにメソッドをパラメータ化することが多いようです。

先ほどと同様にワイルドカードを用いることもできます。次の例は同じく Collections クラスの copy メソッドです。

    public static <T>void copy(List<? super T> dest, List<? extends T> src);

T の派生クラスは T のスーパクラスにはコピーできるということです。

単なるワイルドカードも使えます。この場合はどんなクラスでもいいので、戻り値の前のパラメータ表記は必要ありません。

ワイルドカードの例として Collections クラスの reverse メソッドです。リストの並び順を変えるだけなので、List の要素はどんなクラスのオブジェクトでもいいわけです。

    public static void reverse(List<?> list);

メソッドだけをパラメータ化することはあまりないかもしれませんが、static メソッドだけのユーティリティクラスなどで重宝しそうです。

 
 
Tiger どんどん使おう メソッド編
 
 

今まで例にした Generics の使い方はすべてクラス/インタフェースに対する Generics でした。というのもメソッドの方は定義の仕方を説明してから出ないと、使い方が分かりにくいと思ったからです。

前章でパラメータ化したメソッドの書き方を示したので、さっそく使ってみましょう。

パラメータ化されているメソッドをコールする場合はメソッド名の前に <クラス名 or インタフェース名> を書くようにします。クラスの場合は後ろだったのですが、メソッドは逆に前です。

まずは、先ほどの Collections#replaceAll メソッドを使ってみましょう。

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
 
public class GenericsMethodSample1 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 10; i++) {
            list.add(new Integer(i));
        }
 
        Collections.<Integer>replaceAll(list, new Integer(0),
                                              new Integer(10));
 
        for (int i = 0; i < 10; i++) {
            System.out.println(list.get(i));
        }
    }
}

赤の部分がパラメータ化したメソッドです。メソッド名の前に <Integer> をつけるだけで後は普通のメソッドと同様にコールすることができます。

Collections#copy も使って見ましょう。

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
 
public class GenericsMethodSample2 {
    public static void main(String[] args) {
        List<Serializable> slist = new ArrayList<Serializable>();
        slist.add(null);
        List<C> clist = new ArrayList<C>();
        clist.add(new C());
 
        Collections.<B>copy(slist, clist);
 
        for (int i = 0; i < slist.size(); i++) {
            System.out.println(slist.get(i));
        }
    }
}

class A implements Serializable {}
class B extends A {}
class C extends B {}

クラスの時は単にスーパクラスを使ってみたので、ここではインタフェースを使ってみましたが、問題なく使えています。

最後は Collections#reverse です。単にワイルドカードを使用したメソッドはパラメータの表記を行っていませんでした。使うときも同様で、まったく普通のメソッドと同じように使うことができます。

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
 
public class GenericsMethodSample3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 10; i++) {
            list.add(new Integer(i));
        }
 
        Collections.reverse(list);
 
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

 

 
 
Tiger あまり気にしてもしょうがないんだけど...
 
 

プログラムを作る上ではあまり関係ないかもしれませんが、Generics を導入することによっていくつか言語仕様やクラスファイルの構造が変更されています。

 

シフト演算子と Generics

Generics が入れ子になっていると

    List<List<String>>

なんてふうに >> というのが出てくることがあります。でも、よく考えたらこれはシフト演算子ですね。3 重に入れ子にしたら >>> となってしまいます。これもシフト演算子です。

Generics が導入されたため言語仕様が変更されて、>> や >>> といったシフト演算子と Generics が区別されるようになりました。

 

戻り値だけ違うメソッドのオーバライド

今までは戻り値だけ違うメソッドを派生クラスがオーバライドすることはできませんでした。

たとえば、つぎのようなプログラムはどうでしょう。

public class ReturnTypeTest1 {
    public static void main(String[] args) {
        X x = new X();
        Y y = new Y();
 
        System.out.println(x.foo(10));
        System.out.println(y.foo(10));
    }
}
 
class X {
    public Number foo(int x) {
        return new Integer(x);
    }
}
 
class Y extends X {
    public Integer foo(int x) {
        return new Integer(x);
    }
}

Y クラスでは foo メソッドの戻り値が X クラスのそれと異なり Integer クラスになっています。これを従来の javac でコンパイルすると、

C:\JSR014\examples>C:\j2sdk1.4.2\bin\javac ReturnTypeTest1.java
ReturnTypeTest1.java:18: Y の foo(int) は X の foo(int) をオーバーライドできませ
ん。互換性のない戻り値の型を使おうとしました。
検出値  : java.lang.String
期待値  : java.lang.Integer
    public String foo(int x) {
                  ^
エラー 1 個
C:\JSR014\examples>

当然のごとくエラーが出ます。ところが Generics を導入することで、スーパクラスのメソッドの戻り値のクラスの派生クラスであれば、派生クラスでメソッドをオーバライドしていいようになりました。

先ほどの例だと、Integer クラスは Number クラスの派生クラスなので、コンパイルできることになります。

 

 
 
Tiger 実際はどうなってんだ ????
 
 

J2SDK だと分からなくなってしまいますが、Early Access 版だと java や javac は Windows ならバッチファイル、UNIX だとシェルスクリプトで実現されていました。 たとえば javac.bat と java.bat を見てみると、javac の方はいろいろと変更が加えられています。しかし、java の方は JAR ファイルが 1 つ追加されているだけです。

Windows 版の java.bat で実際に Java を動作させる部分は次のようになっています。

%J2SE14%\bin\java -Xbootclasspath/p:%JSR14DISTR%\gjc-rt.jar %*

-Xbootclasspath/p: というのはコアのライブラリを読み込む前にここで指定している JAR ファイルを読み込ませるためのオプションです。

ようするに gjc-rt.jar という JAR ファイルを読み込んでいるだけで、JVM は全然変更されていないのです。

ということは javac だけで全部行っている???

それじゃ、解析してみましょう。こういうときに役立つのが逆コンパイラ。今回は Jad を使ってみました。

次の例を Jad で逆コンパイルをして見ました。

import java.util.ArrayList;
import java.util.List;
 
public class CastTest3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
 
        list.add("abc");
  
        String str = list.get(0);
    }
}

Jad の結果はデフォルトでは拡張子が jad となります。逆コンパイルの結果です。

import java.util.ArrayList;
import java.util.List;
 
public class CastTest3
{
 
    public CastTest3()
    {
    }
 
    public static void main(String args[])
    {
        ArrayList arraylist = new ArrayList();
        arraylist.add("abc");
        String s = (String)arraylist.get(0);
    }
}

普通の書き方に戻っています !!

要するに、javac でコンパイルするとき、型チェックを行うようにして、Generics の部分は以前の書き方に戻してくれているだけのようです。すごいと思いません。

この解析は Early Access 版で行いましたが、J2SDK でも同じ結果になりました。JVM が変更されていないのはたしかなようです。

ただし、クラスファイルの構造は変化しています。クラスやメソッドの定義部分にはアトリビュートという領域があるのですが、ここに Generics の情報が記入されるようになっています。

 

 
 
Tiger コアライブラリはどこが変わったのか ????
 
 

Tiger ではコアライブラリが Generics を使って書き直されています。

どこが書きかわっているのでしょうか。少し調べてみましょう。

調べるのに必要なのは J2SDK, SE 1.5 に付属しているソースです。

ソースから Generics を使用しているものを抜き出すのは大変です。そこで、Early Access 版に付属するソースから変更されているものを探し出し、それが J2SDK でどうなっているか調べてみました。

変更されているパッケージは次の通りです。

  • java.lang
  • java.lang.ref
  • java.lang.reflect
  • java.util
  • java.util.jar
  • java.util.loggin

特に変更の大きい java.lang, java.lang.reflect, java.util について調べてみましょう。

まず java.lang パッケージからです。

 

java.lang.lang パッケージ、java.lang.reflect パッケージ

この 2 つのパッケージの変更はリフレクションに関する部分の変更です。

まず Class クラスです。

Clas クラスはパラメター化されるようになりました。したがって、次のような書き方ができます。ただし、Object#getClass メソッドの戻り値は Class<? extends Object> となっているのでキャストが必要です。

    String str = "ABC";
    Class<String> cls = (Class<String>)str.getClass();
    String str2 = cls.newInstance();

newInstance メソッドの戻り値が自動的にパラメータ化されたクラスになるのでキャストが必要なくなります。

同様に getConstructor メソッドも戻り値が Constructor<T> になります。

この結果、Constructor クラスもパラメータ化されています。やはり、newInstance メソッドの戻り値がパラメータのクラスになるので、キャストが要らなくなります。

 

java.util パッケージ

一番、変更が大きいのが java.util パッケージのコレクション関連のクラスです。

基本的に、Collection インタフェースの派生クラス、Map インタフェースの派生クラスはすべてパラメータ化されています。Collection インタフェースは

public interface Collection<E> extends Iterable<E> {
	  ...

Map インタフェースが

public interface Map<K,V> {
	  ...

となっています。Iterable インタフェースというのは新しく導入されたインタフェースで、拡張 for 文で使用されます。

また、Iterator インタフェースもパラメータ化されています。

Collections クラスと Array クラスはパラメータ化されてはいませんが、多くの static メソッドがパラメータ化されています。

 

 
 
Tiger おわりに
 
 

Generics 使いだすと、今までの苦労はなんだったんだというふうになります。軽いカルチャーショック状態です ^^;;

ところが、使い出すともうやめられません。今までの生活には戻れない体になってしまいました。

たまに、J2SE 1.4 とかで書くときに、思わず Generics で書いていて、「これはいけない、もどさなくては」ということもしばしば。

今回は C++ のテンプレートとの比較なぞは行いませんでしたが、参考文献をあげましたので、そちらもご参照ください。JavaWorld の記事はかなり古いので現状の Generics とは食い違っている部分もあります。

なにはともあれ、Generics は便利なので、ぜひご活用を。

 

今回使用したサンプルはここからダウンロードできます。

参考

  • JCP JSR-14 http://jcp.org/en/jsr/detail?id=14
    仕様書はここからダウンロードできます
  • 平鍋健児 「Java Generics ファースト・レビュー」 JavaWorld, 2002 January, p.76 - p.97
    Part 2 で Generics と C++ のテンプレートの比較を行っています。
  • 柴田芳樹 「Java Generics 表現力と安全性を向上させる "Tiger" の新機能」 JAVA PRESS Vol. 28 p. 12 -21
    JAVA PRESS Vol.30 の付属 CD にも入っています。

(Oct. 2003)
(改訂 Jan. 2004)

 
 
Go to Contents Go to Java Page