[CS] 자바의 정석 독서 #21 - 이터레이터

2025. 11. 22. 23:09·언어공부/Java | Kotlin

리스트의 메서드를 살펴보다보면 ListIterator라는 자료구조가 등장하는데, 이는 Iterator의 성능을 향상시킨 버전이다.

이터레이터는 컬렉션의 상위클래스이며, 구버전으로는 Enumeration이 있다. 즉, 이터레이터가 핵심

 

 

컬렉션 프레임웍은 컬렉션에 저장된 요소들을 하나씩 읽어와야한다. 그 순서가 중요하던 중요하지 않던 어쨌든 현재 요소에서 다음 요소를 읽어와야하기 때문이다.

 

그래서 컬렉션에서 '다음요소를 읽어오는 방법'을 표준화한 것이 이터레이터이다.


Iterator

public interface Iterator<E> { ... }

컬렉션 프레임웍의 상속트리를 따라 올라가다보면 가장 위에 있는 인터페이스가 이터레이터다.

이터레이터는 순서가 있는 자료구조에서 다음요소에 접근하도록하는 기능을 제공한다.

 

그렇기 때문에 순수한 이터레이터는 다음 자료가 있는지 확인하는 hasNext() 메서드와 다음 자료를 가져오는 next() 메서드만 존재한다. Iterator 인터페이스는 default 메서드로 remove() 지워버린다. next()로 읽어온 요소를 삭제할 수 있다.


Iterable

public interface Iterable<T> {
    Iterator<T> iterator();
    
    ...
}

다만 순수 이터레이터는 다음 자료가 있는지, 그리고 다음 자료를 가져오는 메서드만 존재하기 때문에,

이를 반복하여 실행시키는 과정을 Iterable이 구현했다.

default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

향상된 for문이라고하는 for-each 문을 사용할 수 있는 이유가 Iterable에 구현체이기 때문이다.

 

Iterator<T> iterator();

추가로 이터레이터를 반환하는 iterator() 메서드가 존재하는데, Iterable 내부에서 반복문을 돌릴 때도 이 메서드를 사용하고, Iterable의 구현체에서 직접 이 메서를 호출하여 .next() 메서드를 호출할 수도 있다.

 

즉, iterable을 구현하여 for-each 문을 사용할 수 있는데, for-each문을 사용하면 내부적으로는 iterator() 메서드를 사용해 이터레이터를 계속 불러오고 다음 요소를 가져오고하여 작업을 수행하는 것이다.

 

public interface Collection<E> extends Iterable<E>

그리고 컬렉션은 이터러블을 확장했다.

따라서 컬렉션의 구현체들은 반드시 Iterator() 메서드를 구현해야한다.


    protected transient int modCount = 0;
    
    public Iterator<E> iterator() {
        return new Itr();
    }
    
    
    private class Itr implements Iterator<E> {
        int cursor = 0;

        int lastRet = -1;

        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException(e);
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
            
         final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
         }   
    }

대표적인 컬렉션의 구현체는 ArrayList의 경우 상위추상클래스인 AbstractList에서 이 기능이 구현되어있다.

 

약간 해석을 해보자면..

cursor는 현재 이터레이터가 가리키는 요소의 위치, lastRet은 마지막 커서의 위치다.

 

먼저 다음 요소가 있는지부터 확인해야하니까 hasNext()부터 알아보면 커서가 이터레이터의 크기와 같은지 확인한다. 현재 커서가 이터레이터의 크기와 같은 곳에 있다면 다음 요소가 없으니까 false를 반환.

size() 메서드는 Itr이 AbstractList의 내부클래스이기 때문에 AbstractList의 메서드를 가져온 것이다.

 

다음요소가 있다면 next() 메서드를 통해 다음 요소를 가져와야하는데..

먼저 checkForComodification() 메서드를 실행시킨다. AbstractList 내부 필드에는 modCount라는 값이 있는데, 

 

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

AbstractList의 구현체들은 remove, add, trimToSize 등의 메서드를 사용해 내부 배열의 상태를 변화시키면 modCount 값이 증가하게된다.

 

이터레이터의 초기설정으로 modCount와 expectedModCount 값을 맞춰놨는데, 반복문을 통해 .next() 메서드가 불러져오는 과정에서 ArrayList의 내부배열이 변하게되면 오류가 발생하게된다.

 

        ArrayList<Integer> arrayList = new ArrayList();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        for (Integer in :  arrayList) arrayList.add(in);		//여기서 에러

 

