본문 바로가기
Back-end

11 컬렉션 프레임웍(3)

by 신재권 2021. 7. 7.

Iterator, ListIterator, Enumeration

Iterator, ListIterator, Enumeration은 모두 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다.

Iterator

컬렉션 프레임웍에서는 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화 하였다.

컬렉션에 저장된 각 요소에 접근하는 기능을 가진 Iterator 인터페이스를 정의하고, Collection인터페이스에는 Iterator(Iterator를 구현한 클래스의 인스턴스)를 반환하는 iterator()를 정의하고 있다.

public interface Iterator{
	boolean hasNext();
	Object next();
	void remove();
}
public interface Collection{
	...
	pulbic Iterator iterator();
	...
}

iterator()는 Collection 인터페이스에 정의된 메서드이므로, Collection인터페이스의 자손인 List와 Set에도 포함되어 있다.

그래서 List나 Set인터페이스를 구현하는 컬렉션은 iterator()가 각 컬렉션의 특징에 알맞게 작성되어 있다. 컬렉션 클래스에 대해 iterator()를 호출하여 Iterator를 얻은 다음 반복문, 주로 while문을 사용해서 컬렉션 클래스의 요소들을 읽어 올 수 있다.


boolean hasNext() : 읽어 올 요소가 있는지 확인한다. 있으면 true, 없으면 false를 반환


Object next() : 다음 요소를 읽어온다. next()를 호출하기 전에 hasNext()를 호출해서 읽어 올 요소가 있는지 확인하는 것이 안전한다.


void remove() : next()로 읽어 온 요소를 삭제한다. next()를 호출한 다음에 remove()를 호출해야 한다. (선택적 기능)


ArrayList에 저장된 요소들을 출력하기 위한 코드는 다음과 같이 작성이 가능하다.

Collection c = new ArrayList(); //다른 컬렉션으로 변경시 이 부분만 고치면 된다.
Iterator it = c.iterator();

while(it.hasNext()){
	System.out.println(it.next());
}

ArrayList대신 Collection 인터페이스를 구현한 다른 컬렉션 클래스에 대해서도 이와 동일한 코드를 사용할 수 있다.

첫줄에서 ArrayList대신 Collection인터페이스를 구현한 다른 컬렉션 클래스의 객체를 생성하도록 변경하기만 하면 된다.

Iterator를 이용해서 컬렉션의 요소를 읽어오는 방법을 표준화했기 때문에 이처럼 코드의 재사용성을 높일 수 있다.

이처럼 공통 인터페이스를 정의해서 표준을 정의하고 구현하여 표준을 따르도록 함으로써 코드의 일관성을 유지하여 재사용성을 극대화하는 것이 객체지향 프로그래밍의 목적 중 하나이다.

참조변수의 타입을 ArrayList타입이 아닌 Collection 타입으로 한 이유

Collection에 없고 ArrayList에만 있는 메서드를 사용하는게 아니라면, Collection타입의 참조변수로 선언하는 것이 좋다. 만일 Collection인터페이스를 구현한 다른 클래스, 예를 들어 LinkedList로 바꿔야 한다면 선언문 하나만 변경하면 나머지 코드는 검토하지 않아도 된다.

Map인터페이스를 구현한 컬렉션 클래스는 키(key)와 값(value)을 쌍(pair)으로 저장하고 있기 때문에 iterator()를 직접 호출할 수 없고, 그대신 keySet()이나 entrySet()과 같은 메서드를 통해서 키와 값을 각각 따로 Set의 형태로 얻어온 후에 다시 iterator()를 호출해야 Iterator를 얻을 수 있다.

Map map  = new HashMap();
...
Iterator it = map.entrySet().iterator();

Iterator it = map.entrySet().iterator();는 아래의 두문장을 하나로 합친것이다.

Set eSet = map.entrySet();
Iterator it = eSet.iterator();

