Java - 자료구조 순회 : for, forEach, Enhanced For, Iterator, Stream

BK·2025년 9월 17일
0

Java

목록 보기
9/9

Java에는 자료구조를 순회하기 위한 여러 방법이 있다.

기본적으로 배열을 순회할 때 사용하는 for loop, Collections API가 제공하는 Iterator 를 활용한 순회, stream 을 활용한 순회와 forEach() 등 여러 방식이 있다. 지금 까지 알고리즘 문제를 풀며 기본적인 for 문과 Iterator를 가장 많이 활용했는데 각각의 특징과 동작 방식에 대해 알아보고자 한다.

For

Java의 for문을 생각하면 세 가지가 떠오른다

For Loop

int[] arr = new int[10];

for(int i=0; i<N; i++) { // init; condition; increment
	System.out.println(arr[i]);
}

위와 같이 가장 기본적인 for 문을 통해 배열을 순회할 수 있으며, 아래와 같은 방법을 통해서도 순회할 수 있다

Enhanced For Loop

// 1. Array
int[] arr = new int[10];

for(int elem : arr) { // type var : target
	System.out.println(elem);
}

// 2. Iterable
List<Integer> list = Arrays.asList(1,2,3);
for(Integer i : list) {
	System.out.println(i);
}

위와 같은 문법은 향상된 for 문(Enhanced For Loop)라고 하며, Java 5에서 도입된 기능이다.

Iterable.forEach( )

또한, Java 8에서 도입된 forEach() method도 존재한다.

다만, forEach() method는 Iterable interface에 정의된 method로, 위와 같은 배열에 사용할 수 없으며 아래와 같이 Iterable interface를 구현한 Collection interface에서 사용 가능하다. (Map은 Collections interface를 상속하지 않는다.)

List<Integer> list = Arrays.asList(1, 2, 3);

list.forEach((elem) -> {
	System.out.println(elem);
});

Comparison

위 세가지 방법을 비교해 보자면

for

다른 방식들과 달리 index를 활용한 탐색이 가능하다. 이를 활용해 역순으로 탐색을 하거나, 순회 중 추가적인 조작이 가능하며, 단순한 동작으로 빠른 탐색이 가능한 장점이 있다.

하지만 index가 없는 자료구조(Set, Map 등)에서는 사용할 수 없는 방법이다.

Enhanced For Loop

내부적으로 Iterator를 활용해 순회를 한다. 그렇기에 Iterable interface를 상속한 Collection interface를 상속한 자료구조는 이를 통해서 순회할 수 있다.

위 작성한 예시에서는 기본 배열을 Enhanced For Loop를 통해 순회를 수행하는데, 이는 JVM이 자동으로 기본 for 문으로 컴파일 하기 때문에 가능하다. 이는 컴파일 된 바이트코드를 통해 확인할 수 있다.

하지만, index를 활용하지 않기에, 역순 탐색이나 조작이 불가능한 단점이 있다.

Iterable.forEach( )

Iterable interface에서 정의된 method이다. 이를 상속한 Collection interface를 구현한 자료구조는 Iterator를 활용하여 이를 구현하였다. 따라서 Enhanced For Loop와 같이 Iterator를 활용하여 탐색을 수행한다.

Enhanced For Loop와 문법만 다를 뿐 동일한 것으로 보일 수 있지만, 람다 표현식을 활용하기에 break, continue 를 통한 loop의 제어가 불가능한 단점이 있지만, 간결한 코드 작성이 가능하다.

List<Integer> list = Arrays.asList(1, 2, 3);

// available
for(Integer i : list) {
	if(i==2) break;
}

// error: break outside switch or loop
list.forEach((elem) -> {
	if(e==2) break;
});

Iterator

위 두 방식에서 Iterator가 활용되었다.

Iterator는 자료구조의 순회를 위해 정의된 interface이며, Collection의 상위 interface인 Iterable interface 에서 이를 반환하는 iterator() method가 정의되어 있고, 각 자료구조에 맞게 해당 method가 구현되어 있다.

Iterator interface는 순회를 위해 hasNext(), next() 와 같은 method를 정의하고 있기에 아래와 같은 방법으로 순회할 수 있다.

List<Integer> list = Arrays.asList(2, 3, 4, 5);

Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
	System.out.println(it.next());
}

IntelliJ에서 위와 같이 코드를 작성하면 아래와 같은 메세지를 볼 수 있고

Enhanced For Loop는 Iterator를 활용하기에 간략하게 Enhanced For를 사용하면 된다.

