컬렉션 프레임워크
자바 컬렉션 프레임워크(Collection Framework)는 자바 프로그래밍에서 데이터를 효율적으로 저장하고 관리하기 위한 표준화된 자료구조와 알고리즘의 집합이다. 배열과 달리 크기 제한 없이 동적으로 데이터를 다룰 수 있고, 검색, 정렬, 삽입, 삭제와 같은 다양한 기능을 내장하고 있다.
자바 컬렉션 프레임워크는 크게 인터페이스와 구현 클래스로 구성된다. 인터페이스는 각 자료구조가 가져야 할 공통된 동작 방식을 정의하여 일관된 API를 제공하며, 구현 클래스는 이러한 인터페이스를 실제 자료구조에 맞게 구체적으로 구현한다. 주요 인터페이스로는 모든 컬렉션의 최상위 인터페이스인 Collection, 순서가 보장되고 중복을 허용하는 List, 중복을 허용하지 않는 Set, FIFO 방식의 Queue, 그리고 키와 값의 쌍을 저장하는 Map 등이 있다. 이들 인터페이스 각각은 다양한 구현체를 통해 구체적인 동작 방식과 성능 특성을 제공한다.
인터페이스 특징 주요 구현체
주요 구현체 | 특징 | 주요 구현체 |
Collection | 모든 컬렉션의 최상위 인터페이스 | - |
List | 순서가 있고 중복을 허용하는 컬렉션 | ArrayList, LinkedList, Vector |
Set | 중복 불가, 순서 없음 또는 정렬 가능 | HashSet, LinkedHashSet, TreeSet |
Queue | FIFO 기반, 작업 대기열 | LinkedList, PriorityQueue, ArrayDeque |
Deque | 양쪽 끝에서 삽입, 삭제 가능 | ArrayDeque, LinkedList |
Map | 키-값 쌍 저장, 키 중복 불가 | HashMap, LinkedHashMap, TreeMap, Hashtable |
List 인터페이스
List 인터페이스는 순서가 정해진 데이터의 집합을 나타내며, 인덱스를 통해 각 요소에 접근할 수 있다. List는 중복 요소를 허용하기 때문에 같은 값을 여러 번 저장할 수 있으며, 요소를 삽입하거나 삭제할 때 위치를 지정할 수 있다. 대표적인 구현체인 ArrayList는 내부적으로 동적 배열을 사용하여 인덱스 기반 접근이 빠르고 메모리 접근이 효율적이다. 하지만 중간에 요소를 삽입하거나 삭제할 때는 배열 뒤쪽 데이터를 이동시켜야 하므로 비용이 발생한다. 반면 LinkedList는 양방향 연결 리스트로 구현되어 있어 요소의 삽입과 삭제가 빈번한 상황에서 유리하지만, 인덱스 기반 접근은 느리다. 또한 LinkedList는 Queue와 Deque 인터페이스도 구현하여 큐나 덱으로도 활용 가능하다.
ArrayList
ArrayList는 동적 배열 기반의 List 구현체로, 인덱스를 통한 빠른 접근이 가능하다. 내부적으로 배열을 사용하여 요소를 순차적으로 저장하며, 배열 크기가 부족해지면 자동으로 크기를 늘린다. 이 때문에 읽기 작업이 많고 인덱스 기반 접근이 필요한 경우에 효율적이다. 하지만 배열 크기 조정과 중간 요소 삽입 및 삭제 시 배열의 뒤쪽 데이터를 이동해야 하므로 이러한 작업은 상대적으로 비용이 크다. 따라서 데이터가 자주 변경되지 않는 상황에서 주로 사용된다.
import java.util.ArrayList; import java.util.List; public class ArrayListExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Java"); list.add("Spring"); list.add("Hibernate"); // 인덱스 접근 System.out.println(list.get(1)); // Spring // 요소 삽입 list.add(1, "JPA"); System.out.println(list); // [Java, JPA, Spring, Hibernate] // 요소 삭제 list.remove("Hibernate"); System.out.println(list); // [Java, JPA, Spring] } }
LinkedList
LinkedList는 이중 연결 리스트 기반의 List 구현체로, 각 요소가 이전과 다음 요소를 가리키는 노드로 연결되어 있다. 이 구조 덕분에 중간 위치에 요소를 삽입하거나 삭제할 때 빠른 성능을 제공한다. 반면 임의의 인덱스에 직접 접근하는 경우에는 처음부터 순회해야 하기 때문에 속도가 느리다. 또한 LinkedList는 Deque 인터페이스도 구현하여 큐와 스택처럼도 사용할 수 있어 다양한 용도로 활용된다.
import java.util.LinkedList; import java.util.List; public class LinkedListExample { public static void main(String[] args) { LinkedList<String> linkedList = new LinkedList<>(); linkedList.add("First"); linkedList.addLast("Last"); linkedList.addFirst("Start"); System.out.println(linkedList); // [Start, First, Last] // 큐로서 사용 String head = linkedList.poll(); System.out.println("Poll: " + head); // Poll: Start System.out.println(linkedList); // [First, Last] } }
Set 인터페이스
Set은 중복을 허용하지 않는 데이터 집합을 나타내며, 요소 간에 순서를 보장하지 않는 경우가 많다. 대표적인 구현체인 HashSet은 내부적으로 해시 함수를 사용해 데이터를 저장한다. 해시 알고리즘 덕분에 검색과 삽입 속도가 매우 빠르지만, 저장된 요소의 순서는 보장되지 않는다. 입력된 순서대로 순회를 해야 하거나 특정 순서대로 정렬된 결과가 필요한 경우에는 LinkedHashSet이나 TreeSet을 사용한다. LinkedHashSet은 입력된 순서를 유지하며, TreeSet은 요소를 정렬된 상태로 저장하는데, 이를 위해 요소는 반드시 Comparable 인터페이스를 구현하거나 Comparator를 제공해야 한다.
HashSet
HashSet은 내부적으로 해시 테이블을 이용하여 데이터를 저장하는 Set 구현체로, 중복을 허용하지 않고 빠른 탐색과 삽입이 가능하다. 해시 함수를 통해 요소를 분산시켜 저장하기 때문에 대부분의 연산이 평균적으로 O(1) 시간에 수행된다. 하지만 요소들이 저장되는 순서는 유지되지 않으며, 순서가 중요할 경우 LinkedHashSet 같은 다른 구현체를 사용해야 한다.
import java.util.HashSet; import java.util.Set; public class HashSetExample { public static void main(String[] args) { Set<String> set = new HashSet<>(); set.add("apple"); set.add("banana"); set.add("apple"); // 중복 무시 System.out.println(set); // [banana, apple] (순서 다를 수 있음) } }
LinkedHashSet
LinkedHashSet은 HashSet의 특성을 가지면서도 요소가 입력된 순서를 유지하는 Set 구현체다. 내부적으로 해시 테이블과 이중 연결 리스트를 사용하여 중복 없이 데이터를 저장하며, 순서가 유지되어 출력이나 순회 시 예측 가능한 결과를 제공한다.
import java.util.LinkedHashSet; import java.util.Set; public class LinkedHashSetExample { public static void main(String[] args) { Set<String> set = new LinkedHashSet<>(); set.add("java"); set.add("python"); set.add("c++"); set.add("java"); // 중복 요소, 무시됨 System.out.println("LinkedHashSet 출력 (입력 순서 유지):"); for (String lang : set) { System.out.println(lang); } } }
TreeSet
TreeSet은 이진 탐색 트리 구조를 기반으로 한 Set 구현체로, 저장되는 요소가 자동으로 정렬되어 유지된다. 요소는 반드시 Comparable 인터페이스를 구현하거나 생성자에 Comparator를 제공해야 하며, 이를 통해 범위 탐색과 정렬된 순회가 가능하다. 일반적인 해시 기반 집합보다 탐색과 삽입 비용이 높지만, 순서가 중요한 경우에 적합하다.
import java.util.TreeSet; public class TreeSetExample { public static void main(String[] args) { TreeSet<Integer> treeSet = new TreeSet<>(); treeSet.add(30); treeSet.add(10); treeSet.add(20); System.out.println(treeSet); // [10, 20, 30] } }
Queue & Deque 인터페이스
Queue는 FIFO(First-In-First-Out) 방식을 따르는 자료구조로, 먼저 들어온 데이터가 먼저 처리된다. PriorityQueue는 우선순위 큐로, 요소들이 자연 정렬 순서나 사용자 정의 Comparator에 따라 정렬되어 우선순위가 높은 요소가 먼저 처리된다. 따라서 작업 스케줄링이나 이벤트 처리 등에서 유용하게 쓰인다. 한편, ArrayDeque는 배열을 기반으로 하는 덱(Deque) 구현체로, 양쪽 끝에서 삽입과 삭제가 가능하여 스택과 큐 모두로 활용할 수 있다. 다만 이들 자료구조는 기본적으로 스레드 안전하지 않으므로 멀티스레드 환경에서는 주의해야 한다.
PriorityQueue
PriorityQueue는 내부적으로 힙(Heap) 구조를 사용하여 요소를 저장하는 큐 구현체로, 각 요소가 우선순위에 따라 정렬된다. 기본적으로 최소 힙을 사용하여 우선순위가 가장 높은(가장 작은) 요소가 먼저 꺼내지며, 이를 통해 작업 스케줄링, 이벤트 처리 등에서 우선순위 기반 처리를 쉽게 구현할 수 있다. Comparator를 통해 우선순위 기준을 사용자 정의할 수도 있다.
import java.util.PriorityQueue; public class PriorityQueueExample { public static void main(String[] args) { PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(30); pq.offer(10); pq.offer(20); while (!pq.isEmpty()) { System.out.println(pq.poll()); // 10, 20, 30 순서대로 출력 } } }
ArrayDeque
ArrayDeque는 배열 기반의 덱(Deque, Double Ended Queue) 구현체로, 양쪽 끝에서 삽입과 삭제가 가능하다. 스택과 큐의 기능을 모두 제공하며, 성능이 뛰어나고 메모리 사용 효율도 좋다. 단, 스레드 안전하지 않으므로 멀티스레드 환경에서는 주의가 필요하다.
import java.util.ArrayDeque; import java.util.Deque; public class ArrayDequeExample { public static void main(String[] args) { Deque<String> deque = new ArrayDeque<>(); // 덱의 앞쪽에 요소 추가 deque.addFirst("first"); // 덱의 뒤쪽에 요소 추가 deque.addLast("last"); System.out.println("Deque 상태: " + deque); // 앞쪽 요소 제거 String front = deque.pollFirst(); System.out.println("앞쪽에서 꺼낸 요소: " + front); System.out.println("Deque 상태 (앞쪽 제거 후): " + deque); // 뒤쪽 요소 제거 String back = deque.pollLast(); System.out.println("뒤쪽에서 꺼낸 요소: " + back); System.out.println("Deque 상태 (뒤쪽 제거 후): " + deque); } }
Map 인터페이스
Map은 키와 값의 쌍으로 데이터를 저장하는 구조로, 키는 중복이 허용되지 않으며 각 키는 유일하다. 값은 중복 가능하다. 대표 구현체인 HashMap은 해시 테이블을 기반으로 하여 빠른 검색과 삽입이 가능하며, null 키와 null 값을 허용한다. 다만, 저장된 데이터의 순서를 보장하지 않는다. 만약 입력된 순서대로 데이터 순회를 해야 한다면 LinkedHashMap을 사용하고, 키를 정렬된 상태로 유지해야 한다면 TreeMap을 선택한다. TreeMap은 내부적으로 이진 탐색 트리를 사용하기 때문에 키가 Comparable을 구현하거나 Comparator를 제공해야 하며, 정렬 기준에 따라 요소가 자동으로 정렬된다.
HashMap
HashMap은 키와 값의 쌍으로 데이터를 저장하는 Map 구현체로, 내부적으로 해시 테이블을 사용하여 키를 해시 값으로 분산시켜 빠른 검색과 삽입을 지원한다. 키는 중복이 불가능하며, 값은 중복 가능하다. 해시 충돌이 발생할 수 있지만, 효율적인 해시 함수와 버킷 구조 덕분에 대부분의 연산이 평균적으로 O(1) 시간 내에 수행된다. 다만 저장 순서는 유지하지 않는다.
import java.util.HashMap; import java.util.Map; public class HashMapExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("Alice", 30); map.put("Bob", 25); map.put("Charlie", 35); System.out.println(map.get("Bob")); // 25 // 키 존재 여부 확인 if (map.containsKey("Alice")) { System.out.println("Alice is " + map.get("Alice") + " years old."); } // 반복문으로 Map 탐색 for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " : " + entry.getValue()); } } }
LinkedHashMap
LinkedHashMap은 HashMap의 확장판으로, 키와 값의 쌍을 저장하면서 입력된 순서 또는 최근 접근한 순서를 유지한다. 이는 캐시 구현 시 많이 활용되는데, 예를 들어 LRU(Least Recently Used) 캐시를 쉽게 만들 수 있다. 내부적으로 해시 테이블과 이중 연결 리스트를 함께 사용하여 순서를 관리한다. 일반 HashMap과 마찬가지로 키는 중복될 수 없고, 값은 중복 허용한다.
import java.util.LinkedHashMap; import java.util.Map; public class LinkedHashMapExample { public static void main(String[] args) { // 기본 생성자는 입력 순서 유지 LinkedHashMap<String, Integer> map = new LinkedHashMap<>(); map.put("apple", 3); map.put("banana", 2); map.put("orange", 5); System.out.println("LinkedHashMap 출력 (입력 순서 유지):"); for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " : " + entry.getValue()); } } }
TreeMap
TreeMap은 키를 자동으로 정렬하여 저장하는 Map 구현체다. 내부적으로 이진 탐색 트리(레드-블랙 트리)를 사용한다. 키는 반드시 Comparable 인터페이스를 구현하거나 생성자에 Comparator를 제공해야 하며, 키가 정렬된 상태로 유지되어 범위 탐색이나 정렬된 순회가 필요할 때 유용하다.
import java.util.Map; import java.util.TreeMap; public class TreeMapExample { public static void main(String[] args) { TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.put("banana", 2); treeMap.put("apple", 5); treeMap.put("orange", 3); System.out.println("TreeMap 출력 (키 정렬됨):"); for (Map.Entry<String, Integer> entry : treeMap.entrySet()) { System.out.println(entry.getKey() + " : " + entry.getValue()); } } }
컬렉션 활용 유틸리티 클래스
자바는 Collections라는 유틸리티 클래스를 제공하여 컬렉션을 쉽게 다룰 수 있도록 돕는다. 예를 들어, Collections.sort() 메서드를 통해 List를 손쉽게 정렬할 수 있으며, 동기화가 필요한 컬렉션에 대해서는 Collections.synchronizedList()와 같이 스레드 안전한 래퍼를 제공한다. 또한, Java 9 이상에서는 List.of()와 같은 메서드를 통해 불변(immutable) 컬렉션을 생성할 수 있어 변경 불가능한 데이터를 안전하게 다룰 수 있다.
- sort(List<T> list) – 정렬
- reverse(List<?> list) – 역순 정렬
- shuffle(List<?> list) – 무작위 섞기
- max(Collection<? extends T>), min(Collection<? extends T>) – 최댓값/최솟값
- frequency(Collection<?> c, Object o) – 특정 요소가 얼마나 포함되어있는지
- swap(List<?> list, int i, int j) – 두 인덱스 요소 교체
- copy(List<? super T> dest, List<? extends T> src) – 리스트 복사
- synchronizedList(List<T> list) – 스레드 안전 리스트 생성
- unmodifiableList(List<? extends T>) – 변경 불가능한 리스트 생성
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CollectionsSortExample { public static void main(String[] args) { List<String> names = new ArrayList<>(); names.add("Charlie"); names.add("Alice"); names.add("Bob"); Collections.sort(names); System.out.println(names); // [Alice, Bob, Charlie] } } // 동기화 컬렉션 생성 List<String> syncList = Collections.synchronizedList(new ArrayList<>()); // 불변 컬렉션 생성 (Java 9 이상) List<String> immutableList = List.of("A", "B", "C");
Comparator와 Comparable
Comparator와 Comparable은 Java에서 객체를 정렬할 때 사용하는 인터페이스이지만, 사용하는 방식과 목적이 다르다. Comparable은 클래스 내부에서 자기 자신과 다른 객체를 비교하는 방법을 정의하는 인터페이스로, compareTo() 메서드를 구현해야 한다. 예를 들어 Student 클래스가 Comparable<Student>를 구현하면, Student 객체들끼리 정렬할 수 있게 된다. 이때 정렬 기준은 클래스 안에 고정된다.
public class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public int compareTo(Person other) { return Integer.compare(this.age, other.age); // 나이순 정렬 } }
public class Main { public static void main(String[] args) { List<Person> people = new ArrayList<>(); people.add(new Person("철수", 25)); people.add(new Person("영희", 20)); people.add(new Person("민수", 30)); // Comparable 기준 정렬 (나이순) Collections.sort(people); System.out.println("Comparable을 사용한 기본 정렬 (나이순):"); for (Person p : people) { System.out.println(p); } } }
반면 Comparator는 클래스 외부에서 정렬 기준을 별도로 정의할 수 있는 인터페이스로, compare() 메서드를 구현하여 객체들을 비교한다. Comparator는 여러 정렬 기준이 필요할 때 유용하며, 하나의 클래스에 대해 여러 개의 비교 전략을 만들 수 있다는 점에서 유연하다.
import java.util.Comparator; public class NameComparator implements Comparator<Person> { @Override public int compare(Person p1, Person p2) { return p1.name.compareTo(p2.name); // 이름순 정렬 } }
public class Main { public static void main(String[] args) { List<Person> people = new ArrayList<>(); people.add(new Person("철수", 25)); people.add(new Person("영희", 20)); people.add(new Person("민수", 30)); // Comparator 기준 정렬 (이름순) Collections.sort(people, new NameComparator()); System.out.println("Comparator를 사용한 이름순 정렬:"); for (Person p : people) { System.out.println(p); } } }
요약하자면 Comparable은 "자기 자신이 스스로 정렬 기준을 정함", Comparator는 "외부에서 다른 정렬 기준을 주입"하는 방식이다.
블로그의 정보
Ayden's journal
Beard Weard Ayden