컬렉션

컬렉션(Collection)이란 같은 타입의 레퍼런스를 여러개 저장하기 위한 자바 라이브러리이다.
"배열과 비슷한데 훨씬 더 편리하다." 라는 정도로 접근하자.
다음 그림은 컬렉션 관련 주요 인터페이스의 계층관계를 보여 준다.
Collection Framework
컬렉션 클래스를 선택할 때 다음을 고려하자.1

  • Set - 중복을 허용하지 않고 순서도 가지지 않는다.
  • List - 중복을 허용하고 순서를 가진다.
  • Map - key 와 value의 형태로 저장한다.

다음은 자주 사용되는 컬렉션 클래스이다.
자바 2이후의 6개의 클래스와 자바 2이전의 2개 클래스를 보여준다.

인터페이스 구현 클래스(자바 2) 구현 클래스(자바 2이전)
Set HashSet
TreeSet
List ArrayList Vector
LinkedList
Map HashMap Properties
TreeMap

이들 컬렉션 클래스들을 아래에서 예제로 다룬다.

컬렉션 클래스 예제

Set

예제는 Set 인터페이스의 사용법을 보여 주고 있다.
HashSet을 생성하고 Set인터페이스의 add 메소드를 사용하여 이름을 추가한다.
양효선은 중복 추가를 시도하고 있는데 Set은 중복을 허용하지 않으므로 추가되지 않는다.
System.out.println(set);로 확인할 수 있다.2
System.out.println(set);다음에 기존의 HashSet을 TreeSet으로 처리한다.
TreeSet은 데이터 정렬하여 저장한다.

SetExample.java
package net.java_school.collection;

import java.util.*;

public class SetExample {
	public static void main(String args[]) {
	  
		Set set = new HashSet();
		set.add("양효선");
		set.add("홍용표");
		set.add("황진호");
		set.add("김동진");
		set.add("전경수");
		set.add("양효선");
		    
		System.out.println(set);
		    
		Set sortedSet = new TreeSet(set);
		System.out.println(sortedSet);
	}
}

다음은 제네릭을 사용하여 변경한 코드다.

SetExample.java - 제네릭을 사용
package net.java_school.collection;

import java.util.*;

public class SetExample {
	public static void main(String args[]) {
	  
		Set<String> set = new HashSet<String>();
		set.add("양효선");
		set.add("홍용표");
		set.add("황진호");
		set.add("김동진");
		set.add("전경수");
		set.add("양효선");
		    
		System.out.println(set);
		    
		Set<String> sortedSet = new TreeSet<String>(set);
		System.out.println(sortedSet);
	}
}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.SetExample
[홍용표, 김동진, 전경수, 양효선, 황진호]
[김동진, 양효선, 전경수, 홍용표, 황진호]

예제에서 다룬 컬렉션 클래스를 자바 문서에서 찾아보면 클래스 선언에 <E>, <T>, <K, V>를 볼 수 있다.
이런 인터페이스, 추상클래스, 클래스를 제네릭(Generic)이라 한다.
제네릭은 자바 5에 도입되었다.
<E>는 Element, <T>는 Type, <K, V> 는 Key, Value 의미한다.
이 기호를 이용하여 정해지지 않은 데이터 타입을 선언할 수 있다.
정해지지 않는 데이터 타입은 제네릭으로부터 객체가 생성될 때 결정된다.
다음은 계좌 클래스의 계좌 번호를 제네릭으로 만든 예다.

package net.java_school.collection;

public class Account<T> {
	
	private T accountNo;//accountNo는 어떤 타입도 될 수 있다.
	
	public T getAccountNo() {
		return accountNo;
	}

	public void setAccountNo(T accountNo) {
		this.accountNo = accountNo;
	}

	public static void main(String[] args) {
		Account<String> ac1 = null;
		ac1 = new Account<String>();//계좌번호 데이터 타입은 String으로 결정
		ac1.setAccountNo("111-222-333");//문자열만 가능
		
		Account<Integer> ac2 = null;
		ac2 = new Account<Integer>();//계좌번호 데이터 타입은 Integer로 결정
		ac2.setAccountNo(111222333);//Integer만 가능(아래 래퍼클래스 참조)
	}

}

List

List는 Collection인터페이스를 상속하며, 순서가 있고 중복을 허락한다.
List는 배열과 같이 방마다 차례대로 0부터 시작하는 인덱스 번지가 주어진다.
다음 예제는 가장 많이 사용되고 있는 ArrayList에 대한 예제이다.

ArrayListExample.java
package net.java_school.collection;

import java.util.ArrayList;

public class ArrayListExample {

