JAVA의 가장 기본이 되는 내용

컬렉션 프레임워크 - 순회 (Iterable, Iterator)

cwchoiit 2024. 5. 19. 13:46
728x90
반응형
SMALL

참고자료:

 

김영한의 실전 자바 - 중급 2편 | 김영한 - 인프런

김영한 | 자바 제네릭과 컬렉션 프레임워크를 실무 중심으로 깊이있게 학습합니다. 자료 구조에 대한 기본기도 함께 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전

www.inflearn.com

 

순회

우선, Iterable, Iterator를 다루기 앞서 자료 구조를 순회한다는 것에 대해 이해해보자.

순회라는 것은 여러 곳을 돌아다닌다는 뜻이다. 자료 구조에서 순회는 자료 구조에 들어있는 데이터를 차례대로 접근해서 처리하는 것을 순회라고 한다. 그런데 다양한 자료구조가 있고, 각각의 자료 구조마다 데이터를 접근하는 방법이 전부 다르다.

 

예를 들어보자.

배열의 경우 indexsize까지 차례대로 증가하면서 순회해야 하고, 연결 리스트는 node.next를 사용해서 node의 끝이 null일 때까지 순회해야 한다. 이렇듯 각 자료 구조의 순회 방법이 다르다. 근데 자료 구조는 이 뿐만이 아니라 HashSet, LinkedHashSet, TreeSet 등등 다양한 자료구조가 있다. 각각의 자료 구조마다 순회하는 방법이 다 다르기 때문에 자료 구조마다 순회 방법을 배워야 한다. 순회 방법을 알려면 각각의 자료 구조의 내부 구조도 알아야 한다. 결과적으로 너무 많은 것을 알아야 하는 것이다. 그러나 자료 구조를 사용하는 개발자 입장에서는 단순히 자료 구조에 들어있는 모든 데이터에 순서대로 접근해서 출력하거나 계산하고 싶을 뿐이다.

 

그래서, 자료 구조의 구현과 관계 없이 모든 자료 구조를 동일한 방법으로 순회할 수 있는 일관성 있는 방법이 있다면, 자료 구조를 사용하는 개발자 입장에서는 매우 편리할 것이다.

 

자바는 이런 문제를 해결하기 위해 Iterable, Iterator 인터페이스를 제공한다.

 

Iterable: '반복 가능한' 이라는 뜻이다.

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

 

Iterator: '반복자'라는 뜻이다.

public interface Iterator<E> {
    boolean hasNext();
    E next();
}
  • hasNext(): 다음 요소가 있는지 확인한다. 다음 요소가 없으면 `false`를 반환한다.
  • next(): 다음 요소를 반환한다. 내부에 있는 위치를 다음으로 이동한다.

직접 Iterator, Iterable 만들어보기

Iterator, Iterable을 사용하는 자료 구조를 하나 만들어보자. 둘 다 인터페이스여서 구현체가 필요하다. 

 

MyArray (우리가 종종 사용하는 ArrayList와 비교해서 생각해보기)

package org.example.collection.iterable;

import java.util.Iterator;

public class MyArray implements Iterable<Integer> {
    private int[] numbers;

    public MyArray(int[] numbers) {
        this.numbers = numbers;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new MyArrayIterator(numbers);
    }
}

 

MyArrayIterator

package org.example.collection.iterable;

import java.util.Iterator;

public class MyArrayIterator implements Iterator<Integer> {

    private int currentIndex = -1;
    private int[] targetArr;

    public MyArrayIterator(int[] targetArr) {
        this.targetArr = targetArr;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < targetArr.length - 1;
    }

    @Override
    public Integer next() {
        return targetArr[++currentIndex];
    }
}

 

설명

  • Iterable을 구현하는 구현체는 `iterator`라는 메서드를 재정의해야 한다.
  • Iterator를 구현하는 구현체는 hasNext(), next() 메서드를 재정의한다.
  • Iterable을 구현하는 구현체는 내부에 자료 구조를 가지고 있고 그 자료 구조를 iterator에게 넘겨준다.
  • iterator를 구현하는 구현체는 넘겨받은 자료 구조의 참조값을 가지며 그 참조값을 통해 자료 구조를 순회할 수 있다.
  • iterator를 구현하는 구현체는 넘겨받는 자료 구조가 Array인지, Tree인지, HashSet인지에 따라 전부 가져야하는 필드가 달라진다. 위의 경우 Array에 대한 iterator이기 때문에 인덱스와 배열을 가진다.
  • iterator를 구현하는 구현체는 넘겨받는 자료 구조가 Array인 경우, hasNext() 메서드에서 넘겨받은 자료 구조의 사이즈보다 현재 인덱스가 작은지를 비교해서 다음 데이터가 존재하는지 판단한다.
  • iterator를 구현하는 구현체는 넘겨받는 자료 구조가 Array인 경우, next() 메서드에서 다음 데이터를 반환하기 위해 현재 index + 1을 한 index의 데이터를 반환한다.

 

직접 만든 iterable, iterator 사용해보기

package org.example.collection.iterable;

import java.util.Iterator;

