Java List 선택 기준
ArrayList·LinkedList·List.of·Arrays.asList의 크기 변경 규칙을 구분하고 실전 Study Log 저장소를 완성합니다.
직접 구현은 내부 원리를 이해하기 위한 장치이고 제품 코드에서는 검증된 표준 List를 우선합니다.
구현을 고를 때는 조회 패턴뿐 아니라 변경 가능성, null 정책, 원본 배열과의 공유까지 묻습니다.
특히 Arrays.asList는 set은 되지만 크기 변경은 안 되는 고정 크기 뷰입니다.
고정 크기 배열 뷰의 추가 실패
배열을 List로 보이게 만든 뒤 add를 호출합니다.
인터페이스 타입이 같아도 구현의 변경 가능성 규칙이 다를 수 있음을 보여 줍니다.
import java.util.Arrays;
import java.util.List;
public final class ArraysAsListMutationFailure {
public static void main(String[] args) {
List<String> topics = Arrays.asList("array", "linked");
topics.add("choice");
}
}java.lang.UnsupportedOperationExceptionArrays.asList는 배열 길이를 그대로 반영하는 고정 크기 뷰입니다.
크기를 바꾸려면 new ArrayList<>(view)로 복사해야 합니다.
완전 불변 값 목록은 List.of 또는 List.copyOf로 의도를 드러냅니다.
List 구현별 변경 규칙
- 일반적인 조회·끝 추가 목록은 ArrayList를 기본 선택으로 두고 측정 결과로 반증합니다.
- LinkedList는 양방향
Node와first·last를 유지하며Deque연산이 목적일 때 더 자연스럽습니다. List.of는null과 구조 변경을 허용하지 않아 상수 데이터 의도를 명확히 합니다.- Arrays.asList는 원본 배열과 원소 변경을 공유하지만
add와remove는 지원하지 않습니다. - 외부 목록을 보관할 때
List.copyOf로 스냅숏을 만들면 이후 호출자 변경을 차단합니다. - 변수 타입은
List<E>역할로 두고 생성 지점에서 필요한 구현을 선택합니다.
ArrayList 내부 저장소와 불변 스냅숏
PracticalStudyList는 내부 저장소로 ArrayList를 선택하고 외부 조회에는 List.copyOf 스냅숏을 반환합니다.
첫 스냅숏을 만든 뒤 새 항목을 추가해도 이전 결과가 바뀌지 않습니다.
내부 구현과 외부 불변 규칙을 서로 다르게 선택할 수 있습니다.
import java.util.ArrayList;
import java.util.List;
public final class PracticalStudyList {
public static void main(String[] args) {
StudyLog log = new StudyLog();
log.add("array", 40);
log.add("linked", 50);
List<Entry> snapshot = log.entries();
log.add("choice", 30);
System.out.println("snapshot=" + snapshot.size() + ", current=" + log.entries().size());
System.out.println("total=" + log.total());
}
private static final class StudyLog {
private final List<Entry> entries = new ArrayList<>();
void add(String topic, int minutes) {
if (topic == null || topic.isBlank() || minutes <= 0)
throw new IllegalArgumentException();
entries.add(new Entry(topic, minutes));
}
List<Entry> entries() {
return List.copyOf(entries);
}
int total() {
return entries.stream().mapToInt(Entry::minutes).sum();
}
}
private record Entry(String topic, int minutes) {}
}변경 가능성 결정
List.of는 생성 뒤 add, set, remove를 허용하지 않고 null도 받지 않습니다.
설정값이나 완성된 결과처럼 바뀌지 않아야 하는 데이터에 적합합니다.
List.copyOf는 외부 컬렉션을 불변 스냅숏으로 바꾸어 이후 호출자 변경이 내부 상태에 번지지 않게 합니다.
Arrays.asList는 배열을 고정 크기 List로 보는 뷰입니다.
set은 원본 배열에도 반영되지만 add와 remove는 실패합니다.
가변 목록이 필요하면 new ArrayList<>(Arrays.asList(values))처럼 명시적으로 복사합니다.
표준 가변 구현 선택
ArrayList는 임의 조회, 순차 순회, 끝 추가가 중심인 일반 목록의 기본값입니다. 내부 배열이 가득 찰 때 확장하지만 최적화된 복사와 캐시 지역성을 활용합니다. 초기 용량은 대량 입력 크기를 확실히 알 때만 힌트로 제공합니다.
LinkedList는 List와 Deque를 모두 구현하며 양방향 Node를 유지합니다.
index 기반 중간 접근이 빠른 구조는 아닙니다.
양끝 큐가 목적이라면 다음 장에서 다룰 ArrayDeque가 메모리와 순회 측면에서 더 나은 기본 선택인 경우가 많습니다.
Study Log의 컬렉션 소유권
내부에는 ArrayList<Entry>를 두어 입력 순서와 끝 추가를 처리합니다.
조회 메서드는 내부 객체를 그대로 반환하지 않고 List.copyOf를 사용합니다.
호출자는 결과를 변경할 수 없고 나중에 로그가 추가되어도 이전 스냅숏의 크기는 그대로입니다.
변수와 매개변수 타입은 가능하면 List 역할로 선언하되 생성식에서 구현 선택을 명확히 합니다.
반환 규칙이 불변인지 가변인지 메서드 이름과 문서로 드러내야 호출자가 UnsupportedOperationException을 시행착오로 배우지 않습니다.
표준 ArrayList 기반 완성 기록
누적 CLI는 검증된 ArrayList를 내부에 사용하고 합계를 제공합니다. 직접 만든 목록은 학습 산출물로 남기되 실제 기능은 표준 구현의 예외·최적화·호환성을 활용합니다. 조회 결과는 복사본으로 구분을 닫습니다.
public final class StudyLogCliCH149 {
public static void main(String[] args) {
StudyLog log = new StudyLog();
log.add("standard-list", 46);
log.add("immutable-snapshot", 64);
log.print();
System.out.println("total=" + log.totalMinutes());
}
private static final class StudyLog {
private Entry[] entries = new Entry[2];
private int size;
void add(String topic, int minutes) {
if (topic == null || topic.isBlank()) throw new IllegalArgumentException("topic");
if (minutes <= 0) throw new IllegalArgumentException("minutes");
ensureCapacity(size + 1);
entries[size++] = new Entry(topic, minutes);
}
private void ensureCapacity(int required) {
if (required <= entries.length) return;
Entry[] grown = new Entry[Math.max(required, entries.length * 2)];
System.arraycopy(entries, 0, grown, 0, size);
entries = grown;
}
int totalMinutes() {
int total = 0;
for (int i = 0; i < size; i++) {
total += entries[i].minutes();
}
return total;
}
void print() {
for (int i = 0; i < size; i++) {
Entry entry = entries[i];
System.out.println(i + ":" + entry.topic() + "=" + entry.minutes());
}
}
}
private record Entry(String topic, int minutes) {}
}완성 CLI에서 내부 ArrayList는 끝 추가와 전체 출력이라는 실제 부하에 맞습니다.
조회 메서드를 추가한다면 List.copyOf를 반환해 편집 권한을 넘기지 않습니다.
출력 total 110과 스냅숏 크기는 구현 교체보다 먼저 지켜야 하는 애플리케이션 규칙입니다.
표준 List 구현을 좁히는 순서
| 질문 | 관찰할 값 | 선택 또는 조치 |
|---|---|---|
| 기본 가변 목록인가 | 조회·끝 추가 중심 | ArrayList |
| 양끝 연산이 핵심인가 | Deque 사용 가능 | ArrayDeque 우선 검토 |
| 값을 고정할 것인가 | 변경 허용 여부 | List.copyOf |
| 배열과 공유해야 하는가 | 원본 반영 의도 | Arrays.asList 제한 확인 |
표준 구현 이름보다 규칙을 먼저 고릅니다.
가변 여부, 순서, 중복, null, 스냅숏 여부를 정한 뒤 성능 특성으로 구현을 좁힙니다.
LinkedList를 단순히 중간 삽입 표만 보고 고르는 선택은 피합니다.
연습 문제
가변 ArrayList에 두 값을 넣고 List.copyOf 스냅숏을 만드세요.
원본에 세 번째 값을 추가한 뒤 두 목록 크기를 출력하고 스냅숏 add 실패도 관찰하세요.
정답과 해설
스냅숏은 생성 시점 원소를 보존합니다. 이후 원본 구조 변경은 반영되지 않으며 스냅숏 자체 수정도 거부됩니다.
import java.util.ArrayList;
import java.util.List;
public final class ImmutableListSnapshotSolution {
public static void main(String[] args) {
ArrayList<String> mutable = new ArrayList<>(List.of("array", "list"));
List<String> snapshot = List.copyOf(mutable);
mutable.add("snapshot");
System.out.println("mutable=" + mutable.size() + ", snapshot=" + snapshot.size());
try {
snapshot.add("blocked");
} catch (UnsupportedOperationException exception) {
System.out.println("immutable=" + exception.getClass().getSimpleName());
}
}
}첫 출력은 mutable=3, snapshot=2이고 두 번째는 불변 변경 차단을 보여 줍니다.
예외를 삼키기 위한 패턴이 아니라 규칙을 관찰하는 학습 코드입니다.
표준 List 통합 판단
목록을 고르기 전에 순서·중복·null·변경 가능성을 결정합니다.
일반 가변 목록은 ArrayList에서 시작하고 실제 작업 부하가 반증할 때 바꿉니다.
외부에 돌려주는 값은 소유권 정책에 따라 불변 스냅숏 또는 명시적 가변 복사본으로 구분합니다.