	public static void main(String[] args) {
		ArrayList a = new ArrayList();
		
		a.add("장길산");
		a.add("홍길동");
		
		String hong = (String) a.get(1);//캐스팅 필요하다.
		System.out.println(hong);
		
		//모든 요소를 출력하려면
		for (Object name : a) {//자바 5에 도입된 '확장 for 문'
			System.out.print(name +"\t");
		}
	}

}

다음은 제네릭 ArrayList<E>를 사용한 코드이다.
제네릭을 사용하면 특정 타입 레퍼런스만 저장하게 되므로 값을 가져올 때 타입 캐스팅이 필요없다는 사실에 주목해야 한다.3

ArrayListExample.java - 제네릭을 사용
package net.java_school.collection;

import java.util.ArrayList;

public class ArrayListExample {

	public static void main(String[] args) {
		ArrayList<String> a = new ArrayList<String>();
		
		a.add("장길산");
		a.add("홍길동");
		
		String hong = a.get(1);//캐스팅이 필요없다.
		System.out.println(hong);
		
		//'확장for문'에서 name의 데이터타입을 String둘 수 있다.
		for (String name : a) {
			System.out.print(name +"\t");
		}
	}

}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.ArrayListExample
홍길동
장길산	홍길동

아래 예제는 List의 구현체중에 ArrayList와 LinkedList의 사용법을 비교하여 보여주고 있다.

ListExample.java
package net.java_school.collection;

import java.util.*;

public class ListExample {
	public static void main(String args[]) {
		List list = new ArrayList();
		    
		list.add("A");
		list.add("B");
		list.add("C");
		list.add("D");
		list.add("E");
		
		System.out.println(list);
		System.out.println("2: " + list.get(2));
		System.out.println("0: " + list.get(0));
		
		LinkedList linkedList = new LinkedList();
		
		linkedList.addFirst("A");
		linkedList.addFirst("B");
		linkedList.addFirst("C");
		linkedList.addFirst("D");
		linkedList.addFirst("E");
		    
		System.out.println(linkedList);
		linkedList.removeLast();
		linkedList.removeLast();
		    
		System.out.println(linkedList);
	    
	}
}

다음은 제네릭을 사용하여 변경한 코드이다.

ListExample.java
package net.java_school.collection;

import java.util.*;

public class ListExample {
	public static void main(String args[]) {
		List<String> list = new ArrayList<String>();
		    
		list.add("A");
		list.add("B");
		list.add("C");
		list.add("D");
		list.add("E");
		
		System.out.println(list);
		System.out.println("2: " + list.get(2));
		System.out.println("0: " + list.get(0));
		
		LinkedList<String> linkedList = new LinkedList<String>();
		
		linkedList.addFirst("A");
		linkedList.addFirst("B");
		linkedList.addFirst("C");
		linkedList.addFirst("D");
		linkedList.addFirst("E");
		    
		System.out.println(linkedList);
		linkedList.removeLast();
		linkedList.removeLast();
		    
		System.out.println(linkedList);
	    
	}
}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.ListExample
[A, B, C, D, E]
2: C
0: A
[E, D, C, B, A]
[E, D, C]

Map

Map은 키(key)와 값(value)의 쌍으로 데이터를 저장한다.
다음 예제는 HashMap을 사용하고 있다.
끝부분에서 HashMap을 TreeMap으로 처리한다.
TreeMap은 데이터를 키값으로 정렬한다.

MapExample.java
package net.java_school.collection;

import java.util.*;

public class MapExample {
	public static void main(String args[]) {
	
		Map map = new HashMap();
		
		map.put("1", "양효션");
		map.put("2", "홍용표");
		map.put("3", "황진호");
		map.put("4", "김동진");
		map.put("5", "전경수");
		
		System.out.println(map);
		System.out.println((String) map.get("4"));//캐스팅 필요
		
		Map sortedMap = new TreeMap(map);
		System.out.println(sortedMap);
	
	}
}

다음은 제네릭 HashMap<K,V>과 TreeMap<K,V>을 사용하여 변경한 코드이다.

MapExample.java - 제네릭 사용
package net.java_school.collection;

import java.util.*;

public class MapExample {
	public static void main(String args[]) {
	
		Map<String,String> map = new HashMap<String,String>();
		
		map.put("1", "양효션");
		map.put("2", "홍용표");
		map.put("3", "황진호");
		map.put("4", "김동진");
		map.put("5", "전경수");
		
		System.out.println(map);
		System.out.println(map.get("4"));//캐스팅 필요없다!
		
		Map<String,String> sortedMap = new TreeMap<String,String>(map);
		System.out.println(sortedMap);
	
	}
}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.MapExample
{3=황진호, 2=홍용표, 1=양효션, 5=전경수, 4=김동진}
김동진
{1=양효션, 2=홍용표, 3=황진호, 4=김동진, 5=전경수}

