[자바의 정석] Ch.11 컬렉션 프레임웍(Collections Framework)
1.5 Iterator, ListIterator, Enumeration
Iterator
최근에 듣고있는 JS 강의에서 Iterable을 공부했다. 실습 위주라 코드를 작성하며 듣다보니 그냥 그렇구나 하고 넘어갔는데 Java 컬렉션 프레임웍을 공부할 때 또 나온다. 책 내용을 읽긴 읽었지만 도저히 무슨 말인지 모르겠어서 찾아봤다. Iterator는 컬렉션 프레임웍의 Set, List 인터페이스를 상속받은 ArrayList, HashSet과 같은 클래스에 저장된 각 요소를 읽어오는 기능을 표준화한 인터페이스이다. Iterator를 구현한 클래스의 인스턴스 iterator()는 Collection인터페이스에 저장된 메서드이므로 List와 Set인터페이스에도 포함되어 있다. 이를 구현한 컬렉션 클래스에 대해 iterator()를 호출하여 Iterator를 얻은 다음 while과 같은 반복문을 사용해서 컬렉션 클래스의 요소를 읽어올 수 있다.
메소드 | 설명 |
boolean hasNext() | 읽어 올 요소가 남아있는지 확인. 있으면 true, 없으면 false |
Object next() | 다음 요소를 읽어옴. next()를 호출하기 전 hasNext()를 호출하여 읽어올 요소가 있는지 확인하는 것이 안전 |
void remove() | next()로 읽어 온 요소를 삭제. next()를 호출한 다음 remove()를 호출 |
Collection c = new ArrayList();
Iterator it = c.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
c의 참조변수 타입이 ArrayList가 아니라 Collection인 이유는 나중에 Collection인터페이스를 구현한 다른 컬렉션 클래스의 객체를 생성했을 때, ArrayList부분만 변경하면 코드를 그대로 사용할 수 있기 대문이다. 이처럼 표준화된 Iterator인터페이스를 이용하여 코드의 재사용성을 높일 수 있다. 공통 인터페이스를 정의해서 표준을 정의하고 구현하여 표준을 따르도록 함으로써 코드의 일관성을 유지하여 재사용성을 극대화하는 것이 객체지향 프로그래밍의 중요한 목적 중 하나이다.
Map 인터페이스를 구현한 컬렉션 클래스는 키와 값을 쌍으로 저장하고 있기때문에 iterator()를 직접 호출할 수 없고 keySet()이나 entrySet()과 같은 메서드를 통해 키와 값을 각각 따로 Set의 형태로 얻어 온 후 다시 iterator()를 호출해야 Iterator를 얻을 수 있다.
import java.util.*;
public class ObjectsTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
Set set = new HashSet();
set.add('1');
set.add('2');
set.add('3');
set.add('4');
set.add('5');
list.add('1');
list.add('2');
list.add('3');
list.add('4');
list.add('5');
Iterator setit = set.iterator();
Iterator listit = list.iterator();
while (setit.hasNext()){
System.out.println("set의 element: " + setit.next());
}
while (listit.hasNext()){
System.out.println("list의 element: " + listit.next());
}
}
}
두 개를 구현하면 그 결과값이 다를 거라고 했는데 똑같다. 입력한 수가 적어서일까?
ListIterator와 Enumeration
Enumeration은 Iterator의 구버젼, ListIterator는 Iterator에 양방향 조회기능이 추가된 것이다. Enumeration은 구버젼이므로 ListIterator는 List를 구현한 경우에만 사용 가능하다. 따라서 Iterator를 사용하는 것이 좋지만 Enumeration를 사용하는 사람이 아직 있으므로 알아둬야 한다.
메소드 | 설명 |
boolean hasMoreElements() | 읽어 올 요소가 남아있는지 확인. 있으면 true, 없으면 false |
Object nextElement() | 다음 요소를 읽어옴. nextElement()를 호출하기 전에 hasMoreElement()를 호출해서 읽어올 요소가 남아있는지 확인하는 것이 안전. |
메소드 | 설명 |
void add(Object o) | 컬렉션에 새로운 객체(o)를 추가 |
boolean hasNext() | 읽어 올 다음 요소가 남아있는지 확인. 있으면 true, 없으면 false |
boolean hasPrevious() | 읽어 올 이전 요소가 남아있는지 확인. 있으면 true, 없으면 false |
Object next() | 다음 요소를 읽어옴. next()를 호출하기 전 hasNext()를 호출해서 읽어올 요소가 있는지 확인하는 것이 안전 |
Object previous() | 이전 요소를 읽어옴. previous()를 호출하기 전 hasPrevious()를 호출해서 읽어올 요소가 있는지 확인하는 것이 안전 |
int nextIndex() | 다음 요소의 index를 반환 |
int previousIndex() | 이전 요소의 index를 반환 |
void remove() | next() 또는 previous()로 읽어 온 요소를 삭제. next() 또는 previous()로 호출한 다음 이 메소드를 호출. |
void set(Object o) | next() 또는 previous()로 읽어 온 요소를 지정된 객체(o)로 변경. next() 또는 previous()로 호출한 다음 이 메소드를 호출. |
remove()를 지원하지 않는 Iterator는 UnsupportedOperationException을 발생시키도록 구현해야 한다. remove()는 단독으로 쓰일 수 없고 next()와 같이 써야 한다.
1.6 Arrays
Arrays클래스에는 배열을 다루는데 유용한 메서드가 정의되어 있다.
배열의 복사 - copyOf(), copyOfRange()
copyOf()는 배열 전체를, copyOfRange()는 배열의 일부를 복사해서 새로운 배열을 만들어 반환한다.
배열 채우기 - fill(), setAll()
fill()은 배열의 모든 요소를 지정된 값으로 채운다. setAll()은 배열을 채우는데 사용할 함수형 인터페이스를 매개변수로 받는다. 이 메서드를 호출할 때는 함수형 인터페이스를 구현한 객체를 매개변수로 지정하거나 람다식을 지정해야한다.
int[] arr = new int[5];
Arrays.setAll(arr, () -> (int)(Math.random() * 5) +1);
배열의 정렬과 검색 - sort(), binarySearch()
sort()는 배열을 정렬할 때, 그리고 배열에 저장된 요소를 검색할 때 binarySearch()를 사용한다. binarySearch()는 지정된 값이 저장된 위치(index)를 반환하는데 배열이 정렬되어 있어야 호출할 수 있다.만약 찾고자 하는 값이 배열에 여러 개있다면 어떤 것의 위치를 반환할 지 알 수 없다.
배열의 비교와 출력 - equals(), toString()
toString()은 배열의 모든 요소를 문자열로 출력할 수 있다. 일차원 배열에서만 사용할 수 있어 다차원 배열에서는 deepToString()을 사용해야 한다. equals()는 두 배열에 저장된 모든 요소를 비교하여 같으면 true, 다르면 false를 반환한다. equals()도 일차원 배열에서만 사용할 수 있어 다차원 배열을 비교할 땐 deepToEquals()를 사용해야 한다.
배열을 List로 변환 - asList(Object ...a)
asList()를 호출하면 배열을 List에 담아 반환한다. 이 때 반환한 List는 크기를 변경할 수 없어 추가나 삭제가 불가능하다. 크기를 변경하고자 한다면 다음과 같이 해야 한다.
List list = new ArrayList(Arrays.asList(1, 2, 3, 4, 5));
parallelXXX(), splitIterator(), stream()
parallel로 시작하는 메서드는 쓰레드가 작업을 나누어 처리하게 한다. 찾아보니 parallelPrefix, parallelSetAll, parallelSort 이렇게 세 가지 메소드가 오버로딩 되어있다. spliterator()는 여러 쓰레드가 처리할 수 있게 하나의 작업을 여러 작업으로 나누는 Spliterator를 반환하며, stream()은 컬렉션을 스트림으로 변환한다.
1.7 Comparator와 Comparable
Arrays.sort()를 호출하면 sort()메소드 자체로 정렬이 되는 것 같지만 사실 Character클래스의 Comparable에 의해 정렬된 것이다. Comparable을 구현하고 있는 클래스들은 Integer와 같은 Wrapper클래스와 String, Date와 같이 같은 타입의 인스턴스끼리 비교할 수 있고 오름차순으로 정렬되도록 구현되어 있다.
compare()과 compareTo()는 근본적으로 두 객체를 비교하는 기능을 가진다. compareTo()는 비교하는 두 객체가 같으면 0, 비교하는 값보다 작으면 음수, 크면 양수를 반환하도록 구현해야 한다. java.lang.Comparable은 기본 정렬기준인 오름차순을 구현하는데 사용하고 java.util.Comparator는 오름차순 외 다른 기준으로 정렬할 때 사용한다. 책의 예제를 작성해봤다(자바의 정석 2권, 629P, 예제 11-19)
import java.util.Arrays;
import java.util.Comparator;
public class ComparatorEx {
public static void main(String[] args) {
String[] strArr = {"cat", "Dog", "lion", "tiger"};
Arrays.sort(strArr); //객체배열에 저장된 객체가 구현한 Comparable에 의한 정렬
System.out.println("strArr = " + Arrays.toString(strArr));
Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER); //지정한 Comparator에 의한 정렬
System.out.println("strArr = " + Arrays.toString(strArr));
Arrays.sort(strArr, new Descending()); //지정한 Comparator에 의한 정렬
System.out.println("strArr = " + Arrays.toString(strArr));
}
}
class Descending implements Comparator{
public int compare(Object o1, Object o2){a
if(o1 instanceof Comparable<T> && o2 instanceof Comparable<T>){
Comparable c1 = (Comparable) o1;
Comparable c2 = (Comparable) o2;
return c1.compareTo(c2) * -1;
}
return -1;
}
}
public static void sort(short[] a)
public static <T> void sort(T[] a, Comparator<? super T> c)
static Comparator<String> | CASE_INSENSITIVE_ORDER
A Comparator that orders String objects as by compareToIgnoreCase.
|
String 클래스의 CASE_INTENSIVE_ORDER는 대소문자 무시하고(소문자 쓸 때 Lower Case, 대문자 쓸 때 Upper Case라고 하는거 보면 문서의 Case는 대소문자가 맞는 것 같다) String 객체의 순서를 따르는 Comparator를 반환한다. 이 때 정렬 순서는 유니코드의 순서라고 보면 된다. 따라서 strArr = [cat, Dog, lion, tiger] 와 같은 결과값을 반환한다.
세번째는 Comparator를 상속받은 Descending 클래스로 compare메소드를 직접 구현했다. 우선 객체 o1과 o2가 compare할 수 있는지 Comparable 인터페이스의 요소인지 확인한다. instanceof연산자를 이용한 조건문에 따라 비교할 수 없다면 -1을 반환하고, 요소가 맞아서 비교한다. 여기서는 비교한 값에 -1을 곱해줌으로써 반대로 반환했기 때문에 결과값이 strArr = [tiger, lion, cat, Dog] 역순으로 나왔다. 만약 -1을 곱하지 않았다면 첫번째 Arrays.sort()와 동일한 결과값이 나왔을 것이다.