Effective Javaを勉強します【第5章】
項目23. 新たなコードで原型を使用しない
ジェネリックとは
1つ以上の型パラメータを宣言に持つクラスやインターフェースのこと。
例)
public interface List<E> extends Collection<E> { ... }
List<E>
:ジェネリック型E
:仮型パラメータList<String>
:パラメータ化された型String
:実型パラメータList
:原型
原型を使用すべきでない
以下はリリース1.5以前の原型を用いたコレクション型の例。 コンパイルエラーにもならず、コレクションから要素を取り出し利用するまでエラーに気づくことができない。
// Stampsインスタンスだけを含むことを想定 private final Collection stamps = ...; // 誤ってCoinインスタンスを代入してしまう stamps.add(new Coin(...)); // 要素を取り出す for (Iterator i = stamps.iterator(); i.hasNext(); ) { Stamp s = (Stamp) i.next(); // ClassCastExceptionが発生する }
以下はジェネリックスを用いたコレクション型の例。
stamps
はStamp
インスタンスだけを含んでいることがコンパイラにより保障される。
また要素取り出しの際のキャストも不要である。(コンパイラが自動でキャストしてくれる)
// 型安全 private final Collection<Stamp> stamps = ...; // 以下のように書いた場合はコンパイルエラーになる stamps.add(new Coin(...)); // 要素の取り出し(型安全) for (Stamp s : stamps) { // キャスト不要 ... } for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); ) { Stamp s = i.next(); // キャスト不要 }
List<Object>
List
などの原型は使用すべきでないが、任意のオブジェクトの挿入が可能なList<Object>
を利用することは問題ない。
前者と後者の違いは、後者はコンパイラにより型検査が行われるということ。
例えば、以下のように原型を用いた場合はコンパイル時に警告は出るが実行は可能であり、ClassCastException
がスローされる。
public static void main (String[] args) { List<String> strings = new ArrayList<String>(); unsafeAdd(strings, new Integer(42)); String s = strings.get(0); //ClassCastException } private static void unsafeAdd(List list, Object o) { list.add(o); }
unsafeAdd
の宣言をprivate static void unsafeAdd(List<Object> list, Object o)
に書き換えた場合、コンパイルエラーとなる。
これはList<String>
はList<Object>
のサブタイプではないためである。(ジェネリックス型に継承関係はない)
strings
がList<Object>
型であればInteger
も格納可能である。
非境界ワイルドカード型
ジェネリックスを利用したいが、実際の型パラメータが分からない、または気にしたくない、という場合は非境界ワイルドカード型を利用する。
非境界ワイルドカード型では、型パラメータの代わりに?
記号を用いる。
非境界ワイルドカード型はすべてのパラメータ化された型パラメータのスーパータイプである。
例:List<?>
はList<Object>
、List<String>
のスーパークラス
以下は非境界ワイルドカード型の利用例である。
static int numElementsInCommon(Set<?> s1, Set<?> s2) { int result = 0; for (Object o1:s1) { if (s2.contains(o1)){ result++; } } return result; }
原型のリストと非境界ワイルドカードのリストの違いは型安全性である。
原型のリストにはいかなる要素も代入可能だが、非境界ワイルドカード型のリストにはnull
を除いた通常のオブジェクトは代入不可能である。
例外的に原型を使うべき場合
- クラスリテラルを使う場合
List<String>.class
やList<?>.class
は不可
instanceof
と一緒に使う場合
// Setであるかを確認して処理する if (o instanceof Set) { // 事前にSetであることを検査しているので警告は出ない Set<?> m = (Set<?>) o; ... }
項目24. 無検査警告を取り除く
ジェネリックスの利用時は以下のようなコンパイラの警告をよく目にする。
- 無検査キャスト警告
- 無検査メソッド呼び出し警告
- 無検査ジェネリック配列生成警告
- 無検査変換警告
無検査警告への対処は以下。
- 基本的に取り除くことが可能なすべての無検査警告を取り除く
- 警告を取り除くことができず、かつ該当箇所が型安全であると明示できる場合、
@SuppressWarnings("unchecked")
アノテーションで警告を抑制してよい- できるだけ最小のスコープに対して使用すること(クラス全体に使用することは絶対にしない)
- 型安全であると明示できる理由をコメントに残すこと
項目25. 配列よりリストを選ぶ
ジェネリックスは不変である
配列型Sub[]
はSuper[]
のサブタイプであるが、List<Sub>
とList<Super>
の間に継承関係は存在しない。
以下のような場合、配列を利用すると実行時にエラーがスローされるまで間違いに気づかない可能性があるが、 リストの場合コンパイルエラーで知ることができる。
Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in"; // ArrayStoreExceptionのスロー List<Object> ol = new ArrayList<Long>(); // 互換性のない型でコンパイルエラー ol.add("I don't fit in");
ジェネリックスはイレイジャで実装されている
配列は実行時にその要素型を知っており、それを強制する。 一方でジェネリックスはコンパイル時のみ型制約を強制し、実行時には型情報が廃棄(erase)されるため、柔軟性が高く既存コードとの相互運用も容易である。
配列とジェネリックスが混在している時は、大抵は配列をリストに置き換えた方が良い
配列とジェネリックスは上手く調和しないので、混在している時は大抵は配列をリストに置き換えた方が良い。
項目26. ジェネリック型を使用する
以下のスタック実装を例に、ジェネリック型の書き方を学ぶ。
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null; return result; } public boolean isEmpty() { return size == 0; } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
上記の問題点は、スタックから取り出した値をクライアント側でキャストしなければならない点である。 したがって以下のようにジェネリックスを用いた形に修正する。
public class Stack<E> { //修正 private E[] elements; //修正 private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; //修正 } public void push(E e) { //修正 ensureCapacity(); elements[size++] = e; } public E pop() { //修正 if (size == 0) { throw new EmptyStackException(); } E result = elements[--size]; //修正 elements[size] = null; return result; } ... }
上記ではelements = new E[DEFAULT_INITIAL_CAPACITY];
でコンパイルエラーとなる。
これはE
など具象化不可能型の配列の生成はできないため。
したがって以下のいずれかの方法で回避する。
1. Object
配列を生成して、ジェネリック配列型へキャストする
elements = new E[DEFAULT_INITIAL_CAPACITY];
をelements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
にする。
これだけでは警告が出るので@suppressWanings("unchecked")
アノテーションを使って、警告を抑制する。
この時、項目24で述べた通りできる限り狭いスコープで抑制する必要がある。今回はコンストラクタ内に無検査配列生成の処理のみ含まれているため、
コンストラクタ全体で警告を抑制する。
// elements配列はpush(E)からのEインスタンスのみ挿入されるため問題なし @suppressWanings("unchecked") public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; //修正 }
2. フィールドelements
の型をE[]
からObject[]
に変更する
private E[] elements;
をprivate Object[] elements;
にして、
E result = elements[--size];
をE result = (E) elements[--size];
にすると
方法1同様に警告が発生するため、@suppressWanings("unchecked")
アノテーションを使って、警告を抑制する。
public E pop() { //修正 if (size == 0) { throw new EmptyStackException(); } // pushは要素が型Eであることを要求しているため問題なし @suppressWanings("unchecked") E result = (E) elements[--size]; elements[size] = null; return result; }
型パラメータへの制限
以下のように境界型パラメータを利用することで型パラメータに制限を設けることができる。
例) 実型パラメータがjava.util.concurrent.Delayed
のサブタイプでなければならないことを要求
class DelayQueue<E extends Delayed> implements BlockingQueue<E>;
これにより、明示的なキャストの必要性やClassCastException
の危険性なくDelayQueue
の要素に対してDelayed
のメソッドが利用可能。
項目27. ジェネリックメソッドを使用する
staticのユーティリティメソッドはジェネリックメソッドにできないか検討すべき良い候補である。
ジェネリックstaticファクトリーメソッド
ジェネリックコンストラクタでは、変数宣言の左右で型パラメータを指定しなければならず煩わしい。
Map<String, List<String>> map = new HashMap<String, List<String>>();
ジェネリックstaticファクトリーメソッドを利用すれば、上記を簡潔にできる。
// ジェネリック static ファクトリーメソッド public static <K, V> HashMap<K, V> newHashMap() { return new HashMap<K, V>(); } Map<String, List<String>> map = newHahMap();
が、これはJava7のダイヤモンド演算子で解決された。
// ダイヤモンド演算子によりコンストラクタの実型パラメータは省略可能に Map<String, List<String>> map = new HashMap<>();
ジェネリックシングルトンファクトリー
ジェネリックスを利用することで、多くの異なった型に適用可能な不変オブジェクトを生成することができる。
例) 恒等関数を作成する場合
恒等関数とは何らかの型T
の値を受け取って、その型の値を返す関数のこと。
内部状態は持たない(型が違うのみ)ためインスタンスを複数生成するのは無駄である。
したがってジェネリックスを用いた不変オブジェクトを返すとよい。
// ジェネリックシングルトンファクトリーパターン private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() { public Object apply(Object arg) { return arg; } }; // IDENTITY_FUNCTIONは状態を持たず、その型パラメータは非境界なので // すべての型に対して1つのインスタンスを共有するのは安全 @SuppressWarnings("unchecked") public static <T> UnaryFunction<T> identityFunction() { return (UnaryFunction<T>) IDENTITY_FUNCTION; }
上記は無検査キャスト警告が発生するが、IDENTITY_FUNCTION
は単に引数を返すだけなので安全であるため、
例外を抑制することができる。
再帰型境界
型パラメータがその型パラメータ自身が関係するなんらかの式で制限されていることを、再帰型境界と呼ぶ。
例) Comparableインタフェース
Comparableインタフェースは何らかの型T
と比較できることを示すインタフェースであり、
多くの場合、このインタフェースを実装する型は自分自身の型とのみ比較可能である。
public interface Comparable<T> { int compareTo(T o); }
リストのソート、検索、最小値/最大値の計算をするために、Comparable
を実装した要素のリストを受け取る
多くのメソッドがあるが、これを行うためにはリスト内の個々の要素同士の比較が可能である必要がある。
相互比較可能であることを示すジェネリックスは以下のように記述する。
// 再帰型境界 public static <T extends Comparable<T>> T max(List<T> list) { ... }
これはリストの要素T
はComparable<T>
のサブタイプであることを要求している。
すなわちリストの要素T
は自身の要素T
と比較可能であることという相互比較可能性を要求している。
項目28. APIの柔軟性向上のために境界ワイルドカードを使用する
不変性の扱いづらさ
不変性によりList<Object>
とList<String>
の間に継承関係が存在しないことはジェネリックスの良さではあるが、
故に扱いづらい時がある。
例として以下のスタックAPIを考える。
public class Stack<E> { public Stack() {} public void push(E element) { ... } public E pop() { ... } public boolean isEmpty() { ... } }
このスタックにInteger
型の要素を追加する場合、以下のように記述することになる。
Stack<Number> numbers = new Stack<Number>(); numbers.push(new Integer(1));
Integer
はNumber
のサブタイプであるため、上記は動作する。
ここで複数の要素をまとめて挿入するpushAll
メソッドを以下のように新しく実装するとする。
public void pushAll(List<E> src) { for (E e: src) { this.push(e); } }
この時、先ほどと同様にInteger
型の要素を挿入しようとした場合、コンパイルエラーとなってしまう。
Stack<Number> numbers = new Stack<Number>(); List<Integer> integers = ... ; numbers.pushAll(integers);
エラーとなる原因はジェネリックスの不変性により、List<Number>
とList<Integer>
の間に継承関係が存在しないためだが、
これはAPIとしては直感的でない。
同様に、スタックからInteger
型の要素を取り出す場合を考える。単一の要素を取り出す場合、以下のように記述することになる。
Stack<Number> numbers = new Stack<>(); numbers.add("1"); Object obj = numbers.pop();
Object
はInteger
のスーパータイプであるため、上記は動作する。
次に、引数として渡したリストにすべての要素を追加してくれるpopAll
メソッドを以下のように新しく実装するとする。
public void popAll(List<E> dst) { while (!isEmpty()) { dst.add(pop()); } }
この時、List<Object>
型のリストを渡そうとした場合、コンパイルエラーとなってしまう。
Stack<Number> numbers = new Stack<Number>(); numbers.add("1"); numbers.add("2"); List<Object> objects = ...; numbers.popAll(objects);
エラーとなる原因はジェネリックスの不変性により、List<Number>
とList<Object>
の間に継承関係が存在しないためだが、
やはりこれはAPIとしては直感的でない。
境界ワイルドカード型
上記のような問題を解決するためには境界ワイルドカード型を使う。
pushAll
メソッドの場合、引数の型は「Eのリスト」ではなく「Eの何らかのサブタイプのリスト」とすべきであり、
これは境界ワイルドカード型を利用して、以下のように記述することができる。
public void pushAll(List<? extends E> src) { for (E e: src) { this.push(e); } }
popAll
メソッドの場合、引数の型は「Eのリスト」ではなく「Eの何らかのスーパータイプのリスト」とすべきであり、
これは境界ワイルドカード型を利用して、以下のように記述することができる。
public void popAll(List<? super E> dst) { while (!isEmpty()) { dst.add(pop()); } }
上記のように記述することで、コンパイルエラーなく下記のコードを実行することが可能となる。
Stack<Number> numbers = new Stack<Number>(); List<Integer> integers = ...; numbers.pushAll(integers); List<Object> objects = new ArrayList<Object>(); numbers.popAll(objects);
PECS原則
境界ワイルドカード型のうち、extends
とsuper
のどちらを使えば良いか判断するための基本原則がPECS原則である。
Producer -extends and Consumer -super
pushAll
メソッドの場合、引数src
はスタックの要素を生成する Producer であった。
一方でpopAll
メソッドの場合、引数dst
はスタックの要素を消費する Consumer であった。
明示的型パラメータ
PECS原則を用いて、以下のunion
メソッドを書き換えることを考える。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; }
引数s1
とs2
を用いて、新たなSet
を生成するため、どちらも Producer である。
したがってPECS原則に従うと、宣言を以下のように書き換えることが可能である。
この時、戻り値にはワイルドカード型を使用しないことに注意する。戻り値にワイルドカード型を利用すると クライアント側でワイルドカード型の使用を強制することになる。
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; }
修正したunion
メソッドを以下のように使用することができるように思えるが、コンパイルエラーになってしまう。
Set<Integer> integers = new HashSet<>(); Set<Double> doubles = new HashSet<>(); Set<Number> numbers = union(integers, doubles);
上記エラーの原因は、Javaの型推論の制限によるものである。(ジェネリックの理論的には問題ないように思える) この時以下のように、明示的に型パラメータを記述することでエラーを解決することができる。
Set<Number> numbers = Union.<Number>union(integers, doubles);
上記Union
はunion
メソッドの宣言されたクラスである。
明示的な型パラメータの宣言はコードの冗長性を増すので頻繁に書くべきでないし、大抵は書く必要はない。
Comparableに対するPECS
PECS原則を用いて、以下のmax
メソッドを書き換えることを考える。
public static <T extends Comparable<T>> T max(List<T> list) { Iterator<T> it = list.iterator(); T result = it.next(); while (it.hasNext()) { T t = it.next(); if (t.compareTo(result) > 0) { result = t; } } return result; }
引数list
を用いてresult
を生成するため、list
は Producer である。
この時イテレータit
の型も修正することに注意する。
次に再帰型境界であるComparable
について考える。
<T extends Comparable<T>>
はTと比較可能な(Comparable
を実装した)何らかの型という意味だが、
Tインスタンスを「比較」という形で消費している Consumer と考えることができる。
したがってPECS原則に従うと、宣言を以下のように書き換えることが可能である。
public static <T extends Comparable<? super T>> T max(List<? extends T> list) { Iterator<? extends T> it = list.iterator(); ... (以下変更なし) ... }
コンパレータは Consumer であることを覚えておけば良い。
項目29. 型安全な異種コンテナーを検討する
異種コンテナーとは
ジェネリックスは通常、Set<Integer>
やList<String>
のように、特定の型の要素を持つコンテナーに対して利用する。Map<Integer, String>
のように複数のキーを持つ場合であっても、型は固定されている。
ここでいうコンテナーとは、List
やSet
のように複数の値やオブジェクトの格納先となる入れ物のこと。
本項目では、「クラス」をキー、「オブジェクト」を値とするマップとすることで、様々な型の値を 格納することができる異種コンテナーを実装する。
Classクラス
「クラス」をマップのキーとして扱うためにClass
クラスを利用する。
Class
クラスはJavaの「クラス」自体の情報を保持するためのクラスである。
利用例は以下のとおり。
String str = "Hello World!"; Class stringClazz = str.getClass(); // getName()メソッドでクラス名を取得 // この場合 java.lang.String stringClazz.getName();
クラスリテラル
Object.getClass()
メソッド以外にクラス名.class
(クラスリテラル)でもクラスの情報が取得できる。
Class stringClazz = String.class; // 同様にgetName()メソッドでクラス名を取得可 // この場合も java.lang.String stringClazz.getName();
Class
クラスはジェネリッククラスなので、型パラメータを与えることで型安全になる。
// OK Class<String> stringClazz = String.class; // コンパイルエラー Class<Integer> integerClazz = String.class;
ただしクラスリテラルはジェネリックスなど具象化不可能型では利用できない。
Class<List<String>> clazz; // リテラルを使うとコンパイルエラー Class<List<String>> clazz = List<String>.class;
型安全な異種コンテナー
Class
クラスを利用することで、「クラス」をキーとしたマップ(異種コンテナー)を
実装できる。インターフェースは以下。
public interface Favorites { public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type); }
Favorites
コンテナーの利用例は以下のとおり。
Favorites favorites = new Favorites(); favorites.putFavorite(String.class, "Java"); favorites.putFavorite(Integer.class, new Integer(1)); String favorite = favorites.getFavorite(String.class);
上記のように、コンパイル/実行時の型情報を渡すために利用されるクラスリテラルを「型トークン」と呼ぶ。上記はジェネリックスにより型安全性が保証されているため、キーと値のクラスが異なることは起こり得ない。このように複数の型のオブジェクトを保持する型安全なコンテナーを「型安全異種コンテナー」と呼ぶ。
型安全異種コンテナーの実装は以下。
public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>(); public <T> void putFavorite(Class<T> type, T instance) { if (type == null) { throw new NullPointerException("Type is null"); } favorites.put(type, instance); } public <T> T getFavorite(Class<T> type) { return type.cast(favorites.get(type)); } }
上記のputFavorite
メソッドでは原型を利用することで、警告を無視することにはなるが
無理やりキーと型の異なる値を格納することが可能である。
そのような時はClass.cast
を用いると、実行時にClassCastException
を発生させることができる。
public <T> void putFavroite(Class<T> type, T instance) { // Class.castメソッドにより実行時に型チェックが可能 favorites.put(type, type.cast(instance)); }
境界型トークン
上記の異種コンテナーでは型トークンに関して特に制限はなかったが、境界型トークンを用いることで
制限が可能。下記の場合、型パラメータT
はAnnotation
クラスのサブクラスであると要求されている。
public <T extends Annotation> T getAnnotation(Class<T> annotationType);