Collection Utility

devK08·2026년 1월 8일

1. Collections란?

java.util.Collections: 컬렉션을 조작하는 정적 메서드를 제공하는 유틸리티 클래스

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

List<Integer> list = new ArrayList<>(Arrays.asList(5, 2, 8, 1));
Collections.sort(list);  // [1, 2, 5, 8]

Arrays vs Collections

구분ArraysCollections
대상배열 (int[], String[])컬렉션 (List, Set, Map)
정렬Arrays.sort(arr)Collections.sort(list)
검색Arrays.binarySearch(arr, key)Collections.binarySearch(list, key)
타입Primitive 가능Object만
// Arrays: 배열
int[] arr = {5, 2, 8, 1};
Arrays.sort(arr);

// Collections: List
List<Integer> list = Arrays.asList(5, 2, 8, 1);
Collections.sort(list);

2. 정렬

sort()

List<Integer> numbers = new ArrayList<>(Arrays.asList(5, 2, 8, 1));

// 오름차순
Collections.sort(numbers);
System.out.println(numbers);  // [1, 2, 5, 8]

// 내림차순
Collections.sort(numbers, Collections.reverseOrder());
System.out.println(numbers);  // [8, 5, 2, 1]

// Comparator 사용
List<String> words = Arrays.asList("apple", "pie", "banana");
Collections.sort(words, Comparator.comparing(String::length));
System.out.println(words);  // [pie, apple, banana]

reverse()

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.reverse(list);
System.out.println(list);  // [5, 4, 3, 2, 1]

// sort() + reverse() vs reverseOrder()
Collections.sort(list);
Collections.reverse(list);  // 2단계

Collections.sort(list, Collections.reverseOrder());  // 1단계 (더 효율적)

shuffle()

List<Integer> deck = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.shuffle(deck);
System.out.println(deck);  // [3, 1, 5, 2, 4] (랜덤)

// Random 객체로 시드 제어
Collections.shuffle(deck, new Random(42));  // 재현 가능한 랜덤

3. 검색

binarySearch()

이진 탐색 - O(log n)

List<Integer> sorted = Arrays.asList(1, 3, 5, 7, 9);

// 찾기
int index = Collections.binarySearch(sorted, 5);
System.out.println(index);  // 2

// 없으면 음수 반환
int notFound = Collections.binarySearch(sorted, 6);
System.out.println(notFound);  // -4 (삽입 위치: -(insertion point) - 1)

⚠️ 주의: 반드시 정렬된 리스트여야 함!

List<Integer> unsorted = Arrays.asList(5, 2, 8, 1);
int result = Collections.binarySearch(unsorted, 5);
// 정렬 안 됨 → 잘못된 결과!

// 올바른 사용
Collections.sort(unsorted);
result = Collections.binarySearch(unsorted, 5);  // 정확한 결과

min() / max()

List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);

int min = Collections.min(numbers);  // 1
int max = Collections.max(numbers);  // 9

// Comparator 사용
List<String> words = Arrays.asList("apple", "pie", "banana");
String shortest = Collections.min(words, Comparator.comparing(String::length));
System.out.println(shortest);  // pie

frequency()

List<String> fruits = Arrays.asList("apple", "banana", "apple", "orange", "apple");

int count = Collections.frequency(fruits, "apple");
System.out.println(count);  // 3

// 활용: 최빈값 찾기
Map<String, Integer> freqMap = new HashMap<>();
for (String fruit : new HashSet<>(fruits)) {
    freqMap.put(fruit, Collections.frequency(fruits, fruit));
}

4. 불변 컬렉션

unmodifiableList/Set/Map

불변 래퍼 생성

List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> unmodifiable = Collections.unmodifiableList(original);

// 읽기는 가능
System.out.println(unmodifiable.get(0));  // A

