본문 바로가기

Java

Iterator

Iterator?

Iterator 는 자바의 컬렉션 프레임워크에서 컬렉션에 저장되어 있는 요소를 읽어오는 방법을 표준화한 것이다. (추상적이어서 잘 이해가 안 된다.)

 

Iterator 어떻게 생겼을까?

일단 Iterator가 어떻게 생겼는지 확인해 보자. 직접 java 코드를 열어보면 다음처럼 선언되어 있다.

public interface Iterator<E> {
    
    boolean hasNext();
​
    E next();
​
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
​
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

일단, hasNext(), next() 함수의 역할에 대해 알아보자. java 공식 문서에 따르면 각 함수들의 역할을 다음과 같이 정의하고 있다.

  1. hashNext(): 다음 원소가 있는지 확인하는 역할을 하는 함수이다.
  2. next(): 다음 원소를 가져와주는 역할을 하는 함수이다.

어떻게 사용할까?

위에서 Iterator가 무엇인지와 어떻게 생겼는지에 대해서 알아봤다. 그럼 Iterator을 어떻게 사용할까? 일단 예시를 통해서 알아보자.

 

다음은 Iterator의 사용 예시이다.

public class Main {
​
  public static void main(String[] args) {
​
    //컬렉션을 생성 & 원소 집어넣기
    List<String> list = new ArrayList<>();
​
    list.add("Hi");
    list.add("Hello");
    list.add("Bye");
​
    //Iterator 사용하기
    Iterator<String> it = list.iterator();
    while(it.hasNext()) {
      System.out.println(it.next());
    }
  }
}
​
//output
Hi
Hello
Bye
​

정확하게 모르겠지만 느낌은 list.iterator()를 이용해 Iterator 인터페이스를 구현한 구현 객체를 받고, 이를 Iterator 인터페이스의 사용방법에 따라 사용하면 되는 것 같다. 그런데 과연 내부적으로 어떻게 동작되는 걸까?

 

Iterator 동작 원리

ArrayList에서 어떻게 Iterator를 구현한 구현 객체를 반환해 주고, 그 구현체는 어떻게 생겼는지 코드로 이해해 보자.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  
    private int size; //ArrayList의 크기
    
    ... 
    
    public Iterator<E> iterator() {
        return new Itr();
    }
    
    ...
​
  private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
​
        Itr() {}
​
        public boolean hasNext() {
            return cursor != size;
        }
​
        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
​
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
​
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
       
       ...
  }
}

위 코드에서 보이듯이 iterator() 함수는 Itr이라는 class를 생성한 뒤 우리에게 반환해 준다. 그럼 Itr 클래스가 어떻게 생겼는지 알아보자.

 

Itr 클래스를 보면 Iterator 인터페이스를 상속받아서 구현한 Iterator의 구현 클래스다. 그리고 Itr 구현 클래스는 Iterator 인터페이스에 선언된 각 함수들을 ArrayList에 맞게 구현해 두었다.

 

hasNext() 함수를 보자. ArrayList의 size를 이용해 다음 원소가 있는지 판단해 준다. next() 함수를 보자. ArrayList의 다음 원소를 가져와 반환한다.

 

그럼 위의 내용을 바탕으로 우리의 예시가 어떻게 동작하는지 정리해 보자.

  1. list.iterator()를 이용하면 list(ArrayList의 객체)가 자기한테 맞는 Iterator 인터페이스를 구현한 클래스의 객체를 반환한다.
  2. 우리는 반환받은 Iterator 인터페이스의 구현 객체를 사용하면 된다. 이때 우리는 Iterator 인터페이스를 구현한 객체의 내부 로직은 몰라도 Iterator 인터페이스의 함수 정의를 보고 사용할 수 있다.

즉 우리는 Iterator를 이용하면 컬렉션 객체가 어떻게 동작하는지 몰라도, Iterator를 반환해 준 컬렉션 객체의 원소를 반복적으로 가져올 수 있다. (구현체가 아니라 인터페이스에 의존하면 된다!)

Iterator vs for loop ?

위 설명에서 알 수 있듯이 Iterator을 이용하면 우리는 해당 컬렉션 객체가 어떻게 생겼는지, 내부적으로 어떻게 구성되어 있는지를 몰라도 컬렉션 객체에 저장되어 있는 원소를 반복적으로 가져올 수 있다. 하지만 우리가 For 문을 이용해 어떤 컬렉션 객체의 원소를 반복적으로 가져오기 위해서는 해당 컬렉션이 어떻게 생겼는지 알아야 하고 또, 어떤 컬렉션 객체를 쓰느냐에 따라서 가져오는 방법이 달라질 것이다. 구체적인 예로 ArrayList 객체의 경우는 인덱스를 이용하면 되지만 HashSet 객체의 경우는 인덱스를 이용할 수 없다.

 

정리하자면 Iterator를 사용하면 컬렉션 종류에 상관없이 컬렉션 원소를 반복적으로 가져오는 작업을 할 수 있다.(이게 객체지향적인 코딩이 아닐까?) 하지만 for 문을 이용해서 컬렉션의 원소를 가져오게 되면 어떤 컬렉션을 사용하느냐에 따라 가져오는 방식도 다르고, 우리가 컬렉션이 어떻게 구성되어 있는지도 알아야 한다. (이는 나중에 우리가 사용하는 컬렉션 종류가 바뀌면 유지 보수 측면에서 아주 불편할 것 같다.)

 

reference

  • Do it 자바 프로그래밍 입문
  • Java Docs

'Java' 카테고리의 다른 글

Java 시간관련 객체 정리  (0) 2024.03.09
람다식  (0) 2022.05.29
익명 클래스  (0) 2022.05.15