암튼 next() 메서드를 마저 실행해보면

next() 메서드는 현재 커서가 가리키고 있는 곳의 요소를 반환하고 커서를 그 다음 커서로 옮기므로 그 기능을 구현했다. 값을 복사하고, 커서를 옮기고, lastRet도 옮기고 하는등으로..

 

2025-11-28 수정

이터레이터의 next() 메서드는 현재 커서의 위치 그 다음의 요소를 가져온다. 단, 위의 설명도 틀린 내용은 아니다. 마지막 부분에 커서를 옮겨두기 때문에 커서를 언제 옮기느냐의 차이일 뿐, '아직 읽지 않은 다음요소를 읽어온다.'라는 매커니즘으로 동작한다. 따라서 '커서를 다음요소를 옮기고 값을 읽어온다'와 '미리 다음요소로 커서를 옮겨두고 읽어돈다.' 정도의 차이? 위에서는 전자로 구현되었다.

 

 

 

remove() 메서드는 값을 제거하는데, lastRet이 여기에 사용된다.

AbstractList의 remove() 메서드를 호출하고 커서도 맞춘다. 이터레이터의 remove() 메서드는 checkForComodification() 메서드에서 오류를 발생시키면 안되므로 modCount와 expectModCount의 값도 맞춰주면서 종료

 

가장 마지막으로 읽어온 값의 위치를 나타내는 lastRet은 -1로 초기화되었다. 이는 읽어온 요소가 삭제되었으므로 불러올 수 없다는 것을 의미한다. remove() 메서드는 읽어온 값이 있어야만 호출될 수 있다. 그래서 조건문에서 lastRet이 음수인지 검사하는거고

 

 

암튼 대충 이렇게 작동된다.

hasNext()로 다음 값이 있는지 확인하고, next()로 다음 값을 가져오고, remove()로 값을 지우고

여기에 추가로 반복문 순회 도중 값이 변경되지 않았는지 확인하는 로직까지 추가된 이터레이터 클래스가 되었다.

 

만약 이 AbstractList의 구현체인 ArrayList를 for-each문에 넣어서 반복문을 돌리면

AbstractList 내부에 구현된 Itr 이터레이터를 통해 .hasNext(), .next() 메서드를 계속 불러오면서 작업을 수행하게된다.

 

만약 내가 컬렉션의 하위클래스를 만들어야할 일이 생긴다면

저 이터레이터 클래스와 next(), hasnext() 클래스를 반드시 만들어야한다. 왜냐하면 컬렉션은 iterable 인터페이스를 상속하는데, 이터러블 인터페이스는 이터레이터를 반환하는 iterator() 메서드를 구현해야하기 때문. 저 메서드가 호출되면 무슨 이터레이터를 반환할 것인가? 뭐 상속받아서 사용해도되지만 그렇지 않을 경우엔 내가 직접 구현해야한다.


설명이 길어졌는데 정리하자면,

이터레이터는 순서가 있는 자료구조에서 다음 자료에 도달할 수 있게 해주는 역할

이터러블은 이터레이터의 다음 자료 도달 기능을 반복하여 for-each문을 사용할 수 있게 해주는 역할이다.

 

이터러블은 내부적으로 이터레이터를 반환하는 iterator() 메서드를 통해 이터레이터를 반환받아 hasNext와 next 메서드를 사용하여 반복문을 수행하는 역할을 한다.

 

이처럼 이터레이터를 이용해서 컬렉션 등의 요소를 읽어오는 방법을 표준화했기 때문에 ArrayList, Vector 든 상관없이 그냥 같은 방법으로 값을 읽어올 수 있게된 것이다.


반면 Map 인터페이스의 구현체는 컬렉션의 구현체와는 다르게 key-value 형태로 구조가 이루어져있기 때문에 곧바로 이터레이터를 불러올 수는 없는데, 이럴 때는 Map 클래스의 keySet() (key용), entrySet(value용) 메서드를 활용하여 Set로 변형한 후에 따로 이터레터로 변환하여야한다.

 

다만 List는 순서라는 개념이 존재하는 반면 Set에는 순서라는 개념이 존재하지 않기 때문에 Set의 이터레이터를 통해 값을 가져오면 처음에 저장한 순서대로 값이 반환되지는 않는다.