// 수정 불가
unmodifiable.add("D");  // ❌ UnsupportedOperationException
unmodifiable.remove(0);  // ❌ UnsupportedOperationException
unmodifiable.set(0, "X");  // ❌ UnsupportedOperationException

⚠️ 함정: 원본 변경 시

List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> unmodifiable = Collections.unmodifiableList(original);

// unmodifiable은 수정 불가
unmodifiable.add("D");  // ❌ Exception

// 하지만 원본은 수정 가능!
original.add("D");
System.out.println(unmodifiable);  // [A, B, C, D] ← 변경됨!

// 해결: 원본 복사
List<String> copy = new ArrayList<>(original);
List<String> unmodifiable = Collections.unmodifiableList(copy);

List.of vs unmodifiableList (Java 9+)

// List.of (Java 9+)
List<String> list1 = List.of("A", "B", "C");
- 완전 불변 (원본 없음)
- null 불가
- 컴팩트한 메모리

// Collections.unmodifiableList
List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> list2 = Collections.unmodifiableList(original);
- 래퍼 (원본 존재)
- null 가능
- 원본 변경 시 영향받음

비교표

특징List.ofunmodifiableList
불변성완전 불변래퍼 (원본 의존)
null불가가능
메모리효율적래퍼 오버헤드
Java9+5+

선택 기준

// ✅ List.of 사용 (Java 9+)
- 완전히 새로운 불변 리스트
- null 없음
- 성능 중요

// ✅ unmodifiableList 사용
- 기존 리스트를 불변으로
- null 포함 가능
- Java 8 이하

emptyList/Set/Map

빈 불변 컬렉션

// 빈 리스트
List<String> empty1 = Collections.emptyList();
List<String> empty2 = new ArrayList<>();  // 비효율적

// 빈 셋
Set<String> emptySet = Collections.emptySet();

// 빈 맵
Map<String, Integer> emptyMap = Collections.emptyMap();

왜 사용?

// ❌ null 반환 (NullPointerException 위험)
public List<String> getItems() {
    if (items.isEmpty()) {
        return null;  // 위험!
    }
    return items;
}

// ✅ 빈 컬렉션 반환
public List<String> getItems() {
    if (items.isEmpty()) {
        return Collections.emptyList();  // 안전!
    }
    return items;
}

// 사용하는 쪽에서 null 체크 불필요
for (String item : getItems()) {  // NPE 없음
    // ...
}

성능

// emptyList(): 싱글톤 재사용 (메모리 효율)
List<String> empty1 = Collections.emptyList();
List<String> empty2 = Collections.emptyList();
System.out.println(empty1 == empty2);  // true (같은 객체)

// new ArrayList(): 매번 생성
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
System.out.println(list1 == list2);  // false (다른 객체)

5. 동기화

synchronizedList/Set/Map

Thread-Safe 래퍼

List<String> list = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(list);

// 모든 메서드가 synchronized
syncList.add("A");  // Thread-Safe
syncList.get(0);    // Thread-Safe

⚠️ 주의: Iterator는 수동 동기화 필요

List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// ❌ Iterator는 Thread-Safe 아님
for (String s : syncList) {  // ConcurrentModificationException 가능
    // ...
}

// ✅ 수동 동기화
synchronized (syncList) {
    for (String s : syncList) {
        // ...
    }
}

성능 주의사항

List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// 모든 메서드가 synchronized → 성능 저하
syncList.add("A");  // Lock 획득/해제
syncList.add("B");  // Lock 획득/해제
syncList.add("C");  // Lock 획득/해제
// 매번 Lock 오버헤드!

vs ConcurrentHashMap

비교

특징synchronizedMapConcurrentHashMap
동기화전체 Lock세그먼트 Lock
성능느림빠름
읽기Lock 필요Lock 불필요
null가능불가
// synchronizedMap: 전체 Lock
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Thread 1: put() → 전체 Lock
// Thread 2: get() → 대기 (느림!)

