for-eachループとイテレーターのどちらがより効率的ですか?


206

コレクションをトラバースする最も効率的な方法はどれですか。

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a) {
  integer.toString();
}

または

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();) {
   Integer integer = (Integer) iterator.next();
   integer.toString();
}

これはthisthisthis、またはthisの完全な複製ではありませんが、最後の質問に対する答えの1つが近くなりました。これがだまされない理由は、これらのほとんどがget(i)反復子を使用するのではなく、ループ内で呼び出すループを比較しているためです。

メタで示唆されているように、私はこの質問に対する私の答えを投稿します。


Javaとテンプレートのメカニズムは構文糖に過ぎないので、違いはないと思います
Hassan Syed


2
@OMGポニー:ループがイテレーターと比較されないため、これが重複しているとは思わないが、クラス自体にイテレーターを直接持つのではなく、コレクションがイテレーターを返す理由を尋ねる。
ポールワグランド2010年

回答:


264

コレクションをさまよい、すべての値を読み取る場合、新しい構文ではイテレーターを水中で使用するだけなので、イテレーターを使用しても新しいforループ構文を使用しても違いはありません。

ただし、ループとは、古い「cスタイル」ループを意味します。

for(int i=0; i<list.size(); i++) {
   Object o = list.get(i);
}

次に、基礎となるデータ構造に応じて、新しいforループ、つまりイテレーターがはるかに効率的になります。これは、一部のデータ構造でget(i)は、ループがO(n 2)操作になるO(n )操作であるためです。従来のリンクリストは、このようなデータ構造の例です。すべてのイテレータにはnext()、ループをO(n)にするO(1)操作である必要がある基本的な要件があります。

イテレーターが新しいforループ構文によって水中で使用されていることを確認するには、以下の2つのJavaスニペットから生成されたバイトコードを比較します。最初にforループ:

List<Integer>  a = new ArrayList<Integer>();
for (Integer integer : a)
{
  integer.toString();
}
// Byte code
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 3
 GOTO L2
L3
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 2 
 ALOAD 2
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L2
 ALOAD 3
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L3

次に、イテレータ:

List<Integer>  a = new ArrayList<Integer>();
for (Iterator iterator = a.iterator(); iterator.hasNext();)
{
  Integer integer = (Integer) iterator.next();
  integer.toString();
}
// Bytecode:
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator()Ljava/util/Iterator;
 ASTORE 2
 GOTO L7
L8
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next()Ljava/lang/Object;
 CHECKCAST java/lang/Integer
 ASTORE 3
 ALOAD 3
 INVOKEVIRTUAL java/lang/Integer.toString()Ljava/lang/String;
 POP
L7
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext()Z
 IFNE L8

ご覧のとおり、生成されたバイトコードは事実上同一であるため、どちらの形式を使用してもパフォーマンスが低下することはありません。したがって、for-eachループとなるほとんどの人にとって、ボイラープレートコードが少ないため、最も美的に魅力的なループの形式を選択する必要があります。


4
私は彼が反対のことを言っていたと私は信じています。LinkedListについて考えてください。LinkedListの途中でfoo.get(i)を実行する場合、iに到達するには、以前のすべてのノードをトラバースする必要があります。一方、イテレータは基礎となるデータ構造へのハンドルを保持し、一度に1つずつノードをウォークスルーできます。
Michael Krauklis、2010年

1
大したことではありませんが、for(int i; i < list.size(); i++) {スタイルループもlist.size()各反復の最後に評価する必要があります。使用する場合は、list.size()最初の結果をキャッシュする方が効率的です。
ブレットライアン

3
実際、元のステートメントは、ArrayListおよびRandomAccessインターフェースを実装するその他すべての場合にも当てはまります。「Cスタイル」ループは、Iteratorベースのループより高速です。 docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
andresp

4
foreachかdesugar化されたバージョンかに関係なく、イテレータアプローチではなく古いCスタイルのループを使用する1つの理由は、ガベージです。多くのデータ構造は、.iterator()が呼び出されたときに新しいIteratorをインスタンス化しますが、Cスタイルのループを使用して割り当てなしでアクセスできます。これは、(a)アロケータのヒットまたは(b)ガベージコレクションを回避しようとする特定の高性能環境で重要になる可能性があります。
Dan

3
別のコメントと同じように、ArrayListsの場合、for(int i = 0 ....)ループは、イテレータまたはfor(:)アプローチを使用する場合よりも約2倍高速であるため、実際には基本構造に依存します。また、補足として、HashSetの反復処理は非常に負荷が高い(配列リストよりもはるかに高い)ため、ペストのようなものは避けてください(可能な場合)。
レオ

106

違いはパフォーマンスではなく、機能にあります。参照を直接使用する場合は、タイプのイテレータを明示的に使用するより強力です(例:List.iterator()とList.listIterator()。ほとんどの場合、同じ実装を返します)。ループ内でイテレーターを参照することもできます。これにより、ConcurrentModificationExceptionを取得せずに、コレクションからアイテムを削除するなどの操作を実行できます。

例えば

これはOKです:

Set<Object> set = new HashSet<Object>();
// add some items to the set

Iterator<Object> setIterator = set.iterator();
while(setIterator.hasNext()){
     Object o = setIterator.next();
     if(o meets some condition){
          setIterator.remove();
     }
}

これはそうではありません。同時変更例外がスローされるためです。

Set<Object> set = new HashSet<Object>();
// add some items to the set

for(Object o : set){
     if(o meets some condition){
          set.remove(o);
     }
}

12
これは非常に当てはまりますが、情報提供であるために私が+1してくれた質問に直接答えず、論理的な後続の質問に答えます。
ポールワグランド2010年

1
はい、foreachループでコレクション要素にアクセスできますが、それらを削除することはできませんが、Iteratorで要素を削除できます。
Akash5288 2014年

22

Paul自身の答えを拡張するために、彼はバイトコードがその特定のコンパイラー(おそらくSunのjavac?)で同じであることを示しましたが、異なるコンパイラーが同じバイトコードを生成することは保証されていませよね?2つの間の実際の違いを確認するには、ソースに直接移動して、Java言語仕様、具体的には14.14.2「enhanced forステートメント」を確認します。

拡張forステートメントはfor、次の形式の基本ステートメントと同等です。

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    VariableModifiers(opt) Type Identifier = #i.next();    
    Statement 
}

