
JAVA는 for 문을 일반적인 for 문, 향상된 for 문, list.forEach() 로 쓸 수 있다.
그때 그때 내 기분에 따라 쓰다가,
그래도 실무에선 그 중에 상황에 맞게 좀 좋은 걸 써야할 것 같아서 한 번 탐구해 보았다.
일단 달리기 시합 한 번 해보자.
간단하게 1000번을 돌 때 각각 얼마나 걸리는지 알아보겠다.
public static void main(String[] args) {
long start1 = System.nanoTime();
for (int i=0; i<1000; i++){
//일반적인 for 문
}
long end1 = System.nanoTime();
long start2 = System.nanoTime();
ArrayList<String> list = new ArrayList<>(Collections.nCopies(1000, ""));
for(String a : list){
//forEach 문
}
long end2 = System.nanoTime();
long start3 = System.nanoTime();
list.forEach(a->{}); //list.forEach문
long end3 = System.nanoTime();
System.out.println(end1 - start1);
System.out.println(end2 - start2);
System.out.println(end3 - start3);
}
---
실행 결과
4167
258500
546500
일반적인 for 문이 압도적으로 가장 빨랐다.
가장 먼저 있었던 것이니 오래 전부터 지금까지 최적화를 거듭해서 제일 빠른 것 같다.
일단 단순한 속도 면에선 for >> forEach > List.forEach이다.
우선 비슷해 보이는 for 문과 forEach를 비교해 보자.
for(int i=0; i<arr.length; i++){
sum += arr[i];
}
우선 딱 알 수 있듯이 for 문은 index가 있어 이를 활용해서 작업해야 할 때 좋다.
초반 JAVA 예제에서 많이 본 1~100까지의 짝수 합을 구해야 할 때, 인덱스 증가를 i+=2로 해서 구했던 것 처럼 특정 index만 처리를 할 수 있게도 가능하고 (= 루프 범위를 다루기가 좋다),
int i=arr.length-1 ; i>=0; i-- 이런식으로 하여, 역방향 순회도 가능하다.
하지만 index를 잘못 사용했다가는 IndexOutOfBoundsException 이 생길 수도 있다.
반면에 forEach문은 내부적으로 반복자(Iterator)를 사용하여 순회를 하여 개발자가 인덱스를 신경쓰지 않아도 된다.
IndexOutOfBoundsException이 발생할 가능성이 없지만 ,
역방향으로 순회를 하려면 forEach를 하기 전에 개발자가 직접 배열을 역방향으로 바꿔줘야 한다.
그리고 만약 index를 참조해서 사용해야 하거나, 순회를 하는 것이 아닌 특정 범위만을 탐색해서 작업을 해야 할 때는 index가 없어 개발자가 직접 임의로 index를 역할을 할 친구들을 만들어줘야 하는데 이럴 경우 그냥 for 문을 쓰는게 유리하다.
정리하자면
Index를 통한 접근/조작 (특정 범위, 특정 값, 역순회 등) 이 필요한 상황에서는 for문을, 단순한 순회만 하면 된다면 forEach문이 추천된다.
그리고 속도는 for문이 좀 더 빠르며, 대규모 작업에서 forEach를 난발하면 약간의 성능 오버헤드가 발생될 수도 있다.
얘는 앞선 애들과 특성이 조금 다르다.
for 와 forEach 문은 break 기능이 있다.
반면 List.forEach는 return 은 있지만 break는 없고, 무조건 끝까지 루프문을 돈다.
List.forEach는 람다식으로 함수의 특성을 떠올리면 break가 없을 수 밖에 없다. 일반적인 for문과 겉으로 보기에 실행 방식이 같은 것이지, 엄밀히 말하면 리스트의 사이즈만큼 무조건 돌게 설계되어있는 함수이기 때문이다.
list.forEach는 가급적 안 쓰는게 좋다.
만약 쓸거면 외부 변수에 값을 조정만은 절대절대 하지 않는 것이 좋다.
일단 속도가 제일 느리니까 별로다.
장점은 map과 같은 Collection 들의 처리 코드가 아래와 같이 간편하고,
Map<String, String > map = new HashMap<>();
map.forEach((key,value)->{
System.out.println(key + "나는 키야");
System.out.println(value + "나는 값이야");
});
(이런 식으로 keySet 같은 것들을 안 가져오고 편하게 map 을 순회할 수 있다.)
list.parallelStream().forEach(item -> System.out.println(item));
와 같이 병렬처리가 가능해서 대규모 데이터셋에서 성능 개선이 될 수도 있다.
그런데 작은 데이터셋이면 오히려 오버헤드만 발생하니 별로 좋지 않다.
가끔 list.forEach를 써 봤다면 forEach 함수 내부에서 지역 변수를 쓰려고 할 때 final로 바꿔라 아니면 안 된다, 하는 상황을 보았을 것이다.
list.forEach는 함수라고 하였다.
그래서, 실행되는 환경이 다르다.
ExecutorService executor = Executors.newSingleThreadExecutor();
int i = 10;
executor.submit(() -> {
System.out.println(i); //여기까지는 컴파일 오류 x
i++; //여기서부턴 컴파일 오류 0
});
executor.shutdown();
int i 와 람다식 i는 엄밀히 말하면 다른 객체이다.
람다식에서 외부 변수를 사용할 땐, 컴파일 과정에서 캡처(closure)를 통해 람다식 안으로 들어가게 되고, 이때 람다의 실행환경안에 종속되게 된다.
위의 로직에서는 i는 람다 내부에 사용되지만 람다는 별도의 스레드에서 실행된다. 그래서 i 는 메인 스레드에서 선언되었지만, 다른 스레드에서 참조되게 된다.
따라서 다중 스레드 환경에서 안정성을 확보하기가 어렵다.
그래서 불변한 상태만 람다식 내부에서 사용할 수 있는 것이다.
물론 Synchronized 를 써서 해결할 수는 있지만 오버해드가 많이 발생한다.
list.forEach(a->{
list2.add("안녕");
});
위의 코드는 컴파일 오류 없이 실행은 될 것이다.
하지만 서버에 배포한 다중 사용자 환경에서는 안정성을 확보받기가 어렵다.
따라서 가급적이면 list.forEach (람다식 forEach라고도 한다)를 지양하는 것이 좋아보인다.