はじめに
前回の Java の static な話 のこともあり、もくもくと Java タイムを続けてしてみようと思います.
ゴール
ジェネリックスなのか、ジェネリクスなのかは、さておき.
Java におけるジェネリクスに関して、簡単な例とか、遊びとともに幅広く触れてみる.
ジェネリクス(総称型)と型安全
jdk 1.5 から導入されました.
これを書いているちょうどのタイミングで「jdk 9.0.4のアップデートしてね」と Javaからお声かけがあったり、java10 が出るとかの話も聞いてるので、jdk 1.5... となりましたが.
さらにそれ以前だと、
List lst = new ArrayList();
Map map = new HashMap();
こんな形の原型(raw type)で記述していたようです. 今もできるし動きますが、推奨されません.
どんなオブジェクトでも入ります. 入ってしまいます.
lst.add("sample");
lst.add(11111);
// for-each も1.5以降
// イテレータもまだ raw です
Iterator it = lst.iterator();
while (it.hasNext()) {
Object o = it.next();
System.out.print(o); // sample1111
}
これだと、型安全が保証されません.
コレクションの中の要素を取り出す際は、注意深くキャストしなければなりませんし
lst.add("sample");
lst.add(11111);
Iterator it = lst.iterator();
while (it.hasNext()) {
Object o = it.next();
System.out.print((String) o); // sample と ClassCastException
}
このように、凡ミスであろうが何だろうが、型を間違えてしまった場合、実行時はじめて例外に気づくため、予期せぬタイミングでプログラムが停止する恐れもあります.
Exception に関して
キャスト失敗時に投げられるClassCastException
は、RuntimeException
を継承する例外クラスです.
RuntimeException
は、実行時例外として、例外処理は使用者の任意となります.
つまり、エラーに気づくタイミングはコンパイル時ではなく、その後の実行時になります.
仮型引数<E>
jdk 1.5 以降、Collection
インタフェースはCollection<E>
と進化(?)し、一つのタイプのみを収納できるようになりました.
Collection
の javadoc に書いている通り、E
は "このコレクションの要素のタイプ" のことです.
コレクションを継承しているList
も同じで、List<E>
となりました.
なにが入るかはまだ決まってないけど、何か一つの型だけが入るのは保証するから、仮で入れとくね、みたいな. Element の E なのでしょう.
で、実際リストのインスタンスを作る際はList<Integer> lst = new ArrayList<>();
とか...
ようやく仮型引数の席が埋まり、ここではInteger
がこのリストの実型引数になります.
jdk7 以降のダイヤモンド演算子
.java
List lst = new ArrayList();
List lst = new ArrayList<>(); // 省略可能になった
これらジェネリクスインタフェースおよびその実装クラスを<b>ジェネリクス型</b>と言います.
また、java におけるジェネリックは、クラス、インタフェース、メソッド、コンストラクタに対して宣言できます.
# raw type と ```<Object>``` の違い
例えば、原型 List 同様、```List<Object>```は、どんな型のオブジェクトも格納できます.
```.java
List<Object> objList = new ArrayList<>();
objList.add("a");
objList.add(1);
objList.add(new Sample());
ここまでだったら、大した違いがないように思えますが
まず raw type の List は
private static List rawList() {
return new ArrayList();
}
private static void printList(List rawList) {
rawList.forEach(System.out::println);
}
public static void main(String... args) {
printList(rawList()); // 通る
}
コンパイラからは、何も言われません.
つまり何らかの型ミスマッチが起きてそれを気づくタイミングは、実行時になってしまうため、型安全とは言えません.
反面、List<Object>
は
List<Object> objList = new ArrayList<String>(); // コンパイルエラー
とか
private static List<String> strList() {
return new ArrayList<String>();
}
private static void printObjList(List<Object> objList) {
objList.forEach(System.out::println);
}
public static void main(String... args) {
printObjList(strList()); // コンパイルエラー
}
その逆(?)も
private static List<Object> objList() {
return new ArrayList<Object>();
}
private static void printStrList(List<String> strList) {
strList.forEach(System.out::println);
}
public static void main(String... args) {
printStrList(objList()); // コンパイルエラー
}
できない.
理由は List<String>
は List のサブタイプではあるが、List<Object>
のサブタイプではないためです. つまり、List<String>
とList<Object>
の間には継承関係が成り立ちません.
このようなことから、ジェネリクスを 不変(invariant の方) と呼びます.
そこで、先ほどのコードを一つ修正するとしたら
List<Object> objList = new ArrayList<String>(); // コンパイルエラー
List<?> someTypeList = new ArrayList<String>();
このような形になると思います.
<?>
に関しては、以降のワイルドカードのところで整理したいと思います.
ジェネリクスと配列の違い
配列は共変(covariant)
ジェネリクスは不変、という話の続きです.
共変の性質として、先ほどの「継承関係」の話ですが、配列だとこのようなことができます.
private static void printAry(Object[] objAry) {
Arrays.stream(objAry).forEach(System.out::println);
}
private static String[] strAry(String[] strAry) {
return strAry;
}
printAry(strAry(new String[]{"a", "b", "c"}));
String と Object の間に、継承関係(String が Object のサブ)があるなら、String[] と Object[] の間も継承関係が適応されます.
つまり
Object[] objAry = new String[10];
objAry[0] = 1;
不正な型を格納しようとしているのにも関わらず、コンパイル時には気づけません.
ジェネリクスのイレイジャ(型破棄)
- 具現化
配列は、実行時にはもう要素の型がわかっていて、その型のまま実行することになります.
- 具現化不可能
一方、ジェネリクスは、コンパイル時に一度チェックし終えたら、その後はイレイジャ(実行時には要素の型情報が消してある)で実装を行います. コンパイルの時点と比べて、実行時には情報が一つ失われている状態です.
これにより、jdk 1.5 以前の raw type のコレクションであっても、ジェネリクスを既存の raw type に追加することができたのです.
ジェネリクスな設計
Tuple
タプルは複数かつ異なる要素に順序をつけて格納できるデータ構造です.
public class Pair<V, W> {
private final V v;
private final W w;
Pair(V v, W w) {
this.v = v;
this.w = w;
}
V getV() {
return v;
}
W getW() {
return w;
}
}
Pair<String, Integer> tuple2 = new Pair<>("toilet-score", 100);
String v = tuple2.getV();
Integer w = tuple2.getW();
System.out.println(v + w);
といった、基本となる二つの異種型のデータを格納し取り出すペアの使い型を拡張して、
public class Tuple {
public static class Tuple2<A, B> {
private final A a;
private final B b;
public Tuple2(A a, B b) {
this.a = a;
this.b = b;
}
public A getA() {
return a;
}
public B getB() {
return b;
}
}
public static class Tuple3<A, B, C> extends Tuple2<A, B> {
private final C c;
public Tuple3(A a, B b, C c) {
super(a, b);
this.c = c;
}
public C getC() {
return c;
}
}
// Tuple4, Tuple5 ...
}
(汎用性のため static インナークラスを追加、各コンストラクタのアクセッサーを public に変えました)
三つ、四つ、それ以上の型のデータが、型安全な方法で格納できる汎用的なタプルユーティリティクラスが作れると思います.
先人の知恵を拝借
より確かで多様なパターンとして、すでにライブラリは存在しているので、有名なものだと、 Commons Lang の Pair などなどがあります.
一方、小規模ですが、わざわざ一個のタイプから十個のタイプまで(!) シンプルな構成で提供してくれる javatuples: 公式 github をも参考してください.
javatuples に関して、個人的には、Unit という型を一つしか持たないタプルがスタートになって、そこから美しい継承の形をしているのかな...と考えましたが、二つの型をもつ Tuple クラスを軸にし、 Unit に関しても Tuple を継承させている点が面白かったです.
ワイルドカード
先ほどの話の延長ですが、ジェネリクスは不変です. 仮型<T>
は、何かしら確実な型が一つ入ることは保証していますが、T 型の継承関係のある型まで許しているわけではありません. List<Object>
とList<String>
に継承関係はありません.
つまり、柔軟ではありません.
そこで、ジェネリクスと同じタイミング(jdk 1.5)で、ワイルドカードが登場します.
非境界線ワイルドカード <?>
ジェネリクスを使いたいけど、まだ実型のパラメータが決まっていない場合、仮型<E>
に?
を渡しておきます.
// T が解決できないため, コンパイルエラー
List<T> someList = new ArrayList<String>();
// 実型が決まる前に格納しようとしているため, コンパイルエラー
List<?> anotherList = new ArrayList<>(); anotherList.add("a");
// OK
List<?> theOtherList = new ArrayList<String>(); theOtherList.add("a");
境界線ワイルドカード(上限) <? extend T>
// OK. Integer は Number の sub-class.
List<? extends Number> myList = new ArrayList<Integer>();
// OK. Long は Number の sub-class.
List<? extends Number> yourList = new ArrayList<Long>();
// Object は Number を超える型なので, コンパイルエラー
List<? extends Number> someList = new ArrayList<Object>();
境界線ワイルドカード(下限) <? super T>
// Long は Integer の上位クラスではないため, コンパイルエラー
List<? super Integer> suNumList2 = new ArrayList<Long>();
// OK. Number は Integer の上位クラス.
List<? super Integer> suNumList = new ArrayList<Number>();
Functional Interface とジェネリクス
以下は、jdk 1.8 から導入された主な関数型インタフェースです.
Functional Interface javadoc に書いてありますが、抽象メソッドを一つだけ持っているインターフェースです.
以下は java.util.Objects
に入っている Function インタフェースの一部です.
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
* 与えられた引数を受け付けます.
*
* @param t the function argument
* パラメータ t 関数の引数
*
* @return the function result
* リターン 関数の結果
*/
R apply(T t);
// omit
}
Function BiFunction Consumer Supplier...など、これら関数インタフェースは、
まだ型は決まってはいないけど、何かを受け入れたり、
これもまだ型は決まってはいないけど、結果として何かを返したりする構造を表しています.
IntSupplier
の getAsInt()
メソッドなど、あらかじめ戻り値は int 型だけ、と決めているものもありますが、基本的にはジェネリクスを使って柔軟性を図っていることがわかります.
ジェネリクスと私 - ジェネリクスメソッドっぽい遊び
という妙なサブタイトルですが、単なる遊びのメモのようなものです.
先日、あるアルゴリズムを作成しました.
要素の型にこだわらないリストを受け付けて、そのリストに対して処理を行い、操作結果を詰めこんだ別リストをリターンする、O(N) な関数です.
private static List<Object> doSomething(List<Object> list) {
List<Object> result = new ArrayList<>();
// 処理
return result;
}
でも、
public static void main(String[] args) {
List<String> strListA = Arrays.asList("c", "a", "b", "b", "c", "a", "d", "b");
List<Object> strListB = Arrays.asList("c", "a", "b", "b", "c", "a", "d", "b");
// List<Object> ではなく List<String> を渡すためコンパイルエラー
doSomething(strListA).forEach(System.out::print);
doSomething(strListB).forEach(System.out::print);
}
パラメータに渡すリストの型パラメータはすべて Object 型と制限されるということが気に入りませんでした.
例えば、数字だけを受け付けるリストを生成するメソッドを追加し、パラメータで渡したくなった場合は少し不便です.
private static List<Number> makeNumberList(Number...nums) {
List<Number> list = new ArrayList<>();
for (Number num : nums) list.add(num);
return list;
}
public static void main(String[] args) {
doSomething(makeNumberList(1, 2, 3, 4, 1)).forEach(System.out::print); // コンパイルエラー
}
そこで<Object>
を <T>
に変更します.
コンパイル時に型がチェックできるよう、型トークンを追加しました.
備考 (上記、取り消し線)
.java
// メソッドにクラルリテラル Class を渡すことで
doSomething(List list, Class type)
// 実行時に型情報を伝えることが可能
doSomething(someList, Integer.class)
と、型パラメータと型トークンが一致する時のみ、コンパイルできることを図っていましたが、修正の過程でメソッドの中でのtypeの使い道が失われたため、保留します.
```Something.java
private static <T> List<T> doSomething(List<T> list) {
List<T> result = new ArrayList<>();
// 処理
return result;
}
数字型のリストを作成するメソッドは Number 型とその下位の型が入るようにしてみて...
private static <T extends Number> List<T> makeNumberList(T...args) {
List<T> list = new ArrayList<>();
for (T arg : args) list.add(arg);
return list;
}
public static void main(String[] args) {
List<Integer> someList = makeNumberList(5, 1, 2, 3, 3, 4, 3, 1, 2, 4, 5);
doSomething(someList).forEach(System.out::print);
}
これにより、List<Object>
以外のリストをパラメータとして渡せるようになりました.
おわりまして
なぜかふとしたタイミングで、ジェネリクスが気になりました.
変な言い方になりますが「型安全だけどそこまで安全でもなかった世界にふと舞い降りた型安全で柔軟な魔法道具」(なにを言っているんだ) みたいなみょーなところが気になったのかもしれません.
私自身これを書きながらここが曖昧だったとか、実はここ知らなかったとか、そういったところがたくさんありましたので、間違いなど見つけ次第ご教示くださいませ.
参考
Effective Java 第2版
Oracle Java docs
- https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.1.2
- https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html#package.description
以上