컬렉션 정렬과 불변 변환
List 정렬과 TreeSet 정렬을 구분하고 List.of·Arrays.asList·copyOf·synchronizedList의 변경·공유 규칙을 실행합니다.
표준 컬렉션 유틸리티는 비슷한 모양의 List를 만들지만 변경 가능성, 원본 공유, null 허용이 다릅니다.
List.of, List.copyOf, Arrays.asList, new ArrayList를 구분하지 않으면 add에서 예외가 나거나 외부 배열 변경이 내부 상태에 번집니다.
정렬도 제자리 변경과 새 결과 생성을 먼저 나눠야 합니다.
Arrays.asList의 변경 범위
배열을 List 뷰로 만든 뒤 set은 가능하지만 add·remove는 배열 길이를 바꿀 수 없어 실패합니다.
원본 배열의 원소 변경과 List.set은 서로 반영됩니다.
import java.util.Arrays;
import java.util.List;
public final class FixedSizeListAddFailure {
public static void main(String[] args) {
String[] source = {"array", "list"};
List<String> view = Arrays.asList(source);
source[0] = "changed";
System.out.println("view=" + view);
view.add("failure");
}
}출력 뒤 UnsupportedOperationException이 발생합니다.
독립된 가변 목록이 목적이면 new ArrayList<>(Arrays.asList(source)), 불변 스냅샷이면 List.copyOf(Arrays.asList(source))를 사용합니다.
빈 컬렉션의 값 의미
null List는 호출자마다 분기를 요구하고 “결과 없음”과 “계산 안 함”을 섞습니다.
결과가 0개라면 List.of(), Set.of(), Map.of() 같은 빈 불변 컬렉션을 반환합니다.
호출자는 안전하게 size와 for-each를 사용할 수 있습니다.
Collections.emptyList()도 불변 빈 List이며 제네릭 문맥에서 타입을 추론합니다.
매번 새 가변 ArrayList를 반환하면 호출자가 수정 가능한 규칙으로 오해할 수 있으므로 API 의도를 선택합니다.
제자리 정렬과 불변 정렬 결과 구분
가변 ArrayList의 정렬은 같은 객체의 순서를 바꿉니다.
List.of()로 만든 목록에 정렬을 호출하면 UnsupportedOperationException이 발생합니다.
원본을 보존하려면 스트림의 sorted() 또는 가변 복사본을 사용합니다.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class SortingAndCopyContracts {
public static void main(String[] args) {
List<Entry> source =
List.of(new Entry("thread", 60), new Entry("array", 40), new Entry("hash", 50));
Comparator<Entry> byMinutes = Comparator.comparingInt(Entry::minutes);
List<Entry> immutableSorted = source.stream().sorted(byMinutes).toList();
List<Entry> mutableSorted = new ArrayList<>(source);
mutableSorted.sort(byMinutes.reversed());
System.out.println("source=" + source);
System.out.println("asc=" + immutableSorted);
System.out.println("desc=" + mutableSorted);
mutableSorted.add(new Entry("queue", 30));
System.out.println("mutable-size=" + mutableSorted.size());
}
private record Entry(String topic, int minutes) {}
}Stream.toList 결과는 수정할 수 있다고 가정하지 않습니다.
가변 결과가 필요하면 수집기 또는 ArrayList 생성자로 복사합니다.
List.copyOf는 입력이 이미 적합한 불변 List일 때 같은 인스턴스를 반환할 수 있으므로 “항상 새 객체”를 규칙으로 삼지 않습니다.
TreeSet·List의 중복 처리
List.sort()는 모든 원소를 보존하고 순서만 바꿉니다.
TreeSet은 비교 결과가 0인 원소를 하나만 남깁니다.
정렬된 보고서에 중복 세션이 모두 필요하면 List를 정렬해야 합니다.
고유 정렬 키 집합과 범위 조회가 목적이면 TreeSet이 맞습니다.
Collections.min·max·binarySearch는 정렬 규칙을 전제로 합니다.
binarySearch에 정렬되지 않은 List를 넣은 결과는 믿을 수 없습니다.
정렬 비교자와 검색 비교자도 같아야 합니다.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public final class ListAndTreeSortingDifference {
public static void main(String[] args) {
Comparator<Entry> byScore = Comparator.comparingInt(Entry::score).reversed();
List<Entry> list =
new ArrayList<>(
List.of(
new Entry("hash", 90),
new Entry("set", 90),
new Entry("queue", 80)));
list.sort(byScore);
Set<Entry> tree = new TreeSet<>(byScore);
tree.addAll(list);
System.out.println("list-size=" + list.size() + " " + list);
System.out.println("tree-size=" + tree.size() + " " + tree);
}
private record Entry(String topic, int score) {}
}TreeSet size가 2라면 해시 또는 set 하나를 잃었습니다.
주제 tie-breaker를 추가하거나 List를 유지해야 합니다.
이 차이는 성능보다 먼저 확인할 기능 규칙입니다.
동기화 래퍼와 복합 원자성
Collections.synchronizedList는 개별 메서드 호출을 동기화합니다.
if (!list.contains(x)) list.add(x) 두 호출 전체는 하나의 잠금 구간이 아니므로 다른 스레드가 사이에 들어올 수 있습니다.
중복 없는 동시 저장이면 동시성 Set 등 목적에 맞는 구조를 사용합니다.
래퍼를 순회할 때는 문서대로 해당 컬렉션 객체를 synchronized 블록으로 잠가야 합니다.
Iterator 자체가 전체 순회를 잠그지 않습니다.
이 장에서는 규칙만 확인하고 실제 가시성과 경합은 19장에서 다룹니다.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class StudySnapshotService {
public static void main(String[] args) {
Service service = new Service();
service.add("hash", 50);
service.add("array", 40);
List<Entry> first = service.ranking();
service.add("thread", 60);
System.out.println("first=" + first);
System.out.println("current=" + service.ranking());
}
private static final class Service {
private final List<Entry> entries = new ArrayList<>();
void add(String topic, int minutes) {
entries.add(new Entry(topic, minutes));
}
List<Entry> ranking() {
return entries.stream()
.sorted(Comparator.comparingInt(Entry::minutes).reversed())
.toList();
}
}
private record Entry(String topic, int minutes) {}
}첫 ranking은 이후 add의 영향을 받지 않는 결과 List입니다.
내부 ArrayList 소유권을 노출하지 않고 표현 시점에 정렬합니다.
연습 문제
원본 String 배열을 바꾸어도 영향받지 않고 add와 정렬이 가능한 List를 만드세요.
목록 변경도 원본 배열에 반영되면 안 됩니다.
정답과 해설
Arrays.asList 뷰를 ArrayList 생성자에 전달하면 원소 참조를 새 내부 배열에 복사합니다.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class IndependentMutableListSolution {
public static void main(String[] args) {
String[] source = {"thread", "array"};
List<String> values = new ArrayList<>(Arrays.asList(source));
source[0] = "changed";
values.add("hash");
values.sort(String::compareTo);
System.out.println("source=" + Arrays.toString(source));
System.out.println("values=" + values);
}
}원본은 changed,배열이고 값은 배열,해시,스레드입니다. 두 컨테이너 구조와 원소 위치가 독립입니다.
변환 API를 고를 때는 크기 변경, set 허용, 원본 공유, null 허용을 표로 적습니다.
정렬은 원본 변경 여부와 동률 보존을 더 확인해야 안전한 컬렉션 구분이 됩니다.