안녕하세요! 오늘은 자바 컬렉션 프레임워크의 중요한 자료구조들인 Map, Stack, Queue, 그리고 Deque에 대해 자세히 알아보겠습니다. 각 자료구조의 특징과 활용법, 그리고 실제 코드 예시를 통해 언제 어떤 자료구조를 사용해야 하는지 명확하게 이해할 수 있도록 설명드리겠습니다.
목차
1. Map
Map은 키(Key)와 값(Value) 형태로 데이터를 저장하는 자료구조입니다. 각 키는 유일성을 가지며, 값은 중복이 가능합니다.
주요 특징
- 키-값 쌍: 데이터를 키-값 쌍으로 저장
- 키 유일성: 키는 중복될 수 없음
- 값 중복 가능: 값은 중복 허용
- 순서 보장 없음: 일반 HashMap의 경우 순서 보장 안함 (LinkedHashMap은 예외)
주요 메서드
- put(key, value): 키-값 쌍 추가, dart의 add와 같은 기능
- putIfAbsent(key, value): 해당 키가 없는 경우에만 값 추가
- get(key): 키에 해당하는 값 반환
- containsKey(key): 키 존재 여부 확인
- containsValue(value): 값 존재 여부 확인 (성능이 좋지 않음)
- keySet(): 모든 키를 Set으로 반환
- values(): 모든 값을 Collection으로 반환
- entrySet(): 모든 키-값 쌍을 Set으로 반환
구현체
- HashMap: 가장 많이 사용되는 Map 구현체, 해시 알고리즘 기반
- LinkedHashMap: 삽입 순서를 유지
- TreeMap: 키의 자연 순서대로 정렬
예시 코드
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapExample {
public static void main(String[] args) {
// HashMap 예시
Map<String, Integer> studentScores = new HashMap<>();
// 데이터 추가
studentScores.put("김철수", 85);
studentScores.put("박영희", 92);
studentScores.put("이민수", 78);
// 이미 존재하는 키의 경우 값 덮어쓰기
studentScores.put("김철수", 88);
// 존재하지 않는 경우에만 추가
studentScores.putIfAbsent("정지훈", 90);
studentScores.putIfAbsent("김철수", 77); // 이미 존재하므로 무시됨
System.out.println("학생 점수표: " + studentScores);
// 값 조회
System.out.println("박영희의 점수: " + studentScores.get("박영희"));
System.out.println("홍길동의 점수: " + studentScores.get("홍길동")); // 존재하지 않으면 null 반환
// 키 존재 여부 확인
System.out.println("이민수 존재 여부: " + studentScores.containsKey("이민수"));
// 모든 키 출력
System.out.println("모든 학생 이름: " + studentScores.keySet());
// 모든 값 출력
System.out.println("모든 점수: " + studentScores.values());
// for-each로 맵 순회
System.out.println("\n== 학생별 점수 ==");
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// LinkedHashMap 예시 (삽입 순서 유지)
Map<String, String> linkedMap = new LinkedHashMap<>();
linkedMap.put("첫번째", "First");
linkedMap.put("두번째", "Second");
linkedMap.put("세번째", "Third");
System.out.println("\nLinkedHashMap(삽입 순서 유지): " + linkedMap);
// TreeMap 예시 (키 기준 정렬)
Map<String, String> treeMap = new TreeMap<>();
treeMap.put("Z", "Zebra");
treeMap.put("A", "Apple");
treeMap.put("B", "Banana");
System.out.println("TreeMap(키 기준 정렬): " + treeMap);
}
}
2. Map vs Set
Map과 Set은 모두 해시 테이블을 기반으로 하지만, 사용 목적과 구조에 차이가 있습니다.
주요 차이점
특성MapSet
저장 형태 | 키-값 쌍 | 단일 값 |
중복 | 키는 중복 불가, 값은 중복 가능 | 모든 요소 중복 불가 |
주요 구현체 | HashMap, LinkedHashMap, TreeMap | HashSet, LinkedHashSet, TreeSet |
내부 구현 | 해시 테이블 | Map 기반 (HashSet은 HashMap 사용) |
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class MapVsSetExample {
public static void main(String[] args) {
// Map 예시
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
ageMap.put("Charlie", 22);
// Set 예시
Set<String> nameSet = new HashSet<>();
nameSet.add("Alice");
nameSet.add("Bob");
nameSet.add("Charlie");
nameSet.add("Alice"); // 중복값은 무시됨
System.out.println("Map 크기: " + ageMap.size()); // 3
System.out.println("Set 크기: " + nameSet.size()); // 3 (중복이 제거됨)
// Map은 키로 조회 가능
System.out.println("Bob의 나이: " + ageMap.get("Bob"));
// Set은 존재 여부만 확인 가능
System.out.println("Bob이 Set에 있는가? " + nameSet.contains("Bob"));
}
}
3. Entry Set
Map의 entrySet() 메서드는 Map.Entry 객체의 Set을 반환합니다. 각 Entry는 키와 값을 가지고 있는 객체입니다.
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class EntrySetExample {
public static void main(String[] args) {
Map<String, String> countryCapitals = new HashMap<>();
countryCapitals.put("대한민국", "서울");
countryCapitals.put("일본", "도쿄");
countryCapitals.put("중국", "베이징");
countryCapitals.put("미국", "워싱턴 D.C.");
// Entry 객체를 통해 키와 값에 접근
Set<Map.Entry<String, String>> entries = countryCapitals.entrySet();
System.out.println("국가 및 수도 목록:");
for (Map.Entry<String, String> entry : entries) {
System.out.println(entry.getKey() + "의 수도는 " + entry.getValue());
}
// 람다식을 사용한 방법 (Java 8 이상)
System.out.println("\n람다식 사용:");
countryCapitals.forEach((country, capital) ->
System.out.println(country + "의 수도는 " + capital)
);
}
}
4. Stack
Stack은 한쪽 끝에서만 데이터의 삽입과 삭제가 이루어지는 LIFO(Last In First Out) 구조의 자료구조입니다.
주요 특징
- LIFO: 가장 나중에 들어간 데이터가 가장 먼저 나옴
- push/pop: 데이터 삽입은 push, 데이터 꺼내기는 pop으로 표현
- Legacy: 자바의 Stack 클래스는 Vector를 상속한 레거시 클래스
참고: 자바에서는 Stack 클래스 대신 Deque 인터페이스의 구현체(ArrayDeque)를 사용하는 것이 권장됩니다.
예시 코드
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
// 기존 Stack 클래스 사용 (권장하지 않음)
Stack<String> oldStack = new Stack<>();
oldStack.push("첫번째");
oldStack.push("두번째");
oldStack.push("세번째");
System.out.println("기존 Stack 내용: " + oldStack);
System.out.println("pop: " + oldStack.pop()); // 세번째
System.out.println("peek: " + oldStack.peek()); // 두번째 (꺼내지 않고 조회만)
// 권장: Deque 인터페이스 사용
Deque<String> stack = new ArrayDeque<>();
stack.push("첫번째");
stack.push("두번째");
stack.push("세번째");
System.out.println("\nArrayDeque을 스택으로 사용:");
System.out.println("스택 내용: " + stack);
System.out.println("pop: " + stack.pop()); // 세번째
System.out.println("peek: " + stack.peek()); // 두번째
// 스택 응용: 괄호 검사
System.out.println("\n괄호 검사:");
System.out.println("((){}[]) 유효한가? " + isBalanced("((){}[])"));
System.out.println("({)} 유효한가? " + isBalanced("({)}"));
}
// 스택을 활용한 괄호 균형 검사 함수
public static boolean isBalanced(String expr) {
Deque<Character> stack = new ArrayDeque<>();
for (char ch : expr.toCharArray()) {
if (ch == '(' || ch == '{' || ch == '[') {
stack.push(ch);
} else if (ch == ')' || ch == '}' || ch == ']') {
if (stack.isEmpty()) {
return false;
}
char top = stack.pop();
if ((ch == ')' && top != '(') ||
(ch == '}' && top != '{') ||
(ch == ']' && top != '[')) {
return false;
}
}
}
return stack.isEmpty();
}
}
5. Queue
Queue는 데이터를 순서대로 처리하는 FIFO(First In First Out) 구조의 자료구조입니다.
주요 특징
- FIFO: 먼저 들어간 데이터가 먼저 나옴
- offer/poll: 데이터 삽입은 offer, 데이터 꺼내기는 poll로 표현
- 선입선출: 입력 순서대로 처리해야 하는 작업에 적합
예시 코드
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
// LinkedList로 Queue 구현
Queue<String> queue1 = new LinkedList<>();
queue1.offer("첫번째 고객");
queue1.offer("두번째 고객");
queue1.offer("세번째 고객");
System.out.println("LinkedList 큐: " + queue1);
System.out.println("첫 번째 고객 응대: " + queue1.poll());
System.out.println("다음 고객 확인: " + queue1.peek());
System.out.println("남은 고객 수: " + queue1.size());
// ArrayDeque로 Queue 구현
Queue<String> queue2 = new ArrayDeque<>();
queue2.offer("태스크 1");
queue2.offer("태스크 2");
queue2.offer("태스크 3");
System.out.println("\nArrayDeque 큐: " + queue2);
// 모든 태스크 처리
System.out.println("\n태스크 처리:");
while (!queue2.isEmpty()) {
String task = queue2.poll();
System.out.println(task + " 처리 중...");
}
// 프린터 대기열 시뮬레이션
Queue<PrintJob> printQueue = new LinkedList<>();
printQueue.offer(new PrintJob("보고서.docx", 5));
printQueue.offer(new PrintJob("사진.jpg", 10));
printQueue.offer(new PrintJob("발표자료.pptx", 7));
System.out.println("\n프린터 대기열 처리:");
processPrintJobs(printQueue);
}
static class PrintJob {
String filename;
int pages;
PrintJob(String filename, int pages) {
this.filename = filename;
this.pages = pages;
}
}
static void processPrintJobs(Queue<PrintJob> queue) {
int totalTime = 0;
while (!queue.isEmpty()) {
PrintJob job = queue.poll();
int printTime = job.pages * 1; // 1초당 1페이지 가정
totalTime += printTime;
System.out.println(job.filename + " 인쇄 중... (" + job.pages +
"페이지, " + printTime + "초 소요)");
}
System.out.println("모든 인쇄 작업 완료. 총 " + totalTime + "초 소요.");
}
}
6. Deque
Deque(Double-Ended Queue)는 양쪽 끝에서 데이터의 삽입과 삭제가 가능한 자료구조입니다. Stack과 Queue의 기능을 모두 제공합니다.
주요 특징
- 양방향 접근: 앞/뒤 양쪽에서 요소 추가/삭제 가능
- Stack + Queue: 스택과 큐의 기능을 모두 제공
- 구현체: ArrayDeque, LinkedList
주요 메서드
- 앞쪽 추가: offerFirst(), addFirst(), push()
- 뒤쪽 추가: offerLast(), addLast(), offer(), add()
- 앞쪽 삭제: pollFirst(), removeFirst(), pop()
- 뒤쪽 삭제: pollLast(), removeLast()
- 조회: peekFirst(), peekLast(), peek()
예시 코드
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
public class DequeExample {
public static void main(String[] args) {
// ArrayDeque 사용
Deque<String> deque = new ArrayDeque<>();
// 앞에 추가
deque.offerFirst("첫번째");
deque.offerFirst("새로운 첫번째");
// 뒤에 추가
deque.offerLast("마지막");
deque.offerLast("새로운 마지막");
System.out.println("Deque 상태: " + deque);
// 앞에서 제거
System.out.println("앞에서 제거: " + deque.pollFirst());
// 뒤에서 제거
System.out.println("뒤에서 제거: " + deque.pollLast());
System.out.println("연산 후 Deque: " + deque);
// Deque를 스택으로 사용
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("\nDeque를 스택으로 사용:");
System.out.println("스택: " + stack);
System.out.println("pop: " + stack.pop());
System.out.println("pop 후: " + stack);
// Deque를 큐로 사용
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println("\nDeque를 큐로 사용:");
System.out.println("큐: " + queue);
System.out.println("poll: " + queue.poll());
System.out.println("poll 후: " + queue);
// LinkedList vs ArrayDeque 성능 비교
System.out.println("\n성능 비교 (10,000개 요소 추가):");
performanceTest();
}
static void performanceTest() {
int elements = 1_000_000;
// LinkedList 테스트
long startTime = System.currentTimeMillis();
Deque<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < elements; i++) {
linkedList.offerFirst(i);
}
long endTime = System.currentTimeMillis();
System.out.println("LinkedList 앞에 추가: " + (endTime - startTime) + "ms");
// ArrayDeque 테스트
startTime = System.currentTimeMillis();
Deque<Integer> arrayDeque = new ArrayDeque<>();
for (int i = 0; i < elements; i++) {
arrayDeque.offerFirst(i);
}
endTime = System.currentTimeMillis();
System.out.println("ArrayDeque 앞에 추가: " + (endTime - startTime) + "ms");
}
}
7. 자료구조 선택 가이드
상황에 따라 적절한 자료구조를 선택하는 것이 중요합니다. 다음은 각 상황에 맞는 자료구조 선택 가이드입니다.
Map 관련
- 키-값 쌍 저장이 필요할 때: HashMap 사용
- 삽입 순서 유지가 필요할 때: LinkedHashMap 사용
- 키 기준으로 정렬이 필요할 때: TreeMap 사용
Set 관련
- 중복 없는 데이터 저장이 필요할 때: HashSet 사용
- 삽입 순서 유지가 필요할 때: LinkedHashSet 사용
- 요소를 정렬된 상태로 유지해야 할 때: TreeSet 사용
Stack/Queue 관련
- LIFO(Last In First Out) 구조가 필요할 때: Deque 구현체를 Stack으로 사용
- FIFO(First In First Out) 구조가 필요할 때: Queue 구현체 사용
- 양방향 삽입/삭제가 필요할 때: Deque 구현체 사용
- 빠른 성능이 중요할 때: ArrayDeque 사용
- 메모리 효율성이 중요할 때: LinkedList 사용
실제 사용 예시
import java.util.*;
public class DataStructureSelectionGuide {
public static void main(String[] args) {
// 1. 웹 사이트 사용자 세션 관리 (키-값 저장) - HashMap
Map<String, UserSession> sessions = new HashMap<>();
sessions.put("user123", new UserSession("user123", System.currentTimeMillis()));
// 2. 최근 검색어 기록 (삽입 순서 유지) - LinkedHashMap
Map<String, Integer> recentSearches = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return size() > 10; // 최대 10개만 유지
}
};
// 3. 중복 제거된 로그 ID 저장 - HashSet
Set<String> uniqueLogIds = new HashSet<>();
// 4. 브라우저 방문 기록 (삽입 순서 유지) - LinkedHashSet
Set<String> browserHistory = new LinkedHashSet<>();
// 5. 작업 스케줄링 (우선순위 기반) - PriorityQueue
PriorityQueue<Task> taskQueue = new PriorityQueue<>();
// 6. 웹페이지 탐색 기록 (뒤로가기/앞으로가기) - Deque
Deque<String> browserNavigation = new ArrayDeque<>();
}
static class UserSession {
String userId;
long timestamp;
UserSession(String userId, long timestamp) {
this.userId = userId;
this.timestamp = timestamp;
}
}
static class Task implements Comparable<Task> {
int priority;
String name;
Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.priority);
}
}
}
결론
지금까지 자바 컬렉션 프레임워크의 Map, Stack, Queue, Deque에 대해 알아보았습니다. 각 자료구조는 고유한 특성과 장단점을 가지고 있으며, 상황에 맞게 적절한 자료구조를 선택하는 것이 중요합니다.
Map은 키-값 쌍으로 데이터를 저장할 때 유용하며, Stack은 LIFO, Queue는 FIFO 구조를 제공합니다. Deque는 양방향 접근이 가능하여 Stack과 Queue의 기능을 모두 제공하는 유연한 자료구조입니다.
적절한 상황에 최적화된 자료구조를 사용하면 성능 최적화에 보다 유리하게 작용합니다. 따라서 각 자료구조의 특성을 잘 이해하고 요구사항에 맞게 선택하는 것이 중요합니다.
자바 컬렉션 프레임워크에 관한 질문이나 의견이 있으시면 댓글로 남겨주세요! 다음에는 더 다양한 자바 프로그래밍 주제로 찾아뵙겠습니다. 감사합니다!
'📚 학습 기록 > Java 기초 & 중급' 카테고리의 다른 글
자바 컬렉션 프레임워크의 순회와 정렬 기초 (1) | 2025.04.30 |
---|---|
자바 컬렉션 프레임워크 실전 가이드: Map과 Deque의 숨겨진 활용법 (0) | 2025.04.30 |
자바 Set 컬렉션 완전 정복: HashSet, LinkedHashSet, TreeSet 비교와 활용 (1) | 2025.04.24 |
자바 HashSet 완벽 가이드: equals()와 hashCode()의 중요성과 제네릭 활용법 (1) | 2025.04.22 |
자바 HashSet 완전 정복: 해시 알고리즘과 충돌 해결 전략 구현하기 (1) | 2025.04.21 |