최근 C++과 Java 두 언어를 살펴보면서 자료구조 라이브러리(각각 STL과 컬렉션 프레임워크)에서 느껴지는 차이점이 흥미로웠습니다. 🤩 언어별로 용어가 다르거나 인터페이스가 달라 헷갈리는 경우도 있었고, 알게된 것들을 더 잘 기억하기 위해 Java의 컬렉션(Collection) 프레임워크에 대해 간단하게 정리해보겠습니다 :)
Collection이란?
Java에서 컬렉션(Collection) 은 데이터(객체)를 담고 관리하는 그릇, 즉 컨테이너(container) 역할을 하는 패키지입니다.
자바의 컨테이너는 크게 List
계열, Set
계열, Map
계열 3가지로 구분할 수 있어요.
주의해야할 점은, List
와 Set
은 Collection의 하위 인터페이스지만 Map
은 둘과 구분되는 인터페이스라는 것입니다.
Map
은 Collection이 아니지만, Collection과 마찬가지로 데이터를 담는 역할을 하는 친구이기 때문에 함께 정리할게요.
Collection 프레임워크의 특징
다른 언어들과 비슷하게 자바의 컬렉션 프레임워크는 다음과 같은 특징을 가집니다
- 가변적인 크기: 배열과 달리 크기가 동적으로 변합니다.
- 표준화된 API: List, Set, Queue, Map 등 인터페이스가 표준화되어 있어 사용법이 일관적입니다.
- 검증 및 최적화: 이미 구현된 컬렉션 클래스는 검증과 최적화가 되어 있어 직접 자료구조를 구현할 필요가 없습니다.
- 멀티스레드 환경 지원: ConcurrentHashMap, CopyOnWriteArrayList 등 멀티스레드 환경에서 안전하게 사용할 수 있는 컬렉션 클래스도 제공됩니다.
Collection 계열
- Collection은 객체 단위로 저장한다는 특징이 있습니다. (Map은 Key-Value 단위 저장)
- java.util 패키지에서 제공합니다.
import java.util.Collection;
을 통해 사용할 수 있습니다.- 보통은
import java.util.List;
나import java.util.ArrayList;
처럼 하위 인터페이스나 구현 클래스를 더 자주 사용합니다.
List
List의 특징:
- 순서 있음
- 인덱스로 접근이 가능하고
- 순차적 삽입/삭제에 유리
- 중복 허용
- Iterable
- 순서 있음
대표 타입
- ArrayList: 동적 배열 기반
- LinkedList: 이중 연결 리스트 기반
- Vector: 동기화 지원, 동적 배열 기반 (레거시)
- Stack: LIFO 구조, Vector 상속 (레거시)
주요 메서드
add(E e)
: 요소 추가add(int index, E e)
: 지정 위치에 요소 추가get(int index)
: 인덱스로 요소 조회set(int index, E e)
: 지정 위치의 요소 교체remove(int index)
: 인덱스로 요소 삭제remove(Object o)
: 요소 삭제contains(Object o)
: 요소 포함 여부 확인size()
: 요소 개수 반환indexOf(Object o)
: 요소의 인덱스 반환iterator()
: 반복자 반환
Set
Set의 특징
- 순서 보장X
- 단, LinkedHashSet은 삽입 순서 보장
- TreeSet은 자동 정렬
- 중복 불가
- Set의 특징을 이용하여 중복 데이터 제거할 때 유용
- Iterable
- 순서 보장X
대표 타입
- HashSet: 해시 테이블 기반, 순서 보장X
- LinkedHashSet: 삽입 순서 보장
- TreeSet: 이진 탐색 트리(레드-블랙 트리) 기반, 오름차순 정렬 보장
주요 메서드
add(E e)
: 요소 추가 (중복 불가)remove(Object o)
: 요소 삭제contains(Object o)
: 요소 포함 여부 확인size()
: 요소 개수 반환iterator()
: 반복자 반환isEmpty()
: 비어있는지 확인
Queue
특징
- 일반적으로 FIFO(선입선출) 구조 (혹은 후입선출(LIFO))
- 일부는 우선순위 기반(PriorityQueue)
대표 타입
- LinkedList: Queue 인터페이스 구현 시 FIFO로 동작
- PriorityQueue: 우선순위 큐, 힙 기반
주요 메서드
offer(E e)
: 요소 추가poll()
: 첫 번째 요소 반환 및 삭제peek()
: 첫 번째 요소 조회 (삭제X)remove()
: 첫 번째 요소 반환 및 삭제 (비어있으면 예외 발생)element()
: 첫 번째 요소 조회 (비어있으면 예외 발생)size()
: 요소 개수 반환isEmpty()
: 비어있는지 확인
Deque
특징
- 양쪽에서 삽입/삭제 가능한 큐
대표 타입
- ArrayDeque: 배열 기반, 양쪽 입출력
- LinkedList: Deque 인터페이스 구현 시 양쪽 입출력 가능
주요 메서드
offerFirst(E e)
: 앞쪽에 요소 추가offerLast(E e)
: 뒤쪽에 요소 추가pollFirst()
: 앞쪽 요소 반환 및 삭제pollLast()
: 뒤쪽 요소 반환 및 삭제peekFirst()
: 앞쪽 요소 조회 (삭제X)peekLast()
: 뒤쪽 요소 조회 (삭제X)removeFirst()
: 앞쪽 요소 반환 및 삭제 (비어있으면 예외 발생)removeLast()
: 뒤쪽 요소 반환 및 삭제 (비어있으면 예외 발생)size()
: 요소 개수 반환isEmpty()
: 비어있는지 확인
Map 계열
Map의 특징
- 키를 기준으로 값을 저장하여 Key-Value 쌍을 저장
- Key는 중복 불가, Value는 중복 가능
- 조회 속도 빠름
- 키를 기준으로 값을 저장하여 Key-Value 쌍을 저장
대표 타입
- HashMap: 해시 테이블 기반, 순서 보장X
- LinkedHashMap: 삽입 순서 또는 접근 순서 보장
- TreeMap: 이진 탐색 트리 기반, 정렬 보장
- EnumMap: Enum 타입 키에 특화
- WeakHashMap: 약한 참조 기반
- IdentityHashMap: 객체 식별 기반
- Hashtable: 동기화 지원, (레거시)
주요 메서드:
put(K key, V value)
: 키-값 쌍 추가get(Object key)
: 키로 값 조회remove(Object key)
: 키로 값 삭제containsKey(Object key)
: 키 포함 여부 확인containsValue(Object value)
: 값 포함 여부 확인keySet()
: 키 집합 반환values()
: 값 집합 반환entrySet()
: 키-값 쌍 집합 반환size()
: 요소 개수 반환isEmpty()
: 비어있는지 확인
🤔 레거시(Legacy) 컬렉션은 무엇일까?
Java의 레거시 컬렉션이란, Java 2(JDK 1.2) 이후 등장한 **컬렉션 프레임워크(Collection Framework)**가 도입되기 전에 사용되던 구형 컬렉션 클래스를 의미합니다.
- Vector
- 동적 배열 기반의 자료구조로, 동기화(Synchronized) 지원
- List 인터페이스를 구현한 클래스(요소에 순서가 있고, 중복 허용)
- 내부적으로 모든 메서드가 동기화되어 있어, 멀티스레드 환경에서 안전하게 사용할 수 있음
- 하지만 오늘날에는
ArrayList
와Collections.synchronizedList()
조합을 더 많이 사용
[예시] Vector
Vector<String> vector = new Vector<>();
vector.add("apple");
vector.add("banana");
System.out.println(vector); // [apple, banana]
- Stack
- LIFO(Last In First Out, 후입선출) 구조의 자료구조
- Vector 클래스를 상속받아 구현됨
- push(), pop(), peek() 등 스택 연산 지원
- 현재는 Deque 인터페이스(예: ArrayDeque)를 사용하는 것을 권장
[예시] Stack
Stack<String> stack = new Stack<>();
stack.push("apple");
stack.push("banana");
System.out.println(stack.pop()); // banana
- Hashtable
- Key-Value 쌍을 저장하는 해시 기반의 자료구조
- 동기화(Synchronized) 지원
- Map 인터페이스를 구현한 클래스(키는 중복 불가, 값은 중복 가능)
- 현재는
HashMap
과ConcurrentHashMap이
더 많이 사용
[예시] Hashtable
Hashtable<String, Integer> table = new Hashtable<>();
table.put("apple", 1);
table.put("banana", 2);
System.out.println(table.get("apple")); // 1
- 문제점
- 성능 저하: 내부적으로 모든 메서드가 동기화되어 있어, 단일 스레드 환경에서는 불필요한 성능 저하가 발생할 수 있음
- 기능 제한: 컬렉션 프레임워크의 다양한 인터페이스와 기능을 지원하지 않음
- 유지보수 어려움: 레거시 코드는 최신 코드와 호환성이 떨어질 수 있음
- 대안
- Vector →
ArrayList
- 동기화가 필요하다면
Collections.synchronizedList(new ArrayList<>())
사용
- 동기화가 필요하다면
- Stack →
ArrayDeque
- ArrayDeque는 스택과 큐 모두로 활용 가능
- Hashtable →
HashMap
- 동기화가 필요하다면
ConcurrentHashMap
사용
- 동기화가 필요하다면
- Vector →
✅ 결론: 컬렉션 프레임워크의 ArrayList, ArrayDeque, HashMap, ConcurrentHashMap 사용을 권장합니다.
🤔 Collection과 Collections의 차이는 무엇일까?
Collections는 데이터를 담는 컨테이너가 아니라
컨테이너(List, Set, Map 등)를 조작하거나 생성하는 유틸 메서드 묶음 (쉽게 표현하자면 도구 상자⚒️)입니다.import java.util.Collection;
을 통해 사용할 수 있습니다.
[예시] Collections를 import해서 sort 메서드 사용
import java.util.Collections;
List<String> list = Arrays.asList("C", "A", "B");
Collections.sort(list);
✅ 즉, Collection은 컨테이너 모음, Collections는 컨테이너를 다루기 위한 도구 모음 패키지
🤔 Map은 Iterable할까?
자바에서 Map 인터페이스는 Iterable을 직접 구현하지 않습니다.
즉, Map
객체 자체로는 iterator()
메서드를 직접 호출할 수 없습니다.
Map은 Collection 계층이 아닌 별의 인터페이스이자 key-value 쌍을 저장하는 구조이기 때문입니다.
하지만 Map의 key, value, 또는 entry(키-값 쌍)를 Set이나 Collection 형태로 얻어온 후에는 Iterable을 사용할 수 있습니다.
예를 들어,
map.keySet()
: 키들의 집합(Set) 반환 → Iterablemap.values()
: 값들의 집합(Collection) 반환 → Iterablemap.entrySet()
: 키-값 쌍의 집합(Set) 반환 → Iterable
이렇게 얻은 Set이나 Collection 객체는 Iterable을 구현하므로, for-each문이나 Iterator를 사용해 순회할 수 있습니다.
[예시] Map으로부터 Iterable한 entrySet을 만든 경우
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.Map.Entry;
public class MapIterationExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 1. for-each 루프로 entrySet 순회
for (Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 2. Iterator로 entrySet 순회
Set<Entry<String, Integer>> entrySet = map.entrySet();
Iterator<Entry<String, Integer>> entryIterator = entrySet.iterator();
while (entryIterator.hasNext()) {
Entry<String, Integer> entry = entryIterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
✅ 결론: Map 자체는 Iterable이 아니지만, entrySet() 등으로 얻은 Set 객체는 Iterable합니다.
cf) C++의 std::map
은 직접적으로 “Iterable”이라는 인터페이스를 구현하는 것은 아니지만, STL 컨테이너들은 모두 반복자(iterator)를 제공하여 순회가 가능합니다.
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> myMap;
myMap["apple"] = 1;
myMap["banana"] = 2;
myMap["orange"] = 3;
// 범위 기반 for문 (C++11 이상)
for (const auto& pair : myMap) {
std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;
}
// 반복자를 이용한 순회
for (auto iter = myMap.begin(); iter != myMap.end(); ++iter) {
std::cout << "Key: " << iter->first << ", Value: " << iter->second << std::endl;
}
}
즉, Java의 Map과 다르게 별도의 변환이 없어도 C++의 map은 iterator나 범위 기반 for문을 통해 직접 순회가 가능하기 때문에 실질적으로 iterable하다고 할 수 있습니다. 🤩
글을 마치며
각 컬렉션 및 타입들의 주요 특징을 이해하면, 상황에 맞는 효율적인 코드를 쉽고 빠르게 작성할 수 있습니다. 공부한 내용을 정리하다 보니 글에 부족한 부분이나 잘못된 내용이 있을 수 있습니다. 잘못된 점이나 보완하면 좋을 점, 또는 궁금한 점이 있다면 언제든 댓글로 남겨주세요!
👀 Reference
C++과 Java를 비교하면서 설명하는 좋은 블로그를 발견해서 링크를 함께 남깁니다 :)
- Orientation: https://justkode.kr/java/cpp-to-java-0/
- Collecionts: https://justkode.kr/java/cpp-to-java-8/
GitHub Comments