「ジェネリクス (generics)」は JDK 5 から導入された機能です。Java のジェネリクスは、C++のテンプレート、C# のジェネリック、Scala の多相型などと同様に、データ型をパラメータ化することにより、ひとつのクラス定義や関数定義で複数のデータ型に対応することができます。今回はジェネリクスの基本的な使い方を簡単に説明します。
ジェネリクスの基本的な考え方は簡単です。クラスや関数 (メソッド) を定義するとき、フィールド変数や関数の引数 (返り値) にデータ型を指定しますが、これをパラメータ化できるようにしたものがジェネリクスです。クラスや関数を定義するとき、関数の仮引数のようにデータ型を受け取るパラメータを指定します。これを「型パラメータ」とか「型変数」と呼びます。
実際に使用するときはデータ型を型パラメータに渡します。関数を呼び出すときに渡す実引数と同じと考えてください。C++のテンプレートでは、そのデータ型に合わせたクラスや関数が生成されますが、Java のジェネリクスでは生成されるクラスや関数の実体はひとつで、コンパイル時にデータ型のチェックが行われます。どちらの方法でも、一つのプログラムでいろいろなデータ型に対応することができます。
ジェネリクスの構文を示します。
型パラメータは < ... > の中で指定します。型パラメータは複数指定することができます。名前は英大文字一文字で表すことが一般的です。Java の場合、型パラメータに渡せるデータ型は参照型データだけです。C++ や C# のように int や double など基本的なデータ型を渡すことはできません。ラッパークラス Integer や Double を使ってください。
それでは実際にジェネリクスでクラスを定義してみましょう。まず最初に、ジェネリクスを使わないでクラスを定義します。
jshell> class Foo { ...> int x; ...> Foo() { x = 0; } ...> Foo(int n) { x = n; } ...> int getX() { return x; } ...> void setX(int n) { x = n; } ...> } | 次を作成しました: クラス Foo jshell> var a = new Foo() a ==> Foo@2b71fc7e jshell> a.getX() $3 ==> 0 jshell> a.setX(10) jshell> a.getX() $5 ==> 10
クラス Foo は int 型の整数を格納します。ジェネリクスを使うと、整数型だけではなく他のデータ型を格納するクラスを簡単に作ることができます。次の例を見てください。
jshell> class Foo<T> { ...> T x; ...> Foo() { x = null; } ...> Foo(T n) { x = n; } ...> T getX() { return x; } ...> void setX(T n) { x = n; } ...> } | 次を作成しました: クラス Foo jshell> var a = new Foo<Integer>(0) a ==> Foo@2b71fc7e jshell> a.getX() $3 ==> 0 jshell> a.setX(10) jshell> a.getX() $5 ==> 10 jshell> var b = new Foo<String>("hello, world") b ==> Foo@506c589e jshell> b.getX() $7 ==> "hello, world" jshell> b.setX("good bye!") jshell> b.getX() $9 ==> "good bye!" jshell> var c = new Foo<int[]>(new int[]{1,2,3,4,5}) c ==> Foo@69663380 jshell> c.getX()[0] $12 ==> 1 jshell> c.getX()[4] = 50 $13 ==> 50 jshell> for (int x: c.getX()) System.out.println(x) 1 2 3 4 50
ここでは型パラメータ名を T としました。型パラメータ T は Foo の中で参照することができます。具体的には、int のかわりに T を使ってプログラムを記述するだけです。メンバ変数の宣言は int x から T x に書き換えます。メソッドの定義も int getX() を T getX() に、void setX(int n) を void setX(T n) に書き換えます。
コンストラクタも同様に書き換えます。C++のテンプレートでは、クラス T のコンストラクタは T() で呼び出すことができますが、Java のジェネリクスではできません。Java の場合、パラメータは参照型だけなので、Foo() ではとりあえず x を null で初期化することにします。不要であれば削除してください。
ジェネリクスで定義したクラスから、実際のクラスとインスタンスを生成する場合は次のように宣言します。
クラス名の後ろの < > の中に型 (データ型) を指定します。データ型は "クラス名<型, ...>" になります。本稿ではジェネリクスで定義された型を「ジェネリクス型」と記述することにします。ジェネリクス型はクラス名が同じでも異なる型を与えれば、別のデータ型として扱われます。
Java の場合、1 のように右辺と左辺で同じデータ型を記述しないといけませんが、2 のように右辺の型指定を省略することもできます。この <> をダイヤモンド演算子といいます。局所変数の場合、3 のように var で宣言したほうが簡単です。なお、3 の方式で右辺にダイヤモンド演算子を使用すると、ジェネリクス型は クラス名<Object> になります。コンパイルエラーにはならないので注意してください。
jshell で var a = new Foo<Integer>(0) と入力すれば、整数 (Integer) を格納する Foo のインスタンスを生成して変数 a にセットします。var b = new Foo<String>("...") と入力すれば、変数 b に文字列を格納する Foo のインスタンスが代入され、var c = new Foo<int[]>(new int[]{...}) と入力すれば、変数 c にint 型の配列を格納する Foo のインスタンスが代入されます。
このようにジェネリクスを使うと、一つのクラス定義で複数のデータ型に対応するプログラムを作ることができます。
次はジェネリクスで関数 (メソッド) を作ってみましょう。簡単な例として、恒等関数 (identity function) を取り上げます。次の例を見てください。
jshell> <T> T identity(T x) { return x; } | 次を作成しました: メソッド identity(T) jshell> identity(123) $2 ==> 123 jshell> identity(1.2345) $3 ==> 1.2345 jshell> identity("hello, world") $4 ==> "hello, world"
関数 identity() は引数をそのまま返す関数です。T が型パラメータで、これで任意の型のデータに対応することができます。ジェネリクスを使わない場合、データ型の種類だけ identitiy() を多重定義しなければいけませんが、ジェネリクスを使えば関数をひとつ定義するだけですみます。つまり、Integer, Double, String など Java の参照型データであれば何でも対応することができるわけです。このような関数を「多相型関数 (polymorphic function)」といいます。
多相型関数のように、いろいろな型を取ることができる性質のことを「多相性 (polymmprphism)」といいます。多相性は Java のジェネリクスだけではなく、ML 系の言語 (OCaml, SML/NJ, Haskell など) の特徴のひとつです。これらの言語には「型推論」という機能があり、プログラマがデータ型を宣言する必要はほとんどありません。
それでは簡単な例題として、2 つの異なるデータ型を格納するクラス Pair と、3 つの異なるデータ型を格納するクラス Triple をジェネリクスで作成してみましょう。次のリストを見てください。
リスト : Pair と Triple class Pair<T, U> { private T value1; private U value2; // コンストラクタ Pair(T x, U y) { value1 = x; value2 = y; } // メソッド T first() { return value1; } U second() { return value2; } void setFirst(T x) { value1 = x; } void setSecond(U y) { value2 = y; } // 文字列に変換 @Override public String toString() { return "(" + value1 + ", " + value2 + ")"; } } class Triple<T, U, V> { private T value1; private U value2; private V value3; // コンストラクタ Triple(T x, U y, V z) { value1 = x; value2 = y; value3 = z; } // メソッド T first() { return value1; } U second() { return value2; } V third() { return value3; } void setFirst(T x) { value1 = x; } void setSecond(U y) { value2 = y; } void setThird(V z) { value3 = z; } // 文字列に変換 @Override public String toString() { return "(" + value1 + ", " + value2 + ", " + value3 + ")"; } } public class sample110 { public static void main(String[] args) { var a = new Pair<String, Integer>("foo", 10); System.out.println(a); System.out.println(a.first()); System.out.println(a.second()); a.setFirst("FOO"); a.setSecond(20); System.out.println(a); var b = new Triple<String, Integer, Double>("bar", 123, 1.1234); System.out.println(b); var c = new Pair<String, String>("baz", "oops!"); System.out.println(c); // var d = new Pair<String, Integer>[4]; // ジェネリクス型の配列は作成できない } }
$ javac sample110.java $ java sample110 (foo, 10) foo 10 (FOO, 20) (bar, 123, 1.1234) (baz, oops!)
Pair には 2 つの異なるデータ型を格納するので、型パラメータは T, U の 2 つが必要になります。Triple は T, U, V の 3 つが必要です。フィールド変数 value1 が T 型のデータ、value2 が U 型のデータ、value3 が V 型のデータを格納します。あとは、フィールド変数のアクセスメソッドと文字列に変換するメソッド toString() を定義します。
var a = new Pair<String, Integer>(...) は String, Integer を格納する Pair のインスタンスを生成します。var b = new Triple<String, Integer, Double>(...) は String, Integer, Double を格納する Triple のインスタンスを生成します。なお、T, U, V には同じデータ型を指定してもかまいません。Java の場合、ジェネリクス型の配列は作成することができません。ジェネリクス型のオブジェクトを格納する場合は ArrayList<E> を使います。
Java の ArrayList は可変長の一次元配列を実装したコレクションクラスです。ArrayList<E> はそのジェネリクス版です。Java の場合、配列を宣言したあとで、その大きさを変更することはできません。ArrayList の場合、保持している配列の容量が足りなくなると自動的に拡張してくれます。一般に、大きさを自由に変えることができる配列を「可変長配列」といいます。Perl, Python, Ruby などスクリプト言語の多くは可変長配列をサポートしています。
ArrayList はパッケージ java.util 内に定義されているので、使用するときは java.util.ArrayList をインポートしてください。変数の宣言は次のように行います。
ArrayList<E> はインターフェース List<E> を継承しています。変数の型を List<E> で宣言しておくと、ArrayList<E> 以外のコレクションクラスに切り替えることも簡単にできます。局所変数の場合、3 のように var で宣言すると簡単です。なお、List もパッケージ java.util 内に定義されているので、使用するときは java.util.List をインポートしてください。
上記の場合、空 (要素数が 0) の ArrayList が生成されます。引数に正整数 n を指定すると、容量が n で空の ArrayList が生成されます。また、イテレータを実装しているコレクションクラスを指定すると、その要素を格納した ArrayList が生成されます。
List<E> の基本的なメソッドを下表に示します。
メソッド | 機能 |
---|---|
boolean add(E e) | 末尾に e を追加する |
void add(int i, E e) | i 番目に e を挿入する |
E get(int i) | i 番目の要素を求める |
E remove(int i) | i 番目の要素を削除する |
void set(int i, E e) | i 番目の要素を e を書き換える |
int size() | 格納している要素数を返す |
boolean isEmpty() | コレクションが空であれば true を返す |
void clear() | コレクションを空にする |
C++のテンプレート vector は配列と同様に角カッコで要素にアクセスすることができますが、Java の List<E> はできません。ご注意くださいませ。この他にも List<E> には便利なメソッドが用意されています。詳細は Java のマニュアル インタフェース List<E> をお読みください。
簡単な使用例を示しましょう。
jshell> /imports | import java.io.* | import java.math.* | import java.net.* | import java.nio.file.* | import java.util.* | import java.util.concurrent.* | import java.util.function.* | import java.util.prefs.* | import java.util.regex.* | import java.util.stream.* jshell> var a = new ArrayList<Integer>() a ==> [] jshell> for (int i = 1; i <= 8; i++) a.add(i) jshell> a a ==> [1, 2, 3, 4, 5, 6, 7, 8] jshell> var b = new ArrayList<Integer>(a) b ==> [1, 2, 3, 4, 5, 6, 7, 8] jshell> for (int i = 0; i < b.size(); i++) b.set(i, a.get(i) * 10) jshell> b b ==> [10, 20, 30, 40, 50, 60, 70, 80]
JShell は java.util.* をインポートしてあるので、そのままで ArrayList を使用することができます。ジェネリクスではない ArrayList では、要素のデータ型を Object で宣言しています。したがって、どんなデータ型でも代入することができますが、要素を参照するときはダウンキャストが必要になります。プログラマが間違って想定外のデータを代入すると、ダウンキャストに失敗することもあるでしょう。
ジェネリクス版の場合、データ型が指定されているので、コンパイル時にデータ型のチェックが行われます。異なるデータ型をセットすることはできませんし、参照するときのダウンキャストも不要になります。つまり、データ型に対して安全なプログラムを作ることができるわけです。
それでは、Pair<T, U> を ArrayList<E> に格納してみましょう。Java のジェネリクスはジェネリクスを入れ子にすることができます。次の例を見てください。
jshell> /open sample110.java jshell> var a = new ArrayList<Pair<String, Integer>>() a ==> [] jshell> a.add(new Pair<String, Integer>("foo", 10)) $5 ==> true jshell> a.add(new Pair<String, Integer>("bar", 20)) $6 ==> true jshell> a.add(new Pair<String, Integer>("baz", 30)) $7 ==> true jshell> a.add(new Pair<String, Integer>("oops", 40)) $8 ==> true jshell> a a ==> [(foo, 10), (bar, 20), (baz, 30), (oops, 40)]
「インスタンス初期化子 (Instance Initializer)」を使うと、List の初期化は次のように記述することができます。
リスト : インスタンス初期化子 var a = new ArrayList<Pair<String, Integer>>() { { add(new Pair<String, Integer>("foo", 10)); add(new Pair<String, Integer>("bar", 20)); add(new Pair<String, Integer>("baz", 30)); add(new Pair<String, Integer>("oops", 40)); } };
インスタンス初期化子は {{ ... }} の中のメソッドを順番に実行します。なお、インスタンス初期化子を使う場合、ダイヤモンド演算子を使うことはできません。ご注意くださいませ。
もうひとつ簡単な例題として、拙作のページ 連結リスト で作成した片方向連結リスト SinglyLinkedList をジェネリクスで書き直してみましょう。クラス定義は次のようになります。
リスト : クラス定義 class SinglyLinkedList<E> implements Iterable<E> { // セル private class Cell { // フィールド変数 private E value; private Cell link; // コンストラクタ Cell(E item, Cell xs) { value = item; link = xs; } // アクセスメソッド E getValue() { return value; } Cell getLink() { return link; } void setValue(E item) { value = item; } void setLink(Cell xs) { link = xs; } } // フィールド変数 private Cell head; private int size; // コンストラクタ SinglyLinkedList() { head = new Cell(null, null); // ヘッダーセル size = 0; } // メソッドの定義 ・・・ 省略 ・・・ }
要素のデータ型を型パラメータ E で表します。イテレータを実装するため、Iterable のジェネリクス版 Iterable<E> を継承します。内部クラス Cell は SinglyLinkedList の型パラメータ E のスコープ内にあるので、そのままで E を参照することができます。フィールド変数 value の型を E に、コンストラクタとアクセスメソッドで要素の型を E に変更するだけです。
なお、static なフィールド変数、メソッド、内部クラスでは、クラスの型パラメータを参照することはできません。コンパイルエラーになります。ご注意くださいませ。
あとはメソッドで要素の型を E に変更します。メソッドの仕様を下表に示します。
メソッド | 機能 |
---|---|
E get(int n) | n 番目の要素を求める |
void insert(int n, E x) | n 番目の位置にデータ x を追加する |
E remove(int n) | n 番目の要素を削除する |
E set(int n, E x) | n 番目の要素をデータ x に置き換える 返り値は置き換えられた古い要素を返す |
void clear() | 連結リストを空にする |
int size() | 要素の個数を返す |
boolean isEmpty() | 空リストであれば true を返す |
List<E> toList() | ArrayList<E> に変換する |
String toSting() | 文字列に変換する |
Iterator<E> iterator() | イテレータを返す |
イテレータを返すプログラムは次のようになります。
リスト : イテレータ public Iterator<E> iterator() { // 無名クラス return new Iterator<E>() { Cell xs = head.getLink(); public boolean hasNext() { return xs != null; } public E next() { E item = xs.getValue(); xs = xs.getLink(); return item; } public void remove() { throw new UnsupportedOperationException(); } }; }
iterator() は Iterator<E> を返します。Iterator<E> は Iterator のジェネリクス版です。あとは要素の型を E に変更するだけです。あとのプログラムは簡単なので説明は省略いたします。詳細は プログラムリスト をお読みください。
それでは簡単なテストを行ってみましょう。プログラムリストと実行結果を示します。
リスト : 簡単なテスト public class slist2 { static <E> void printCollection(Iterator<E> iter){ while(iter.hasNext()){ System.out.print(iter.next() + " "); } System.out.println(); } public static void main(String[] args) { var xs = new SinglyLinkedList<Integer>(); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); for (int i = 0; i < 10; i++) { System.out.println("insert: " + i + ", "+ i); xs.insert(i, i); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); } for (Integer n: xs.toList()) System.out.print(n + " "); System.out.println(); for (int i = 0; i < 10; i++) { xs.set(i, xs.get(i) + 10); System.out.print(xs.get(i) + " "); } System.out.println(); for(int i = 0; i < 5; i++) { System.out.println("remove: " + i); System.out.println(xs.remove(i)); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); } System.out.println("clear:"); xs.clear(); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); SinglyLinkedList<Integer> ys = new SinglyLinkedList<>(); for(int i = 0; i < 10; i++){ ys.insert(0, i); } printCollection(ys.iterator()); for (Integer n: ys) System.out.print(n + " "); System.out.println(); } }
$ javac slist2.java $ java slist2 () 0 true insert: 0, 0 (0) 1 false insert: 1, 1 (0 1) 2 false insert: 2, 2 (0 1 2) 3 false insert: 3, 3 (0 1 2 3) 4 false insert: 4, 4 (0 1 2 3 4) 5 false insert: 5, 5 (0 1 2 3 4 5) 6 false insert: 6, 6 (0 1 2 3 4 5 6) 7 false insert: 7, 7 (0 1 2 3 4 5 6 7) 8 false insert: 8, 8 (0 1 2 3 4 5 6 7 8) 9 false insert: 9, 9 (0 1 2 3 4 5 6 7 8 9) 10 false 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 remove: 0 10 (11 12 13 14 15 16 17 18 19) 9 false remove: 1 12 (11 13 14 15 16 17 18 19) 8 false remove: 2 14 (11 13 15 16 17 18 19) 7 false remove: 3 16 (11 13 15 17 18 19) 6 false remove: 4 18 (11 13 15 17 19) 5 false clear: () 0 true 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
正常に動作していますね。要素のデータ型が型パラメータで指定されているので、get() で要素を取り出すときキャストしなくてもよいので、より安全なプログラムを作ることができます。
// // slist2.java : 片方向連結リスト (ジェネリクス版) // // Copyright (C) 2016-2021 Makoto Hiroi // import java.util.Iterator; import java.util.ArrayList; import java.util.List; // 例外クラス class ListIndexOutOfBoundsException extends IndexOutOfBoundsException { public ListIndexOutOfBoundsException() { } public ListIndexOutOfBoundsException(String msg) { super(msg); } } // 連結リスト class SinglyLinkedList<E> implements Iterable<E> { // セル private class Cell { // フィールド変数 private E value; private Cell link; // コンストラクタ Cell(E item, Cell xs) { value = item; link = xs; } // アクセスメソッド E getValue() { return value; } Cell getLink() { return link; } void setValue(E item) { value = item; } void setLink(Cell xs) { link = xs; } } // フィールド変数 private Cell head; private int size; // コンストラクタ SinglyLinkedList() { head = new Cell(null, null); // ヘッダーセル size = 0; } // n 番目のセルを求める private Cell nth(int n) { int i = -1; Cell xs = head; while (xs != null) { if (n == i) return xs; i++; xs = xs.getLink(); } throw new ListIndexOutOfBoundsException("SinglyLinkedList"); } // 参照 public E get(int n) { return nth(n).getValue(); } // 挿入 public void insert(int n, E item) { Cell xs = nth(n - 1); Cell ys = new Cell(item, xs.getLink()); xs.setLink(ys); size++; } // 削除 public E remove(int n) { Cell xs = nth(n - 1); Cell ys = xs.getLink(); if (ys == null) { throw new ListIndexOutOfBoundsException("SinglyLinkedList"); } xs.setLink(ys.getLink()); size--; return ys.getValue(); } // 書き換え public E set(int n, E item) { Cell xs = nth(n); E old = xs.getValue(); xs.setValue(item); return old; } // 空にする public void clear() { head.setLink(null); size = 0; } // 個数を求める public int size() { return size; } // 空リストか public boolean isEmpty() { return size == 0; } // List 型に変換する public List<E> toList(){ List<E> a = new ArrayList<>(); Cell xs = head.getLink(); for (int i = 0; i < size; i++) { a.add(xs.getValue()); xs = xs.getLink(); } return a; } // 文字列に変換 public String toString(){ String buff = "("; Cell xs = head.getLink(); while (xs != null) { buff += xs.getValue().toString(); xs = xs.getLink(); if (xs != null) buff += " "; } buff += ")"; return buff; } // イテレータ public Iterator<E> iterator() { // 無名クラス return new Iterator<E>() { Cell xs = head.getLink(); public boolean hasNext() { return xs != null; } public E next() { E item = xs.getValue(); xs = xs.getLink(); return item; } public void remove() { throw new UnsupportedOperationException(); } }; } } // 簡単なテスト public class slist2 { static <E> void printCollection(Iterator<E> iter){ while(iter.hasNext()){ System.out.print(iter.next() + " "); } System.out.println(); } public static void main(String[] args) { var xs = new SinglyLinkedList<Integer>(); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); for (int i = 0; i < 10; i++) { System.out.println("insert: " + i + ", "+ i); xs.insert(i, i); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); } for (Integer n: xs.toList()) System.out.print(n + " "); System.out.println(); for (int i = 0; i < 10; i++) { xs.set(i, xs.get(i) + 10); System.out.print(xs.get(i) + " "); } System.out.println(); for(int i = 0; i < 5; i++) { System.out.println("remove: " + i); System.out.println(xs.remove(i)); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); } System.out.println("clear:"); xs.clear(); System.out.println(xs); System.out.println(xs.size()); System.out.println(xs.isEmpty()); SinglyLinkedList<Integer> ys = new SinglyLinkedList<>(); for(int i = 0; i < 10; i++){ ys.insert(0, i); } printCollection(ys.iterator()); for (Integer n: ys) System.out.print(n + " "); System.out.println(); } }