컬렉션 프레임워크
컬렉션은 다수의 데이터, 프레임워크는 표준화된 프로그래밍 방식을 의미한다. 따라서 컬렉션 프레임워크란 데이터 그룹을 저장하는 클래스들을 표준화한 설계이다. (출처: 자바의 정석) 컬렉션 프레임워크를 활용하면 객체 지향적이고 재사용성이 높은 코드를 작성할 수 있다.
컬렉션 프레임워크의 주요 인터페이스로는 List, Set, Map이 있다. List와 Set은 공통된 부분이 많아 둘의 공통 메서드만 모아서 새로운 인터페이스인 컬렉션으로 정의한다. Map은 List와 Set와 달리 키와 값을 쌍으로 관리하는 구조라서 독립된 인터페이스이다. 따라서 위 상속계층도에 포함되지 못한다.
각 인터페이스의 특징과 구현 클래스는 아래와 같다.
인터페이스 | 특징 | 구현 클래스 |
---|---|---|
List | 순서 유지, 저장, 중복 저장 O | ArrayList, Vector, Stack, LinkedList 등 |
Set | 순서 유지, 저장, 중복 저장 X | HashSet, TreeSet 등 |
List | 키와 값을 쌍으로 저장, 순서 유지 X, 키 중복 저장 X | HashMap, Hashtable, TreeMap, Properties 등 |
List
<E>
List 인터페이스는 순서를 유지해주고, 저장과 중복 저장을 허용한다. 따라서 이러한 목적에 맞는 컬렉션을 구현할 때 사용된다. 정의된 메서드는 다음과 같다.
메서드 | 설명 |
---|---|
void add (int index, Object element) | 주어진 인덱스에 객체 추가 |
boolean addAll (int index, Collection c) | 주어진 인덱스에 컬렉션 추가 |
Object set (int index, Object element) | 지정된 위치(인덱스)에 객체 저장 |
Object get (int index) | 지정된 위치(인덱스)에 있는 객체 반환 |
int indexOf (Object o) / int lastIndexOf(Object o) | 지정된 객체의 위치(인덱스) 반환 (첫번째 요소부터 순방향 / 마지막 요소부터 역방향) |
ListIterator listIterator() / listIterator (int index) | List의 객체에 접근할 수 있는 ListIterator 반환 |
List subList (int fromIndex, int toIndex) | 지정된 범위에 있는 객체 반환 |
Object remove (int index) | 주어진 인덱스에 있는 객체를 삭제하고 지운 객체를 반환 |
boolean remove (Object o) | 주어진 객체 삭제 |
void sort (Comparator c) | 주어진 비교자(comparator)로 List 정렬 |
ArrayList는 List 인터페이스를 구현한 클래스로, 컬렉션 프레임워크에서 가장 많이 사용되는 컬렉션 클래스이다. 기존의 Vector를 개선하여서 구현원리나 기능이 비슷한데, Vector는 기존 소스와의 호환성을 위해 남겨 두고 있는 것이기 때문에 웬만하면 ArrayList를 사용하는 것이 권장된다.
ArrayList는 Object 배열을 이용해 데이터를 순차적으로 저장한다. 따라서 객체가 인덱스로 관리되는데, 배열은 생성할 때 크기가 고정되는 반면에 ArrayList는 배열의 저장 공간을 초과하게 되는 경우에 더 큰 배열을 생성해 기존 배열을 복사해서 새로이 사용하기 때문에 자동으로 용량을 늘려준다.
List<타입 파라미터> 객체명 = new ArrayList<타입 파라미터>(초기 저장용량);
List<Integer> number = new ArrayList<Integer>(); // 초기 용량은 객체 10개
List<String> name = new ArrayList<String>(50);
ArrayList의 생성 방법은 위와 같다. 저장할 객체 타입을 제네릭/타입 파라미터로 표기하고 기본 생성자로 호출해 만든다. 생성한 ArrayList에 객체를 추가하면 인덱스 0부터 순차적으로 저장된다. 중간의 객체를 제거하면, 바로 뒤의 객체부터 마지막 객체까지 모두 앞으로 1 인덱스씩 앞당겨진다. 따라서 객체를 자주 넣었다 빼게 되는 경우에는 ArrayList보다는 LinkedList를 쓰는 게 좋다.
public class ArrayListPractice {
public static void main(String[] args) {
List<String> fruit = new ArrayList<String>();
fruit.add("apple");
fruit.add("banana");
fruit.add("melon");
fruit.add("lemon");
int size = fruit.size();
for(int i = 0; i < size; i++) {
String str = fruit.get(i);
System.out.println(i + " : " + str);
}
fruit.remove(0);
}
}
// 출력
// 0 : apple
// 1 : banana
// 2 : melon
// 3 : lemon
Fruit라는 ArrayList를 만들고 객체를 추가해 인덱스값과 해당 인덱스에 저장된 객체를 출력하는 코드를 작성해보았다. 마지막에서는 0번 인덱스의 객체를 삭제했다.
LinkedList는 불연속적으로 저장된 데이터를 서로 연결한 형태로 구성된 자료구조이자 컬렉션이다. 자료구조의 기본인 배열이 가진 단점 중 크기를 변경할 수 없다는 점과 비순차적인 데이터를 삽입/제거하는데 시간이 많이 걸린다는 점을 보완하기 위해 고안되었다. 때문에 데이터를 효율적으로 추가, 삭제, 변경하기 위해 사용한다.
LinkedList의 각 요소(node)들은 연결된 요소의 주소값과 데이터로 구성되어 있다. 때문에 데이터를 지우고자 할 때, 이전 요소가 지우고자 하는 요소의 다음 요소를 참조하도록 변경해주면 된다. 배열처럼 데이터를 복사하고 이동할 필요가 없기 때문에 처리 속도가 빠르다.
💁♀️ ArrayList와 LinkedList의 차이?
컬렉션 | 읽기(접근시간) | 추가/삭제 | 비고 |
---|---|---|---|
ArrayList | 빠르다 | 느리다 | 순차적 추가삭제는 더 빠르다. 메모리를 비효율적으로 쓴다. |
LinkedList | 느리다 | 빠르다 | 데이터가 많을수록 접근성 떨어진다. |
Iterator 인터페이스는 자바 컬렉션 프레임워크에서 표준화된 컬렉션에 저장된 요소를 읽어오는 방법이다. Iterator 인터페이스는 각 요소에 접근하는 기능을 가졌고, Collection 인터페이스에는 Iterator를 반환하는 iterator() 메서드가 정의되어 있다. 이 메서드는 Collection 인터페이스에 정의되어 있기 때문에, List와 Set 인터페이스에도 포함되어 있다.
메서드 | 설명 |
---|---|
boolean hasNext() | 읽어 올 요소가 있는지 확인한다. 있다면 true, 없다면 false 반환. |
Object next() | 다음 요소 읽어오기. ( hasNext()로 확인 후 사용하는 것이 안전 ) |
void remove() | 읽어온 요소 삭제하기. ( next()로 읽어온 후 사용해야 함 ) |
Iterator<String> iterator = fruit.iterator();
while(iterator.hasNext()){
String str = iterator.next();
if(str.equals("banana")){
iterator.remove();
}
}
System.out.println(fruit.subList(0,2)); // 출력: [melon, lemon]
위에서 생성한 fruit ArrayList에 iterator로 banana와 일치하는 객체를 삭제하고, 0부터 2 인덱스까지의 값을 출력한 예제 코드이다.
Set
<E>
Set 인터페이스는 저장 순서를 유지하지 않고, 요소의 중복을 허용하지 않는다. Set 인터페이스를 구현하는 대표적인 클래스에는 HashSet, TreeSet이 있다.
HashSet은 Set 인터페이스를 구현한 가장 대표적인 컬렉션이다. HashSet에 새로운 요소를 추가할 때는 add 메서드나 addAll 메서드를 사용한다. 이미 저장된 요소를 또 추가하려고 하면, 이 메서드들이 false를 반환해 추가에 실패했다는 것을 알린다. 이 특성을 잘 이용하면 컬렉션 내 중복 저장을 쉽게 방지할 수 있다.
public class HashSetPractice {
public static void main(String[] args) {
HashSet<String> coffee = new HashSet<String>();
coffee.add("americano");
coffee.add("americano");
coffee.add("latte");
coffee.add("espresso");
coffee.add("cold brew");
Iterator it = coffee.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
// 출력:
// espresso
// americano
// cold brew
// latte
}
입력값과 출력값을 보면 중복 입력한 americano는 한 번만 출력되고, 출력 순서가 입력 순서와 다르다는 것을 확인할 수 있다. 저장 순서를 지키고자 한다면 LinkedHashSet을 이용해야 한다.
TreeSet은 이진 검색 트리(binary search tree) 형태로 데이터를 저장하는 컬렉션 클래스이다. Set 인터페이스의 특징인 중복 저장 비허용과 순서 유지 안 됨을 똑같이 가지고 있다.
이진 검색 트리는 여러 노드가 서로 연결된 구조인데, 한 노드에는 2개의 노드를 연결할 수 있다. 트리는 루트(root)라는 하나의 노드로부터 시작해 계속 확장할 수 있다. 한 노드와 노드로부터 확장된 두 노드는 서로 부모-자식 관계에 있는데, 부모로부터 오른쪽으로 확장된 자식 노드는 반드시 부모 노드의 값보다 커야 한다. 왼쪽으로 확장된 자식 노드는 반드시 부모 노드의 값보다 작아야 한다. 이진 검색 트리는 정렬과 검색(범위검색)에 특화되어 있으며, 비순차적으로 저장하기 때문에 노드의 추가와 삭제에 시간이 걸린다.
Comparator와 Comparable은 자바에서 제공하는 인터페이스로, 컬렉션 정렬에 필요한 메서드를 정의하고 있다. Arrays.sort() 메서드를 호출하면 알아서 배열 정렬을 하는 것 같지만, 사실은 Comparable 인터페이셔의 구현에 의해 정렬되었던 것이다.
public interface Comparator {
int compare(Object o1, Object o2);
boolean equals(Object obj);
}
public interface Comparable {
public int compareTo(Object o)
}
두 인터페이스의 실제 소스 코드는 위와 같다. compare()와 compareTo() 메서드는 형태가 조금 다르지만 기본적으로 두 객체를 비교하는 같은 기능을 목적으로 한다. compareTo()는 두 객체가 같으면 0, 비교하는 값보다 작으면 음수, 크면 양수를 int형으로 반환한다. compare()도 0, 음수, 양수를 반환하도록 구현해야 한다.
Comparable을 구현한 클래스는 기본적으로 오름차순으로 정렬되어 있다. 내림차순 혹은 다른 기준으로 정렬하고 싶다면 Comparator를 구현해 다른 정렬기준을 제공할 수 있다.
public class ComparatorPractice {
public static void main(String[] args) {
String[] arr = {"apple", "Banana", "melon", "grape"};
Arrays.sort(arr);
System.out.println("arr = "+Arrays.toString(arr));
Arrays.sort(arr, String.CASE_INSENSITIVE_ORDER);
System.out.println("arr = "+Arrays.toString(arr));
Arrays.sort(arr, new Descending());
System.out.println("arr = "+Arrays.toString(arr));
}
static class Descending implements Comparator {
public int compare(Object o1, Object o2) {
if(o1 instanceof Comparable && o2 instanceof Comparable) {
Comparable c1 = (Comparable) o1;
Comparable c2 = (Comparable) o2;
return c1.compareTo(c2)*-1;
}
return -1;
}
}
// 출력:
// arr = [Banana, apple, grape, melon]
// arr = [apple, Banana, grape, melon]
// arr = [melon, grape, apple, Banana]
위 코드를 통해 볼 수 있듯이, 첫번째 Arrays.sort() 메서드 호출해서 기본 정렬기준인 오름차순으로 정렬이 되었다. Comparator를 지정해주지 않았기 때문이다. 문자열의 오름차순 정렬은 공백 ➡️ 숫자 ➡️ 대문자 ➡️ 소문자 순 정렬이다. 정확히는 유니코드 작은 값 순에서 큰 값 순으로 가는 것이다.
CASE_INSENSITIVE_ORDER를 쓰면 대소문자를 구분하지 않고 정렬할 수 있다. 문자열 내림차순 구현은 compareTo() 결과에 -1을 곱하기만 하면 된다. 또는 비교 객체의 위치를 바꿔서 c2.compareTo(c1)
와 같이 해도 된다.
Map
<K, V>
Map 인터페이스는 키(key)와 값(value)로 구성된 Entry 객체를 저장하는 구조를 가지고 있다. 키와 값이라는 두 데이터를 연결하는 것이 매핑이다. 또한, 해싱(hashing)을 사용하기 때문에 방대한 양의 데이터를 검색하는데 있어서 뛰어난 성능을 보인다. Map 인터페이스를 구현하는 클래스는 HashMap, Hashtable, TreeMap, SortedMap 등이 있다. (Hashtable과 HashMap은 Vector와 ArrayList의 관계와 같아서 새로운 버전인 HashMap 사용을 권장한다.)
다음은 Map 인터페이스를 구현한 클래스에서 공통적으로 쓸 수 있는 메서드이다. List가 인덱스를 기준으로 관리되듯이, Map에서는 키(key)로 객체를 관리하기 때문에 매개값을 키로 갖는 메서드가 많다.
메서드 | 설명 |
---|---|
Object put(Object key, Object value) | Map에 value객체를 key객체에 연결(mapping)하여 저장 (새로운 키일 경우 null 리턴, 동일한 키가 있을 경우 값 대체하고 이전값 리턴) |
boolean containsKey(Object key) | 주어진 key객체와 일치하는 Map의 key객체가 있으면 true, 없으면 false 리턴 |
boolean containsValue(Object value) | 주어진 value객체와 일치하는 Map의 value객체가 있으면 true, 없으면 false 리턴 |
Set entrySet() | key-value쌍으로 구성된 모든 Map.Entry 타입의 객체를 Set에 담아서 리턴 |
Object get(Object key) | 주어진 key객체에 대응하는 value객체를 찾아서 반환 |
boolean isEmpty() | 컬렉션이 비어 있는지 확인 |
Set keySet() | Map에 저장된 모든 key를 Set 객체에 담아서 리턴 |
int size() | 저장된 key-value 쌍의 총 갯수 리턴 |
Collection values() | Map에 저장된 모든 value객체 반환 |
void clear() | 모든 Map.Entry(key-value) 삭제 |
Object remove(Object key) | 주어진 key객체와 일치하는 key-value 객체 삭제 |
HashMap은 Map 인터페이스를 구현하는 대표적인 클래스이다. HashMap이 데이터를 저장하는 방식은 아래 코드를 통해 차근차근 이해할 수 있다.
public class HashMapExample extends ... {
transient Entry[] table;
static class Entry implements Map.Entry {
final Object key;
Obeject value;
코드를 통해 알 수 있듯, HashMap은 Entry라는 내부 클래스를 정의하고, 다시 Entry 타입의 배열을 선언한다. key와 value는 연관된 값이기 때문에 하나의 클래스 내에서 하나의 배열로 다뤄 데이터 무결성을 지킨다.
HashMap은 키와 값을 각 Object 타입으로 저장한다. 여기서 키는 컬랙션 내의 키 중 유일한 것이어야 하고, 값은 키와 달리 데이터 중복이 허용된다. 키가 유일해야 하는 이유는 키로 값을 검색했을 때 그에 따른 검색 결과가 하나여야 하기 때문이다.
Map<Key, Value> map = new HashMap<Key, Value>();
HashMap을 생성하는 코드는 위와 같다. 키 타입과 값 타입 파리미터를 주고 기본 생성자를 호출하면 된다. 여기서 키와 값의 타입은 기본 타입(byte, short, int, float, double, boolean, char)을 사용할 수 없고 클래스 타입과 인터페이스 타입만 가능하다. 참고로 HashMap은 키와 값에 null 값을 허용하지만 Hashtable은 허용하지 않는다.
public class HashMapPractice {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("빨", 1);
map.put("주", 2);
map.put("노", 3);
map.put("초", 4);
map.put("파", 5);
map.put("남", 6);
map.put("보", 7);
System.out.println("총 entry 수 : " + map.size());
System.out.println("빨 : " + map.get("빨"));
Set<String> keySet = map.keySet();
Iterator<String> keyIterator = keySet.iterator();
while(keyIterator.hasNext()){
String key = keyIterator.next();
Integer value = map.get(key);
System.out.println(key + " : " + value);
}
}
}
// 출력:
//총 entry 수 : 7
//빨 : 1
//보 : 7
//빨 : 1
//노 : 3
//초 : 4
//남 : 6
//주 : 2
//파 : 5
String 타입 키와 Integer 타입 값으로 이뤄진 HashMap은 위와 같이 생성할 수 있다.
map. remove("보");
System.out.println("---");
System.out.println("총 entry 수 : " + map.size());
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
Iterator<Map.Entry<String, Integer>> entryIterator = entrySet.iterator();
while(entryIterator.hasNext()){
Map.Entry<String, Integer> entry = entryIterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " : "+ value);
}
// 출력:
// ---
//총 entry 수 : 6
//빨 : 1
//노 : 3
//초 : 4
//남 : 6
//주 : 2
//파 : 5
Map은 키와 값을 쌍으로 저장하기 때문에 iterator()
를 직접 호출할 수 없다. 때문에 keySet()
, entrySet()
와 같은 메서드를 이용해 Set 형태로 반환된 컬렉션에 iterator()
를 호출해야 한다.