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

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

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



답변

모든 값을 읽기 위해 컬렉션을 방황하고 있다면 새로운 구문은 수 중에서 반복자를 사용하기 때문에 반복자를 사용하는 것과 새로운 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 루프가 될 대부분의 사람들에게는 가장 미적으로 매력적인 루프 형식을 선택해야합니다.


답변

차이점은 성능이 아니라 기능입니다. 참조를 직접 사용하는 경우 반복자 유형 (예 : 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?) 에서 동일 하지만 다른 컴파일러가 동일한 바이트 코드를 생성 한다고 보장 하지는 않습니다 . 이 둘의 실제 차이점을 확인하려면 소스로 가서 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()

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


답변

기본 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기반 목록 모음 임을 알 때 완벽 합니다.


답변

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를 구성했지만 그 이유가 아닐 수도 있습니다.

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


답변

반복자는 컬렉션을 순회하거나 반복하는 메소드를 제공하는 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 패키지의 콜렉션 클래스의 경우에만 적용 가능합니다. 동시 콜렉션에는 본질적으로 페일 세이프하므로 적용 할 수 없습니다)


답변

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