인덱스가 그저 거치적거릴 때
카운터를 쓰는 for 반복문은 카운터, 조건, 갱신 단계를 줍니다. 하지만 실제로는 요소의 위치 따위 신경 쓰지 않는 경우가 정말 많습니다. 그저 각 요소에 대해, 순서대로, 처음부터 끝까지 무언가를 하고 싶을 뿐이죠. 그걸 위해 인덱스를 관리하는 건 쓸데없는 일이며, 바로 거기서 off-by-one(하나 차이) 버그가 슬그머니 끼어듭니다.
for-each 반복문(자바에서의 이름은 향상된 for)은 카운터를 완전히 없앱니다. 변수에 이름을 붙이고 그것을 컬렉션으로 향하게 하면, 반복문이 각 요소를 차례로 건네줍니다.
기본 문법
형태는 for (타입 element : collection)입니다. 콜론을 "안의"라는 단어로 읽으세요:
i도, scores.length도, scores[i]도 없습니다. 매 회마다 score는 바로 다음 요소입니다. 반복문은 요소마다 한 번씩 실행되며 더 이상 요소가 없으면 자동으로 멈춥니다. 끝을 넘어 달리거나 한 요소 일찍 시작할 수 없습니다.
리스트 순회하기
같은 반복문은 순회 가능한(iterable) 모든 것에 대해 작동하며, 여기에는 List, Set을 비롯한 다른 컬렉션 타입이 포함됩니다. 요소 타입은 변수 이름 앞에 옵니다:
langs가 배열로 뒷받침되는지, 연결 리스트인지, 그 외 다른 것인지 알 필요도 신경 쓸 필요도 없었다는 점에 주목하세요. for-each는 그 모두에서 똑같이 동작합니다. 그것이 바로 진짜 강점입니다. 모든 컬렉션에 대해 하나의 읽기 쉬운 문법인 거죠.
var는 타입 이름을 적는 수고를 덜어 줍니다
요소 타입이 길거나 뻔하다면, var를 쓰면 컴파일러가 그것을 추론하게 하여 반복 작성을 피할 수 있습니다:
var가 없으면 그 반복 변수는 입에 잘 안 붙는 Map.Entry<String, Integer>가 됩니다. var는 읽기 쉽게 유지해 주면서도, 타입은 여전히 컴파일 시점에 완전히 검사됩니다. 느슨한 동적 타입이 아닙니다.
수정의 함정
여기 누구나 걸리는 규칙이 있습니다. for-each 반복문이 컬렉션을 훑는 동안에는 그 컬렉션에 요소를 추가하거나 제거할 수 없습니다. 그렇게 하면 ConcurrentModificationException을 던집니다:
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if (item.equals("b")) {
items.remove(item); // ConcurrentModificationException을 던짐
}
}
반복문은 아래에서 리스트가 바뀌었음을 알아채고, 요소를 조용히 건너뛰거나 반복하는 대신 중단합니다. 안전하게 제거하려면, 반복문이 알고 있는 remove()를 가진 명시적인 Iterator로 내려가세요:
흔히 쓰는 지름길은 items.removeIf(item -> item.equals("b"))이며, 같은 일을 한 줄로 해냅니다.
읽기만, 재할당은 안 됨
또 다른 미묘한 한계: 반복 변수에 값을 할당하면 지역 복사본만 바뀌고 컬렉션은 바뀌지 않습니다. 반복 변수가 살아 있는 참조인 언어에서 온 사람들은 이 점에 놀랍니다:
배열에 다시 써넣어야 한다면 인덱스가 필요하고, 그것은 곧 전통적인 카운터 for 반복문을 뜻합니다: for (int i = 0; i < nums.length; i++) nums[i] = nums[i] * 10;. 객체 요소의 경우, 변수가 가리키는 객체를 (예를 들어 세터를 호출해) 변경할 수는 있지만, 컬렉션 안에서 그것을 다른 것으로 교체할 수는 없습니다.
break와 continue는 여전히 작동합니다
for-each는 진짜 반복문이므로 break와 continue는 다른 곳에서와 똑같이 동작합니다. break는 반복문을 빠져나가고, continue는 다음 요소로 건너뜁니다:
이건 keep을, 이어서 keep을 출력합니다. "skip"을 건너뛰고 "never"에 닿기 전에 "stop"에서 멈춥니다. 그러니 모든 요소를 방문하도록 강제되는 것이 아닙니다. 더 깔끔한 코드를 얻는 대가로 인덱스를 포기할 뿐이죠.
다음: 배열
지금까지 배열이 실제로 무엇인지 깊이 들여다보지 않고 몇 번 순회해 봤습니다. 배열이란 바로 그 .length 필드를 가진, 고정 크기에 인덱스가 있는 컨테이너입니다. 다음 페이지에서는 처음으로 돌아가 배열을 제대로 다룹니다. 선언하는 방법, 처음에 들어 있는 기본값, 그리고 그 고정 크기가 늘어날 수 있는 ArrayList와 어떻게 다른지를 살펴봅니다.
자주 묻는 질문
자바에서 for-each 반복문이란 무엇인가요?
for-each 반복문(향상된 for라고도 함)은 카운터 없이 배열이나 컬렉션의 모든 요소를 훑습니다: for (타입 item : collection) { ... }. "컬렉션의 각 item에 대해"라고 읽으면 됩니다. 요소 자체만 필요하고 인덱스는 전혀 필요 없을 때, 카운터를 쓰는 for 반복문보다 더 깔끔합니다.
자바에서 for 반복문과 for-each 반복문의 차이는 무엇인가요?
전통적인 for 반복문은 명시적인 카운터(for (int i = 0; i < arr.length; i++))를 사용하므로 인덱스와 방향을 직접 제어합니다. for-each 반복문 for (타입 x : arr)에는 인덱스가 없어 모든 요소를 순서대로 방문합니다. 읽기 전용으로 처음부터 끝까지 훑을 때는 for-each를, 인덱스가 필요하거나 요소를 건너뛰고 싶거나 컬렉션의 구조를 변경하려면 카운터 반복문을 쓰세요.
왜 제 for-each 반복문이 ConcurrentModificationException을 던지나요?
for-each로 컬렉션을 순회하는 도중에 그 컬렉션에 대해 add()나 remove()를 호출했기 때문입니다. 반복문은 구조적 변경을 감지하고, 정의되지 않은 동작으로부터 당신을 보호하기 위해 예외를 던집니다. 요소를 안전하게 제거하려면 명시적인 Iterator와 그 remove() 메서드를 사용하거나, 삭제할 요소를 모아 두었다가 반복문이 끝난 뒤에 제거하세요.