前置き
コレクション毎の繰り返し処理で、for文と拡張for文どっちを使うべきか?について書きました。Iteratorパターンを使用している 拡張for文 を使うべきという話です。
本記事において集約オブジェクトとは
- 配列
- コレクション
を含めます。上記の通り、コレクションと配列を区別します。
この記事の文脈で気にしているのは、配列は不変(immutable)・コレクションは可変(mutable)な点です。immutable な Listとかもありますが、そこは応用なのでここでは言及しません。
コレクションは以下のインターフェースを実装したオブジェクトを指します。
- リスト(List)
- セット(Set)
- マップ(Map)
定義ガバガバですが、イメージだと思ってもらえれば。
また、こちらの記事では限定的な場面の運用についてしか言及していません。 具体的にはシーケンシャルアクセスで処理を行いたい場合です。ランダムアクセスしたい場合はfor分の方が柔軟性が高いのでそちらの方が良いのではと思います。
シーケンシャルアクセス・ランダムアクセスについてや、それぞれのメリットデメリットの詳細な解説を他の記事でしておりますので、ご興味あればご覧ください。3万文字越えなので必要なところだけ見た方が良いかもです。目次の「for文と拡張for文の違い」という項目があります。
結論:拡張for文
理由は三つですが、特に三番目の理由
③コードの柔軟性が上がる→集約オブジェクトの構造を気にする必要がない
の重要さが8割くらいあるので是非、読んでみてほしいです。 そして、間違ってるとかもっといい理由あるとかあれば是非ご意見いただきたいです!!!
目次
配列とコレクション どっちにしても拡張for文を使った方がいい
理由
- 拡張for文の方が見た目がシンプル
- 取り扱う集約オブジェクトによってパフォーマンスが違う
- コードの柔軟性が上がる→集約オブジェクトの構造を気にする必要がない
①拡張for文の方が見た目がシンプル
public static void main(String[] args) { for (int i = 0; i < 5; i++) { System.out.println(list.get(i)); } // 拡張for文 for (Integer i : list) { System.out.println(i); } }
みたまんまですね。
②取り扱う集約オブジェクトによってパフォーマンスが違う
根拠となる数値はいろんな検証してるサイトを見てって感じ 自身のPCでもやってみました。下に書いたので見たかったら見てください。
- ArrayList型・Array型の場合: for でget の方がパフォーマンスが良い(大体20〜30%)
- LinkedList型の場合:for の get では 拡張for文 と比べ99%以上パフォーマンスが下がる
③コードの柔軟性が上がる→集約オブジェクトの構造を気にする必要がない
for文だと集約オブジェクトの要素数の上限が解らない場合は使えない。つまり、for文を所持するオブジェクトAが集約体オブジェクトBの構造を知っておかないと使用できないんですよね。
i < 5;
ここの部分で、終端判定してるけど判定条件を知るために、for文を所持するオブジェクトもしくはクラスが、集約オブジェクトの構造を把握する責任を持つことになってしまいます。
結果、AとBが密結合になってしまいます。AもくしはBどちらかに変更があった場合、変更の影響がもう片方にも及んでしまう。
よくないですね。
実装の詳細が隠蔽されているので普段気にすることはありませんが、拡張for文の内部実装はIteratorパターンです。
集約オブジェクトがなんであれ、 次があるか判定しあれば次の要素を取得する。これだけです。
多次元配列などの場合、for文であれば集約オブジェクトの内部構造を考慮したコード、配列にタッチした実装が必要になります。for文のネストしたりですね。それに対して、拡張for文の場合は順番に取り出すという事だけ。集約オブジェクトの内部構造がなんであれ、順番に一個一個要素を取り出すという処理だけを行います。
集約オブジェクトの各要素に対する繰り返し処理の抽象化です。具体的に記述しなくても、理解していなくても、集約オブジェクトに対して一個一個要素を取り出すという反復作業が可能になります。
これにより、一個一個取り出すという実装と集約オブジェクトを独立させることが出来る様になり疎結合が実現できる。 一個一個取り出すことが終わった後に使うコードの再利用・共有することも可能になると。
まとめ
- 可読性がいい
- コード量が減る
- 性能を気にしなくていい
- 変更時の影響をを少なく抑える
- 繰り返し処理実装が隠蔽されている
拡張for文使いましょう。
拡張for文はで、取り扱う集約オブジェクトが何かによってコンパイラ翻訳時に自動的にリスト又は配列に適したコードが生成されます。
配列の場合はごくごく普通のインデックスを用いたアクセスになっていました。相手を見て変換の方法を変えているようです。
実装の詳細が隠蔽されているので拡張forさえ使えば何も考えなくていい、何も心配しなくていいということです。実装者からすると楽ですよね。
余談
foreach が裏でなにしてるかとか気にしないで使ってませんか?私もデザインパターンのIteratorパターン勉強するまで気にしたことなかったです。笑
forの中で、配列から要素取得する時
array.get(i);
みたいな記述しなくていいんだ〜、不思議〜、しか思ってなかった。笑
あと一個一個取り出すという反復処理において、幅・深さ、どちらの走査を優先するかによってまた色々と使い方ありますよね
アルゴリズムの話ですが、これもかなり深い話になりそう。いつか記事にしたい。
というか、stream 使いこなせる様になりたい。
- for文やif文のネストが減って見やすい。
- リストのインスタンス化をしなくてもよくて良い。
- .addとかも書かなくて良いから、コード量が減って良い
- テストしてくれてるらしいから純粋関数作りやすい
foreachの記事 書いてる場合じゃねぇ.......
コレクション毎の繰り返し処理のパフォーマンス
2,000,000,000(20億)回ループさせたら、配列の時点で
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.Arrays.copyOf(Arrays.java:3512) at java.base/java.util.Arrays.copyOf(Arrays.java:3481) at java.base/java.util.ArrayList.grow(ArrayList.java:237) at java.base/java.util.ArrayList.grow(ArrayList.java:244) at java.base/java.util.ArrayList.add(ArrayList.java:454) at java.base/java.util.ArrayList.add(ArrayList.java:467) at jp.co.sss.designPatterns.Iterator.A1.Main.main(Main.java:43)
次に1,000,000,000(1億)回でループさせました。
LinedListの場合では1,000,000(100万)回ですら応答が5分なかったので100,000(10万)回でループさせました。
結果
Array→for文: 115 ms Array→拡張for文: 127 ms ArrayList→for文: 1087 ms ArrayList→拡張for文: 1055 ms LinkedList→for文: 11664 ms LinkedList→拡張for文: 3 ms
圧倒敵に拡張for文で良いですね。 Listインターフェースをループさせる時に集約オブジェクトの内部構造、つまりArrayListかLinkedListかによって、for か 拡張for かを気にしなくていけないのでは柔軟性に欠けますよね。性能的にも、どの集約オブジェクトでも拡張for文でいいですしね。
検証コード
検証環境
public class Main { public static void main(String[] args) { // -------------------- 配列 -------------------- int[] array = new int[100000000]; for (int i = 0; i < 100000000; i++) { array[i] = i; } // 開始 long start = System.currentTimeMillis(); // 1億ループ for (int i = 0; i < array.length; i++) { int tmp = array[i]; } // 終了 long end = System.currentTimeMillis(); System.out.println("Array→for文: " + (end - start) + " ms"); // 開始 start = System.currentTimeMillis(); // 1億ループ for (int i : array) { int tmp = i; } // 終了 end = System.currentTimeMillis(); System.out.println("Array→拡張for文: " + (end - start) + " ms"); // -------------------- ArrayList -------------------- List<Integer> list = new ArrayList<Integer>(); for (int i = 0; i < 100000000; i++) { list.add(i); } // 開始 long start1 = System.currentTimeMillis(); // 1億ループ for (int i = 0; i < list.size(); i++) { int tmp = list.get(i); } // 終了 long end1 = System.currentTimeMillis(); System.out.println("ArrayList→for文: " + (end1 - start1) + " ms"); // 開始 start1 = System.currentTimeMillis(); // 1億ループ for (Integer i : list) { int tmp = i; } // 終了 end1 = System.currentTimeMillis(); System.out.println("ArrayList→拡張for文: " + (end1 - start1) + " ms"); // -------------------- LinkedList -------------------- List<Integer> list2 = new LinkedList<>(); for (int i = 0; i < 100000; i++) { list2.add(i); } // 開始 long start2 = System.currentTimeMillis(); // 10万ループ for (int i = 0; i < list2.size(); i++) { int tmp = list2.get(i); } // 終了 long end2 = System.currentTimeMillis(); System.out.println("LinkedList→for文: " + (end2 - start2) + " ms"); // 開始 start2 = System.currentTimeMillis(); // 10万ループ for (Integer i : list2) { int tmp = i; } // 終了 end2 = System.currentTimeMillis(); System.out.println("LinkedList→拡張for文: " + (end2 - start2) + " ms"); } }
ついで。 ArrayListとLinkedListの違い
ArrayListの特徴
- 要素を配列で保持している
- 配列がメモリ上でインデックス化されている
- インデックスの修正、コピーに対するコストが要素数に比例して大きくなる
- 要素がメモリ上のインデックスに保管されているため、n番目の要素へのアクセスが早い
良い点
- インデックスを指定して要素を読み出す速度が速い[get]
- インデックスを指定して要素を書き換える速度が速い[set]
- 上記から導かれる特徴で、先頭から順に全ての要素をなめる処理が早い
悪い点
- 要素の挿入が遅いことがある(先頭に近い位置への挿入は遅い。末尾に近い位置への挿入は早い時もあるが、遅い時もある)[add]
- 要素の削除が遅いことがある(先頭に近い位置の削除ほど遅く、末尾に近い位置の削除ほど早い。最末尾の削除は高速)[remove]
- 条件に合致した要素を検索する処理の速度はあまり早くない(工夫によりかなりはやくもできる)[contains,indexOf,lastIndexOf]
使い所
- 配列内の要素に対してランダムなアクセスを必要とし、配列内の要素に対して挿入/削除の操作があまり必要ない場合
- 例えば... データベースからデータを大量に読み込み、以後それを順に参照しつつ複雑な計算を行うような場合
メモリ使用量という観点
- ArrayListは連続したメモリ領域を使いうため、一度確保した連続メモリ領域のサイズを容易に広げられない場合、新しい連続メモリ領域を確保してそこに古い領域のデータをコピーする挙動になる。そのため、たとえ末尾への要素の挿入であっても上述のコピー処理がとても重くなる可能性があるので注意が必要となる。解決策としては、予め要素数の上限を確保しておくことが挙げられる。
- ensureCapacity():ArrayListのサイズを確保する。コンストラクタで指定した初期サイズを大きく超えそうなとき使用
LinkedListの特徴
- 要素を数珠つなぎに保持している(双方向連結リスト)
- 前後の要素に対するリファレンスを保持している
- 要素の追加、削除はリファレンスの変更のみでOKのため、コストが一定
- ある要素が何番目かが分からないため、n番目の要素に対して1から順にたどる必要がある
- (頭か末尾の近い方からたどっていくので、最大でも n / 2 ステップで到達)
良い点
- 要素の挿入が早い(ただし多くの場合、挿入の前に検索があることに注意)[add]
- 要素の削除が早い(ただし多くの場合、削除の前に検索があることに注意)[remove]
悪い点
- インデックスを指定して要素を読み出す速度はあまり速くない[get]
- インデックスを指定して要素を書き換える速度はあまり速くない[set]
- 条件に合致した要素を検索する処理の速度はあまり速くない[contains,indexOf,lastIndexOf]
使い所
- 配列内の要素に対してランダムなアクセスを必要とせず、配列内の要素に対して挿入/削除の操作を頻繁に行う場合
- 例えば... プログラム中で発生するデータの入れ物として使われ、時折データベースへ書き出してデータの永続化が図られるような用途
「使い所」の引用元:LinkedList と ArrayList の使い分け方 qiita.com
そもそもどういう仕組みなのかっていうところも非常に勉強になります。
特に、同じリスト型でなにが異なるのか? 「Array」と「Linked」の命名はなるほどな〜と思います。まさに「名は体を表す」です。 それゆえに、なぜfor文で LinkedList の要素取得が手こずるのかとかわかるかと思います。
あと、コメントがめちゃくちゃ勉強になります。
ランダムアクセスとかシーケンシャルアクセスとかの知識も必要になります。
その辺りのことを以下の記事で説明してるので、気になったらご覧ください。