// ConcurrentHashMap: 세그먼트 Lock
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Thread 1: put() → 일부만 Lock
// Thread 2: get() → Lock 없음 (빠름!)

선택 기준

// ✅ ConcurrentHashMap 사용 (권장)
- 높은 동시성
- 읽기 많음
- null 불필요

// ✅ synchronizedMap 사용
- 단순한 동기화
- null 필요
- 레거시 코드

6. 기타 유틸리티

fill()

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Collections.fill(list, "X");
System.out.println(list);  // [X, X, X]

swap()

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.swap(list, 0, 4);  // 첫 번째와 마지막 교환
System.out.println(list);  // [5, 2, 3, 4, 1]

copy()

List<Integer> source = Arrays.asList(1, 2, 3);
List<Integer> dest = new ArrayList<>(Arrays.asList(0, 0, 0, 0, 0));

Collections.copy(dest, source);
System.out.println(dest);  // [1, 2, 3, 0, 0]

// ⚠️ 주의: dest 크기 >= source 크기
List<Integer> small = new ArrayList<>(Arrays.asList(0, 0));
Collections.copy(small, source);  // ❌ IndexOutOfBoundsException

rotate()

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.rotate(list, 2);  // 오른쪽으로 2칸 회전
System.out.println(list);  // [4, 5, 1, 2, 3]

Collections.rotate(list, -2);  // 왼쪽으로 2칸 회전
System.out.println(list);  // [1, 2, 3, 4, 5]

addAll()

List<String> list = new ArrayList<>(Arrays.asList("A", "B"));
Collections.addAll(list, "C", "D", "E");
System.out.println(list);  // [A, B, C, D, E]

// vs list.addAll()
list.addAll(Arrays.asList("C", "D", "E"));  // List 생성 필요
Collections.addAll(list, "C", "D", "E");     // 더 간결

replaceAll()

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "A", "C", "A"));
Collections.replaceAll(list, "A", "X");
System.out.println(list);  // [X, B, X, C, X]

7. 실전 패턴

패턴 1: 불변 리스트 만들기

// ❌ 나쁨: 수정 가능
public class User {
    private List<String> roles = new ArrayList<>();
    
    public List<String> getRoles() {
        return roles;  // 외부에서 수정 가능!
    }
}

user.getRoles().add("ADMIN");  // 의도하지 않은 수정!

// ✅ 좋음: 불변 반환
public class User {
    private List<String> roles = new ArrayList<>();
    
    public List<String> getRoles() {
        return Collections.unmodifiableList(roles);
    }
}

user.getRoles().add("ADMIN");  // ❌ UnsupportedOperationException

// ✅ 더 좋음: Java 9+
public List<String> getRoles() {
    return List.copyOf(roles);  // 완전 불변
}

패턴 2: 정렬 후 이진 탐색

public class ProductSearch {
    private List<Product> products = new ArrayList<>();
    
    // 초기화 시 정렬
    public void init() {
        Collections.sort(products, Comparator.comparing(Product::getPrice));
    }
    
    // 이진 탐색 (빠름!)
    public Product findByPrice(int price) {
        int index = Collections.binarySearch(
            products, 
            new Product(price), 
            Comparator.comparing(Product::getPrice)
        );
        return index >= 0 ? products.get(index) : null;
    }
}

패턴 3: Thread-Safe 컬렉션

public class Cache {
    // ❌ Thread-Safe 아님
    private Map<String, String> cache = new HashMap<>();
    
    // ✅ 방법 1: synchronizedMap
    private Map<String, String> cache = 
        Collections.synchronizedMap(new HashMap<>());
    
    // ✅ 방법 2: ConcurrentHashMap (더 빠름)
    private Map<String, String> cache = new ConcurrentHashMap<>();
}

패턴 4: 빈 컬렉션 반환

public class UserRepository {
    // ❌ null 반환 (위험)
    public List<User> findByAge(int age) {
        List<User> result = queryDatabase(age);
        if (result == null || result.isEmpty()) {
            return null;  // NullPointerException 위험!
        }
        return result;
    }
    
