普通の(業務)Javaアプリケーションでは配列をなるべく使用しない方がよい
以前、業務系のJavaプログラマーが知っておくべき10個のBad Partsとその対策 - 達人プログラマーを目指してにて、Java言語の配列はListなど他のコレクションとの不統一が顕著であるという点を説明しました。Java言語の配列は
- 要素に[ ]演算子を使って簡単にアクセスできる
- 構文がC言語やC++言語に近いため親しみやすい
- 型情報を持っているため、要素の取得時にキャストが不要
- int[ ]、byte[ ]など大量の基本型データを効率的に処理できる
- 型パラメーターに対して型の共変性*1があり直感的に理解しやすい
などの特徴があります。特にJDK1.4以前は総称型(Generics)という仕組みが存在せず、配列は要素にキャストなしでアクセスできる唯一のコレクションでした。そのような理由もあり、Java5以降を利用しているプロジェクトでも、ListなどのコレクションAPIのクラスを利用せずに配列を過剰に使用する傾向があるのではないかと思われます。*2
ただし、配列は総称型とは水と油のような関係ということがあり、また、Javaの型安全性を損なう原因になっているという事実があります。*3ここでは、Javaの配列の問題点についてあらためて考えてみたいと思います。
配列を使う以上Javaのプログラムは型安全でない
Java言語では、配列の型と要素型との間に共変性が存在します。したがって、String[ ]やNumber[ ]はObject[ ]のサブクラスとなります。この性質は直感的には非常にわかりやすいのですが、以下の例を考えてみるとわかるように、既にこの時点でコンパイル時の型安全性が損なわれています。
String[] strArray = {"test1", "test2"}; Object[] objArray = strArray; objArray[0] = 3; //java.lang.ArrayStoreException
以上のコードはコンパイル時にはエラーにも警告にもならず、実行時例外となります。ClassCastExceptionではないため、型安全性とは別であるというつっこみもあるかもしれませんが、配列を使うと型に関して以上のような例外が実行時に出る可能性があることを覚えておいてもよいと思います。
Javaの総称型は型消去(type erasure)によって実現されている
JDK1.4までのJavaではGenericsが存在しなかったため、ソースコード中で宣言された変数の型はコンパイルされても基本的にそのままバイトコード中に型情報が残ると考えることができます。実際、Javaのバイトコードの仕様を見てみるとわかるのですが、すべての基本型ごとに別々の命令セットがあり、参照型についても対象とするクラスに関する情報が渡されるようになっています。プロフェッショナルのJavaプログラマーにも意外に知られていない事実なのかもしれませんが、バイトコードにコンパイルした状態でも相当型安全性を意識しているということですね。そして、配列の型の情報も次元も含めて厳密にバイトコード中に保存されるしくみになっています。(型情報や名前を含めてほぼ完全にソースに逆コンパイル可能)前節の例で実行時例外になるのは、String[ ]とObject[ ]との間でVMがきちんと型情報を区別してチェックしている証拠です。
この時代までは、このようにJavaの型システムは非常に単純でした理解しやすいものでした。しかし、Java5になって総称型の仕組みが導入されることになり、正しく理解する上で注意が必要になっています。Java5では総称型のクラスを定義することができるように拡張されており、ArrayList
直感的には配列と同様ArrayList
型消去について理解を深めるには、実際にコンパイル後のバイトコードを調査してみるのが一番です。たとえば、以下のソースコードを考えます。
List<String> list = new ArrayList<String>(); list.add("test1"); list.add("test2"); String item1 = list.get(0); String item2 = list.get(1);
これをコンパイルすると、実際にバイトコード上では以下と等価なコードに変換されていることがわかります。*5
List list = new ArrayList(); list.add("test1"); list.add("test2"); String item1 = (String)list.get(0); String item2 = (String)list.get(1);
これはJDK1.4のコードそのものですね。*6もともとのソースのArrayList
型消去では具象化可能型(reifiable type)という考えを理解することがポイント
このように一般にはコンパイル時に型消去が行われるのですが、型消去の影響を受けずに最終的にバイトコード中で型情報が失われない型は具象化可能型(reifiable type)と呼ばれています。具象化可能型には以下のようなものがあります。
- 基本型(intなど)
- 総称化されていないクラスやインターフェース(String、Number、Runnableなど)
- すべての総称パラメータに対して境界のないワイルドカードを持つ型(List<?>、Map<?, ?>)
- 総称パラメーター未指定のraw型(List、Mapなど)
- 具象化可能型の要素を持つ配列(int[ ]、String[ ]、List<?>[ ]、int[ ][ ]など)
逆に、それ以外の型はすべて具象化可能型ではありません。
- 総称型変数そのもの(Tなど)
- 実型パラメーターを持つパラメーター化型(List
、Map など) - 境界付きワイルドカードを持つパラメーター化型(List<? extends Number>、Comparable <? super String>など)
- 具象化可能型でない要素を持つ配列(T[ ]、List
[ ]など)
型消去を正しく理解するにはどの型が具象化可能型であるのかを正しく知っておくことがポイントです。
配列の要素型は具象化可能型でなくてはnewできない
前置きが長くなりましたが、これから配列と総称型の相性が悪いという点について見ていきます。まず、Java言語では任意の要素型の配列変数を定義できるという事実があります。当然と思われるかもしれませが、以下のように任意の要素型の配列変数の宣言をすることが可能です。以下の宣言はすべて問題なくコンパイルが通ります。
int[] a1; String[] a2; T[] a3; List<String>[] a4; List<? extends Number>[] a5;
しかし、配列の生成に関しては要素型が具象化可能型でなくてはnewできないという重大な制約があります。実際に試してみると以下のように下の3つの文はコンパイルエラーとなります。
int[] a1 = new int[10]; // OK String[] a2 = new String[3]; // OK T[] a3 = new T[3]; // Tの総称配列を作成できません。 List<String>[] a4 = new List<String>[3]; // List<String>の総称配列を作成できません。 List<? extends Number>[] a5 = new List<? extends Number>[3]; // List<? extends Number>の総称配列を作成できません。
配列が持つこの制約は存在理由が分かりにくいのですが、バイトコード中で配列の生成時に配列要素の型が必要になるという点を思い出せば納得ができます。よって、配列要素の型が具象化可能型でない場合、実際の型が残らないため配列を生成するバイトコードに直接変換できないのです。具象化可能でない場合、型消去された要素型の配列を代わりに生成するという仕様でもよかったかもしれませんが、ソースコード中の意図に反して別の要素型の配列が生成されるというのは混乱を招くという判断なのだと思われます。
どうしても、コンパイルを通すためには、以下のようにして具象化可能型の要素を持つ配列を生成してから、キャストするしかありません。
int[] a1 = new int[10]; // OK String[] a2 = new String[3]; // OK T[] a3 = (T[])new Object[3]; // 警告、Object[]からT[]への未検査キャスト List<String>[] a4 = (List<String>[])new List<?>[3]; // 警告、List<?>[]からList<String>[]への未検査キャスト List<? extends Number>[] a5 = (List<? extends Number>[])new List<?>[3]; // 警告、List<?>[]からList<? extends Number>[]への未検査キャスト
ただし、このキャストは型安全ではなく今度は警告となります。このキャストが実際にどうして型安全でないかの理由を考えるのは意外と難しいですが、たとえば以下の例で、実際に実行時例外となります。
public static void main(String[] args) { String[] result = hello("hello1", "hello2"); // ClassCastException ---(A) } public static <T> T[] hello(T t1, T t2) { T[] result = (T[])new Object[]{t1, t2}; // 警告、Object[]からT[]への未検査キャスト ---(B) return result; }
ただし、ClassCastExceptionは一見想定外の行で発生していることに注意してください。明示的にキャストを行っている(B)の行では例外は発生せず、(A)の行でClassCastExceptionが発生します。どうしてこのようになるかというと、実際バイトコードにコンパイルした結果を逆コンパイルするとわかるのですが、型消去により、上記のコードは以下と等価なコードに変換されるからです。
public static void main(String[] args) { String[] result = (String[])hello("hello1", "hello2");// ClassCastException ---(A') } public static Object[] hello(Object t1, Object t2) { Object[] result = new Object[]{t1, t2}; // ---(B') return result; }
(A')の行でObject[ ]のインスタンスをString[ ]にキャストすることは不正ですから例外となるのです。このように任意の要素型の配列を宣言できるのに、要素が具象化可能な型の配列しかnewできないというちょっと矛盾した仕様であるため、要素が具象化可能でない配列を使うにはどこかで警告が出ること(つまり型安全でないということ)を避けられないというのが事実なのです。
このような制約は配列に固有のものです。たとえば、配列をListに置き換えると、すべてのケースでまったく問題なくコンパイル可能ですし、型安全性も保障されます。*8
List<Integer> list1 = new ArrayList<Integer>(); // OK。ただしList<int>は不可。 List<String> list2 = new ArrayList<String>(); // OK List<T> list3 = new ArrayList<T>(); // OK List<List<String>> list4 = new ArrayList<List<String>>(); // OK List<List<? extends Number>> list5 = new ArrayList<List<? extends Number>>(); //OK
メソッドの可変長パラメーターの型に関する制約
実は、配列に関しては明確にnewしない場合でも暗黙的に生成される場合があります。Java5では可変長パラメーターの仕組みが導入されたのですが、可変長パラメーターは糖衣構文に過ぎず、バイトコード上は配列の生成に変換されることになります。したがって、以下のように具象化可能型でない型の可変長パラメーターで宣言されたメソッドを呼び出す側で警告となります。結果として具象化可能型でない要素型を持つ配列の生成が必要になるからです。
public void test(T test) { test1("test", "test2"); test2(test, test); // 警告、List<T>の総称配列は可変引数パラメーターに対して生成されます。 test3(Arrays.asList("test"), new ArrayList<String>()); // 警告、List<String>の総称配列は可変引数パラメーターに対して生成されます。 } public void test1(String... args) { ... } public void test2(T... args) { ... } public void test3(List<String>... args) { ... }
ただし、この場合はnewと違って、エラーではなく警告になります。たとえば、上記のtest3の場合、本来は暗黙的にList
public void test(Object test) { test1(new String[] { "test", "test2" }); test2(new Object[] { test, test }); test3(new List[] { Arrays.asList(new String[] { "test" }), new ArrayList() }); } public void test1(String[] args) { ... } public void test2(Object[] args) { ... } public void test3(List[] args) { ... }
ただし、これも理論上型安全でないケースがあるということであり、実際に問題となるケースを探すのは容易なことではありません。*10たとえば、実際上はありえないコードですが、以下のコードでは警告もエラーも出ませんがClassCastExceptionを発生させることができます。
public void test3(List<String>... args) { List<?>[] list = args; List<Integer> list0 = new ArrayList<Integer>(); list0.add(1); list0.add(2); list[0] = list0; String item = args[0].get(0); // ClassCastException }
結局何が問題かというと、本来newが禁止されているList
配列を使ったAPIにおける「景品表示法の原理」と「公然わいせつ罪の原理」
以前にもJava言語で固定要素のListを初期化する際のイディオム - 達人プログラマーを目指してで紹介したJava Generics and Collections: Speed Up the Java Development Processでは、第6章で配列を使って型安全なAPIを正しく設計する上で心がけるべき二つの原理が紹介されています。結構微妙な問題を扱っているため、理解するのは簡単ではないのですが、覚えやすいようにそれぞれに印象的でユニークな名前が付いています。
景品表示法の原理(Principals of truth in advertising)
この原理は、以下のように定義されています。
The reified type of an array must be a subtype of the erasure of its static type.
(配列の具象化型はその静的な型に対する消去型のサブタイプでなくてはならない。)
この一文を読んでもわかりにくいのですが、例としては以下のようなケースがこの原理に違反するケースとなります。
import java.util.Arrays; import java.util.Collection; import java.util.List; public class TruthInAdvertising { public static void main(String[] args) { List<String> sampleList = Arrays.asList("test1", "test2"); String[] sampleArray = toArray(sampleList); // ClassCastException } public static <T> T[] toArray(Collection<T> c) { @SuppressWarnings("unchecked") // 不適切な警告の無視 T[] result = (T[])new Object[c.size()]; int i = 0; for (T t : c) { result[i++] = t; } return result; } }
以上の例では、Collectionから配列に変換する共通ルーチンの中で不適切に警告が無視されています。その結果、見かけ上型安全のように見せかけていながら、実際にはClassCastExceptionが警告なしで発生するようになっています。この場合、配列の具象化型はObject[ ]ですが、sampleArrayの型消去後の静的型はString[ ]でTもStringにバインドされますから、この原理に違反しています。この原理が無視されると本当はObject[ ]でしかない配列が、コンパイラーに対しては見かけ上T[ ]つまりString[ ]のように見えてしまうことになるのですが、その点が誇大広告のようで景品表示法違反だということが言いたいのでしょう。
公然わいせつ罪の原理(Principals of indecent exposure)
もう一つの公然わいせつ罪の原理は以下のように定義されます。
Never publicly expose an array where the components do not have a reifiable type.
(配列の要素型が具象化型可能型でない限り、配列を公に晒してはならない。)
これについては、原理に違反しているかどうかは簡単に見分けがつきますが、以下のような例が考えられます。
import java.util.Arrays; import java.util.List; public class IndecentExposure { public static void main(String[] args) { List<Integer>[] intLists = intLists(1); List<? extends Number>[] numLists = intLists; numLists[0] = Arrays.asList(1.01); int i = intLists[0].get(0); // ClassCastException } public static List<Integer>[] intLists(int size) { @SuppressWarnings("unchecked") // 不適切な警告の無視 List<Integer>[] intLists = (List<Integer>[])new List[size]; for (int i = 0; i < size; i++) { intLists[i] = Arrays.asList(i + 1); } return intLists; } }
すでに説明したように、型安全性のために要素型が具象化型可能型でない配列のnewは禁止されているのですから、こうした配列がpublicなAPIから警告なしに取得できるということは、どこかで警告が握りつぶされているということに他なりません。
ちなみに、JDKのjava.lang.Class#getTypeParameters()の戻り値の型はTypeVariable
総称型配列を扱うにはどうすればよいのか
くさいものには蓋をするという考えで
このように配列と総称型は言語仕様上水と油の関係であり、警告なしで(型安全性を犠牲にせずに)共存できません。このことに対する対処としてはまず、総称型の配列を使っていることを特定のクラスの内部にカプセル化してしまうという考え方があります。そのクラスの中で型安全であるということをロジック上保障した上でならば、堂々とコンパイラーの警告を無視することができます。そのようにがんばって総称型配列の仕様を隠蔽している例は、おなじみのJava5のArrayListクラスの実装に見ることができます。この実装では総称配列E[ ]をフィールドで宣言しているのですが、本エントリで説明した事情でE[ ]は生成できないため代わりにObject[ ]を生成してキャストするようになっています。コンパイラーによる型安全性のなさはロジックで保障しているということです。
(Java5のArrayListより抜粋) public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private transient E[] elementData; private int size; public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = (E[])new Object[initialCapacity]; } ... public E get(int index) { RangeCheck(index); return elementData[index]; } public E set(int index, E element) { RangeCheck(index); E oldValue = elementData[index]; elementData[index] = element; return oldValue; } }
ただし、ちょっと興味深いことですがJava6では以下のように総称配列を利用しない形式にリファクタリングされていました。今度は総称配列を利用する代わりに直接Object[ ]をフィールドで宣言し、型を返す際に明示的に型キャストするような実装に変更されています。
(Java6のArrayListより抜粋) public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private transient Object[] elementData; public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } ... public E get(int index) { RangeCheck(index); return (E) elementData[index]; } public E set(int index, E element) { RangeCheck(index); E oldValue = (E) elementData[index]; elementData[index] = element; return oldValue; } }
結局、newが禁止されている要素型が具象化可能でない配列型を、宣言できるということ自体がよくない仕様だったのではないかということが、最近言われるようになってきているところがあるのかもしれません。総称型の配列宣言を使わず原始的なキャストで済ますというのが最近のトレンドということでしょうか?
総称型配列のnewを避けるためにリフレクションを利用する
総称型配列はnewすることができませんが、すでに既存の配列のインスタンスが存在していれば、その実行時の型情報を利用して配列のインスタンスを生成することができます。一種のプロトタイプパターンのような感じですが、このテクニックはJava5のArrayListのtoArray()メソッドで利用されています。
public <T> T[] toArray(T[] a) { if (a.length < size) a = (T[])java.lang.reflect.Array. newInstance(a.getClass().getComponentType(), size); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
総称配列を生成する別の手段としては、Java6のArrays.copyOf()で利用されているようにClassクラスのインスタンスを渡すという方法も考えられます。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
まとめ
以上、総称型と配列の問題点についてまとめます。
- 配列はパラメーター型の共変性と具象化という点で特異な型である。
- Java5以降では総称型やパラメーター化された型を要素にもつ配列変数を宣言できる。
- しかし、要素型が具象化可能型でない限りnewすることができないという矛盾がある。
- キャストにより無理やりこうした配列のインスタンスを生成することはできるが型安全性が損なわれる。
このようにJavaの配列は型消去に基づくJavaの総称型の実装と非常に相性が悪いという事実があります。画像処理やバイトデータの処理などのように低水準のデータ処理が必要なケースやArrayListの実装などのようにどうしても必要なケースを除くと、なるべく配列を使わないようにするというのも一つの解決策なのかもしれません。特に、Java EE上の業務アプリケーションではJPAと配列の相性が悪いということもありますし、なるべく配列の使用を控えるのが良いのではないかと思います。
ただし、そうはいっても配列の[ ]演算子による簡単な要素のアクセスは魅力的というところもあります。この場合
- 型安全性がいらないならGroovy
- 型安全性が必要ならScala
などの言語を併用するのもよいかもしれません。
なお、Java総称型については本文で紹介した書籍のほかに以下のサイトが参考になります。
AngelikaLanger.com - Java Generics FAQs - Frequently Asked Questions - Angelika Langer Training/Consulting
*1:AがBの親クラスならA[ ]もB[ ]の親クラスという性質。
*2:実際、Javaプログラミング能力認定試験のサンプルが配列を過剰に利用するアンチパターンの好例です。SI業界(日本)のJavaプログラマーにはオブジェクト指向より忍耐力が求められている? - 達人プログラマーを目指して
*3:もちろん、それ以前に配列に対しては他の参照型のオブジェクトと異なりメソッドが普通に呼び出せないといったオブジェクト指向プログラミング上の制約ももちろんありますが。
*4:Java5以降で普通にコンパイルした場合、リフレクション用のメタ情報として総称化された型の情報はクラス定義中に残ります。したがってリフレクションAPIを用いて総称型の情報やパラメーター化型の実際のクラスを実行時に取得することができます。型消去されると言っているのは実行時のオブジェクトのデータ構造やロジックの部分についてです。この点はちょっと誤解しやすいので理解する上で注意が必要です。
*5:Javaの逆コンパイラーを利用するとよいです。http://java.decompiler.free.fr/などがお勧め。
*6:Comparable
*7:このような事実を正しく理解すれば、総称型はキャストが不要だから高速になるといった理解はJavaにおいては間違いであることもわかります。
*8:ただし、new ArrayList<?>()やnew ArrayList<? extends String>()のようにトップレベルにワイルドカードをパラメーターに持つパラメーター化型の生成は仕様上認められていません。一方、new ArrayList<List<?>>()はトップレベルにワイルドカードが無いためOKです。難しいですね。この制約については実質問題になるケースはないとはいえ、どうして必要か理由は実は不明確なところがあります。
*9:ただし、最近の逆コンパイラだとリフレクション情報から総称型の情報をある程度復元してしまうものもあるため、解釈には注意が必要。
*10:だから、ほとんどの場合において、実用上は@SuppressWarningsで警告を無視しても大きな問題にはならないという考え方もできます。