言い換えると、JLSは2つが同等であることを要求します。理論的には、バイトコードのわずかな違いを意味する可能性がありますが、実際には、拡張されたforループは次のことを行う必要があります。

  • .iterator()メソッドを呼び出す
  • 使用する .hasNext()
  • ローカル変数を利用可能にする .next()

つまり、言い換えると、すべての実用的な目的のために、バイトコードは同一またはほぼ同一になります。2つの間の有意差をもたらすコンパイラーの実装を想定することは困難です。


実際、私が行ったテストはEclipseコンパイラーでのテストでしたが、あなたの一般的なポイントはまだ残っています。+1
ポールワグランド2010年

3

foreachボンネットが作成されるiteratorのhasNext()を呼び出すと、値を取得するには()の次の呼び出し。パフォーマンスの問題は、RandomomAccessを実装するものを使用している場合にのみ発生します。

for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
   CustomObj custObj = iter.next();
   ....
}

イテレータベースのループのパフォーマンスの問題は、次の理由によります。

  1. リストが空の場合でもオブジェクトを割り当てる(Iterator<CustomObj> iter = customList.iterator(););
  2. iter.hasNext() ループのすべての反復中に、invokeInterface仮想呼び出しがあります(すべてのクラスを通過し、次にジャンプの前にメソッドテーブル検索を実行します)。
  3. イテレータの実装は、hasNext()コールフィギュアを値にするために、少なくとも2つのフィールドルックアップを行う必要があります。#1は現在の数を取得し、#2は合計数を取得します。
  4. bodyループ内には、別のinvokeInterface仮想呼び出しがありiter.next(つまり、すべてのクラスを調べて、ジャンプの前にメソッドテーブルの検索を行います)、フィールドの検索も行う必要があります。#1はインデックスを取得し、#2は参照を取得します配列へのオフセットを行う配列(すべての反復で)。

潜在的な最適化はindex iteration、キャッシュサイズルックアップを使用するに切り替えることです。

for(int x = 0, size = customList.size(); x < size; x++){
  CustomObj custObj = customList.get(x);
  ...
}

ここにあります:

  1. customList.size()forループの最初の作成時に1つのinvokeInterface仮想メソッドを呼び出してサイズを取得します
  2. customList.get(x)ループの本体でのgetメソッド呼び出し。これは配列へのフィールド検索であり、配列へのオフセットを実行できます。

メソッドの呼び出し、フィールド検索のトンを減らしました。これLinkedListは、RandomAccessコレクションobjではない、またはコレクションobjではない何かで実行したくない場合、そうしないと、反復ごとにcustomList.get(x)を走査する必要があるものLinkedListになります。

これは、任意のRandomAccessベースリストコレクションであることがわかっている場合に最適です。


1

foreachとにかくフードの下でイテレータを使用します。それは実際には単なる構文上の砂糖です。

次のプログラムを検討してください。

import java.util.List;
import java.util.ArrayList;

public class Whatever {
    private final List<Integer> list = new ArrayList<>();
    public void main() {
        for(Integer i : list) {
        }
    }
}

レッツは、とそれをコンパイルしjavac Whatever.java
そして逆アセンブルのバイトコードを読んでmain()、使用しましたjavap -c Whatever

public void main();
  Code:
     0: aload_0
     1: getfield      #4                  // Field list:Ljava/util/List;
     4: invokeinterface #5,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
     9: astore_1
    10: aload_1
    11: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
    16: ifeq          32
    19: aload_1
    20: invokeinterface #7,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
    25: checkcast     #8                  // class java/lang/Integer
    28: astore_2
    29: goto          10
    32: return

