JavaMedium#57
Java Collections Framework에서 List, Set, Map의 차이와 주요 구현체를 설명해주세요.
#Java#컬렉션#자료구조#핵심개념
힌트
순서, 중복, 키-값 구조의 차이를 생각해보세요.
정답 및 해설
Java Collections Framework에서 List, Set, Map의 차이와 주요 구현체를 설명해주세요.
Java Collections Framework는 데이터를 효율적으로 저장하고 조작하기 위한 자료구조 인터페이스와 구현체의 집합입니다. 크게 List, Set, Map 세 가지 인터페이스로 나뉘며, 각각 고유한 특성을 가집니다.
List — 순서 있음, 중복 허용
List는 삽입 순서를 유지하며 중복 요소를 허용합니다. 인덱스로 접근할 수 있습니다.
ArrayList
내부적으로 동적 배열을 사용합니다.
- 인덱스로 조회: O(1)
- 중간 삽입/삭제: O(n) — 이후 요소들을 이동해야 함
- 끝에 추가(
add(e)): 평균 O(1) (용량 초과 시 O(n) 재할당)
List<String> arrayList = new ArrayList<>();
arrayList.add("Java");
arrayList.add("Python");
arrayList.add("Java"); // 중복 허용
System.out.println(arrayList.get(0)); // "Java" — O(1) 조회
System.out.println(arrayList); // [Java, Python, Java]
// 초기 용량 지정 (재할당 비용 절감)
List<Integer> withCapacity = new ArrayList<>(100);
LinkedList
**이중 연결 리스트(Doubly Linked List)**를 사용합니다.
- 앞/뒤 삽입/삭제: O(1)
- 인덱스 조회: O(n) — 순차 탐색 필요
Deque인터페이스도 구현하여 큐/스택으로 활용 가능
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("Python"); // O(1)
linkedList.addLast("Java"); // O(1)
linkedList.add(1, "Kotlin"); // 중간 삽입
// Deque로 활용
Deque<String> deque = new LinkedList<>();
deque.push("first");
deque.push("second");
System.out.println(deque.pop()); // "second" — 스택처럼 동작
ArrayList vs LinkedList
| 연산 | ArrayList | LinkedList |
|---|---|---|
| 조회 (get) | O(1) | O(n) |
| 끝에 삽입 | O(1) 평균 | O(1) |
| 중간 삽입/삭제 | O(n) | O(1)* |
| 메모리 | 연속 배열 | 노드+포인터 |
| 캐시 효율 | 높음 | 낮음 |
*중간 삽입 자체는 O(1)이지만 위치 탐색에 O(n)이 소요됩니다.
Set — 순서 없음(예외 있음), 중복 불허
Set은 중복 요소를 허용하지 않습니다. 요소를 추가할 때 equals()와 hashCode()로 중복 여부를 판단합니다.
HashSet
내부적으로 HashMap을 사용합니다.
- 추가/삭제/포함 여부: O(1) 평균
- 순서 보장 없음
Set<String> hashSet = new HashSet<>();
hashSet.add("banana");
hashSet.add("apple");
hashSet.add("cherry");
hashSet.add("apple"); // 중복 — 무시됨
System.out.println(hashSet.size()); // 3
System.out.println(hashSet.contains("apple")); // true — O(1)
// 출력 순서 불규칙: [banana, cherry, apple] 등
LinkedHashSet
HashSet에 삽입 순서를 유지하는 연결 리스트가 추가됩니다.
- 추가/삭제/포함 여부: O(1) 평균
- 삽입 순서 보장
HashSet보다 약간 더 많은 메모리 사용
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("banana");
linkedHashSet.add("apple");
linkedHashSet.add("cherry");
System.out.println(linkedHashSet); // [banana, apple, cherry] — 삽입 순서 유지
TreeSet
내부적으로 **레드-블랙 트리(Red-Black Tree)**를 사용합니다.
- 추가/삭제/포함 여부: O(log n)
- 자연 정렬 순서 보장 (또는 Comparator 지정 가능)
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(1);
treeSet.add(3);
System.out.println(treeSet); // [1, 3, 5] — 정렬 순서
// 역순 정렬
Set<Integer> descSet = new TreeSet<>(Comparator.reverseOrder());
descSet.addAll(List.of(5, 1, 3));
System.out.println(descSet); // [5, 3, 1]
// 범위 조회 (TreeSet 특화 기능)
NavigableSet<Integer> nav = (NavigableSet<Integer>) treeSet;
System.out.println(nav.headSet(3)); // [1] — 3 미만
System.out.println(nav.tailSet(3)); // [3, 5] — 3 이상
Map — 키-값 쌍, 키는 중복 불허
Map은 키(key)와 값(value)의 쌍으로 데이터를 저장합니다. 키는 중복될 수 없으며, 값은 중복 가능합니다.
HashMap
- 조회/삽입/삭제: O(1) 평균
- 순서 보장 없음
null키 하나,null값 여러 개 허용- Java 8+: 해시 충돌이 많을 경우 연결 리스트 → 트리(TreeNode)로 전환
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Alice", 30);
hashMap.put("Bob", 25);
hashMap.put("Alice", 31); // 키 중복 — 값이 업데이트됨
System.out.println(hashMap.get("Alice")); // 31
System.out.println(hashMap.getOrDefault("Charlie", 0)); // 0 — 키 없을 때 기본값
// 편리한 조작 메서드
hashMap.putIfAbsent("Dave", 28);
hashMap.computeIfAbsent("Eve", k -> k.length()); // 키가 없으면 함수로 값 계산
LinkedHashMap
- 삽입 순서(또는 접근 순서) 보장
- LRU 캐시 구현에 활용 가능
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("banana", 1);
linkedHashMap.put("apple", 2);
linkedHashMap.put("cherry", 3);
// 삽입 순서 보장
linkedHashMap.forEach((k, v) -> System.out.println(k + ": " + v));
// banana: 1, apple: 2, cherry: 3
// LRU 캐시 구현
Map<String, Integer> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return size() > 3; // 최대 3개 유지
}
};
TreeMap
- 키의 자연 정렬 순서 보장
- 조회/삽입/삭제: O(log n)
- 범위 조회 기능 제공
TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("banana", 1);
treeMap.put("apple", 2);
treeMap.put("cherry", 3);
System.out.println(treeMap); // {apple=2, banana=1, cherry=3} — 키 정렬
System.out.println(treeMap.firstKey()); // apple
System.out.println(treeMap.lastKey()); // cherry
System.out.println(treeMap.subMap("apple", "cherry")); // {apple=2, banana=1}
ConcurrentHashMap
멀티스레드 환경에서 안전하게 사용할 수 있는 HashMap입니다.
- 세그먼트(Segment) 단위 잠금 (Java 8+: 버킷 단위 잠금)으로 높은 동시성 지원
Collections.synchronizedMap()보다 성능 우수null키/값 허용하지 않음
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// 멀티스레드 환경에서 안전
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int count = i;
executor.submit(() -> concurrentMap.put("key" + count, count));
}
// 원자적 연산
concurrentMap.putIfAbsent("key", 1);
concurrentMap.compute("key", (k, v) -> v == null ? 1 : v + 1);
전체 비교 정리
| 구분 | 구현체 | 순서 | 중복 | 시간복잡도 | 특징 |
|---|---|---|---|---|---|
| List | ArrayList | 삽입 순서 | 허용 | 조회 O(1) | 일반적인 목록 |
| List | LinkedList | 삽입 순서 | 허용 | 삽입/삭제 O(1) | 빈번한 삽입/삭제 |
| Set | HashSet | 없음 | 불허 | O(1) | 중복 제거 |
| Set | LinkedHashSet | 삽입 순서 | 불허 | O(1) | 순서 있는 중복 제거 |
| Set | TreeSet | 정렬 순서 | 불허 | O(log n) | 정렬된 집합 |
| Map | HashMap | 없음 | 키 불허 | O(1) | 일반적인 맵 |
| Map | LinkedHashMap | 삽입 순서 | 키 불허 | O(1) | LRU 캐시 |
| Map | TreeMap | 키 정렬 | 키 불허 | O(log n) | 정렬된 맵 |
| Map | ConcurrentHashMap | 없음 | 키 불허 | O(1) | 스레드 안전 |