for-loop vs for-each (향상된 for문)
// for-loop
for(int i = 0; i < size; i++) {}
// for-each
for(var i : list) {}
for-loop | ||
장점 | 인덱스 조작 가능 | ArrayList, Array 순회 시 비교적 빠르다 |
단점 | Out of range 발생 가능 | LinkedList 순회 시 매우 느림 |
for-each | ||
장점 | Out of range 발생 여지 없음 | LinkedList 순회 시 준수한 성능 |
단점 | 인덱스 조작 불가능 |
for-each(향상된 for문) vs forEach method
// for-each
for(var i : list) {}
// forEach
collection.forEach(i -> {
//
});
사실 forEach 메서드는 내부적으로 for-each(향상된 for문)을 사용합니다. 아래는 컬렉션에서 Iterable 인터페이스를 상속받아 사용하는 forEach 메서드입니다.
// Iterable interface
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
즉 for-each 메서드 내부 동작을 정의하는 Consumer를 구현 객체나 익명 구현 객체 또는 람다로 다루는 함수형 프로그래밍이 가능한 것이지, 근본적인 차이는 없다고 볼 수 있습니다.
하지만 사용 시 주의해야 하는 점들이 있습니다.
- non-final 변수를 사용할 수 없다.
- 함수적 인터페이스를 익명 구현 객체를 통해 구현한다면 이 객체 내부에서 사용하는 외부 환경(lexical environment)을 객체로 복사합니다. 그런데 복사되는 것 중에 non-final 외부 지역 변수가 있다면 멀티 스레드 환경에서 동시성 문제로 인해 의도치 않은 문제가 발생할 수 있기 때문에 사용이 제한됩니다. 만약 지역 변수가 effectively final (유사 final - 변경 가능성이 없는)이라면 컴파일러가 알아서 final을 붙여주지만 이에 의존하는 것은 좋지 않은 방식이라고 생각합니다.
- 그런데 reference type을 이용하면 이를 해결할 수 있습니다. reference type 내부 값을 메서드를 통해 변경하는 것은 허용하기 때문입니다. 다만 멀티 스레드 환경이라면 동기화에 신경 써줘야 하는 것은 당연합니다.
- 그래서 atomic 패키지의 AtomicInteger나 AtomicLong 사용을 추천하는 것입니다. 우선 reference type이고, 멀티 스레드 환경에서도 동기화가 보장되는 CAS 알고리즘을 내부적으로 이용하기 때문에 동시성 문제를 해결할 수 있습니다.
- checked exception을 핸들링 하기 불편하다.
- Try-Catch로 예외 처리를 할 수 있지만 코드가 매끄럽지 않고, forEach 내부에서 예외가 처리된다는 단점(?)이 있습니다.
- 흐름제어가 어렵다.
- for-each loop는 break 등을 이용하여 쉽게 흐름 제어가 가능하지만 forEach 메서드의 경우 그렇지 않습니다. 파일을 읽거나 순회를 종료해야 하는 경우 불편할 수 있습니다.
- 디버깅이 어렵다.
그 외에도 제가 참고한 답변에서는 더 많은 단점을 이야기하고 있는데, 솔직히 와닿지 않는 부분들도 있어서 어느 정도 필터링을 한 후 작성하였습니다. non-final에 대한 이야기는 제가 살을 덧붙였기 때문에 원문과 다를 수 있습니다. 원문이 궁금하신 분은 아래 링크를 참조하시면 좋을 것 같습니다.
https://stackoverflow.com/questions/16635398/java-8-iterable-foreach-vs-foreach-loop
그리고 어떤 분이 벤치마크를 진행해두신 기록이 있길래 이 또한 첨부하도록 하겠습니다. 벤치마크 결과를 살펴보면 분명 차이가 있으나 왜 그런지에 대해서는 따로 분석해두지는 않았습니다. 저는 거의 차이가 없을 거라고 생각했는데, 어째서인지 조금 차이가 나는 것으로 보입니다.
https://medium.com/@KosteRico/for-each-loop-vs-foreach-method-in-java-11-which-one-is-faster- 8a5120c0c8c3
Collection forEach vs Stream forEach
public void test(List<Integer> nums) {
nums.forEach(System.out::println);
nums.stream().forEach(System.out::println);
}
표면적인 차이
바로 알 수 있는 차이점은 Stream forEach를 사용하기 위해서 stream 객체의 생성이 필요하다는 것입니다. 단순히 순회가 목적이라면 굳이 stream 객체를 생성하면서 forEach를 실행할 필요는 없을 것 같습니다.
만약 순회하다가 중간 처리가 필요하다면 Stream의 다양한 연산 도구(filter, map 등)을 이용하고, 그렇지 않다면 Collection의 forEach나 향상된 for문 중에서 선택하는 것이 좋을 것 같습니다.
실질적인 차이
Stream을 이용하는 것이 어떤 영향을 끼칠까요? 그에 관하여 너무 잘 정리된 글이 있어서 링크부터 첨부하겠습니다.
링크에 포스팅을 쭉 읽어보시면 밴치마킹을 기반으로 여러 가지 측면에서 결과를 분석하고 있습니다. 일단 Stream이 느린 이유는 상대적으로 다른 순회 방식들에 비해 최적화가 많이 되지 않았기 때문입니다. for-loop의 경우 30년 정도 최적화가 진행되었다고 하니.. 차이가 날 수밖에 없습니다.
그런데 순회 비용이 크거나, 부하가 큰 작업을 수행할 때 for-loop와 순차 스트림의 속도 차이가 거의 없는 것을 확인할 수 있습니다. 즉 순회 비용과 계산 비용 둘 중 어느 하나가 극단적으로 커진다면, 두 방식의 속도 차이는 체감할 수 없을 정도로 줄어드는 것을 확인할 수 있습니다.
그리고 병렬 스트림에 대해서도 잠깐 다루는데요, 병렬 스트림의 경우 데이터가 충분히 크고, split 하기 쉬우며(fork-join framework를 이용하기 때문) 연산이 stateless 한 경우에 효과적인 것을 확인할 수 있습니다.
동시성 문제
Collection의 forEach를 수행하는 중간에 remove 또는 add 등의 컬렉션 조작 메서드를 수행하면 문제가 발생할 수 있습니다. 아래는 ArrayList의 remove 메서드입니다. remove 내부 흐름을 살펴보시면 fastRemove를 호출하시는 것을 볼 수 있습니다.
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
// fastRemove
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
fastRemove는 내부적으로 modCount를 증가시키는데, 이 modCount는 iterator을 이용할 때 동기화가 보장되도록 설계되어 있습니다. 왠 갑자기 iterator냐 하실 수 있습니다. 위에서 살펴보았지만 Collection의 forEach는 내부적으로 향상된 for문을 이용하고, 향상된 for문은 내부적으로 iterator가 사용됩니다.
즉 Collection의 forEach는 내부적으로 다음과 같은 의미로 해석할 수 있습니다.
nums.forEach(System.out::println);
// 변환
Iterator<Integer> itr = nums.iterator();
while(itr.hasNext()) {
System.out.println(itr.next());
}
그럼 예시로 ArrayList의 iterator()를 살펴보겠습니다. 내부적으로 Itr 객체를 생성하는데, 이때 expectedModCount에 modCount의 값을 할당하고 있습니다.
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.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; // <<--------------------------------여기요 여기!
// prevent creating a synthetic constructor
Itr() {}
...
그리고 iterator의 next()를 살펴보겠습니다. 내부적으로 checkForComodification()을 호출하여 modCount와 expectedModCount의 동기화 여부를 확인하고 있습니다.
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];
}
// checkForComodification()
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
즉, modCount와 expectedModCount가 다르다면 ConcurrentModificationException이 발생하는 것을 알 수 있습니다.
따라서 컬렉션의 forEach를 수행하다가 remove나 add 등의 컬렉션을 조작하는 메서드를 이용한다면, modCount가 증가되어 ConcurrentModificationException이 발생할 수 있습니다.
컬렉션의 데이터 일관성을 보장하기 위해 이런 식으로 설계된 것으로 보이며, 컬렉션의 환경이 변하면서 발생할 수 있는 비정상적인 동작을 방지하기 위함으로 보입니다. 따라서 Collection의 forEach 내부에서 컬렉션을 조작하는 행위는 피해야겠습니다.
불변 컬렉션을 사용해서 참사를 방지하자
혹시나 모를 이런 참사를 막기 위해 불변 컬렉션을 이용하는 것이 좋을 것 같습니다. Java에서는 UnmodifiableCollection을 이용하여 컬렉션 조작이 불가능하도록 할 수 있습니다. 일반 ArrayList에서는 그래도 set 정도는 문제가 없었는데, UnmodifiableCollection을 사용하면 set도 사용할 수 없습니다.
불변 컬렉션을 조작하고 싶다면?
이땐 stream의 중간 연산을 통해 새로운 컬렉션을 생성하는 것이 좋다고 생각합니다.
Ref
https://m.blog.naver.com/tmondev/220393974518
https://tecoble.techcourse.co.kr/post/2020-09-30-collection-stream-for-each/
'JAVA' 카테고리의 다른 글
제네릭 (Contravariant, PECS, Type erasure, Heap pollution with varargs) (1) | 2022.11.04 |
---|---|
예외 처리와 SQLException (0) | 2022.10.24 |
Java.nio Selector의 필요성 (0) | 2022.09.20 |
[기본 시리즈] java.nio 버퍼 (1) | 2022.09.19 |
[기본 시리즈] JAVA 스레드에 대하여 (0) | 2022.09.08 |