예제를 아래와 같이 바꾸어 테스트한다. Integer는 int에 대응하는 래퍼(Wrapper) 클래스이다. Integer 타입의 키값을 주면 HashMap도 정렬이 된다.

MapExample.java
package net.java_school.collection;

import java.util.*;

public class MapExample {
	public static void main(String args[]) {
	
		Map<Integer,String> map = new HashMap<Integer,String>();
		
		map.put(1, "양효션");
		map.put(2, "홍용표");
		map.put(3, "황진호");
		map.put(4, "김동진");
		map.put(5, "전경수");
		
		System.out.println(map);
		System.out.println(map.get(4));
		
		Map<Integer,String> sortedMap = new TreeMap<Integer,String>(map);
		System.out.println(sortedMap);
	
	}
}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.MapExample
{1=양효션, 2=홍용표, 3=황진호, 4=김동진, 5=전경수}
김동진
{1=양효션, 2=홍용표, 3=황진호, 4=김동진, 5=전경수}

Vector

과거에 자주 쓰였던 Vector에 관한 예제다. 현재는 Vector 대신에 ArrayList가 더 많이 사용되고 있다.4

VectorExample.java
package net.java_school.collection;

import java.util.*;

public class VectorExample {

	public static void main(String[] args) {
		Vector v = new Vector();
		    
		for (int i = 0; i < 10; i++) {
			v.addElement(String.valueOf(Math.random() * 100));
		}
		    
		for (int i = 0; i < 10; i++) {
			System.out.println(v.elementAt(i));//Object 타입 레퍼런스 반환
		}
	}
  
}

다음은 제네릭 Vector<E>을 사용하여 변경한 코드이다.

VectorExample.java - 제네릭 사용
package net.java_school.collection;

import java.util.*;

public class VectorExample {
	public static void main(String[] args) {
	
		Vector<String> v = new Vector<String>();
	
		for (int i = 0; i < 10; i++) {
			v.addElement(String.valueOf(Math.random() * 100));
		}
		
		for (int i = 0; i < 10; i++) {
			System.out.println(v.elementAt(i));//String 타입 레퍼런스 반환
		}
	}
  
}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.VectorExample
64.93767837163008
1.7024404924644077
56.445592597123806
23.41304656773643
92.55620070095163
41.6525553754475
47.39373268828609
83.84855063525016
67.34657837510855
41.04715452201211

Properties

자바에서 설정 파일로부터 값을 읽을 때 많이 사용하는 클래스이다.
키와 값의 쌍으로 데이터를 저장한다.

PropertiesStore.java

package net.java_school.collection;

import java.util.*;
import java.io.*;

