java collection을 다루다 보면, iteration 순회 정도는 아무렇지 않게 쓰셨을 겁니다. 이들이 어떠한 순서로 작동하는지 알아봅니다. 그리고, 실제 ArrayList에서 어떻게 동작하는지 같이 알아보도록 하겠습니다. 이 글에서는 어레이 리스트만 설명하나, 다른 collection 에서도 기작은 크게 다르지 않습니다. 자료구조에서 두 메서드의 구현이 다를 뿐입니다.
java 17 버전에서 실행하였음을 염두에 두시면 좋겠습니다.
java iterator for loop 기작
아래 코드를 봅시다. 이 코드가 실행될 때, 어떤 일이 일어날까요?
참조자 무효화가 떠오르는 것 같지만, 이 단락에서는 말하지 않겠습니다. 여기에서는 7번째 for loop가 돌 때 마다, 어떤 일이 일어날까? 에 초점을 먼저 맞춰보겠습니다. iterator에는 2가지 속성의 메서드가 있습니다.
- hasNext
- next
먼저 hasNext는 다음 원소가 있는지 검사합니다. next는 다음 원소로 cursor를 옮기게 됩니다. iterator는 처음 초기화가 될 때 중요한 정보들을 초기화 합니다. 이 중에 modCount와 expectedModCount를 동기화 하는 부분도 포함되어 있습니다.
그리고, 매 for loop 마다, 아래와 같은 순서로 메서드를 호출합니다.
hasNext 먼저 호출되고, 그 다음에 next가 호출됩니다.
hasNext 함수는 단순히 다음 원소가 있는지 검사합니다.
next 함수에서는 cursor를 하나 증가시킵니다. 배열에서 다음 원소는 이전 원소 위치에 1을 더하면 되기 때문입니다. 그런데, 이 함수에서는 단순히 cursor만 옮기지 않습니다. 몇 가지 검사를 추가로 하게 됩니다. 이 중 불완전한 상태를 검사하는 부분이 있는데 967번째 줄에서 하게 됩니다.
여기까지 java iterator for loop 기작을 정리해 봅시다.
- iterator 의 정보가 대상을 기반으로 초기화 됩니다.
- 특히 modCount 동기화는 중요한 요소입니다.
- 매 루프마다 hasNext와 next 순서로 호출 됩니다.
이제, 두 개의 예제 프로그램을 보도록 하겠습니다. 예제 1 ~ 2번은 arrayList 에서의 순회를 다룹니다.
예제 1번 따라가 보기
이를 토대로 예제 1번의 흐름을 따라가 봅시다.
이 중 중요한 것은 expectedModCount 입니다. 기대하는 원소 크기입니다. 예제 1번에서는 원소를 2개만 넣었으니까 2가 됩니다. 위에서 서술했다시피, hasNext, next 순으로 루프가 돌 때 마다 호출하게 됩니다.
이제 예제 1의 상황을 봅시다. A, B 순서대로 추가했고, 루프를 돌면서 B랑 일치하면 B를 제거합니다.
처음 상황은 이런 상황입니다. cursor가 0이고, size가 2이니 0 != 2입니다. hasNext 조건 통과했습니다. 고로, 다음 원소가 있습니다. 따라서 next 메서드가 수행됩니다. arrayList 에서는 cursor의 값만 하나 증가합니다. 즉, 다음 원소를 가리키게 됩니다.
첫 번째 for loop를 탈 때 초기 상태는 위와 같습니다. A를 순회했고, cursor는 다음 위치인 B를 가리키는 상태입니다. 두 번째 for loop를 탈 때는 어떨까요?
B를 순회했고, cursor는 그 다음 위치인 끝을 가리키게 됩니다. 그런데, 이 상태에서 B를 제거합니다.
그러면 list의 size는 2가 아닌 1이 되어버립니다. 그런데 cursor는 2가 됩니다. cursor != size 이기 때문에 또 next 함수를 타게 됩니다. 이 메소드에서 아래 함수를 호출하게 됩니다.
위 메소드는 modCount와 expectedModCount가 다르면 예외를 뱉습니다. expectedModCount는 2입니다. 여기서 modCount는 대상체의 modCount를 의미합니다. 이 둘 중 하나가 어떤 이유로 동기화 되지 않으면 1012번째 줄에 걸릴 수 있습니다.
remove 할 때 상황을 봅시다.
call stack을 보면 remove에서 fastRemove를 호출함을 볼 수 있습니다.
이 메소드를 보면, modCount++ 를 하고 있습니다. modCount 횟수를 증가시켜요. 한 쪽만 증가되는 상황입니다.
이 시점에서 invalid 한 상태가 되었습니다. 당연히, 그 이후에 next 를 호출했다면 문제가 되겠지요. 이 이후에 hasNext를 호출했을 때 cursor는 2였지만, size가 1이였습니다.
- 2 != 1이였고
- invalid한 상태입니다.
따라서, 예외가 발생합니다.
예제 2번 따라가 보기
아래 예제 2번 코드는 어떨까요?
예외가 발생할 것 같지만 아닙니다. 왜 그런가? 위에서 hasNext 다음에 next가 호출된다고 했습니다. 이터레이터가 invalid한 상태로 변하는 시점부터 봅시다.
먼저, B를 뽑아온 시점에 cursor는 다음 원소인 C를 가리키고 있습니다. 이 때 for loop 안의 if 절에 걸리기 때문에 ma.remove(“B”)를 수행하게 됩니다. 이 구문이 수행되면 아래와 같이 됩니다.
cursor와 size가 같습니다. 고로 hasNext 조건이 맞지 않기 때문에, 루프가 끝나게 됩니다.
valid한 상태인가요? 그렇지 않습니다. 당연하게도, modCount만 증가했기 때문입니다. invalid 상태가 되었음에도 불구하고요. 어떻게 된 일인가요? next가 호출되기 전에 hasNext가 호출되었습니다. 그리고, hasNext에서 False를 리턴해서 끝내버렸습니다. 예외가 발생하기 전에 끝나버린 것입니다.