자료구조 중 List, Map, Set 인터페이스와 컬렉션 프레임워크의 종합적 활용 부분에 대해서 정리해보고자 한다.
컬렉션 프레임워크의 개념
: Java에서 제공하는 데이터 구조를 표준화한 설계 구조. 다수의 데이터를 효율적이고 체계적으로 관리할 수 있는 방법을 제공
컬렉션 프레임워크 사용목적
- 프로그래밍 효율성
- 이미 구현된 자료구조를 사용하여 개발 시간을 단축할 수 있음
- 최적화된 알고리즘을 통해 성능을 보장받을 수 있음
- 재사용성
- 표준화된 방식으로 데이터를 관리할 수 있음
- 여러 프로젝트에서 일관된 방식으로 사용할 수 있음
주요 인터페이스와 클래스
- List 계열 : 데이터를 순차적으로 저장
- ArrayList : 배열 기반으로 데이터를 저장하며, 조회 속도가 빠름
- LinkedList : 노드 기반으로 데이터를 저장하며, 삽입과 삭제가 빠름
- Set 계열 : 중복을 허용하지 않는 데이터 집합
- HashSet : 데이터를 해시 값으로 관리하여 검색 속도가 빠름
- TreeSet : 데이터를 정렬된 상태로 유지하며, 이진 검색 트리 기반으로 동작
- Map 계열 : 키와 값을 쌍으로 저장하며, 키는 중복을 허용하지 않음
- HashMap : 해시 기반으로 키-값 데이터를 저장하며, 빠른 검색 속도를 제공
- TreeMap : 키를 기준으로 정렬된 키-값 쌍 저장
List 인터페이스
List 컬렉션의 특징과 사용 목적
List 컬렉션은 순서를 유지하며 중복을 허용하는 요소들의 집합을 나타내는 인터페이스, 구현체에 따라 동작 방식이 달라짐