public class MyArrayMain {
    public static void main(String[] args) {
        MyArray myArray = new MyArray(new int[]{1, 2, 3, 4});
        Iterator<Integer> iterator = myArray.iterator();
        System.out.println("iterator 사용");
        

        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

실행결과

iterator 사용
1
2
3
4

 

향상된 for 문의 비밀

향상된 for문은 정말 많이 사용하는 자료 구조를 순회하는 방법이다. 근데 이게 재밌는 사실이 있다.

 

MyArrayMain

package org.example.collection.iterable;

public class MyArrayMain {
    public static void main(String[] args) {
        MyArray myArray = new MyArray(new int[]{1, 2, 3, 4});

        for (Integer i : myArray) {
            System.out.println("i = " + i);
        }
    }
}

실행결과

i = 1
i = 2
i = 3
i = 4

 

당연히 잘 된다. 근데 만약에 MyArray 클래스에서 Iterable을 구현하지 않으면 어떻게 될까?

package org.example.collection.iterable;

public class MyArray {
    private int[] numbers;

    public MyArray(int[] numbers) {
        this.numbers = numbers;
    }
}

이렇게 Iterable을 구현하겠다는 키워드와 iterator() 메서드를 재정의한 것을 빼버렸다.

컴파일 오류가 난다. 에러 메시지는 다음과 같다.

required: array or java.lang.Iterable

 

그렇다. 향상된 for문을 사용하려면 배열 그 자체이거나 Iterable을 구현해야 한다. 그러니까 돌려서 말하면 모든 향상된 for문을 사용할 수 있었던 자료구조는 다 Iterable을 구현한 구현체고 `iterator()` 메서드가 반환하는 Iterator가 있다는 사실이다.

 

그래서 향상된 for문은 컴파일이 되면 다음과 같은 모양으로 변경이 된다.

컴파일 전

for (Integer i : myArray) {
    System.out.println("i = " + i);
}

컴파일 후

while (iterator.hasNext()) {
    System.out.println("i = " + iterator.next());
}

 

향상된 for문이 워낙 깔끔하고 사용하기 좋다 보니 저렇게 사용을 많이 하지만 이러한 과정이 있다는 사실을 알아두면 좋을 것 같다.

 

자바가 제공하는 Iterable, Iterator

이제 Iterable, Iterator가 뭔지 알게됐다. 위에서 말했지만 각 자료 구조마다 순회하는 방식이 다르니까 Iterator의 생김새도 달라진다고 했다. 근데 결정적인건 그걸 사용하는 개발자 입장에서는 알 필요가 없다는 것이다. 이렇게 순회 가능한 자료 구조에 대해 Iterable, Iterator를 구현해서 내부 구조에 대해 깊이 있게 알지 못해도 순회할 수 있는 디자인 패턴을 Iterator 패턴이라고 한다.

 

위 그림에서 알 수 있듯 Collection 인터페이스는 Iterable 인터페이스를 상속한다. 그렇기 때문에 Collection 하위에 있는 모든 구현체는 향상된 for문을 사용할 수 있고 그 말은 각각이 전부 iterator() 메서드를 재정의했다는 말이다.

 

근데, Map은 보니까 아예 동떨어져 있다. Map을 향상된 for문으로 돌릴 수 없는 이유가 여기에 있다. Map은 순회의 개념이 아니라 Key-Value 쌍을 저장하는 자료구조이기 때문에 그렇다. 이렇게 공부를 하고 나니까 뭔가 전체적인 그림이 눈에 들어오기 시작했다. 

 

그럼 개발할 때 종종 사용했던 ArrayList, HashSet 같은 녀석들의 iterator를 한번 사용해보자.

 

JavaIterableMain

package org.example.collection.iterable;

import java.util.*;

public class JavaIterableMain {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> iterator = list.iterator();
        printAll(iterator);
        forEach(list);

        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);

        Iterator<Integer> iterator1 = set.iterator();
        printAll(iterator1);
        forEach(set);
    }

    private static void forEach(Iterable<Integer> iterable) {
        System.out.println("iterable = " + iterable.getClass());
        for (Integer i : iterable) {
            System.out.println(i);
        }
    }

    private static void printAll(Iterator<Integer> iterator) {
        System.out.println("iterator = " + iterator.getClass());
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

실행결과

iterator = class java.util.ArrayList$Itr
1
2
3
iterable = class java.util.ArrayList
1
2
3
iterator = class java.util.HashMap$KeyIterator
1
2
3
iterable = class java.util.HashSet
1
2
3

 

이게 또 자바의 다형성에서 얻는 이점이다. 내부적으로 어떤 iterator가 들어오던 상관없이 Iterator를 구현한 구현체라면 같은 메서드를 사용할 수 있다. 그래서 실행 결과를 보면 하나는 ArrayListiterator, 하나는 HashMapKeyIterator다. Set은 내부적으로 Map을 사용한다고 했다. MapKey가 해시 기반의 셋 자료 구조라서 그렇다. 

 

그리고 또 향상된 for문 역시 Iterable을 구현한 구현체면 사용할 수 있다. 그래서 어떤 구현체이든 상관없이 Iterable을 구현했다면 향상된 for문을 사용할 수 있는것이다. 이게 바로 다형성이 주는 강력한 이점이라고 할 수 있다. 

728x90
반응형
LIST