具象型ではなく抽象型で扱え、インタフェースを使え、みたいなお話に対して。
前置き
Javaの話。他の言語だと話は変わります。
「こうするのが絶対的に正解」と言うものではありません。私の現在の選択の説明です。明日になったら違うこと言ってるかも。
主な登場人物は掲題の java.util.ArrayList
および java.util.List
、そして java.util.Collection
と java.lang.Iterable
です。
こんな世界観。他のインタフェースやクラスもたくさんありますが、この話の本筋では無いので触れません。
前提として以下を置いています。
- フレームワークやライブラリではなく、一つの業務アプリケーションに閉じた話です。ゆえに不特定多数から使われる型ではなく、影響を与えるコードは全て目が届く範囲にあるものとします。
- 計算量は別の話です。扱うドメインにもよりますが、
ArrayList
の計算量が問題になるのはそもそも稀ですし、今となっては設計の問題です。Stream#parallel
とかに話吹っ飛ばすと帰ってこれなくなるし……。検討の優先順位としてはかなり下位になります。 - ジェネリクスは触れません。コレクションの型の話には強い影響を与えるジェネリクスさんだけど、今回は影響ないので。
var
はスコープ外。別で扱います。そのうち。- 先頭にある通り「具象型ではなく抽象型で扱え、インタフェースを使え、みたいなお話」が主題です。今回の登場人物になっているのは、この話に対して抽象度の高低両方を話せるから。
String
とCharSequence
でもよかったんだけど、一方向だけになるし、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;
}
外から直接フィールドを参照したりしなければ、自由度が高く意図を込めやすいところです。そう言う意味で一時期は Collection
や Iterable
にしていました。でもやめて List
に落ち着いています。
ここで ArrayList
にすると Collections#emptyList
や Arrays#asList
、 Collectors#toList
が使えなくなります。
そしてフィールドはメソッドで操作するものなので Collection
や Iterable
にすると足りないこともしばしばあります。
ArrayList
じゃなかったら add
メソッドとかがオプションだから危険!うっかり Collections#emptyList
とか使うと実行時エラーになる!!とかもあります。そんな時は ArrayList
にすることもあります。これはオプションのメソッドを持っていると言う List
と言うか Collection
の負債をどこで処理するかと言うことも絡みますので、そこそこ慎重な判断が必要だったり。
ちょっと脱線になりますが、クラスが Serializable
を実装している場合はフィールドも Serializable
にしないとなので、そのような場合は ArrayList
を使ったりします。
とはいえ Serializable
に関しては別の話があって、重視すべきじゃないときはそんな気にしなかったりも。まあ Serializable
要求されたからって半端に使いすぎじゃ?ってコードはよく見ます。
だって ArrayList
も ArrayList<E extends Serializable>
とかじゃないわけで……まあそんなことしちゃったら過去のコード軒並みコンパイルできなくなるからやらなかったんだろうけれど……
メソッドの引数
void method(List<MyEntity> list);
引数型は、そのメソッドが処理するために必要な制約を十分につけられる最も抽象的な型で扱うのが良いはずです。その方がいろんなところから使えるから。
なので List
ではなく Collection
か Iterable
辺りで扱うのが適切なはずなのですが、ここも List
に落ち着きます。
なぜならここまでの選択でローカル変数やフィールドがList
となっているので、引数も必然 List
でよくなるのです。
フレームワークやライブラリのように不特定多数に使用されるインタフェースならともかく、そうでないのならば不要な抽象化かと。
メソッドの戻り値
List<MyEntity> method();
戻り値型を具象型にせず List
にするのは、戻り値に対して余計な制約をつけないためと言うのもありますが、メソッド自身の戻り値はローカル変数やフィールドを返すことが多く(防御的コピーは基本的にノイズなのでしないです。取得したコレクションをいじるような行儀の悪いことしないので。)、戻り値型を ArrayList
などにするためにはキャストや新しい ArrayList
インスタンスの生成が必要になり、手間です。
Iterable
などにしない理由は、そうすると呼び出し側も Iterable
で受けざるをえなくなり、使い勝手が悪くなることがしばしばあるからです。
コレクション同士の操作、たとえば addAll
などもできないですし、contains
などが欲しくなった時に型を変える必要がでてきます。
Collection
にすればこれは解決するので Collection
にしていたこともありますが、一つ目の理由もあってわざわざ Collection
にはしていません。
ということで
全部 List
になっています。
Listにする「べき」だと言うお話とお返し
List
にするべきだ、と大上段にお話ししてくれる方もいらっしゃいます。なんでも「List
で扱うことで実装から切り離すことができ、ArrayList
や LinkedList
、その他の List
実装を選択できる。実装に依存してはいけない。」とかなんとか。他の説明をしていただけるかもしれません。なるほどなるほど。
ところで List
はJavadocに書かれているように「順序付けられたコレクション」です。つまり、 順序が不要であれば本来は List
を使うべきではありません。
順序が必要な場合も多くありますが、List
を使う際に 常に 順序が必要かと言うと、そんなことはないはずです。
コレクションが欲しいのであれば List
でなく Collection
が適切であり、全件ループできればいいだけであれば Iterable
が適切です。
と言うことで、なんか格好つけた理由で List
にするべきだとか言うのは片手落ちなのです。 なんかドヤられてイラっとしたらこの返しでだいたい大丈夫。
あと、こと業務アプリケーション開発に限定すればList
の実装型を変更するような場面は稀です。変更したことも実際ありますが、 List
にしてなくても別に問題なく変更できたと思います。
とか言ってListを使う理由
List
って、一番文字数少ないんですよ(今回の登場人物の中では。)。文字数が影響するのは書く時ではなく読む時です。書く時はIDEがなんとかするので関係ありません。文字数少ないほうが目に優しい。
繰り返しになるけれど var
は別の話ね。
で、それより重要なのがノイズの少なさです。
これは List
がよく使われるがゆえですが、余計な情報が少ないはずの Collection
や Iterable
のほうが「わざわざそれを使っている」と言うメッセージが入ってしまいます。
例えば「ん、 Collection
と言うことは Set
だったりするのかな?」とよぎってしまう。「 List
じゃなく Iterable
にしてるのは何故なんだろう」とか。
不思議なことに考えることが逆に増えてしまうわけです。これは慣習的にList
がよく使われることに起因するものかなと思っています。
先に書いた「一時期 Collection
や Iterable
を使ってみたけれどやめた」の筆頭の理由がこのノイズです。
もう一つ、型が伝播しちゃうと言うのも挙げられます。一箇所 Iterable
にしたら芋づる式に Iterable
になっていきますし、それで不足して List
に変えたらまた芋づる式に変えていく必要がでてくる。これは設計にも依存するので、ファーストクラスコレクションを基本に据えてからは芋づる式の変更はほぼ不要になりましたが。。
本当に欲しいのは、順序づけられたコレクションではなく、ノイズが少なく取り回しのよいコレクションです。
最も使い勝手の良かった ArrayList
、そのインタフェースでありほぼ全てのメソッドを備えている List
がその座を占拠してしまっています。
その結果、多くのライブラリのメソッド引数は List
を要求しているため Collection
の使い勝手は正直悪いです。
Collection
を使う方がメリットが多いはずなんですが、現実としてはデメリットがうわまってしまい、わざわざ Collection
を使う理由がないんですよね……。
弊害として、 List
を使っているのに順序が必要になったときに「こいつ順序の意識もって使われてるんだっけ?」と疑うハメになってる。順序を持つかすらオプションとか、酷い話だ。
実際やってみて欲しい
「一時期 Collection
や Iterable
を使ってみた」と書きましたが、これで「へーダメなんだ」とか「そりゃそうでしょ」で終わって欲しくない思いもあります。私が苦労したことを他の人にもやって欲しいとかいうわけではなく、実際やることを通しての気づきというか、理解の深さが変わるので。実際挑んだら説明できるようにはなる、少なくともその材料は揃う。説明も結論も私と違って全然いいのです。
この手の「実際やってみた方がいい」と言う話は、実践から得たものを形式知にしきれていないと言うことなんだけど、まあそもそも共同化できてないから表出化も難しいのだよとか自己擁護しておく。
結論
コレクションとしてこだわりがない場合は List
で扱っています。
何かの原則とかに沿ったキレイなものではなく、単に現実的な使い勝手による選択です。
冒頭で触れた String
と CharSequence
だと String
を選ぶわけですが、その説明もだいたいそんな感じです。
Java言語としては適切な型があって欲しいとは思うのですが、サードパーティのコレクションライブラリを使ってみて感じるのは、置き換える労力をかけるところでもないと言うことです。コレクションはコレクションだけで完結せず、他のライブラリやフレームワークに引き渡すインタフェース(文字通りの)となっているため、サードパーティのコレクションライブラリをうまく使うのは難しく、労力に見合わないこともしばしばです。
そう言う意味でCollectionsFrameworkは返済できない負債となってしまっているのですが、List
を使っていて不利益を被ることはほとんどないのが実状。
学び方によっては苦労しちゃうんだろうなぁ、それは残念なところかなぁ、とは思います。
まあCollectionsFrameworkなんてJava1.2時代のもの、ツッコミどころなんてない方がおかしい。にも関わらずいまだに使えてるのって凄いなぁ(ただの感想
蛇足
ところで9年前に
「List list = new ArrayList();」を定型句として扱うなんて、もってのほかです。
とか言ってた。なんか機嫌悪かったんですかね。