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

마지막 질문에 대한 답변 중 하나가 가까이 오지만 이것은 this , this , this 또는 this 의 정확한 사본이 아닙니다 . 이것이 get(i)속이 아닌 이유는 대부분 반복자를 사용하는 대신 루프 내부에서 호출 하는 루프 를 비교 하기 때문입니다.

메타에 제안 된 대로이 질문에 대한 답변을 게시 할 것입니다.


자바와 템플릿 메커니즘이 구문 설탕에 지나지 않기 때문에 차이가 없다고 생각합니다.
Hassan Syed


2
@OMG Ponies : 반복문과 반복자를 비교하지는 않지만 반복자가 클래스 자체에 직접 있기보다는 반복자가 반환하는 이유를 묻기 때문에 이것이 복제본이라고 생각하지 않습니다.
Paul Wagland

답변:


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 루프 구문에서 반복자가 수 중에서 사용되는지 확인하려면 다음 두 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
나는 그가 foo.get (i)이 훨씬 덜 효율적 일 수 있다고 반대라고 말하고 있다고 생각합니다. LinkedList를 생각하십시오. LinkedList의 중간에 foo.get (i)을 수행하면 i에 도달하기 위해 이전의 모든 노드를 통과해야합니다. 반면 이터레이터는 기본 데이터 구조에 대한 핸들을 유지하며 한 번에 하나씩 노드를 살펴볼 수 있습니다.
Michael Krauklis

1
큰 것이 아니라 for(int i; i < list.size(); i++) {스타일 루프도 list.size()각 반복이 끝날 때 평가해야합니다. 사용되는 경우 list.size()첫 번째 결과를 캐시하는 것이 때로는 더 효율적 입니다.
Brett Ryan

3
실제로 ArrayList 및 RandomAccess 인터페이스를 구현하는 다른 모든 경우의 원래 명령문도 마찬가지입니다. "C 스타일"루프는 반복자 기반 루프보다 빠릅니다. docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
오전

4
foreach 또는 desugar'd 버전인지 여부에 관계없이 Iterator 접근 방식이 아닌 이전 C 스타일 루프를 사용하는 한 가지 이유는 쓰레기입니다. 많은 데이터 구조는 .iterator ()가 호출 될 때 새로운 반복자를 인스턴스화하지만 C 스타일 루프를 사용하여 할당없이 액세스 할 수 있습니다. 이는 (a) 할당 자 타격 또는 (b) 가비지 수집을 피하려고하는 특정 고성능 환경에서 중요 할 수 있습니다.
Dan

3
다른 의견과 마찬가지로 ArrayLists의 경우 for (int i = 0 ....) 루프는 반복자 또는 for (:) 접근 방식을 사용하는 것보다 약 2 배 빠르므로 기본 구조에 따라 다릅니다. 그리고 참고로, HashSets를 반복하는 것도 매우 비싸기 때문에 (배열 목록보다 훨씬 큽니다) 전염병과 같은 것을 피하십시오 (가능한 경우).
Leo

106

차이점은 성능이 아니라 기능입니다. 참조를 직접 사용하는 경우 반복자 유형 (예 : 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);
     }
}

12
이것은 정보를 제공하고 논리적 인 후속 질문에 대답하기 위해 +1을 한 질문에 직접 대답하지 않더라도 매우 사실입니다.
Paul Wagland

1
예, foreach 루프를 사용하여 컬렉션 요소에 액세스 할 수 있지만 제거 할 수는 없지만 Iterator를 사용하여 요소를 제거 할 수 있습니다.
Akash5288

22

Paul의 대답을 확장하기 위해 바이트 코드가 특정 컴파일러 (예 : Sun의 javac?) 에서 동일 하지만 다른 컴파일러가 동일한 바이트 코드를 생성 한다고 보장 하지는 않습니다 . 이 둘의 실제 차이점을 확인하려면 소스로 가서 Java 언어 사양, 특히 14.14.2, "향상된 구문"을 확인하십시오 .

향상된 for설명은 for다음 형식 의 기본 설명과 같습니다.

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

다시 말해, JLS는 두 개가 동등해야합니다. 이론적으로 바이트 코드의 한계 차이를 의미 할 수 있지만 실제로는 향상된 for 루프가 다음을 수행해야합니다.

  • .iterator()메소드를 호출하십시오.
  • 사용하다 .hasNext()
  • 를 통해 지역 변수를 사용할 수있게하십시오 .next()

즉, 모든 실제적인 목적으로 바이트 코드는 동일하거나 거의 동일합니다. 컴파일러 구현을 구상하기는 어렵 기 때문에이 둘 사이에 큰 차이가 생길 수 있습니다.


실제로, 내가 한 테스트는 Eclipse 컴파일러를 사용한 것이지만 일반적인 요점은 여전히 ​​존재합니다. +1
Paul Wagland

3

기본 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 개의 필드 조회를 수행해야 합니다.
  4. 본문 루프 안에 또 다른 invokeInterface 가상 호출이 있습니다 iter.next(따라서 점프하기 전에 모든 클래스를 거치고 메소드 테이블 조회를 수행하십시오). 또한 필드 조회를 수행해야합니다. (반복마다) 오프셋을 수행하는 배열.

잠재적 인 최적화는index iteration 캐시 된 크기 조회 로로 전환하는 것 입니다.

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

여기에 우리가 있습니다 :

  1. customList.size()for 루프의 초기 생성시 크기를 얻기위한 하나의 invokeInterface 가상 메소드 호출
  2. customList.get(x)본문 for 루프 동안 get 메소드 호출 . 배열에 대한 필드 조회이며 배열에 대한 오프셋을 수행 할 수 있습니다.