위 문장의 실행순서는

  1. map.entrySet()의 실행결과가 set이므로
  2. Set인스턴스.iterator();
  3. map.entrySet()으로 얻은 Set인스턴스의 iterator()를 호출해 Iterator인스턴스를 얻는다.
  4. Iterator it = Iterator인스턴스;
  5. 마지막으로 Iterator인스턴스의 참조가 it에 저장된다.
import java.util.*;
public class IteratorEx1 {

	public static void main(String[] args) {
		ArrayList list = new ArrayList();
		list.add("1");
		list.add("2");
		list.add("3");
		list.add("4");
		list.add("5");
		
		Iterator it = list.iterator();
		
		while(it.hasNext()){
			Object obj = it.next();
			System.out.println(obj);
		}

	}

}
=============================
1
2
3
4
5

List클래스들은 저장순서를 유지하기 때문에 Iterator를 이용해서 읽어 온 결과 역시 저장순서와 동일하지만 Set클래스들은 각 요소간의 순서가 유지되지 않기 때문에 Iterator를 이용해서 저장된 요소들을 읽어와도 저장된 순서와 같지 않다.

ListIterator와 Enumeration

Enumeration은 컬렉션 프레임웍이 만들어지기 전에 사용하던 것으로 Iterator의 구버전이다. 가능하면 Iterator를 사용하는 것이 좋다.

ListIterator는 Iterator를 상속받아서 기능을 추가한 것으로, 컬렉션의 요소에 접근할 때 Iterator는 단방향으로만 이동할 수 있는데 반해, ListIterator는 양방향으로의 이동이 가능하다.

다만 ArrayList나 LinkedList와 같이 List인터페이스를 구현한 컬렉션에서만 사용할 수 있다.

Emumeration : Iterator의 구버전

ListIterator : Iterator에 양방향 조회 기능 추가(List를 구현한 경우만 사용 가능)


Enumeration


boolean hasMoreElements() : 읽어 올 요소가 남아있는지 확인한다. 있으면 true, 없으면 false를 반환한다. Iterator의 hasNext()와 같다.


Object nextElement() : 다음 요소를 읽어온다. nextElement()를 호출하기전에 hasMoreElements()를 호출해서 읽어올 요소가 남아있는지 확인하는 것이 안전한다. Iterator의 next()와 같다.



ListIterator


