Iterator
- 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다.
- Collection 인터페이스에 정의된 메서드이므로 Collection 인터페이스의 자손인 List와 Set에도 포함되어 있다.
- Map 인터페이스를 구현한 컬렉션 클래스는 Key와 Value을 쌍으로 저장하고 있기 때문에 iterator()를 직접 호출할 수 없고, 그 대신 ketSet()이나 entrySet()과 같은 메서드를 통해서 키와 값을 각각 따로 Set의 형태로 얻어온 후 다시 iterator()을 호출해야 iterator을 얻을 수 있다.
- 이렇게 iterator을 얻은 다음 반복문, 주로 while문을 사용해서 컬렉션 클래스의 요소들을 읽어 올 수 있다.
Iterator 인터페이스에 정의된 메서드는 다음과 같다.
boolean hasNext() | 읽어 올 요소가 남아있는지 확인하여 있으면 true, 없으면 false를 반환한다. |
Obejct next() | 다음 요소를 읽어온다. next()를 호출하기 전에 hasNext()를 호출해서 읽어 올 요소가 있는지 확인하는 것이 좋다. |
void remove() | next()로 읽어 온 요소를 삭제한다. next()를 호출한 다음에 remove()를 호출해야 한다.(remove는 필수 기능이 아닌 선택적 기능이다. 즉, next() 후 요소를 필히 삭제할 필요가 없다. |
ArrayList에 저정된 요소들을 iterator을 통해 출력하기
...
Collection c = new ArrayList();
Iterator it = c.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
여기서 참조변수 타입이 ArrayList가 아닌 Collection인 이유는 Collection이 ArrayList의 상위 타입이며, 위 동작에서 ArrayList만의 메서드가 필요하지 않기 때문이다. 물론 ArrayList나 List타입으로 사용하여도 상관 없다.
ListIterator
- 기존 Iterator은 단방향이다. 따라서 한 번 이동하면 이전 요소로 접근이 불가능하다. ListIterator은 이 문제를 해결한다.
- 즉, 이전 요소로 접근이 가능하다.
- 다만, ArrayList나 LinkedList와 같이 List 인터페이스를 구현한 컬렉션에서만 사용할 수 있다.
Iterator 인터페이스에 정의된 메서드는 다음과 같다.
void add(Object o) | 컬렉션에 새로운 객체(o)를 추가한다. (선택적 기능) |
boolean hasNext() | 읽어 올 다음 요소가 남아있는지 확인하여 있으면 true, 없으면 false를 반환한다. |
boolean hasPrevious() | 읽어 올 이전 요소가 남아있는지 확인하여 있으면 true, 없으면 false를 반환한다. |
Object next() | 다음 요소를 읽어온다. next()를 호출하기 전에 hasNext()를 호출해서 읽어 올 요소가 있는지 확인하는 것이 좋다. |
Object previous() | 이전 요소를 읽어온다. next()를 호출하기 전에 hasNext()를 호출해서 읽어 올 요소가 있는지 확인하는 것이 좋다. |
int nextIndex() | 다음 요소의 index를 반환한다. |
int previousIndex() | 이전 요소의 index를 반환한다. |
void remove() | next() 또는 previous() 로 읽어 온 요소를 삭제한다. 반드시 next() 혹은 previous()를 호출한 다음에 remove()를 호출해야 한다.(remove는 필수 기능이 아닌 선택적 기능이다. 즉, next() 후 요소를 필히 삭제할 필요가 없다.) |
void set(Object o) | next() 또는 previous() 로 읽어 온 요소를 지정된 객체 o로 변경한다. 반드시 next() 또는 previous()를 먼저 호출한 다음에 이 메서드를 호출해야한다.(선택적 기능) |
- 위 표에서 선택적 기능이라 명시되어 있는 기능은 말 그대로 선택적으로 구현하면 된다.
- 하지만 인터페이스에 정의되어 있기 때문에 메서드의 바디는 만들어여 한다. 따라서 다음과 같이 바디를 작성한 후, 메서드가 호출되면 이 구현 메서드라는 예외를 발생하도록 한다.
- 빈 바디 보다는 미구현 메서드라는 예외를 발생시켜 호출하는 쪽에서 알 수 있도록 하는 것이 좋다.
public void set(Object o) { throw new UnsupportedOperationException(); }
Iterator 동작
iterator의 hasNext(), next(), remove() 가 어떻게 동작하는지 간략하게 확인해보자.
...
int cursor = 0;
int lastRef = -1;
public Iterator iterator() {
cursor = 0;
lastRet = -1;
return this;
}
public boolean hasNext() {
return cursor != this.size();
}
public Object next() {
Object next = this.get(cursor);
lastRef = cursor++;
return next;
}
public void remove() {
if(lastRef == -1) {
throw new IllegalStateException();
} else {
remove(lastRef);
cursor--;
lastRef = -1;
}
}
- cursor은 앞으로 읽어 올 요소의 위치를 저장하는데 사용된다.
- lastRef는 마지막으로 읽어 온 요소의 위치를 저장하는데 사용된다.
- hasNext()는 현재 사이즈와 cursor가 같은지 비교하여 다음 요소의 유무를 반환한다.
- next()는 cursor위치의 값을 가져오고, lastRef를 cursor로 설정한 뒤 cursor의 값을 1 증가시킨다. 이후 가져온 값을 반환한다.
- remove()는 현재 값(마지막으로 가져온 값)의 인덱스가 저장되어 있는 lastRef를 인덱스로 하여 데이터를 제거한다. 이후 cursor의 값을 1 감소시키고 lastRef를 -1로 설정한다.
- cursor의 값을 1 감소시키는 이유는 remove를 통해 객체를 삭제하고 나면, 삭제된 위치 이후의 객체들이 빈 공간을 채우기 위해 자동적으로 이동되기 때문이다.
- lastRef가 -1로 설정되는 것은 읽어 온 값이 없다는 의미이다.
For-Each와 Iterator
for-each문이 존재하는데 Iteretor나 ListIterator가 필요할까?
이 의문점을 해결하기 위해서 Iterator 인터페이스와 Iterable 인터페이스의 차이를 이해해야한다.
우리가 위해서 Iterator는 remove()를 제공한다는 것을 배웠다. 그리고 어떻게 remove() 로직이 동작하는지 확인했다. 그런데 Iterable 인터페이스는 remove()를 제공하지 않는다. 따라서 반복문 중간에 요소를 제거한다면 ConcurrentModificationException 예외가 발생하게 된다.
@Test
void test() {
List<Integer> integerList = new ArrayList<>();
integerList.add(1);integerList.add(2);
integerList.add(3);integerList.add(4);
for (Integer integer: integerList) {
integerList.remove(integer);
}
}
// ConcurrentModificationException 예외가 발생한다.
이렇게 For-Each와 Iterator이 같은 것이 아니라는 것을 확인할 수 있었고, 어떤 경우에 Iterator가 필요한지 확인할 수 있었다.
참고자료
- 자바의 정석
'개발 > Java' 카테고리의 다른 글
JVM의 기본 개념 (0) | 2022.02.23 |
---|