    // ✅ 빈 컬렉션 반환
    public List<User> findByAge(int age) {
        List<User> result = queryDatabase(age);
        if (result == null || result.isEmpty()) {
            return Collections.emptyList();  // 안전!
        }
        return result;
    }
}

// 사용
for (User user : repo.findByAge(25)) {  // null 체크 불필요
    // ...
}

8. 주의사항

Arrays.asList() 함정

// ❌ 고정 크기 리스트
List<String> list = Arrays.asList("A", "B", "C");
list.add("D");  // ❌ UnsupportedOperationException (크기 변경 불가)
list.set(0, "X");  // ✅ 가능 (요소 변경은 가능)

// ✅ 가변 리스트
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.add("D");  // ✅ 가능

unmodifiableList 함정

// 원본 변경 시 영향받음
List<String> original = new ArrayList<>(Arrays.asList("A", "B"));
List<String> unmodifiable = Collections.unmodifiableList(original);

original.add("C");
System.out.println(unmodifiable);  // [A, B, C] ← 변경됨!

// 해결: 복사본 생성
List<String> copy = new ArrayList<>(original);
List<String> unmodifiable = Collections.unmodifiableList(copy);

binarySearch 정렬 필수

List<Integer> list = Arrays.asList(5, 2, 8, 1);

// ❌ 정렬 안 함
int index = Collections.binarySearch(list, 5);  // 잘못된 결과!

// ✅ 정렬 필수
Collections.sort(list);
index = Collections.binarySearch(list, 5);  // 정확한 결과

synchronized 성능

// synchronizedList: 느림 (모든 메서드 Lock)
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 1000; i++) {
    syncList.add("item" + i);  // 1000번 Lock 획득/해제
}

// 개선: 배치 작업
List<String> temp = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    temp.add("item" + i);
}
syncList.addAll(temp);  // 1번만 Lock

// 또는 ConcurrentHashMap 사용 (더 빠름)

핵심 요약

자주 쓰는 메서드

정렬/검색

Collections.sort(list);                    // 정렬
Collections.binarySearch(list, key);       // 이진 탐색 (정렬 필수)
Collections.min(list) / max(list);         // 최솟값/최댓값

불변

Collections.unmodifiableList(list);        // 불변 래퍼
Collections.emptyList();                   // 빈 불변 리스트
List.of("A", "B");                         // 불변 리스트 (Java 9+)

동기화

Collections.synchronizedList(list);        // Thread-Safe 래퍼
new ConcurrentHashMap<>();                 // 더 빠른 대안 (Map)

선택 가이드

불변 리스트

Java 9+: List.of() (완전 불변, null 불가)
Java 8-: Collections.unmodifiableList() (래퍼, null 가능)

Thread-Safe

Map: ConcurrentHashMap (빠름)
List: Collections.synchronizedList() (간단)

빈 컬렉션

반환: Collections.emptyList() (null 대신)
성능: 싱글톤 재사용 (메모리 효율)

검색

정렬됨: Collections.binarySearch() (O(log n))
정렬 안 됨: list.contains() (O(n))

대안 Stream API

// Collections 대신 Stream 사용 가능

// 정렬
list.stream().sorted().collect(Collectors.toList());

// 최댓값
list.stream().max(Comparator.naturalOrder());

// 필터링 + 정렬
list.stream()
    .filter(x -> x > 10)
    .sorted()
    .collect(Collectors.toList());

// 불변 리스트 (Java 10+)
list.stream().collect(Collectors.toUnmodifiableList());

Collections vs Stream

상황사용
단순 정렬Collections.sort() (간결)
복잡한 처리Stream (가독성)
불변 생성List.of() (Java 9+)
기존 리스트 수정Collections (제자리 수정)
profile
안녕하세요. 개발자 지망 고등학생입니다.

0개의 댓글