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 | Collections |
|---|---|---|
| 대상 | 배열 (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);
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]
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단계 (더 효율적)
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)); // 재현 가능한 랜덤
이진 탐색 - 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); // 정확한 결과
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
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));
}
불변 래퍼 생성
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 (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.of | unmodifiableList |
|---|---|---|
| 불변성 | 완전 불변 | 래퍼 (원본 의존) |
| null | 불가 | 가능 |
| 메모리 | 효율적 | 래퍼 오버헤드 |
| Java | 9+ | 5+ |
선택 기준
// ✅ List.of 사용 (Java 9+)
- 완전히 새로운 불변 리스트
- null 없음
- 성능 중요
// ✅ unmodifiableList 사용
- 기존 리스트를 불변으로
- null 포함 가능
- Java 8 이하
빈 불변 컬렉션
// 빈 리스트
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 (다른 객체)
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 오버헤드!
비교
| 특징 | synchronizedMap | ConcurrentHashMap |
|---|---|---|
| 동기화 | 전체 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 필요
- 레거시 코드
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Collections.fill(list, "X");
System.out.println(list); // [X, X, X]
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]
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
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]
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"); // 더 간결
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]
// ❌ 나쁨: 수정 가능
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); // 완전 불변
}
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;
}
}
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<>();
}
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 체크 불필요
// ...
}
// ❌ 고정 크기 리스트
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"); // ✅ 가능
// 원본 변경 시 영향받음
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);
List<Integer> list = Arrays.asList(5, 2, 8, 1);
// ❌ 정렬 안 함
int index = Collections.binarySearch(list, 5); // 잘못된 결과!
// ✅ 정렬 필수
Collections.sort(list);
index = Collections.binarySearch(list, 5); // 정확한 결과
// 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))
// 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 (제자리 수정) |