public class PropertiesStore {
	public static void main(String[] args) {
	
		Properties prop = new Properties();
		prop.put("name", "장길산");
		prop.put("address", "황해도 구월산");
		
		try {
			prop.store(new FileOutputStream("test.properties"),"My Favorite Bandit");
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}
}
PropertiesLoad.java
package net.java_school.collection;

import java.util.*;
import java.io.*;

public class PropertiesLoad {
	public static void main(String[] args) {
	
		Properties prop = new Properties();
		try {
			prop.load(new FileInputStream("test.properties"));
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
		System.out.println(prop.getProperty("name"));
		System.out.println(prop.getProperty("address"));
	}
}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.PropertiesStore

C:\java\Collection\bin>java net.java_school.collection.PropertiesLoad
장길산
황해도 구월산

PropertiesStore를 실행하면 파일시스템에 test.properties파일이 만들어진다.5
파일을 열어보면 다음과 같다.

test.properties
#My Favorite Bandit
#Thu Apr 10 13:07:41 KST 2014
address=\uD669\uD574\uB3C4 \uAD6C\uC6D4\uC0B0
name=\uC7A5\uAE38\uC0B0

예상과 달리 한글 부분이 이상한 문자로 되어 있다.
자바 프로그램의 설정 데이터를 위해 만든 자바 프러퍼티에는 비영어권을 위한 배려는 없다.
프로퍼티 파일에서 한글은 자바에서 사용하는 유니 코드로 저장되어야 한다.

Enumeration 인터페이스

열거 형태로 저장된 객체를 처음부터 끝까지 차례로 조회하는데 유용한 인터페이스이다.
메소드는 다음 2개가 전부이다.

hasMoreElements()
nextElement()

아래 코드 조각은 Vector<E> v 의 모든 요소를 출력한다.

for (Enumeration<E> e = v.elements(); e.hasMoreElements();) {
  System.out.println(e.nextElement());
}

이 코드를 이용하여 우리의 벡터 예제를 수정하면 다음과 같다.
성능으로만 보면 이 예제는 전 예제보다 떨어진다.

VectorExample.java - 제네릭, Enumeration 사용
package net.java_school.collection;

import java.util.*;

public class VectorExample {
	public static void main(String[] args) {
	
		Vector<String> v = new Vector<String>();
	
		for (int i = 0; i < 10; i++) {
			v.addElement(String.valueOf(Math.random() * 100));
		}
		
		for (Enumeration<String> e = v.elements(); e.hasMoreElements();) {
			System.out.println(e.nextElement());
		}
	}
  
}

Iterator 인터페이스

Collection인터페이스의 iterator() 메소드는 Iterator를 반환한다.6
Iterator는 Enumeration인터페이스와 비슷하나 Enumeration보다 나중에 만들어졌다.
Enumeration보다 메소드명이 간단하며 Enumeration과는 달리 값을 삭제하는 메소드가 추가되어 있다.

hasNext()
next()
remove()

래퍼(Wrapper) 클래스

컬렉션은 레퍼런스만 담을 수 있고 기본 타입 값은 담을 수 없다.
기본 타입 값을 컬렉션에 담으려면 래퍼 클래스를 이용해야 한다.
모든 기본 타입에 대응하는 래퍼 클래스가 존재한다.
기본 타입 값을 멤버 변수에 저장하고 값 주위로 값을 가공하는 메소드들이 감싸고 있다고 해서 래퍼(Wrap:감싸다)클래스라고 불린다.

기본 타입 래퍼 클래스
boolean Boolean
byte Byte
char Character
short Short
int Integer
long Long
float Float
double Double
IntegerExample.java
package net.java_school.collection;

public class IntegerExample {

