Effective Java 3rd [Chapter 5] - ジェネリクス
ジェネリクス
Effective Java 第 3 版の個人的メモ
- 項目 26 原型を使わない
- 項目 27 無検査警告を取り除く
- 項目 28 配列よりリストを選ぶ
- 項目 29 ジェネリック型を使う
- 項目 30 ジェネリックメソッドを使う
- 項目 31 API の柔軟性向上のために境界ワイルドカードを使う
- 項目 32 ジェネリックと可変長引数を注意して組み合わせる
- 項目 33 型安全な異種コンテナを検討する
- 参考 URL
1. 項目 26 原型を使わない
- Java 1.4 までジェネリクスがなかった。
- Java 5 でジェネリクスが登場した。
1.1. 結論
ジェネリクスの型パラメータが指定できるときは必ず指定しましょう。
|
|
List<String> list = new ArrayList();
とすれば Integer や DTO をいれようとすると コンパイルエラーになるので、早期検出できる。
- Java 1.4 までだと、誤って Integer を add しても、実行時にしかわからなかった。
- 結合テストなどまですり抜けていきやすい。
- 事象が観測された時点であちこちに散財している状態になっている。
- ジェネリクス変数には色々な書き方がある。
|
|
2. 項目 27 無検査警告を取り除く
2.1. 結論
@SuppressWarnings(“unchecked”) は気をつけて使いましょう
@SuppressWarnings("unchecked")
は、クラス、メソッド、変数宣言に付与できる。- 付与すると、その範囲の「型チェック実施していない」警告を抑止する。
- 大きな範囲を抑止すると、抑止しようと思った対象以外も、抑止してしまうので、最小範囲で抑止すること。
3. 項目 28 配列よりリストを選ぶ
3.1. 結論
配列とリストは次の 2 点で大きく異なる。 リストの方は、コンパイル時に型安全性を提供するから、バグが発生しにくい。(配列は実行時に型安全性を提供する。)
-
配列は共変で、リストは不変。
- 共変:
Sub
がSuper
のサブタイプであれば、配列型Sub[]
がSuper[]
のサブタイプということ。 - 不変:2 つの型
Type1
,Type2
があったとき、List<Type1>
とList<Type2>
には互換性がない。
- 共変:
-
配列は具象化されていて、リストは erasure で実装。
- 具象化:実行時に、その要素型を強制し、問題があれば
ArrayStoreException
が発生する。(コンパイル時は確認しない。) - erasure:コンパイル時に型制約を強制し、実行時には要素の型情報を廃棄する。
erasureの意味は「廃棄」
- 具象化:実行時に、その要素型を強制し、問題があれば
3.1.1. 配列の場合
|
|
3.1.2. リストの場合
|
|
3.2. リストと配列を混在させた場合
リストと配列を混在させてしまった場合は、配列の代わりにリストを使うようにしましょう。
- 混在させて、リストを配列にキャストする特殊なケースとして、次項「項目 29 ジェネリック型を使う」があります。
単純なジェネリックメソッドで確認する。
3.2.1. ステップ 1(リスト → 配列)
|
|
3.2.2. ステップ 2(リスト[キャスト]→ 配列)
リストを配列(E[])
にキャストした。
|
|
実行時の
E
の型が不明で、実行時にリスト(ジェネリクス)の型情報は erasure(廃棄)されているから、キャストの安全性が検査できない。)
3.2.3. ステップ 3(リスト → リスト)
配列(E[])
を使わず、リストにした。
|
|
4. 項目 29 ジェネリック型を使う
4.1. 結論
以下の場合は、ジェネリクスを利用する良い機会なので使ってみましょう。
- 利用者が、メソッドからの戻り値をキャストしないといけない
- 利用者が、実行時にキャストが失敗する可能性がある
スタックの実装をサンプルを作成します。 作成時のポイントは 2 点です。
- 無検査キャストが安全であると明確に示せる場合、警告を抑制しましょう。
- 利用者はキャストしなくて済むので、楽になる。
Stack.java
|
|
Main.java
|
|
4.2. 補足 1(配列を使う理由)
Java はリストを直接はサポートしていない。
- ArrayListなどのジェネリック型は配列を使用している。
リストよりも配列の方が、パフォーマンスが良い。
- HashMapなどの他のジェネリック型は配列を使用している。
4.3. 補足 2(ジェネリクスの型の制限)
ジェネリクスの型が制限できる場合は、制限しましょう。
例:java.util.concurrent.DelayQueueでは、DelayQueue<E extends Delayed>
としている。
5. 項目 30 ジェネリックメソッドを使う
5.1. 結論
- ジェネリックメソッドの作り方、使い方を理解しましょう
- 再帰型境界を使うことで、型パラメータの機能を制限できる
5.2. ジェネリックメソッド
-
ジェネリッククラスと同じように、メソッド単位で型安全性を確保できる
-
ジェネリッククラスとの文法上の違いは、メソッドの戻り値の型の手前にのように型パラメータを書くこと
ジェネリックメソッド1 2 3 4 5
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
raw タイプのメソッド
1 2 3 4 5
public static Set union(Set s1, Set s2) { Set result = new HashSet(s1); result.addAll(s2); return result; }
5.3. 再帰型境界
- 再帰型境界は、
T extends HogeInterface
のように型パラメータを書くことで、T は HogeInterface が実装されている型であることを制限すること。 - よくある利用法は Comparable インターフェース
- 下の最大値を返す max メソッドは T を Comparable インターフェースが実装されている、つまり比較できること(compareTo メソッドが使えること)を保証しているので、最大値を求めることができる
1 2 3 4 5 6 7 8 9 10 11
public static <E extends Comparable<E>> E max(Collection<E> c) { if (c.isEmpty()){ throw new IllegalArgumentException("Empty collection"); E result = null; } for (E e : c){ if (result == null || e.compareTo(result) > 0) result = Objects.requireNonNull(e); } return result; }
1 2 3
public interface Comparable<T> { int compareTo(T o); }
- 下の最大値を返す max メソッドは T を Comparable インターフェースが実装されている、つまり比較できること(compareTo メソッドが使えること)を保証しているので、最大値を求めることができる
5.4. 余談
- 型パラメータの文字としてよく使われる
T
、E
、K
、V
は何の略なのか
表記 | 意味 | 説明 |
---|---|---|
T | Type | 汎用的 |
E | Element | 配列の要素など |
K | Key | 連想配列やマップのキーなど |
V | Value | 配列の値や結果 |
N | Number | 数値を表す |
A | Array | A[]という形式で使われる |
Args | Arguments | C++で使われることが多い |
F, Fn, Func | Function | コールバック関数、クロージャー、ラムダ等 |
6. 項目 31 API の柔軟性向上のために境界ワイルドカードを使う
6.1. 結論
- 最大限の柔軟性のために、プロデューサーかコンシューマーを表す入力パラメータに対してワイルドカード型を使用するべき
- PECS(producer extends, consumer super)と覚えよう!
6.2. 境界ワイルドカードを使用しない場合の問題点
-
下記のような Stack クラスがある。
1 2 3 4 5 6
public class Stack<E> { public Stack() {} public void push(E element) { ... } public E pop() { ... } public boolean isEmpty() { ... } }
-
一連の要素を受け取りそれら要素を全てスタックにプッシュするメソッド、pushAll()を下記のように追加したとする。
1 2 3 4 5
public void pushAll(List<E> src) { for (E e: src) { this.push(e); } }
-
このとき、Number 型の Stack を定義して、一つの Interger 変数を push することはできるが、List
を pushAll することはできない。なぜなら、List は List のサブクラスではないから。 1 2 3 4 5 6
Stack<Number> numberStack = new Stack<>(); numberStack.push(new Integer(1));//これはOK List<Integer> integers = ...; numbers.pushAll(integers);//これはコンパイルエラー
-
ひとつだと追加できるのに、複数だと追加できないのは非常に不便。
-
次に、渡したリストにすべての要素を追加してくれるメソッド、popAll()を下記のように追加したとする。
1 2 3 4 5
public void popAll(List<E> dst) { while (!isEmpty()) { dst.add(pop()); } }
-
このとき、Number 型の Stack を定義して、pop して1つの Object 変数に代入することはできるが、popAll して List
1 2 3 4 5 6 7 8 9 10
Stack<Number> numberStack = new Stack<>(); numberStack.push(1); numberStack.push(2); numberStack.push(3); Object obj = numbers.pop();//これはOK List<Object> objects = ...; numbers.popAll(objects);//これはコンパイルエラー
-
これも不便。
6.3. 境界ワイルドカードでの解決
-
前述の問題は境界ワイルドカードを使うことで解決できる
-
pushAll()
やpopAll()
を下記のように境界ワイルドカードを使って定義することで、List<? extends E>
は「E のサブタイプをパラメータとするリスト」、List<? super E>
「E のスーパータイプをパラメータとするリスト」とすることができ、解決できる。1 2 3 4 5 6 7 8 9
public void pushAll(List<? extends E> src) { // 同じ ... } public void popAll(List<? super E> src) { // 同じ ... }
6.4. GET & PUT 原則(PECS)
<? extends E>
と<? super E>
のどちらを使うか決めるための原則- 関数内において、ジェネリックス型の引数の役割が「プロデューサー(Producer)」であれば extends
- 「コンシューマー(Consumer)」であれば super
- プロデューサーは、関数内で何らかの値を生成(提供)する引数
- pushAll()の引数 src は値を提供するのでプロデューサー
- コンシューマーは、関数内で何らかの値を消費(利用)する引数
- popAll()の引数 dst は値を消費するのでコンシューマー
- プロデューサーは、関数内で何らかの値を生成(提供)する引数
- 作用の方向で考えるとよい
6.5. GET & PUT 原則の例(問題)
-
問 1. 前項目で登場した下記の union メソッドはどのように境界ワイルドカードを付けるべきか?
1 2 3 4 5
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
-
問 1 の解答 s1、s2 は値を提供しているから。
1 2 3 4 5
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
-
問 2. 前項目で登場した下記の max メソッドはどのように境界ワイルドカードを付けるべきか?(ヒント:2 つ付ける。)
1 2 3 4 5 6 7 8 9 10 11
public static <E extends Comparable<E>> E max(Collection<E> c) { if (c.isEmpty()){ throw new IllegalArgumentException("Empty collection"); E result = null; } for (E e : c){} if (result == null || e.compareTo(result) > 0) result = Objects.requireNonNull(e); } return result; }
-
問 2 の解答
1 2 3 4 5 6 7 8 9
public static <E extends Comparable<? super E>> E max(Collection<? extends E> c) { if (c.isEmpty()) throw new IllegalArgumentException("Empty collection"); E result = null; for (E e : c) if (result == null || e.compareTo(result) > 0) result = Objects.requireNonNull(e); return result; }
E extends Comparable<? super E>
の理由は以下。[PECSの原則に従うと] Comparableは常に消費者(consumer)なので、Comparable<T>ではなくComparable<? super T>を常に使用するべき。
-
上記の実際の例としては、ScheduledFuture インターフェースがある。
下記のリストをComparable<E>
のままの max メソッドで使おうとすると、コンパイルエラーになる。
なぜなら、ScheduledFuture
はComparable<ScheduledFuture>
で拡張されていないからである。
しかし、ScheduledFuture
はDelayed
のサブインターフェースであり、Delayed
はComparable<Delayed>
を拡張しているので、ScheduledFuture
はComparable<Delayed>
で比較可能である。
したがって、Comparable<? super E>
とすれば、下記のリストに max メソッドを使えるようになる。
|
|
7. 項目 32 ジェネリックと可変長引数を注意して組み合わせる
7.1. 結論
- ジェネリクスと可変長引数を同時に使用する場合は、以下を守った上で
@SafeVarargs
を付与する- 配列を変更していない(要素に値を格納していない)
- 引数を呼び出し側に参照させていない(return していない)
- コレクションをネストさせる等で対処することも検討する
7.2. ジェネリクスと可変長引数を同時に使用すると、タイプアンセーフになる。
以下のように、ジェネリック型の可変長引数に値を格納するのは安全でないケースが存在する。
|
|
なお、このような可能性があるため、コンパイル時にジェネリクスと可変長引数を組み合わせたメソッドを呼び出す部分で
unchecked generic array creation for varargs parameter
と警告がでる。
とはいえ、以下の標準 API ではジェネリック型の可変長引数が定義されており、必ず問題となるわけではない。これらはタイプセーフ。
Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c, T... elements)
また、本当に安全ならば、メソッドに @SafeVarargs
を付与することで、コンパイル時の警告は抑止できる。
|
|
7.3. 本当にタイプセーフなのかを知るには
可変長配列の引数が以下の両方を満たす状態ならばタイプセーフと言える
- 配列を変更していない(要素に値を格納していない)
- 引数を呼び出し側に参照させていない(return していない)
以下は 2.に違反している NG ケース
|
|
そもそも、以下のようにすればよいという話はある。
|
|
コレクションがネストした時点で、利用側にミスを誘発しやすい。
ネストの何段目を操作しているのか混乱しやすく、ミスをコンパイルで発見しにくくなる。
その場合は、クラスでくるむことで、意図を明確にしやすくなる。
|
|
8. 項目 33 型安全な異種コンテナを検討する
8.1. 結論
8.1.1. 前提
- 型安全とは、String 型のインスタンスを要求したときに、その型のインスタンスを返すこと。
- 異種コンテナとは、クラスをキー(
Key
)、そのクラスのインスタンスを値(Value
)とするもの
8.1.2. ポイント
- Map は キーとバリューの対応を、型では保証していない。
- put は何も考えずに格納する
- get 時に辻褄が合うように型変換する
8.2. 型パラメータの個数を可変にする方法
Set<E>
は 1 個、 Map<K,V>
は 2 個とパラメータ数が固定だが、これを固定したくない(できない)ケースがある。
代表的なものは、コンテナを実装するとき。
このケースでは、キーをパラメータ化することで様々な型を受け取れるようにする。
|
|
ポイントは以下。
- Map は キーとバリューの対応を、型では保証していない。
- put は何も考えずに格納する
- get 時に辻褄が合うように型変換する
注意点は以下。
- raw タイプな Class オブジェクトを使って Favorites クラスのタイプセーフを破ってくる
- putFavorite メソッドにおいて、 cast メソッドを使って確かめればよい。
- non-reifiable(具象化可能= new できる)な型を利用できない。つまり、
String
や、String[]
を格納することはできるが、List<String>
を格納することはできない。 ジェネリクスはイレイジャで実行時に消されてしまうので。
前述の型パラメータは extends を用いて境界を設けることも可能。