日々常々

ふつうのプログラマがあたりまえにしたいこと。

ArrayListじゃなくListを使うという話

具象型ではなく抽象型で扱え、インタフェースを使え、みたいなお話に対して。

前置き

Javaの話。他の言語だと話は変わります。

「こうするのが絶対的に正解」と言うものではありません。私の現在の選択の説明です。明日になったら違うこと言ってるかも。

主な登場人物は掲題の java.util.ArrayList および java.util.List、そして java.util.Collectionjava.lang.Iterable です。

f:id:irof:20190809210315p:plain

こんな世界観。他のインタフェースやクラスもたくさんありますが、この話の本筋では無いので触れません。

前提として以下を置いています。

  • フレームワークやライブラリではなく、一つの業務アプリケーションに閉じた話です。ゆえに不特定多数から使われる型ではなく、影響を与えるコードは全て目が届く範囲にあるものとします。
  • 計算量は別の話です。扱うドメインにもよりますが、 ArrayList の計算量が問題になるのはそもそも稀ですし、今となっては設計の問題です。 Stream#parallel とかに話吹っ飛ばすと帰ってこれなくなるし……。検討の優先順位としてはかなり下位になります。
  • ジェネリクスは触れません。コレクションの型の話には強い影響を与えるジェネリクスさんだけど、今回は影響ないので。
  • varはスコープ外。別で扱います。そのうち。
  • 先頭にある通り「具象型ではなく抽象型で扱え、インタフェースを使え、みたいなお話」が主題です。今回の登場人物になっているのは、この話に対して抽象度の高低両方を話せるから。StringCharSequence でもよかったんだけど、一方向だけになるし、 CharSequence はそんな馴染みがないだろうから回避です。

前提が変わったら結論も変わるかもしれませんし、結論が変わらなくても説明は変わると思います。

Short Answer

List を使っておけばいいと思います。

特に「御託はいいからどうしたらいいの?」と言うのであれば、慣習も含めて無難な選択です。 面倒なことに煩わされずに他の重要な(と思っている)ことに注力するためのとりあえずの選択としては、悪く無いはず。

と言うことで、以下は長々とした私の選択理由の説明。

使っているところを見てみる

この話題の対象はローカル変数、フィールド、メソッドの引数と戻り値が挙げられます。 結論は List に収束するのですが、理由が違うので分けて説明します。

自分で生成するローカル変数

List<MyEntry> list = new ArrayList<>();

ここの選択にはそれほど強い理由はないです。

メソッド等で使用するローカル変数の変数型は影響も少ないので ArrayList としても問題はありません。 もちろん前提として、メソッドが十分に小さいこと。1000行とかなるならもうちょっと考えるかもしれない。 けどローカル変数の型をどうするかの前に考えることあるでしょ?

ローカル変数型は他のメソッドの引数にしたり、自身の戻り値として使います。 ここで ArrayList としていると、IDEでメソッド生成したりフィールド生成したりするときに ArrayList がデフォルトで選択されることになります。 メソッドシグネチャやフィールドでは後述の理由で List を使いたいので、ローカル変数で List を使う理由の一つに、手間の軽減が挙げられます。

これだけであれば手間のタイミングが前後するだけで、外に出ないインスタンスであれば ArrayList にしておくのが良さげにも見えますが、そこに意図を込めて「ArrayList にしていると言うことはこのスコープに閉じたインスタンスなんだな!」を読み取って貰うのは酷というか。

自分で生成しないローカル変数

メソッドの戻り値とか、わざわざ型を変えたりしません。だって面倒だし、大抵 List になってるのでここで何か主張する必要もないわけで。

フィールド

class MyClass {

    List<MyEntry> list;
}

外から直接フィールドを参照したりしなければ、自由度が高く意図を込めやすいところです。そう言う意味で一時期は CollectionIterable にしていました。でもやめて List に落ち着いています。

ここで ArrayList にすると Collections#emptyListArrays#asListCollectors#toList が使えなくなります。 そしてフィールドはメソッドで操作するものなので CollectionIterable にすると足りないこともしばしばあります。

ArrayList じゃなかったら add メソッドとかがオプションだから危険!うっかり Collections#emptyList とか使うと実行時エラーになる!!とかもあります。そんな時は ArrayList にすることもあります。これはオプションのメソッドを持っていると言う List と言うか Collection の負債をどこで処理するかと言うことも絡みますので、そこそこ慎重な判断が必要だったり。

ちょっと脱線になりますが、クラスが Serializable を実装している場合はフィールドも Serializable にしないとなので、そのような場合は ArrayList を使ったりします。 とはいえ Serializable に関しては別の話があって、重視すべきじゃないときはそんな気にしなかったりも。まあ Serializable 要求されたからって半端に使いすぎじゃ?ってコードはよく見ます。 だって ArrayListArrayList<E extends Serializable> とかじゃないわけで……まあそんなことしちゃったら過去のコード軒並みコンパイルできなくなるからやらなかったんだろうけれど……

メソッドの引数

void method(List<MyEntity> list);

引数型は、そのメソッドが処理するために必要な制約を十分につけられる最も抽象的な型で扱うのが良いはずです。その方がいろんなところから使えるから。 なので List ではなく CollectionIterable 辺りで扱うのが適切なはずなのですが、ここも List に落ち着きます。

なぜならここまでの選択でローカル変数やフィールドがList となっているので、引数も必然 List でよくなるのです。 フレームワークやライブラリのように不特定多数に使用されるインタフェースならともかく、そうでないのならば不要な抽象化かと。

メソッドの戻り値

List<MyEntity> method();

