2015/05/09

[Java]asListが返すリストは固定長

言語が進化していくにつれ、配列とコレクションの垣根は低くなっていきますが、依然としてそれは同じではありません。
さまざまな言語がさまざまなアプローチをしている分野で、この部分を俯瞰で見ていると、言語毎の思想が見えてきて面白いのです。

配列で保持しているデータをシャッフルして別なデータを生成するコードを書いていました。
具体的にはマッチメーカープログラムで、プレイヤーを一時的にシャッフルして対戦の組み合わせを決めるプログラムです。
プレイヤーのリストは順位で並んでいるので、これを壊したくはありません。
Javaに配列をシャッフル関数があれば良かったのですが、Arraysにはsortはあるもののshuffleはありません。
やむを得ずCollections.shuffleを使うことにしました。

Player[] generateShuffledArray(){
 List<Player>list = Arrays.asList(players);
 list.shuffle();
 return list.toArray();
}

直感的にasListは配列から新たにリストのインスタンスを作ると思っていましたから、シャッフルしたリストをさらにtoArrayで配列に戻して返しています。
効率の悪いロジックだなぁと思っていたのですが、実行してみると予想外の振る舞いになっていました。
この関数を実行した後、返り値の配列はともかく、オリジナルの配列playersまでがシャッフルされていたのです。
最初はどこかでgenerateShuffledArrayの結果を上書きしてしまったんだろうと思いましたが、その形跡はありません。
発想の転換が必要でしたが、listの変化がplayersに反映していると考えるしかありませんでした。
さて、仕様をみると、ちゃんと記述がありました。

As List
指定された配列に連動する固定サイズのリストを返します。返されたリストへの変更は、そのまま配列に書き込まれます。このメソッドは、Collection.toArray() と組み合わせることで、配列ベースの API とコレクションベースの API の橋渡し役として機能します。また、返されるリストは直列化可能で、RandomAccess を実装します。 

実装についての記述はありませんが、Arrays.asListから返ってくるListは公開されているどんなListサブクラスとも異なる、生の配列をラップする実装となっているクラスであると考えるのが妥当です。
(手元で確認したところ、クラス名はArrays$ArrayListとなっていましたので、ここではその名前を使います。名前は実装依存です。)
固定長であるのは、サイズ拡張によって与えられた配列と別インスタンスにならないための制限でしょう。

通常、実装に依存するようなデザインはぜい弱で、美しくありません。
ネット上にはこのデザインについて、”初期実装のバグを引き継いだ”ような説を唱えている方もいるようですが、これには理由があり、Javaという言語を理解する上で大事なポイントだと思っています。

開発初期のJavaは今よりはるかにパフォーマンスについての要求が厳しいものでした。
ネイティブコードを生成するCやFortranでさえ満足できる処理速度が出せず、クリティカルな個所はアセンブラで書くというのがまだ選択肢にあった時代です。
バーチャルマシン上で動作するというコンセプトのJavaはおもちゃ扱いされていました。
配列の処理は実装効率をてきめんに反映します。
データを物理的にフラットにバイト単位で並べてオフセット値でアクセスする配列は、ある意味抽象化からもっとも遠い存在です。
Javaが仮装言語であることに固執したなら、配列そのものを否定する選択肢もあったはずです。
その場合、配列は必ずコレクションの内部形式にコピーされますから、パフォーマンスは目に見えて落ちます。
理想と現実のせめぎあいの中で、配列をそのままラップするArrays$ArrayListという発明は絶妙だと思います。
実行効率をほぼ落とすことなくリストとしての機能を提供することができるからです。
Arraysをビルダーにしたデザインも賢明でした。実装に強依存するクラスがオーバーライドされて拡散するのとを防いでいます。
Javaにはコレクションを初期化する際にコレクションを引数に取るコンストラクタはありますが、配列を引数にとるコンストラクタはありません。恐らくasListのデザインとひとつながりなのでしょう。初期化の違いで動作が変わるか、実行効率を最適化するチャンスを逃すか、究極の選択をしないという判断です。
実にうまくできています。

さて、当初の想定通り、オリジナルをシャッフルしないなら以下のように修正します。


Player[] generateShuffledArray(){

 List<Player>
 list = Arrays.asList(players.clone());
 list.shuffle();
 return list.toArray();

}

オリジナルがシャッフルされることを容認するなら実装はこれだけです。もうメソッドにする必要すらありません。

void generateShuffledArray(){
 Arrays.asList(players).shuffle();
}
どちらのコードもパフォーマンスは悪くない筈です。少なくとも言語的にはそういう実装ができるデザインになっています。

0 件のコメント:

コメントを投稿