Java では、配列よりも、List などのコレクション・オブジェクトを使用する方がメリットが遥かに大きい。
特にジェネリック型の配列は、発見しにくいバグも生みやすく、出来るだけ使用を避けるべきである。
ジェネリック型の配列を使わない、これが大原則である。
しかし、何かしらの理由でジェネリック型の配列を使わざるを得ないときには、
「広告の真実性」と「露出禁止」の2つの原則を押さえておくとよい。
この2つの原則は「Java Generics and Collections」で説明されているものである。
以下では、配列は参照型配列を意味し、基本データ型の配列は扱わない。
これは、参照型の配列は共変であるが、基本データ型の配列はそうではないことに関係する。
1つ目の原則は The Principle of Truth in Advertising (広告の真実性の原則)である。
この「広告の真実性の原則」を一言で言えば、「見かけ上の型は、実際の型と不整合があってはなりませんよ」ということ。The Principle of Truth in Advertising:
the reified type of an array must be a subtype of the erasure of its static type.訳)広告の真実性の原則
Java Generics and Collection — 6.5 The Principle of Truth in Advertising
配列の具象型は、その静的な型に対するイレイジャのサブタイプでなくてはならない。
そんな難しいことを言っているわけではない。
ジェネリック導入前なら、この原則は次のように言われるだろう。
配列の変数および引数、戻り値の型は、配列の実際の型のスーパータイプでなければならない。
言い換えれば、
配列の実際の型は、配列の変数および引数、戻り値の型のサブタイプでなければならない。
ダウンキャストしなければ、これはコンパイラによって保証されるが、
このルールを守らずダウンキャストするなら、実行時エラーが発生する。
以下のコードを例とすれば、3行目の変数の型は Double であり、配列の具象型 Integer のスーパータイプではない。
これは原則に違反しており、このコードは、実行時に ClassCastException を引き起こす。
Integer[] ints = new Integer[] { 1, 2, 3 }; Number[] nums = ints; Double[] dbls = (Double[]) nums; // ClassCastException
この簡単なルールを、ジェネリック型システムに適応させたものが The Principle of Truth in Advertising である。
では、原則の説明を詳しくみていく。上の引用文を再掲すると、
用語の意味であるが、具象型(reified type)とは実行時の実際の型のことである。The Principle of Truth in Advertising:
the reified type of an array must be a subtype of the erasure of its static type.訳)広告の真実性の原則
Java Generics and Collection — 6.5 The Principle of Truth in Advertising
配列の具象型は、その静的な型に対するイレイジャのサブタイプでなくてはならない。
詳細は次の記事を参照して欲しい。
静的な型(static type)とは、変数の宣言された型や式の型のことだと思ってよい。
コンパイル時の型 (compile-time type) とも呼ばれる。
さて、文全体の解釈であるが、元の文を次のように言い換えてみる。
配列の具象型(実際の型)は、変数や引数、戻り値の静的な型に対して、それらのイレイジャのサブタイプでなければならない。
逆にも言い換えてみる。
配列の変数や引数、戻り値の静的な型は、そのイレイジャが、配列の具象型のスーパータイプでなければならない。
例を挙げると
public static void main(String[] args) { List<Integer> ints = Arrays.asList(1, 2, 3); Collection<Integer>[] colls = (List<Integer>[]) new List<?>[]{ints}; for(Collection<Integer> coll: colls) for(Integer i: coll) System.out.println(i); Set<Integer>[] sets = (Set<Integer>[]) colls; // ClassCastException }
3行目の代入式では、型変換が2度行われている。 Every parameterized type is a subtype of the corresponding raw type.
まずは List<?>[] から List<Integer>[] の明示的な変換である。
変換対象の配列の具象型は List<?>[] である。そして List<Integer>[] のイレイジャは、List である。
と書いてある通り、List<?> は List のサブタイプである。したがって、この変換は妥当である。
続いて、代入式の右辺から左辺への List<Integer>[] から Collection<Integer>[] への暗黙的な型変換が行われるが、
確認すべきは、配列の具象型 List<?>[] と Collection<Integer>[] との関係が原則に従っているか否かである。
これは、Collection<Integer>[] は List<Integer>[] のスーパータイプであることから、結果的に
配列の具象型のスーパータイプとなっていることがわかる。よって、この代入式は妥当である。
最後の代入式であるが、これは「広告の真実性の原則」を破っている。
配列の具象型は List<?>[] であるが、Set<Integer>[] のイレイジャ Set と互換性がない。
結果、この代入では、ClassCastException がスローされる。
さて、この原則はプログラムが全体として問題なく動作することの必要条件であって、
コーディングの指針を言っているわけではない事に注意してほしい。
本書では、広告の真実性の原則を説明するのに次のコードを例としている。
import java.util.*; class Wrong { public static <T> T[] toArray(Collection<T> c) { T[] a = (T[]) new Object[c.size()]; // unchecked cast int i = 0; for (T x : c) a[i++] = x; return a; } public static void main(String[] args) { List<String> strings = Arrays.asList("one", "two"); String[] a = toArray(strings); // class cast error } }
下から3行目の class cast error の原因を特定するため、本書では、コンパイラによって
生成されたイレイジャによる実装を確認している。以下がその実装である。
import java.util.*; class Wrong { public static Object[] toArray(Collection c) { Object[] a = (Object[])new Object[c.size()]; // unchecked cast int i=0; for (Object x : c) a[i++] = x; return a; } public static void main(String[] args) { List strings = Arrays.asList(args); String[] a = (String[])toArray(strings); // class cast error } }
このエラーを本書は以下のように説明する。
原則が破られているのは確かにクライアント側(呼び出し側)であるが、The principle is obeyed within the body of toArray itself, where the erasure of T is Object, but not within the main method, where T has been bound to String but the reified type of the array is still Object.
訳)toArray の本体内では T のイレイジャは、Object であるで原則に従っています。しかし、main メソッドでは、T が String にバインドされますが、配列の具象型が Object であることにより、原則が破られています。
Java Generics and Collection — 6.6 The Principle of Truth in Advertising
未検査警告を黙殺したのはライブラリ(呼び出され側)であって、クライアントは無実である。
ライブラリの設計に問題がある。
さて、次の露出禁止の原則である。
とあるが、むしろ、次の記述の方が主体がはっきりしていてわかりやすい。Principle of Indecent Exposure:
Java Generics and Collection — 6.6 Principle of Indecent Exposure (P.88)
never publicly expose an array where the components do not have a reifiable type.
以下の例は、この原則に違反しており ListHolder が具象化可能型でない配列 List<Integer>[] を返している。In particular, a library should never publicly expose an array with a nonreifiable type.
訳)特に、ライブラリは具象化可能型ではない型を持つ配列を公開するべきではありません。
Java Generics and Collection — 6.6 Principle of Indecent Exposure (P.86)
そのため、このコードを実行すると、main の最後の行で ClassCastException がスローされる。
これもライブラリの設計が悪いため、コンパイル時、main 側は、型変換については、何も警告されない。
import java.util.Arrays; import java.util.List; class ListHolder { @SuppressWarnings("unchecked") List<Integer>[] ints = (List<Integer>[]) new List<?>[5]; // unchecked cast public List<Integer>[] getLists() { return ints; } } public class ListHolderClient { public static void main(String[] args) { ListHolder holder = new ListHolder(); List<?>[] lists = holder.getLists(); lists[0] = Arrays.asList("a"); List<Integer> ints = holder.getLists()[0]; int i = ints.get(0); // ClassCastException } }
広告の真実性の原則は、配列オブジェクトの実際の型となる具象型と、静的な型の実行時の姿であるイレイジャとの整合性について述べており、どちらかというと実行時のルールである。一方、露出禁止の原則は、型宣言について述べており静的な型についてのルールである。これらの特徴を本書では次のように説明する。
The Principle of Truth in Advertising and the Principle of Indecent Exposure are closely linked. The first requires that the run-time type of an array is properly reified, and the second requires that the compile-time type of an array must be reifiable.
訳)広告の真実および露出禁止の原則は、密接な繋がりがあります。前者は配列の実行時の型が適切に具象化されている必要があり、後者は、配列のコンパイル時の型が具象化可能型あることを要求しています。
Java Generics and Collection — 6.6 Principle of Indecent Exposure (P.88)
これらの原則についてはこちらも参考になる。
普通の(業務)Javaアプリケーションでは配列をなるべく使用しない方がよい - 達人プログラマーを目指して