List 컬렉션의 특징
- 순서 유지
- 요소가 삽입된 순서가 유지되며, 인덱스를 통해 각 요소를 관리
- 요소는 0부터 시작하는 인덱스를 가지며, 원하는 위치의 요소에 직접 접근 가능'
- 유연한 크기
- 배열과 달리 크기가 고정되지 않고, 동적으로 변함
- 요소를 추가하거나 제거할 때 자동으로 크기가 조정됨
- 다양한 구현체
- ArrayList : 배열 기반으로 빠른 접근 속도를 제공하지만, 중간 삽입/삭제는 느림
- 접근이 빠름 O(1)
- 중간 삽입/삭제 O(n)
- 메모리 연속적
- LinkedList : 이중 연결 리스트 기반으로 삽입/삭제가 빠르지만, 랜덤 접근은 느림
- 접근이 느림 O(n)
- 중간 삽입/삭제 O(n)
- 메모리 분산
- ArrayList : 배열 기반으로 빠른 접근 속도를 제공하지만, 중간 삽입/삭제는 느림
어떤 List를 사용해야 하는가?
ArrayList를 사용하면 좋은 경우
- 데이터 검색/조회가 빈번한 경우
- 인덱스를 통해 데이터에 직접 접근할 수 있어 시간 복잡도가 O(1)
- 특정 위치의 데이터를 빠르게 가져올 수 있음
- 데이터의 삽입/삭제가 주로 마지막 위치에서 일어나는 경우
- 끝에서의 추가 및 삭제는 O(1)로 처리됨
- 리스트 크기 조정이 필요한 경우에만 재할당이 발생
- 데이터가 자주 변경되지 않고 주로 읽기 작업이 많은 경우
- 읽기 작업에서 ArrayList의 효율성이 극대화
LinkedList를 사용하면 좋은 경우
- 데이터의 삽입/삭제가 리스트의 시작이나 끝에서 빈번히 발생하는 경우
- 리스트의 맨 앞이나 뒤에서의 삽입 및 삭제 작업이 O(1)로 효율적
- ArrayList처럼 재할당이나 데이터 이동 비용이 발생하지 않음
- 데이터의 크기가 예측이 어렵고 변동이 심한 경우
- 메모리 재할당이 필요 없으며, 노드 단위로 메모리를 관리
실제로 ArrayList가 선호되는 이유
- 현대 컴퓨터 환경에서 메모리 접근 속도가 매우 빠름
- ArrayList는 연속된 메모리 구조를 가지므로 CPU 캐시 활용이 효율적
- 대부분의 애플리케이션에서 데이터 조회 빈도가 매우 높으므로, 인덱스 접근이 빠른 ArrayList가 유리
배열과 ArrayList의 차이점
핵심 차이점 쉽게 이해하기
| 구분 | 배열(Array) | ArrayList |
| 크기 | 크기가 고정되어 있으며 생성 시 크기를 지정해야 함 | 크기가 동적으로 변경되며, 요소 추가 시 자동으로 확장 됨 |
| 타입 | 기본 데이터 타입(int[], double[] 등) 사용 가능 | 제네릭을 사용하여 객체만 저장 가능 (ArrayList<Integer>) |
| 메모리 사용 | 연속적인 메모리 공간을 사용하여 효율적 | 내부적으로 배열을 사용하지만 크기 변경 시 새로운 배열을 생성해 데이터를 복사 |
| 성능 | 데이터 접근이 빠름(O(1)), 크기 변경 시 데이터 이동 불필요 | 데이터 접근이 빠르지만(O(1)), 크기 확장 시 복사가 필요해 성능 저하 가능 |
| 기능 | 단순한 데이터 저장/읽기만 가능하며 별도의 메서드가 없음 | add(), remove(), contains() 등 다양한 메서드 제공 |
| 유연성 | 크기가 고정되어 유연성 부족 | 크기가 동적으로 변해 유연하게 사용 가능 |
언제 무엇을 사용할까?
- 배열
- 크기가 고정된 데이터 집합을 처리할 때 적합
- 데이터 구조가 단순하고 성능(접근 속도)이 중요한 경우 유리
- 메모리 사용이 효율적이어야 하거나 기본 타입 데이터를 다룰 때
- ArrayList
- 동적으로 데이터 크기가 변할 가능성이 있을 때
- 요소 추가/삭제가 빈번하고, 코드를 간단하고 가독성 있게 작성하고 싶을 때
- 데이터 조작에 다양한 메서드가 필요할 때
Collections 유틸리티 클래스 활용
Collections의 주요 기능
정렬(Sorting)
- 컬렉션의 요소를 자연 정렬 순서 또는 사용자 정의기준으로 정렬할 수 있음
- 주요 메서드
- Collections.sort(List<T>) : 자연 정렬
- Collections.sort(List<T>, Comparator<T>) : 사용자 정의 정렬
import java.util.*;
public class CollectionsSortingExamples {
public static void main(String[] args) {
// 1. 기본 리스트 정렬
// === 기본 정렬 예제 ===
// 숫자 오름차순 정렬: [1, 2, 5, 8, 9]
// 숫자 내림차순 정렬: [9, 8, 5, 2, 1]
// 문자열 정렬: [apple, banana, cherry]
demonstrateBasicSorting();
// 2. 중첩 리스트 정렬
// === 중첩 리스트 정렬 예제 ===
// 첫 요소 기준 정렬: [[1, 2, 8], [3, 5, 2], [4, 7, 6, 9]]
// 합계 기준 정렬: [[3, 5, 2], [1, 2, 8], [4, 7, 6, 9]]
demonstrateNestedListSorting();
// 3. 객체 리스트 정렬
// === 객체 정렬 예제 ===
// 나이순 정렬: [Park(20), Kim(25), Lee(30)]
// 이름순 정렬: [Kim(25), Lee(30), Park(20)]
demonstrateObjectSorting();
// 4. 다중 기준 정렬
// === 다중 기준 정렬 예제 ===
// 나이 + 이름 정렬: [Park(20), Kim(25), Lee(25)]
demonstrateMultiCriteriaSorting();
}
private static void demonstrateBasicSorting() {
System.out.println("\n=== 기본 정렬 예제 ===");
// 숫자 리스트 정렬
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);
Collections.sort(numbers); // 자연 정렬 순서
System.out.println("숫자 오름차순 정렬: " + numbers);
Collections.sort(numbers, Collections.reverseOrder()); // 역순 정렬
System.out.println("숫자 내림차순 정렬: " + numbers);
// 문자열 리스트 정렬
List<String> words = Arrays.asList("banana", "apple", "cherry");
Collections.sort(words);
System.out.println("문자열 정렬: " + words);
}
private static void demonstrateNestedListSorting() {
System.out.println("\n=== 중첩 리스트 정렬 예제 ===");
List<List<Integer>> nestedList = new ArrayList<>();
nestedList.add(Arrays.asList(3, 5, 2));
nestedList.add(Arrays.asList(1, 2, 8));
nestedList.add(Arrays.asList(4, 7, 6, 9));
// 첫 번째 요소 기준 정렬
nestedList.sort(Comparator.comparing(list -> list.get(0)));
System.out.println("첫 요소 기준 정렬: " + nestedList);
// 리스트 합계 기준 정렬
nestedList.sort(Comparator.comparing(list ->
list.stream().mapToInt(Integer::intValue).sum()));
System.out.println("합계 기준 정렬: " + nestedList);
}
private static void demonstrateObjectSorting() {
System.out.println("\n=== 객체 정렬 예제 ===");
List<Person> people = Arrays.asList(
new Person("Kim", 25),
new Person("Lee", 30),
new Person("Park", 20)
);
// 나이 기준 정렬
people.sort(Comparator.comparing(Person::getAge));
System.out.println("나이순 정렬: " + people);
// 이름 기준 정렬
people.sort(Comparator.comparing(Person::getName));
System.out.println("이름순 정렬: " + people);
}
private static void demonstrateMultiCriteriaSorting() {
System.out.println("\n=== 다중 기준 정렬 예제 ===");
List<Person> people = Arrays.asList(
new Person("Kim", 25),
new Person("Lee", 25),
new Person("Park", 20)
);
// 나이 기준 정렬 후, 이름으로 정렬
people.sort(Comparator.comparing(Person::getAge)
.thenComparing(Person::getName));
System.out.println("나이 + 이름 정렬: " + people);
}
// Person 클래스 정의
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + "(" + age + ")";
}
}
}
최솟값, 최댓값 찾기 (Min/Max)
- 컬렉션에서 최솟값과 최댓값을 쉽게 찾을 수 있음
- 주요 메서드
- Collections.min(Collection<T>)
- Collections.max(Collection<T>)
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);
System.out.println("최솟값: " + Collections.min(numbers)); // 출력: 1
System.out.println("최댓값: " + Collections.max(numbers)); // 출력: 9
빈도 계산 (Frequency)
- 요소가 얼마나 자주 등장했는지 확인하는 문제에 유용
- 메서드
- Collections.frequency(Collection<T>, T)
List<String> words = Arrays.asList("apple", "banana", "apple", "cherry", "banana");
int count = Collections.frequency(words, "apple");
System.out.println("apple 등장 횟수: " + count);
이진 탐색 (Binary Search)
- 정렬된 리스트에서 빠르게 값을 찾는 문제에 적합
- 입력 리스트는 반드시 정렬 상태여야 함
- 메서드
- Collections.binarySearch(List<T>, T key)
List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9);
Collections.sort(numbers); // 정렬 필수
int index = Collections.binarySearch(numbers, 5);
System.out.println("5의 위치: " + index);
요소 치환 (Replace)
- 컬렉션에서 특정 값을 다른 값으로 변경
- 주요 메서드
- Collections.replaceAll(List<T> , T oldVal, T newVal)
List<String> words = Arrays.asList("apple", "banana", "apple");
Collections.replaceAll(words, "apple", "pear");
System.out.println(words); // 출력: [pear, banana, pear]
역순 변환 (Reversing)
- 컬렉션의 순서를 뒤집음
- 주요 메서드
- Collections.reverse(List<T>)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Collections.reverse(numbers);
System.out.println(numbers); // 출력: [5, 4, 3, 2, 1]
요소 회전 (Rotation)
- 컬렉션의 요소를 지정된 거리만큼 이동
- 주요 메서드
- Collections.rotate(List<T>, int distance)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Collections.rotate(numbers, 2); // 요소를 2칸 이동
System.out.println(numbers); // 출력: [4, 5, 1, 2, 3]
Map 인터페이스
Map 컬렉션의 특징과 사용 목적
Map은 키(Key)와 값(Value)을 하나의 쌍으로 묶어서 저장하는 자료구조. 특정한 키를 통해 대응하는 값을 빠르게 조회할 수 있는 구조를 제공
Map의 핵심 특징
| 구분 | 특징 |
| 구조 | 키와 값의 쌍으로 이루어진 데이터 구조 |
| 키(Key) | 중복 불가, null 허용(HashMap의 경우) |
| 값(Value) | 중복 가능, null 허용 |
| 순서 | 입력 순서 보장되지 않음 (LinkedHashMap 제외) |
| 구현체 | HashMap, TreeMap, LinkedHashMap 등이 대표적 |
| 성능 | HashMap: 평균적으로 O(1) 접근 속도, TreeMap : O(log n) |
Map의 사용 목적과 활용 상황
- 사용하는 이유
- 빠른 검색/조회 : 키를 통해 O(1) 시간에 데이터 접근 가능
- 연관 데이터 관리 : 관련된 데이터를 쌍으로 관리 가능
- 중복 제거 : 키의 중복을 허용하지 않아 유니크한 데이터 관리 용이
- 주요 활용 상황
- 캐시/임시 저장소 : 특정 키로 빠르게 데이터를 가져와야 하는 경우
- 빈도수 계산 : 데이터 출현 횟수 집계 (문자 빈도수, 단어 빈도수 등)
- ID와 객체 매핑 : 회원 ID -> 회원 정보, 상품 코드 -> 상품 정보 등 매핑
- 데이터 그룹화/분류 : 특정 기준으로 데이터를 묶어 관리
HashMap 주요 메서드 및 활용
HashMap은 Map 인터페이스를 구현한 대표적인 클래스이며, 가장 자주 사용됨
▼ 선언 및 주요 메서드
// 1. 선언
HashMap<String, Integer> map = new HashMap<>();
// 2. 데이터 추가 및 수정
map.put("A", 1); // 키가 "A"인 값에 1 저장
map.putIfAbsent("A", 10); // "A"키가 없을 때만 10 저장
int value = map.getOrDefault("B", 0);// "B"키 조회, 없으면 기본값 0
// 3. 존재 여부 확인
if (map.containsKey("A")) { ... } // "A"키 존재 여부 확인
if (map.containsValue(1)) { ... } // 값 1 존재 여부 확인
// 4. 삭제
map.remove("A"); // "A"키 삭제
map.clear(); // 전체 삭제
// 5. 값 변경하기 (다양한 방법)
HashMap<String, Integer> scores = new HashMap<>();
scores.put("Kim", 80);
// 5-1. 일반적 방식
if (scores.containsKey("Kim")) {
scores.put("Kim", scores.get("Kim") + 10);
}
// 5-2. compute 활용 (람다 사용)
scores.compute("Kim", (key, oldVal) -> oldVal + 10);
// 5-3. 키가 있을 때만 변경 (computeIfPresent)
scores.computeIfPresent("Kim", (key, oldVal) -> oldVal + 10);
// 6. 크기 확인
int size = map.size();
boolean isEmpty = map.isEmpty();
▼ 순회 방법
HashMap<String, Integer> map = new HashMap<>();
map.put("사과", 1000);
map.put("바나나", 2000);
map.put("오렌지", 3000);
// 1. entrySet() 활용 - (키, 값) 쌍 직접 접근
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
int val = entry.getValue();
// 처리 로직
}
// 2. keySet() 활용 - 키만 필요할 때
for (String key : map.keySet()) {
// 키를 이용한 값 조회 또는 처리
}
// 3. values() 활용 - 값만 필요할 때
for (int val : map.values()) {
// 값만을 이용한 처리
}
▼ HashMap 활용 패턴 실전 예시
▶︎ 패턴 1 : 등장 횟수 세기
- 특정 요소가 몇 번 나왔는지 세는 경우
// 문제: 배열에서 각 숫자가 몇 번 나왔는지 세기
int[] nums = {1, 2, 3, 1, 2, 1};
HashMap<Integer, Integer> count = new HashMap<>();
// 방법 1: getOrDefault 사용
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 결과: {1=3, 2=2, 3=1}
▶︎ 패턴 2 : 그룹화 하기
- 같은 특성을 가진 것들을 묶을 때 사용
// 문제: 같은 길이를 가진 단어들을 그룹화하기
String[] words = {"dog", "cat", "apple", "banana", "rat"};
HashMap<Integer, List<String>> groups = new HashMap<>();
// 방법: computeIfAbsent 사용
for (String word : words) {
groups.computeIfAbsent(word.length(), k -> new ArrayList<>())
.add(word);
}
// 결과: {3=[dog, cat, rat], 5=[apple], 6=[banana]}
▶︎ 패턴 3 : 첫 등장 위치 기록
- 요소가 처음 나타난 위치를 기록할 때 사용
// 문제: 각 숫자가 처음 나온 위치 기록하기
int[] nums = {1, 2, 3, 1, 2, 1};
HashMap<Integer, Integer> firstPos = new HashMap<>();
// 방법: putIfAbsent 사용
for (int i = 0; i < nums.length; i++) {
firstPos.putIfAbsent(nums[i], i);
}
// 결과: {1=0, 2=1, 3=2} // 처음 나온 위치만 저장됨
Set 인터페이스
Set 컬렉션의 특징과 사용 목적
Set은 중보되지 않는 요소들을 저장하는 자료구조 집합이라는 이름처럼 같은 값이 2번 이상 등장하는 것을 허용하지 않으며 각 요소는 유일함
순서는 보장하지 않지만 LinkedHashSet을 사용하면 삽입 순서를 유지할 수 있음
Set의 핵심 특징
| 구분 | 특징 |
| 구조 | 중복을 허용하지 않는 요소들의 집합 구조 |
| 순서 | 일반적으로 입력 순서 보장 안 함 (단, LinkedHashSet은 유지) |
| null 처리 | 하나의 null 요소만 저장 가능 |
| 구현체 | HashSet, TreeSet, LinkedHashSet 등이 대표적 |
| 성능 | HashSet : 평균적으로 빠른 검색(O(1)), TreeSet: 정렬된 상태 유지(O(log n)) |
Set의 사용 목적과 활용 상황
- 핵심 사용 목적
- 중복 제거 : 대용량 데이터에서 같은 요소가 반복될 때, 효율적으로 중복을 제거할 수 있음
- 유일성 보장 : 비즈니스 로직 상 고유해야 하는 값(예: 상품 코드, 회원 ID) 관리
- 집합 연산 처리 : 합집합, 교집합, 차집합과 같은 집합 연산을 쉽게 수행
- 메모리 절약 : 불필요한 중복데이터를 제거하여 메모리 사용량 감소
- 실무 활용 예시
- 데이터 처리 측면
- 로그 분석 : 다양한 IP나 사용자 ID 중에서 유일한 값을 추출
- 비즈니스 로직 측면
- 회원 관리 : 중복 불가능한 식별자(이메일, 전화번호) 관리
- 데이터 처리 측면
HashSet 주요 메서드 및 활용
HashSet은 가장 널리 사용되는 Set 구현체 중 하나로, 해시 기반으로 요소를 관리. 일반적으로 순서를 보장하지 않으며, 빠른 검색과 삽입 성능을 자랑함
▼ 선언 및 주요 메서드
// 1. 선언
HashSet<String> set = new HashSet<>();
// 2. 데이터 추가 및 삭제
set.add("A"); // 요소 "A" 추가 (중복 요소는 추가되지 않음)
set.remove("A"); // "A" 삭제
set.clear(); // 모든 요소 삭제
// 3. 데이터 확인
boolean exists = set.contains("A"); // "A"가 존재하는지 확인
int size = set.size(); // Set의 크기 확인
boolean empty = set.isEmpty(); // 비었는지 확인
// 4. 집합 연산
Set<String> set1 = new HashSet<>();
Set<String> set2 = new HashSet<>();
// 합집합: set1에 set2의 요소 모두 추가
set1.addAll(set2);
// 교집합: set1과 set2에 모두 있는 요소만 남김
set1.retainAll(set2);
// 차집합: set1에서 set2의 요소 제거 (set1 - set2)
set1.removeAll(set2);
▼ 순회 방법
HashSet<String> fruits = new HashSet<>();
fruits.add("사과");
fruits.add("바나나");
fruits.add("오렌지");
// for-each문 활용
for (String fruit : fruits) {
System.out.println(fruit);
}
// 출력 결과 (순서는 무작위):
// 바나나
// 오렌지
// 사과
▼ HashSet 활용 패턴 실전 예시
▶︎ 패턴 1 : 데이터 중복 제거
- 대용량 데이터나 배열 내 중복 요소를 제거하는 데 유용
// 문제: 문자열 배열에서 중복된 이름 제거하기
String[] names = {"Tom", "John", "Tom", "Jane", "John", "Tom"};
// 방법 1: HashSet 생성자를 통한 중복 제거
Set<String> uniqueNames = new HashSet<>(Arrays.asList(names));
System.out.println("중복 제거된 이름: " + uniqueNames);
// 결과: [Jane, Tom, John] (순서는 보장 안 됨)
// 방법 2: 순차적으로 add하여 중복 체크
Set<String> nameSet = new HashSet<>();
for (String name : names) {
if (nameSet.add(name)) {
System.out.println("새로운 이름 추가: " + name);
} else {
System.out.println("중복된 이름 발견: " + name);
}
}
▶︎ 패턴2 : 방문 체크(Visited Set) 활용
- 어떤 데이터나 이벤트를 이미 처리했는지 빠르게 확인할 수 있음
// 문제: 작업 목록에서 중복 실행 방지하기
String[] tasks = {"Task1", "Task2", "Task1", "Task3", "Task2"};
Set<String> processedTasks = new HashSet<>();
for (String task : tasks) {
if (!processedTasks.contains(task)) {
System.out.println("작업 처리 중: " + task);
// 실제 작업 처리 로직 수행
processedTasks.add(task);
} else {
System.out.println("이미 처리된 작업: " + task);
}
}
// 결과:
// 작업 처리 중: Task1
// 작업 처리 중: Task2
// 이미 처리된 작업: Task1
// 작업 처리 중: Task3
// 이미 처리된 작업: Task2
'JAVA' 카테고리의 다른 글
| [JAVA] 불변객체(Immutable Object) (0) | 2025.10.13 |
|---|---|
| [JAVA] 예외처리 (1) | 2025.10.13 |
| [Java Algorithm] 2차원 배열 (0) | 2025.10.02 |
| [JAVA Algorithm] 배열 개념 정리 (0) | 2025.10.01 |
| [JAVA] 완전탐색 Vs 그리디 알고리즘 이해하기 (0) | 2025.09.30 |