web-dev-qa-db-ja.com

for-eachループと反復子のどちらが効率的ですか?

コレクションを横断する最も効率的な方法はどれですか?

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 の正確な複製ではないことに注意してください。最後の質問への回答のうち、近いものです。これがだまされていない理由は、これらのほとんどが、イテレータを使用するのではなく、ループ内でget(i)を呼び出すループを比較しているためです。

Meta で提案されているように、この質問に対する回答を投稿します。

191
Paul Wagland

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

ただし、ループが古い「cスタイル」ループを意味する場合:

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

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

新しい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ループとなるほとんどの人にとっては、ボイラープレートコードが少ないため、最も審美的に魅力的なループの形式を選択する必要があります。

253
Paul Wagland

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

例えば.

大丈夫です:

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);
     }
}
101

Paul自身の答えを拡張するために、彼はバイトコードがその特定のコンパイラー(おそらくSunのjavac?)で同じであるが、異なるコンパイラーではないことを実証しましたguaranteed同じバイトコードを生成しますか? 2つの実際の違いを確認するために、ソースに直接進み、Java言語仕様、特に 14.14.2、「拡張forステートメント」 を確認します。

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

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

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

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

つまり、すべての実用的な目的のために、バイトコードは同一、またはほぼ同一になります。 2つの間に大きな違いが生じるコンパイラー実装を想定することは困難です。

22
Cowan

foreachアンダーフードはiteratorを作成し、hasNext()を呼び出して値を取得するためにnext()を呼び出しています。パフォーマンスの問題は、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. 本体ループ内には、別のinvokeInterface仮想呼び出しiter.nextがあります(そのため、ジャンプの前にすべてのクラスを通過し、メソッドテーブルのルックアップを実行します)。また、フィールドルックアップを実行する必要があります。 (各反復で)オフセットを行う配列。

潜在的な最適化は、キャッシュされたサイズのルックアップでindex iterationに切り替えることです:

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

ここにあります:

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

メソッド呼び出し、フィールド検索のトンを減らしました。これは、LinkedListRandomAccessコレクションオブジェクトではない何かでやりたくないです。そうしないと、customList.get(x)は、繰り返しごとにLinkedListを横断しなければならないものになります。

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

1
denis_lor

イテレータは、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ループと反復子の場合に戻ります。

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

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

0
eccentricCoder

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でコンパイルしてみましょう。
そして、javap -c Whateverを使用して、main()の逆アセンブルされたバイトコードを読み取ります。

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()を使用してイテレータを作成します
  • Iterator.hasNext()の場合:Iterator.next()を呼び出してループを続行します

「なぜこの役に立たないループがコンパイルされたコードから最適化されないのですか?リスト項目で何もしないことがわかります」:まあ、.iterator()がサイド-効果、または.hasNext()が副作用または意味のある結果をもたらすようにします。

データベースからのスクロール可能なクエリを表すiterableが.hasNext()で劇的な何かをするかもしれないと簡単に想像できます(データベースに接続したり、結果セットの終わりに達したためにカーソルを閉じたりするなど)。

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

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

enter image description here

0
Birchlabs