하나의 일, 여러 도구
데이터를 모았습니다 - ArrayList, HashSet, HashMap 안에 - 이제 모든 요소를 방문하고 싶습니다. 자바는 이를 위한 몇 가지 방법을 제공하며, 어느 것을 고를지는 인덱스가 필요한지, 루프 중간에 항목을 제거해야 하는지, 메서드 형태와 루프 형태 중 어떤 구문을 선호하는지에 달려 있습니다.
좋은 소식은, 모든 Collection(리스트, 셋, 큐)이 동일한 향상된 for 루프를 지원한다는 것입니다. 그래서 한 번만 익히면 그것들을 모두 같은 방식으로 순회할 수 있습니다.
콜론을 "안의"로 읽으세요: "langs 안의 각 lang에 대해". 인덱스를 전혀 건드리지 않으므로 잘못될 것이 없습니다.
for-each 루프
향상된 for는 단순히 "각 요소로 무언가를 한다"를 위한 기본 선택지입니다. 읽기 쉽고 컬렉션 타입에 상관없이 동일하게 동작합니다 - 여기서는 인덱스가 전혀 없는 HashSet입니다:
HashSet에 대해 한 가지 기억할 점: 정해진 순서가 없으므로 요소가 어떤 순서로든 출력될 수 있습니다. 그래도 for-each는 모든 요소를 정확히 한 번씩 방문합니다.
인덱스가 필요할 때
for-each는 요소를 주지만 그 위치는 주지 않습니다. 인덱스가 정말로 필요하다면 - 줄에 번호를 매기거나 이웃한 요소를 보려면 - size()와 get(i)를 사용하는 카운트 루프를 쓰세요. 이는 위치 기반인 List에서 동작합니다. 셋과 맵에는 인덱스가 없으므로 이 방식은 적용되지 않습니다.
단지 습관 때문에 이것에 손을 뻗지 마세요. i를 get(i) 외에 아무 데도 쓰지 않는다면 for-each 버전이 더 짧고 틀리기도 더 어렵습니다.
Map 순회하기
Map은 Collection이 아니므로 그 위에서 직접 for-each를 할 수 없습니다. 대신 세 가지 뷰 중 하나를 순회합니다. 가장 흔한 것은 각 키-값 쌍을 함께 건네주는 entrySet()입니다:
키만 필요하면 ages.keySet()을, 값만 필요하면 ages.values()를 순회하세요. 둘 다 필요할 때는 entrySet()을 선호하세요 - 키를 순회한 뒤 안에서 ages.get(key)를 호출하면 매 반복마다 아무 이유 없이 두 번째 조회를 하게 됩니다.
Iterator
for-each는 사실 Iterator 위에 얹힌 문법적 설탕입니다 - Iterator는 hasNext()와 next()를 통해 컬렉션을 한 번에 한 요소씩 걸어가는 객체입니다. 이 루프를 직접 손으로 작성하는 일은 드물지만, 중요한 예외가 하나 있습니다: 순회하면서 요소를 제거하는 안전한 방법이라는 점입니다.
it.remove()는 next()가 마지막으로 반환한 요소를 삭제하며, 이터레이터는 유효한 상태로 남습니다. 이것이 수동 루프 중에 컬렉션을 변경하는 유일하게 허용된 방법입니다.
함정: ConcurrentModificationException
for-each 루프 안에서 컬렉션 자체에 대해 add나 remove를 호출하면 ConcurrentModificationException이 발생합니다 - 이터레이터는 발밑에서 컬렉션이 바뀐 것을 알아차리고 계속하기를 거부합니다. 이것은 초보자가 가장 흔히 저지르는 실수 중 하나입니다.
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : nums) {
if (n % 2 == 0) {
nums.remove(Integer.valueOf(n)); // throws ConcurrentModificationException
}
}
해결책은 거의 항상 removeIf입니다. 의도를 한 줄로 표현하고 순회를 대신 처리해 줍니다:
removeIf는 어떤 Collection에서도 동작하므로, 같은 호출로 HashSet도 정리할 수 있습니다.
forEach 메서드
모든 컬렉션에는 람다를 받아 각 요소에 대해 실행하는 forEach 메서드도 있습니다. 루프보다 더 함수형적이고 식 스타일의 대안으로, 짧은 한 줄 처리에 편리합니다:
Map.forEach는 두 인자를 받는 람다 (key, value)를 직접 받는다는 점에 유의하세요 - entrySet()이 필요 없습니다. 빠른 부수 효과에는 forEach를 사용하고, 본문이 커지거나 일찍 빠져나오기 위해 break가 필요하면 일반 for 루프로 돌아가세요. 람다는 break를 할 수 없습니다.
다음: 메서드
이제 데이터를 컬렉션에 담고, 자바가 제공하는 모든 방법으로 그것을 순회해 봤습니다. 다음 단계는 동작을 담는 것입니다: 직접 메서드를 작성하여 논리 블록에 이름을 붙이고, 입력을 건네고, 재사용할 수 있게 하는 것 - 그것이 다음 페이지입니다.
자주 묻는 질문
자바에서 리스트를 어떻게 순회하나요?
가장 깔끔한 방법은 향상된 for 루프(for-each)입니다: for (String s : list) { ... }. ArrayList, HashSet 등 어떤 Collection에서도 동작합니다. get(i)를 쓰는 인덱스 루프는 위치가 정말로 필요할 때만 사용하고, 루프 도중에 요소를 제거해야 할 때는 Iterator를 사용하세요.
자바에서 HashMap을 어떻게 순회하나요?
Map은 Collection이 아니므로 그 뷰 중 하나를 순회합니다. 보통은 for (Map.Entry<K, V> e : map.entrySet())을 선택하는데, 한 번의 순회로 키(e.getKey())와 값(e.getValue())을 모두 얻을 수 있습니다. 키만 필요하면 map.keySet()을, 값만 필요하면 map.values()를 순회할 수도 있습니다.
순회할 때 왜 ConcurrentModificationException이 발생하나요?
for-each 루프가 컬렉션을 순회하는 도중에 그 컬렉션에 대해 add나 remove를 호출했기 때문입니다. for-each 루프는 내부적으로 Iterator를 사용하며, 컬렉션이 구조적으로 변경되었음을 감지합니다. Iterator 자체의 remove() 메서드로 제거하거나, 루프 대신 removeIf(...)를 호출하여 해결하세요.