우리는 수많은 메소드 호출, 필드 조회를 줄였습니다. 이것은 컬렉션 obj LinkedList가 아닌 무언가 와 관련이 있거나 원하지 RandomAccess않는 경우, 그렇지 않으면 모든 반복 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()부작용 또는 의미있는 결과가있는 것입니다.

데이터베이스에서 스크롤 가능한 쿼리를 나타내는 iterable .hasNext()이 데이터베이스에 접속하거나 결과 세트의 끝에 도달하여 커서를 닫는 것과 같이 극적인 일을 할 수 있다고 쉽게 상상할 수 있습니다 .

따라서 루프 본문에서 아무 일도 일어나지 않는다는 것을 증명할 수 있지만 반복 할 때 의미 있고 결과적인 일이 발생하지 않음을 증명하는 것이 더 비쌉니다. 컴파일러는이 빈 루프 본문을 프로그램에 그대로 두어야합니다.

우리가 기대할 수있는 최선은 컴파일러 경고 일 것 입니다. 이 빈 루프 바디에 대해 경고 javac -Xlint:all Whatever.java하지 않는 것이 흥미 롭습니다 . 그러나 IntelliJ IDEA는 그렇게합니다. 필자는 Eclipse 컴파일러를 사용하도록 IntelliJ를 구성했지만 그 이유가 아닐 수도 있습니다.

여기에 이미지 설명을 입력하십시오


0

반복자는 컬렉션을 순회하거나 반복하는 메소드를 제공하는 Java Collections 프레임 워크의 인터페이스입니다.

반복자와 for 루프는 컬렉션을 가로 질러 해당 요소를 읽으려고 할 때 비슷하게 작동합니다.

for-each 컬렉션을 반복하는 한 가지 방법입니다.

예를 들면 다음과 같습니다.

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 루프와 반복자의 경우로 돌아갑니다.

컬렉션을 수정하려고 할 때 차이점이 있습니다. 이 경우 iterator는 fail-fast 특성 으로 인해 더 효율적 입니다. 즉. 다음 요소를 반복하기 전에 기본 컬렉션 구조의 수정 사항을 확인합니다. 수정 사항이 있으면 ConcurrentModificationException이 발생 합니다.

(참고 :이 반복자의 기능은 java.util 패키지의 콜렉션 클래스의 경우에만 적용 가능합니다. 동시 콜렉션에는 본질적으로 페일 세이프하므로 적용 할 수 없습니다)


1
차이점에 대한 귀하의 진술은 사실이 아니며, 각 루프마다 수중 반복자를 사용하므로 동일한 동작을합니다.
Paul Wagland

@Pault Wagland, 실수를 지적 해 주셔서 감사합니다.
eccentricCoder

업데이트가 여전히 정확하지 않습니다. 두 코드 스 니펫은 언어에 의해 동일하게 정의됩니다. 동작에 차이가있는 경우 구현에 버그가 있습니다. 유일한 차이점은 반복기에 액세스 할 수 있는지 여부입니다.
Paul Wagland

@Paul Wagland 반복자를 사용하는 각 루프에 대해 기본 구현을 사용하더라도 동시 작업 중에 remove () 메소드를 사용하려고 시도하면 여전히 예외가 발생합니다. 자세한 내용은 다음을 확인하십시오 .
eccentricCoder

1
for 각 루프를 사용하면 반복기에 액세스 할 수 없으므로 remove를 호출 할 수 없습니다. 그러나 그것은 요점 옆에 있습니다. 당신의 대답에서 당신은 하나는 스레드 안전하고 다른 하나는 그렇지 않다고 주장합니다. 언어 사양에 따르면 이들은 동등하므로 기본 컬렉션만큼 스레드 안전합니다.
Paul Wagland

-8

컬렉션으로 작업하는 동안 전통적인 for 루프를 사용하지 않아야합니다. 내가 줄 간단한 이유는 for 루프의 복잡성이 O (sqr (n))이고 Iterator의 복잡성 또는 향상된 for 루프가 O (n)에 불과하기 때문입니다. 따라서 성능 차이가 발생합니다. 1000 개 정도의 항목 목록을 가져 와서 두 가지 방법으로 인쇄하십시오. 또한 실행 시차를 인쇄합니다. 차이점을 볼 수 있습니다.


귀하의 진술을 뒷받침 할 수있는 예시를 추가하십시오.
Rajesh Pitty

@Chandan 죄송 합니다만, 작성한 내용이 잘못되었습니다. 예를 들어 : std :: vector는 컬렉션이지만 액세스 비용은 O (1)입니다. 따라서 벡터에 대한 전통적인 for 루프는 O (n)입니다. 기본 컨테이너의 액세스가 O (n)의 액세스 비용을 가지고 있다면 std :: list에 대한 것이므로 O (n ^ 2)의 복잡성이 있습니다. 이 경우 반복자를 사용하면 반복자가 요소에 직접 액세스 할 수 있으므로 비용을 O (n)으로 줄일 수 있습니다.
카이저

시차 계산을 수행하는 경우 두 세트가 정렬되어 있는지 (또는 상당히 분산 된 무작위 정렬되지 않은지) 확인하고 각 세트에 대해 테스트를 두 번 실행하고 각 세트의 두 번째 실행 만 계산하십시오. 이것으로 타이밍을 다시 확인하십시오 (테스트를 두 번 실행 해야하는 이유에 대한 자세한 설명). 이것이 사실인지 입증해야 할 것입니다 (아마도 코드로). 그렇지 않으면 내가 아는 한 성능 측면에서는 동일하지만 기능은 아닙니다.
ydobonebi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.