	public static void main(String[] args) {
		Integer a = new Integer(2000000000);//20억
		int intValue = a.intValue();
		System.out.println(intValue);

		byte byteValue = a.byteValue();
		System.out.println(byteValue);
		
		short shortValue = a.shortValue();
		System.out.println(shortValue);
		
		long longValue = a.longValue();
		System.out.println(longValue);
		
		float floatValue = a.floatValue();
		System.out.println(floatValue);
		
		double doubleValue = a.doubleValue();
		System.out.println(doubleValue);
		
		String strValue = a.toString();
		System.out.println(strValue);

		System.out.println(Integer.MAX_VALUE);
		System.out.println(Integer.MIN_VALUE);
		System.out.println(Integer.parseInt("1004"));

		/* 
		* 아래 코드는 컴파일러에 의해 
		* Integer b = new Integer(200000000);로 바뀐다. 
		* 이를 오토박싱이라고 한다.
		* 타입 캐스팅이 아니다. 기본 타입이 레퍼런스 타입으로 바뀌는 캐스팅은 없다.
		*/
		Integer b = 2000000000;
		
		/* 
		 * == 은 항상 값이 같은가를 묻는다. 
		 * 레퍼런스가 올 때는 같은 객체인지를 판단한다.
		*/
		if (a == b) {
			System.out.println("a == b true");
		} else {
			System.out.println("a == b false");
		}
		
		/* 
		 * a와 b가 같은 int값을 가지고 있는지 판단하기 위해선 Integer의 equals() 메소드를 사용한다.
		 * Integer의 equals 메소드는 관리하는 int값이 같은지를 판단하도록 Object의 equals 메소드를 오버라이딩했다.
		 if (obj instanceof Integer) {
		    return value == ((Integer)obj).intValue();
		 }
		 return false;  
		 */
		if (a.equals(b)) {
			System.out.println("a.equals(b) true");
		} else {
			System.out.println("a.equals(b) false");
		}
		
		
		/*
		 * a와 b가 가진 값을 다양하게 판단하기 위해선
		 * Integer의 compareTo() 메소드를 이용한다.
		 */
		int check = a.compareTo(b);
		System.out.println(check);
		if (check == 0) {
			System.out.println("a(int) == b(int)");
		} else if (check < 0) {
			System.out.println("a(int) < b(int)");
		} else {
			System.out.println("a(int) > b(int)");
		}
		
		/*
		 * 오토박싱의 또다른 예제
		 * equals 메소드의 아규먼트는 new Integer(c)의 레퍼런스로
		 * 컴파일 단계에서 변경된다.
		 */
		int c = 2000000000;
		if (a.equals(c)) {
			System.out.println("a.equals(c) true");
		} else {
			System.out.println("a.equals(c) false");
		}
		
		
		/*
		 * 오토언박싱
		 * a가 참조하는 Integer 객체안에 있는
		 * int값의 복사본이 객체 밖으로 나와 d에 대입
		 * 이 역시 컴파일 단계에서 
		 * int d = a.intValue();로 변경된다. 
		 */
		int d = a;
		System.out.println(d);
		
		
		/*
		* obj에는 1를 안에 내포하는 Integer 레퍼런스가 할당
		* 컴파일 단계에서 Object obj = new Integer(1);로 변경
		* Object타입의 레퍼런스로 Integer 고유의 메소드를 호출할 수 없다.
		* 호출하려면 타입 캐스팅이 필요하다.
		*/
		Object obj = 1;
		System.out.println(obj);
		System.out.println(((Integer)obj).intValue());
		
	}

}
C:\ Command Prompt
C:\java\Collection\bin>java net.java_school.collection.IntegerExample
2000000000
0
-27648
2000000000
2.0E9
2.0E9
2000000000
2147483647
-2147483648
1004
a == b false
a.equals(b) true
0
a(int) == b(int)
a.equals(c) true
2000000000
1
1

유리로 사면이 막힌 상자에 동전을 넣고 다시 밖으로 꺼내는 마술이 있다.
비유하면 상자 안에 동전을 넣는 것이 오토박싱이고, 상자 밖으로 동전을 꺼내는 것이 오토언박싱이다.
오토박싱(AutoBoxing)과 오토언박싱(AutoUnboxing)은 개발상 편의를 위해 만들어졌다.

주석
  1. 컬렉션 클래스는 Set 인터페이스를 구현했거나 List 인터페이스를 구현했거나 Map 인터페이스를 구현한 것으로 나뉜다.
  2. HashSet의 toString()이 어떻게 오버라이딩하고 있는지 결과를 보면 확인할 수 있다.
    Set은 저장한 값이 순서가 없으므로 인덱스를 주고 값을 반환하는 메소드가 없다.
    저장한 값을 하나씩 가져올 수는 있는데 뒤에 학습할 Enumeration 또는 Iterator 인터페이스의 구현체를 반환하는 메소드를 이용해야 한다.
  3. 자바 5 이전 컬렉션 클래스에서 값을 추가하는 메소드는 모두 파라미터 타입이 Object이다.
    모든 레퍼런스를 저장할 수 있다고 하여 마치 자바의 강점처럼 설명되었다.
    하지만 다양한 타입 레퍼런스를 컬렉션에 저장하는 경우가 많지 않을 뿐 아니라, 저장하더라도 값을 가져올 때 필요한 타입 캐스팅이 귀찮은 작업이라는 사실을 경험으로 깨닫게 되었다.
  4. ArrayList와 Vector는 큰 차이가 있다.
    Vector는 스레드 안전Thread Safe하다.
    반면, ArrayList는 그렇치 않다.
    스레드 안전과 스레드 안전하지 않음은 성능상에 차이가 크다.
    따라서 스레드 안전함을 선택할 때는 그만한 이유가 있어야 한다.
    대부분의 경우 스레드 안전하지 않음을 선택하는 것이 옳다.
    참고로, JDBC에서 다루게 되는 사용자 정의 JDBC 커넥션 풀링 소스는 Vector를 사용한다.
  5. 이때 이클립스로 실행하는 경우와 콘솔에서 실행하는 경우의 파일 위치가 다르다.
    이클립스에서 실행하면 프로젝트 루트 디렉토리에 생성된다.
    모호하다고 생각되면 FileOutputStream과 FileInputStream의 생성자에 파일 시스템의 전체 경로를 전달하면 확실해 진다.
    new FileOutputStream("C:/java/Collection/test.properties"), new FileInputStream("C:/java/Collection/test.properties")
  6. Set 이나 List 인터페이스를 구현한 모든 클래스에는 iterator() 메소드를 가진다.
    Set과 List 인터페이스는 모두 Collection 인터페이스를 상속하기 때문이다.
    iterator() 메소드의 반환 타입은 Iterator이다.
    물론 Iterator가 인터페이스이기 때문에, 실제로는 Iterator를 구현한 클래스로부터 생성된 객체가 반환된다.
    하지만 반환 타입이 구체적으로 어떤 클래스인지 관심을 가질 필요가 없다.
    반환 타입이 Iterator 인터페이스를 구현하고 있다는 것으로 정보는 충분하기 때문이다.
참고