ListIterator

 

리스트의 구현체들은 ListIterator를 반환하는 메서드가 있는데, ListIterator는 이터레이터의 하위클래스이다. 

 

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();

    E next();
    
    boolean hasPrevious();

    E previous();

    int nextIndex();

    int previousIndex();

	//선택적기능
    void remove();

	//next() 또는 previous()로 읽어온 객체를 지정된 객체 (e)로 변경.
    //반드시 next() 또는 previous()를 호출한 다음에 호출해야한다.
    //선택적 기능
    void set(E e);
	
	//컬렉션에 새로운 객체 추가
    void add(E e);
}

LinkedList의 단방향이동 단점을 보완하기 위해 DoubleLinkedList가 등장했듯이, ListIterator도 Iterator의 단방향 이동의 단점을 보완하기 위해 생겨난 자료구조다.

 

메서드들은 위와같은데, ListIterator의 특징과 메서드 이름에서 이미 이 메서드가 무엇을 담당하는지 추론이 가능하기 때문에 몇 개를 제외하고는 주석을 달지 않았다.

 

원래라면 반복문을 통해서 hasNext()가 true일 경우 next()를 불러와 다음요소에 대한 작업을 수행했다면,

ListIterator를 통해 hasPrevious()가 true일 경우 previous()를 불러와 이전요소에 대한 작업을 진행할 수 있게되었다.

 

선택적 기능은 딱히 구현하지 않아도되지만 호출할 경우 에러를 터뜨리는 것도 방법. 대표적인 것이 remove() 메서드.


위에서 AbstractList 기능에 ListIterator가 추가된다면..

다른 건 모두 동일할테지만 hasPrevious와 Previous를 구현해야한다.

 

hasPrevious() 커서의 위치가 0인지 확인하고 0이라면 false, 아니면 true를 반환하고

Previous() 메서드는 현재 커서가 가리키는 곳의 요소를 반환하고 커서값을 1 줄이면 될 듯하다. 마지막으로 읽어온 요소의 위치를 나타내는 lastRet은 커서보다 1 작은 위치에 두고

 

nextIndex나 previousIndex 메서드 모두 그냥 커서 위치에서 1 더하거나 빼면서 반환하면 될 것이고, 조건문을 통해 에러부분만 보강하면 쉽게 구현할 수 있을 듯하다.


Enumeration

컬렉션 프레임웍이 만들어지기 이전에 사용되던, 이터레이터의 구버전이지만 Enumeration으로 작성된 코드의 호환성을 위해서 남겨진 클래스이다. 현재는 이터레이터의 사용을 권장한다.


2025-11-28 08:46 추가

이터레이터의 next() 메서드와 hasNext() 메서드는 이터레이터의 다음 요소와 관련되어있는데, 컬렉션 같은 이터레이터의 구현체는 자동으로 반복문을 수행하며 다음 요소를 불러올 수 있다. 따라서 굳이 이터레이터가 필요하지 않으며 (물론 이터레이터를 통해 반복문을 구성할 수 있다.) 이터레이터가 필요할 때는 순회 중 요소를 제거하거나 반복제어를 좀 더 세밀하게 하고 싶을 때 선정하면 된다.

'언어공부 > Java | Kotlin' 카테고리의 다른 글

[Java] 자바의 정석 독서 #23 - HashSet과 HashMap  (0) 2025.12.01
[CS] 자바의 정석 독서 #22 - Arrays로 배열 다루기  (0) 2025.11.28
[CS] 자바의 정석 독서 #20 - 스택과 큐 in Java  (0) 2025.11.22
[CS] 자바의정석 독서 #19 - ArrayList, Vector, LinkedList  (1) 2025.11.21
[CS] 자바의정석 독서 #18 - 컬렉션은 무엇인가?  (1) 2025.11.20
'언어공부/Java | Kotlin' 카테고리의 다른 글
  • [Java] 자바의 정석 독서 #23 - HashSet과 HashMap
  • [CS] 자바의 정석 독서 #22 - Arrays로 배열 다루기
  • [CS] 자바의 정석 독서 #20 - 스택과 큐 in Java
  • [CS] 자바의정석 독서 #19 - ArrayList, Vector, LinkedList
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[CS] 자바의 정석 독서 #21 - 이터레이터
상단으로

티스토리툴바