コレクションを横断する最も効率的な方法はどれですか?
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();
}
これは this 、 this 、 this 、または this の正確な複製ではないことに注意してください。最後の質問への回答のうち、近いものです。これがだまされていない理由は、これらのほとんどが、イテレータを使用するのではなく、ループ内でget(i)
を呼び出すループを比較しているためです。
Meta で提案されているように、この質問に対する回答を投稿します。
コレクションをさまよってすべての値を読み取る場合、新しい構文は水中でイテレータを使用するだけなので、イテレータを使用するか、新しい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ループとなるほとんどの人にとっては、ボイラープレートコードが少ないため、最も審美的に魅力的なループの形式を選択する必要があります。
違いはパフォーマンスではなく、機能にあります。参照を直接使用する場合、イテレータのタイプ(たとえば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);
}
}
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つの間に大きな違いが生じるコンパイラー実装を想定することは困難です。
foreach
アンダーフードはiterator
を作成し、hasNext()を呼び出して値を取得するためにnext()を呼び出しています。パフォーマンスの問題は、RandomomAccessを実装するものを使用している場合にのみ発生します。
for (Iterator<CustomObj> iter = customList.iterator(); iter.hasNext()){
CustomObj custObj = iter.next();
....
}
反復子ベースのループのパフォーマンスの問題は、次の理由によるものです。
Iterator<CustomObj> iter = customList.iterator();
);iter.hasNext()
は、ループの各反復中にinvokeInterface仮想呼び出しがあります(すべてのクラスを調べ、ジャンプの前にメソッドテーブルのルックアップを行います)。hasNext()
呼び出しの値を値にするために、少なくとも2つのフィールドルックアップを行う必要があります。#1は現在のカウントを取得し、#2は合計カウントを取得しますiter.next
があります(そのため、ジャンプの前にすべてのクラスを通過し、メソッドテーブルのルックアップを実行します)。また、フィールドルックアップを実行する必要があります。 (各反復で)オフセットを行う配列。潜在的な最適化は、キャッシュされたサイズのルックアップでindex iteration
に切り替えることです:
for(int x = 0, size = customList.size(); x < size; x++){
CustomObj custObj = customList.get(x);
...
}
ここにあります:
customList.size()
customList.get(x)
を呼び出します。これは、配列に対するフィールドルックアップであり、配列へのオフセットを実行できますメソッド呼び出し、フィールド検索のトンを減らしました。これは、LinkedList
やRandomAccess
コレクションオブジェクトではない何かでやりたくないです。そうしないと、customList.get(x)
は、繰り返しごとにLinkedList
を横断しなければならないものになります。
これは、RandomAccess
ベースのリストコレクションであることを知っている場合に最適です。
イテレータは、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パッケージのコレクションクラスの場合にのみ適用されます。本質的にフェールセーフであるため、同時コレクションには適用されません)
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を構成しましたが、それが理由ではないかもしれません。