void add(Obejct 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()를 먼저 호출한 다음에 이 메서드를 호출해야한다. (선택적 기능)


import java.util.*;

public class ListIteratorEx1 {

	public static void main(String[] args) {
		ArrayList list = new ArrayList();
		list.add("1");
		list.add("2");
		list.add("3");
		list.add("4");
		list.add("5");

		ListIterator it = list.listIterator();

		while (it.hasNext()) {
			System.out.print(it.next()); // 순방향 진행
		}
		System.out.println();

		while (it.hasPrevious()) {
			System.out.print(it.previous()); // 역방향
		}
		System.out.println();

	}

}
========================================
12345
54321

ListIterator의 사용방법을 보여주는 예제이다.

Iterator은 단방향으로만 이동하기 때문에 컬렉션의 마지막 요소에 다다르면 더 이상 사용할 수 없지만, ListIterator는 양방향으로 이동하기 때문에 각 요소간의 이동이 자유롭다.

다만 이동하기 전에 반드시 hasNext()나 hashPrevious()를 호출해서 이동할 수 있는지를 확인해야 한다.

위의 메서드 목록중 선택적 기능 이라고 표시된 것들은 반드시 구현하지 않아도 된다.

예를 들어 Iterator 인터페이스를 구현하는 클래스에서 remove()는 선택적인 기능이므로 구현하지 않아도 된다. 그렇다하더라도 인터페이스로부터 상속받은 메서드는 추상메서드라 메서드의 몸통(body)를 반드시 만들어 주어야 하므로 다음과 같이 처리한다.

public void remove(){
	throw new UnsupportedOperationException();
}

단순히 public void remove() {}; 와같이 구현하는 것보다는 이처럼 예외를 던져서 구현되지 않은 기능이라는 것을 메서드를 호출하는 쪽에 알리는 것이 좋다. 그렇지 않으면 호출하는 쪽에서 소스를 구해보기 전까지는 이 기능이 바르게 동작하지 않는 이유를 알 방법이 없다.

Iterator의 remove()는 단독으로 쓰일 수 없고, next()와 같이 써야한다. 특정 위치의 요소를 삭제하는 것이 아니라 읽어 온 것을 삭제한다. next()의 호출없이 remove()를 호출하면 , IllegalStateException이 발생한다.

email클라이언트에서 메일 서버에 있는 메일을 가져올 때 서버에 있는 메일을 읽어만 올 것인지, 메일을 가져오면서 서버에서 삭제할 것인지를 선택할 수 있따. 이와같은 기능을 구현하고자 할 때 쓸 목적으로 remove()를 정의해 놓은 것이다.

단순히 서버에서 읽어오기만 할 때는 next()를 사용하면 되고, 읽어 온 메일을 서버에 남기지 않고 지울 때는 next()와 함께 remove()를 사용하면 이와 같은 기능을 구현할 수 있다.

import java.util.*;
public class IteratorEx2 {

	public static void main(String[] args) {
		ArrayList original = new ArrayList(10);
		ArrayList copy1 = new ArrayList(10);
		ArrayList copy2 = new ArrayList(10);
		
		for(int i=0; i<10; i++)
			original.add(i+"");
		
		Iterator it = original.iterator();
		
		while(it.hasNext())
			copy1.add(it.next());
		
		System.out.println("Original에서 copy1로 복사(copy)");
		System.out.println("Original:"+original);
		System.out.println("copy1"+copy1);
		System.out.println();
		
		it = original.iterator(); //Iterator는 재사용이 불가, 다시 얻어와야 한다.

		while(it.hasNext()){
			copy2.add(it.next());
			it.remove();
		}
		
		System.out.println("Original에서 copy2로 이동(move)");
		System.out.println("Original:"+original);
		System.out.println("copy2"+copy2);
		System.out.println();
	}

}
===========================
Original에서 copy1로 복사(copy)
Original:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
copy1[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Original에서 copy2로 이동(move)
Original:[]
copy2[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
import java.util.*;

public class MyVector2 extends MyVector implements Iterator {
	int cursor  = 0;
	int lastRet = -1;
	
	public MyVector2(int capacity) {
		super(capacity);		
	}
	
	public MyVector2() {
		this(10);		
	}

	public String toString() {
		String tmp = "";
		Iterator it = iterator();

		for(int i=0; it.hasNext();i++) {
			if(i!=0) tmp+=", ";
			tmp += it.next(); 	// tmp += next().toString();
		}

		return "["+ tmp +"]";		
	}

	public Iterator iterator() {
		cursor=0;		// cursor와 lastRet를 초기화 한다.
		lastRet = -1;
		return this;		
	}	
	
	public boolean hasNext() {
	    return cursor != size();
	}
	
    public Object next(){
		Object next = get(cursor);
		lastRet = cursor++;
		return next;
    }
	
	public void remove() {
         // 더이상 삭제할 것이 없으면 IllegalStateException를 발생시킨다.
		if(lastRet==-1) {  
			throw new IllegalStateException();
		} else {
			remove(lastRet);
			cursor--;           // 삭제 후에 cursor의 위치를 감소시킨다.
			lastRet = -1;		// lastRet의 값을 초기화 한다.	
		}
	}		
} // class

cursor는 앞으로 읽어 올 요소의 위치를 저장하는데 사용되고, lastRet는 마지막으로 읽어온 요소의 위치(index)를 저장하는데 사용된다.

그래서 lastRet은 cursor보다 항상 1이 작은 값이 저장되고 remove()를 호출하면 이미 next()를 통해서 읽은 위치의 요소, 즉 lastRet에 저장된 값의 위치에 있는 요소를 삭제하고 lastRet의 값을 -1로 초기화 한다.

만일 next()를 호출하지 않고 remove()를 호출하면 lastRet의 값은 -1이 되어 IllegalStateException이 발생한다.

위의 코드에서보면 remove(lastRet)를 호출하여 lastRet에 위치에 있는 객체를 삭제한 다음에 cursor의 값을 감소시킨다. 그리고 lastRet의 값을 초기화(-1)한다.

그 이유는 remove메서드를 호출해서 객체를 삭제하고 나면, 삭제된 위치 이후의 객체들이 빈 공간을 채우기 위해 자동적으로 이동되기 때문에 cursor의 위치도 같이 이동시켜주어야 한다. 그리고 읽어온 요소가 삭제되었으므로 읽어온 요소의 위치를 저장하는 lastRet의 값은 -1로 초기화해야 한다. lastRet의 값이 -1이라는 것은 읽어온 값이 없다는 것을 의미한다.

import java.util.*;

public class MyVector2Test {

	public static void main(String[] args) {
			MyVector2 v = new MyVector2();
			v.add("0");
			v.add("1");
			v.add("2");
			v.add("3");
			v.add("4");
			
			System.out.println("삭제 전 : "+v);
			Iterator it = v.iterator();
			it.next();
			it.remove();
			it.next();
			it.remove();
			
			System.out.println("삭제후 : "+v);

	}

}
===========================
삭제 전 : [0, 1, 2, 3, 4]
삭제후 : [2, 3, 4]

MyVector2클래스를 테스트 하는 예제이다.

Arrays

Arrays클래스에는 배열을 다루는데 유용한 메서드가 정의되어 있다. 같은 기능의 메서가 배열의 타입만 다르게 오버로딩되어 있어서 많아 보이지만, 실제로는 그리 많지 않다.

아래는 Arrays에 정의된 toString()인데, 모든 기본형 배열과 참조형 배열 별로 하나씩 정의되어 있다.

  • Arrays에 정의된 메서드는 모두 static메서드이다.
static String toString(boolean[] a)
static String toString(byte[] a)
static String toString(char[] a)
static String toString(short[] a)
static String toString(int[] a)
static String toString(long[] a)
static String toString(float[] a)
static String toString(double[] a)
static String toString(Object[] a)

배열의 복사-copyOf(), copyOfRange()

copyOf()는 배열 전체를, copyOfRange()는 배열의 일부를 복사해서 새로운 배열을 만들어 반환한다. copyOfRange()에 지정된 범위의 끝은 포함되지 않는다.

int[] arr = {0,1,2,3,4};
int[] arr2 = Arrays.copyOf(arr,arr.length); //arr2 =[0,1,2,3,4]
int[] arr3= Arrays.copyOf(arr,3);  //arr3 = [0,1,2]
int[] arr4 = Arrays.copyOf(arr,7); //arr4 = [0,1,2,3,4,0,0]
int[] arr5 = Arrays.copyOfRange(arr, 2, 4); //arr5 =[2,3] <-4는 불포함
int[] arr6 = Arrays.copyOfRange(arr,0,7); //arr6 =[0,1,2,3,4,0,0]

배열 채우기 - fill(), setAll()

fill()은 배열의 모든 요소를 지정된 값을 채운다. setAll()은 배열을 채우는데 사용할 함수형 인터페이스를 매개변수로 받는다. 이 메서드를 호출할 때는 함수형 인터페이스를 구현한 객체를 매개변수로 지정하던가 아니면 람다식을 지정해야 한다.

int[] arr= new int[5];
Arrays.fill(arr,9);  // arr=[9,9,9,9,9]
Arrays.setAll(arr, () -> (int)(Math.random()*5)+1); //arr=[1,5,2,1,1]

위의 문장에 사용된 Arrays.setAll(arr, () -> (int)(Math.random()*5)+1)은 람다식(lambda expression)인데, 1~5의 범위에 속한 임의의 정수를 반환하는일을 한다.

그리고 setAll()메서드는 이 람다식이 반환한 임의의 정수로 배열을 채운다.

배열의 정렬과 탐색 -sort(), binarySearch()

sort()는 배열을 정렬할 때, 그리고 배열에 저장된 요소를 검색할 때는 binarySearch()를 사용한다. binarySearch()는 배열에서 지정된 값이 저장된 위치(index)를 찾아서 반환하는데, 반드시 배열이 정렬된 상태이어야 올바른 결과를 얻는다. 그리고 만일 검색한 값과 일치하는 요소들이 여러 개있다면, 이 중에서 어떤 것의 위치가 반환될지는 알 수 없다.

int[] arr= {3,2,0,1,4};
int idx = Arrays.binarySearch(arr, 2) : // idx = -5 <-잘못된 결과

Arrays.sort(arr); //배열 arr을 정렬한다.
System.out.println(Arrays.toString(arr));  //[0,1,2,3,4]
int idx =Arrays.binarySearch(arr, 2);  //idx=2 <- 올바른 결과

배열의 첫 번째 요소부터 순대대로 하나씩 검색하는 것을 순차 검색(linear search)이라고 하는데, 이 검색 방법은 배열이 정렬되어 필요는 없지만 배열의 요소를 하나씩 비교하기 때문에 시간이 많이 걸린다. 반면 이진 탐색(binary search)은 배열의 검색할 때 범위를 반복적으로 절반씩 줄여나가면서 검색하기 때문에 검색속도가 상당히 빠르다. 단 배열이 정렬이 되어 있는 경우에만 사용할 수 있다는 단점이 있다.

배열의 비교와 출력 -equals(), toString()

toString()은 배열의 모든 요소를 문자열로 편하게 출력할 수 있다.

toString()은 일차원 배열에만 사용할 수 있으므로, 다차원배열에는 deepTo String()을 사용해야 한다. deepToString()은 배열의 모든 요소를 재귀적으로 접근해서 문자열을 구성하므로 2차원뿐만 아니라 3차원 이상의 배열에도 동작한다.

int[] arr= {0,1,2,3,4};
int[][] arr2D = {{11,12}, {21,22}};

System.out.println(Arrays.toString(arr)); //[0,1,2,3,4]
System.out.println(Arrays.deepToString(arr2D)); //[[11,12], [21,22]]

equals()는 두 배열에 저장된 모든 요소를 비교해서 같으면 true, 다르면 false를 반환한다. equals()도 일차원 배열에만 사용가능하므로, 다차원 배열의 비교에는 deepEquals()를 사용해야 한다.

String[][] str2D = new String[][] {{"aaa","bbb"}, {"AAA","BBB"}};
String[][] str2D2 = new String[][] {{"aaa","bbb"}, {"AAA","BBB"}};

System.out.println(Arrays.equals(str2D, str2D2)); //false
System.out.println(Arrays.deepEquals(str2D, str2D2)); //true;

위와 같이 2차원 String배열을 equlas9)로 비교하면 배열에 저장된 내용이 같은데도 false를 결과로 얻는다. 다차원 배열은 배열의 배열의 형태로 구성하기 때문에 equals()로 비교하면 ,문자열을 비교하는 것이 아니라 배열에 저장된 배열의 주소를 비교하게 된다. 서로 다른 배열은 항상 주소가 다르므로 false를 결과로 얻은다.

배열을 List로 변환 -asList(Object ...a)

asList()는 배열을 List에 담아서 반환한다. 매개변수의 타입이 가변인수라서 배열 생성없이 저장할 요소들만 나열하는 것도 가능하다.

List list = Arrays.asList(new Integer[]{1,2,3,4,5});
List list = Arrays.asList(1,2,3,4,5);
list.add(6); // UnsupportedOperationException 예외발생

한 가지 주의할 점은 asList()가 반환한 List의 크기를 변경할 수 없다는 것이다. 즉 추가, 또는 삭제가 불가능하다. 저장된 내용은 변경가능하다. 만일 크기를 변경할 수 있는 List가 필요하다면 다음과 같이 하면된다.

List list= new ArrayList(Arrays.asList(1,2,3,4,5));

parallelXXX(), spliterator(), stream()

이 외에도 parallel로 시자갛는 이름의 메서드들이 있는데, 이 메서드들은 보다 빠른 결과를 얻기 위해 여러 쓰레드가 작업을 나누어 처리하도록 한다. spliterator()는 여러 쓰레드가 처리할 수 있게 하나의 작업을 여러 작업으로 나누는 Spliterator를 반환하며 stream()은 컬렉션을 스트림으로 변환한다.

import java.util.*;
public class ArraysEx {

	public static void main(String[] args) {
		int[] arr = {0,1,2,3,4};
		int[][] arr2D = {{11,12,13}, {21,22,23}};
		
		System.out.println("arr="+Arrays.toString(arr));
		System.out.println("arr2D="+Arrays.deepToString(arr2D));
		
		int[] arr2= Arrays.copyOf(arr,arr.length);
		int[] arr3 = Arrays.copyOf(arr, 3);
		int[] arr4 = Arrays.copyOf(arr, 7);
		int[] arr5= Arrays.copyOfRange(arr, 2, 4);
		int[] arr6= Arrays.copyOfRange(arr, 0, 7);
		
		System.out.println("arr2="+Arrays.toString(arr2));
		System.out.println("arr3="+Arrays.toString(arr3));
		System.out.println("arr4="+Arrays.toString(arr4));
		System.out.println("arr5="+Arrays.toString(arr5));
		System.out.println("arr6="+Arrays.toString(arr6));
		
		int[] arr7 = new int[5];
		Arrays.fill(arr7, 9); //arr=[9,9,9,9,9]
		System.out.println("arr7"+Arrays.toString(arr7));
		
		Arrays.setAll(arr7, i -> (int)(Math.random()*6)+1);
		System.out.println("arr7"+Arrays.toString(arr7));
		
		for(int i : arr7){
			char[] graph = new char[i];
			Arrays.fill(graph, '*');
			System.out.println(new String(graph)+i);
		}
		
		String[][] str2D = new String[][] {{"aaa","bbb"}, {"AAA","BBB"}};
		String[][] str2D2 = new String[][] {{"aaa","bbb"}, {"AAA","BBB"}};
		
		System.out.println(Arrays.equals(str2D, str2D2));  //false
		System.out.println(Arrays.deepEquals(str2D, str2D2)); //true
		
		char[] chArr ={'A','D','C','B','E'};
		
		System.out.println("chArr="+Arrays.toString(chArr));
		System.out.println("index of B ="+Arrays.binarySearch(chArr, 'B'));
		System.out.println("After Sorting");
		Arrays.sort(chArr);
		System.out.println("chArr="+Arrays.toString(chArr));
		System.out.println("index of B ="+Arrays.binarySearch(chArr, 'B'));
	}	

}
===================================
arr=[0, 1, 2, 3, 4]
arr2D=[[11, 12, 13], [21, 22, 23]]
arr2=[0, 1, 2, 3, 4]
arr3=[0, 1, 2]
arr4=[0, 1, 2, 3, 4, 0, 0]
arr5=[2, 3]
arr6=[0, 1, 2, 3, 4, 0, 0]
arr7[9, 9, 9, 9, 9]
arr7[4, 1, 2, 3, 5]
****4
*1
**2
***3
*****5
false
true
chArr=[A, D, C, B, E]
index of B =-2
After Sorting
chArr=[A, B, C, D, E]
index of B =1

'Back-end' 카테고리의 다른 글

11 컬렉션 프레임웍(5)  (0) 2021.07.09
11 컬렉션 프레임웍(4)  (0) 2021.07.08
11 컬렉션 프레임웍(2)  (0) 2021.07.06
03 SELECT문의 기본 형식  (0) 2021.07.06
11 컬렉션 프레임웍(1)  (0) 2021.07.06