戻り値型を具象型にせず List にするのは、戻り値に対して余計な制約をつけないためと言うのもありますが、メソッド自身の戻り値はローカル変数やフィールドを返すことが多く(防御的コピーは基本的にノイズなのでしないです。取得したコレクションをいじるような行儀の悪いことしないので。)、戻り値型を ArrayList などにするためにはキャストや新しい ArrayList インスタンスの生成が必要になり、手間です。

Iterable などにしない理由は、そうすると呼び出し側も Iterable で受けざるをえなくなり、使い勝手が悪くなることがしばしばあるからです。 コレクション同士の操作、たとえば addAll などもできないですし、contains などが欲しくなった時に型を変える必要がでてきます。 Collection にすればこれは解決するので Collection にしていたこともありますが、一つ目の理由もあってわざわざ Collection にはしていません。

ということで

全部 List になっています。

Listにする「べき」だと言うお話とお返し

List にするべきだ、と大上段にお話ししてくれる方もいらっしゃいます。なんでも「List で扱うことで実装から切り離すことができ、ArrayListLinkedList 、その他の List 実装を選択できる。実装に依存してはいけない。」とかなんとか。他の説明をしていただけるかもしれません。なるほどなるほど。

ところで ListJavadocに書かれているように「順序付けられたコレクション」です。つまり、 順序が不要であれば本来は List を使うべきではありません。 順序が必要な場合も多くありますが、Listを使う際に 常に 順序が必要かと言うと、そんなことはないはずです。 コレクションが欲しいのであれば List でなく Collection が適切であり、全件ループできればいいだけであれば Iterable が適切です。 と言うことで、なんか格好つけた理由で List にするべきだとか言うのは片手落ちなのです。 なんかドヤられてイラっとしたらこの返しでだいたい大丈夫。

あと、こと業務アプリケーション開発に限定すればListの実装型を変更するような場面は稀です。変更したことも実際ありますが、 List にしてなくても別に問題なく変更できたと思います。

とか言ってListを使う理由

List って、一番文字数少ないんですよ(今回の登場人物の中では。)。文字数が影響するのは書く時ではなく読む時です。書く時はIDEがなんとかするので関係ありません。文字数少ないほうが目に優しい。 繰り返しになるけれど var は別の話ね。

で、それより重要なのがノイズの少なさです。 これは List がよく使われるがゆえですが、余計な情報が少ないはずの CollectionIterable のほうが「わざわざそれを使っている」と言うメッセージが入ってしまいます。 例えば「ん、 Collection と言うことは Set だったりするのかな?」とよぎってしまう。「 List じゃなく Iterable にしてるのは何故なんだろう」とか。 不思議なことに考えることが逆に増えてしまうわけです。これは慣習的にListがよく使われることに起因するものかなと思っています。 先に書いた「一時期 CollectionIterable を使ってみたけれどやめた」の筆頭の理由がこのノイズです。

もう一つ、型が伝播しちゃうと言うのも挙げられます。一箇所 Iterable にしたら芋づる式に Iterable になっていきますし、それで不足して List に変えたらまた芋づる式に変えていく必要がでてくる。これは設計にも依存するので、ファーストクラスコレクションを基本に据えてからは芋づる式の変更はほぼ不要になりましたが。。

本当に欲しいのは、順序づけられたコレクションではなく、ノイズが少なく取り回しのよいコレクションです。 最も使い勝手の良かった ArrayList 、そのインタフェースでありほぼ全てのメソッドを備えている List がその座を占拠してしまっています。 その結果、多くのライブラリのメソッド引数は List を要求しているため Collection の使い勝手は正直悪いです。 Collection を使う方がメリットが多いはずなんですが、現実としてはデメリットがうわまってしまい、わざわざ Collection を使う理由がないんですよね……。

弊害として、 List を使っているのに順序が必要になったときに「こいつ順序の意識もって使われてるんだっけ?」と疑うハメになってる。順序を持つかすらオプションとか、酷い話だ。

実際やってみて欲しい

「一時期 CollectionIterable を使ってみた」と書きましたが、これで「へーダメなんだ」とか「そりゃそうでしょ」で終わって欲しくない思いもあります。私が苦労したことを他の人にもやって欲しいとかいうわけではなく、実際やることを通しての気づきというか、理解の深さが変わるので。実際挑んだら説明できるようにはなる、少なくともその材料は揃う。説明も結論も私と違って全然いいのです。

この手の「実際やってみた方がいい」と言う話は、実践から得たものを形式知にしきれていないと言うことなんだけど、まあそもそも共同化できてないから表出化も難しいのだよとか自己擁護しておく。

結論

コレクションとしてこだわりがない場合は List で扱っています。 何かの原則とかに沿ったキレイなものではなく、単に現実的な使い勝手による選択です。

冒頭で触れた StringCharSequence だと String を選ぶわけですが、その説明もだいたいそんな感じです。

Java言語としては適切な型があって欲しいとは思うのですが、サードパーティのコレクションライブラリを使ってみて感じるのは、置き換える労力をかけるところでもないと言うことです。コレクションはコレクションだけで完結せず、他のライブラリやフレームワークに引き渡すインタフェース(文字通りの)となっているため、サードパーティのコレクションライブラリをうまく使うのは難しく、労力に見合わないこともしばしばです。

そう言う意味でCollectionsFrameworkは返済できない負債となってしまっているのですが、List を使っていて不利益を被ることはほとんどないのが実状。 学び方によっては苦労しちゃうんだろうなぁ、それは残念なところかなぁ、とは思います。 まあCollectionsFrameworkなんてJava1.2時代のもの、ツッコミどころなんてない方がおかしい。にも関わらずいまだに使えてるのって凄いなぁ(ただの感想

蛇足

ところで9年前に

「List list = new ArrayList();」を定型句として扱うなんて、もってのほかです。

とか言ってた。なんか機嫌悪かったんですかね。