추가로 Iterable interface에는 forEach() method가 default method로 정의되어 있는데, 이 코드를 보면 Enhanced For Loop를 통해 동작함을 알 수 있다.

Stream

마지막으로 많이 사용하는 stream이다.

Stream API

Java Stream의 정식 명칭은 Stream API이다.

Java 8에서 도입되었으며, Stream interface에서는 아래와 같이 소개하고 있다.

“A sequence of elements supporting sequential and parallel aggregate operations.”

순차적이고 병렬적인 집계 작업을 지원하는 것 정도로 해석할 수 있을 것 같다.

Stream interface에서는 filter(), map(), sorted() 등 자료구조에 대한 순회, 가공 등의 작업을 위한 method를 정의하고 있으며, PredictateFunction 등을 인자로 받아 람다 표현식을 활용함을 알 수 있다. 단순 순회를 위한 forEach() method도 지원하며, 아래와 같은 방법으로 사용할 수 있다.

list.stream().forEach((e) -> System.out.println(e));

다만, Stream.forEach()의 경우 Iterable.forEach()와 유사하지만, 병렬 처리를 사용하는 경우 순서를 보장할 수 없는 차이가 있다.

이외에도 데이터를 가공할 수 있는 filter(), map(), sorted() 등 다양한 method를 제공하기에 대량의 데이터를 병렬로 처리 및 가공하거나, 람다 표현식을 활용해 간략하게 코드를 작성할 수 있는 장점이 있는 API이다.

Stream API의 사용은 생성 → 중간 연산 → 최종 연산 순서로 이루어 진다. 다만, 중간 연산들은 바로 실행되지는 않는다.

생성은 Collection interface에 정의된 default method인 Collection.stream()을 통해 이루어진다.

위와 같이 정의 및 구현되어 있으며, spliterator() method는 Collection interface에서 확인할 수 있는데 이는 Spliterator 객체를 생성하는 method이다. Stream을 생성하는 과정에서 Spliterator가 사용됨을 확인할 수 있다.

SpliteratorIterator와 같이 탐색 기능을 제공하면서 분할하여 병렬 처리에 용이한 Interface 정도로 생각하면 될 것 같다. 아래는 Spliterator에 대한 설명이다.

“An object for traversing and partitioning elements of a source. The source of elements covered by a Spliterator could be, for example, an array, a Collection, an IO channel, or generator function.
A Spliterator may traverse elements indivisually (tryAdvance()) or sequentially in bulk (forEachRemaining())”

tryAdvance()forEachRemaining()을 통해 요소를 각각 또는 bulk로 제공하며, 글에 작성하지는 않았지만, 뒤 내용을 읽어보면 trySplit()을 통해 partitioning을 할 수 있음을 알려주고 있다. 두 method의 정의는 아래 사진과 같은데

Iteratornext() method를 통해 값을 반환하는 반면, SpliteratorConsumer인자로 받아 작업을 처리함을 알 수 있다. 이 Consumer는 Stream의 중간 연산, 최종 연산 등 모든 작업을 포함하며, Stream은 중간 연산을 매번 수행하지 않고 pipeline을 통해 모두 한번에 수행하도록 해 데이터 가공을 효율적으로 수행할 수 있게 구현되어 있다.

Conclusion

Java에서 자료구조를 순회할 수 있는 방법들에 대해 알아보았다.

방법은 크게 세 가지로 구분할 수 있을 것 같다

  • for문을 활용한 방법
  • Iterator를 활용한 방법
  • Spliterator를 활용한 방법

Enhanced For Loop, forEach() method, Stream API 등 여러 이름이 있지만, 내부 동작은 위 세가지 방법을 통해서 이루어 진다고 생각하면 될 것 같다. 다른 자료구조에 쓰이는 여러 방법이 있을 수 있지만, 글에서 작성한 내용에 대해서는 이렇게 마무리 할 수 있을 것 같다.

  • for 문 : index를 활용 가능하며 데이터 원본 조작 가능
  • Iterator : 모든 Collection 자료 구조와 호환 가능(stream 쓰면 Spliterator도 다 가능하긴 하다) (ListIterator라는 것도 있다)
  • Spliterator : 병렬 처리 지원 및 Consumer를 활용한 데이터 가공에 용이(원본은 가공하지 않는다)

각각의 성능을 외우기 보다는 위 특징을 알고 적절히 사용하면 될 것 같다. 물론 편한 것도 중요하다

아래는 for loop와 stream의 속도에 대해 비교한 글이다 사실 이거 보고 글 쓰게 되었다

Java Stream API는 왜 for-loop보다 느릴까?

0개의 댓글