以下のforeachようなプログラムにコンパイルされることがわかります。

  • を使用してイテレータを作成します List.iterator()
  • If Iterator.hasNext()Iterator.next()ループを呼び出して続行します

「この役に立たないループがコンパイルされたコードから最適化されないのはなぜですか?リスト項目では何も実行されないことがわかります」:.iterator()副作用のある反復可能コードをコーディングすることは可能です、またはそのため、.hasNext()副作用または意味のある結果をもたらします。

データベースからのスクロール可能なクエリを表すイテラブルが劇的な何かをする.hasNext()(データベースに接続する、または結果セットの最後に到達したためにカーソルを閉じるなど)と簡単に想像できます。

したがって、ループ本体で何も起こらないことを証明できたとしても、反復しても意味のある/結果が何も起こらないことを証明する方がコストがかかります(扱いにくい?)。コンパイラーはこの空のループ本体をプログラムに残さなければなりません。

私たちが期待できる最高のものは、コンパイラの警告です。この空のループ本体について警告javac -Xlint:all Whatever.javaないのは興味深いことです。IntelliJ IDEAはそうします。確かに、Eclipseコンパイラを使用するようにIntelliJを設定しましたが、それが理由ではないかもしれません。

ここに画像の説明を入力してください


0

イテレーターは、コレクションを走査または反復するメソッドを提供するJavaコレクションフレームワークのインターフェースです。

コレクションをトラバースしてその要素を読み取ることが目的である場合、イテレーターとforループはどちらも同じように動作します。

for-each コレクションを反復する方法の1つにすぎません。

例えば:

List<String> messages= new ArrayList<>();

//using for-each loop
for(String msg: messages){
    System.out.println(msg);
}

//using iterator 
Iterator<String> it = messages.iterator();
while(it.hasNext()){
    String msg = it.next();
    System.out.println(msg);
}

また、for-eachループは、反復子インターフェースを実装するオブジェクトでのみ使用できます。

forループとイテレータのケースに戻ります。

コレクションを変更しようとすると、違いが生じます。この場合、フェイルファストプロパティがあるため、イテレータの方が効率的です。すなわち。次の要素を繰り返す前に、基礎となるコレクションの構造に変更がないかチェックします。変更が見つかった場合は、ConcurrentModificationExceptionをスローします。

(注:イテレータのこの機能は、java.utilパッケージのコレクションクラスの場合にのみ適用できます。本質的にフェイルセーフであるため、並行コレクションには適用できません。)


1
違いについてのあなたの声明は真実ではありません、for eachループも水中でイテレーターを使用するので、同じ振る舞いをします。
ポールワグランド2017

@Pault Waglandは、私は私の答えの間違いを指摘してくれてありがとう修正した
eccentricCoder

更新はまだ正確ではありません。使用している2つのコードスニペットは、言語によって同じであると定義されています。動作に違いがあり、実装にバグがある場合。唯一の違いは、イテレータにアクセスできるかどうかです。
ポールワグランド2017

@Paul Waglandイテレータを使用するfor eachループのデフォルト実装を使用しても、並行操作中にremove()メソッドを使用しようとすると、例外がスローされ、例外が発生します。詳細については、以下をチェックしてください
eccentricCoder

1
for eachループでは、イテレーターにアクセスできないため、そのループに対してremoveを呼び出すことはできません。しかし、それは要点の外です、あなたの答えでは、あなたは一方がスレッドセーフであるが他方はそうでないと主張します。言語仕様によると、これらは同等であるため、どちらも基本となるコレクションと同じくらいスレッドセーフです。
ポールワグランド2017

-8

コレクションの操作中は、従来のforループを使用しないでください。私が与える単純な理由は、forループの複雑さはO(sqr(n))のオーダーであり、Iteratorまたはさらに強化されたforループの複雑さは単なるO(n)であることです。だから、それはパフォーマンスの違いを与えます..ちょうど約1000の項目のリストを取り、両方の方法を使用してそれを印刷します。また、実行の時差も出力します。違いがわかります。


あなたの発言をサポートするために、いくつかの実例を追加してください。
Rajesh Pitty

@チャンダン申し訳ありませんが、あなたが書いた内容は間違っています。例:std :: vectorもコレクションですが、そのアクセスにはO(1)がかかります。したがって、ベクトルに対する従来のforループはO(n)です。下層のコンテナーのアクセスにO(n)のアクセスコストがある場合、O(n ^ 2)の複雑さよりもstd :: listの場合がそうだと思います。その場合、反復子を使用すると、要素への直接アクセスが可能になるため、O(n)のコストが削減されます。
カイザー、2015

時間差の計算を行う場合は、両方のセットがソートされている(または公平に分散されたランダムにソートされていない)ことを確認し、各セットに対して2回テストを実行して、それぞれの2回目の実行のみを計算します。これを使用してタイミングをもう一度確認してください(テストを2回実行する必要がある理由についての長い説明)。これが真実である方法を(おそらくコードで)示す必要があります。それ以外の点では、私が知る限り、パフォーマンスは同じですが機能は同じではありません。
ydobonebi 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.