본문 바로가기
빌드노트 검색

빌드노트

Arrays.asList에서 set은 되는데 add·remove는 실패하는 이유

Arrays.asList는 원본 배열과 연결된 고정 크기 뷰라서 set은 되지만 원소 추가·삭제는 실패한다. 실제 Java 출력으로 양방향 연결과 ArrayList 복사 뒤의 크기 변경·연결 해제를 비교하고 List.of와의 차이를 설명한다.

배열을 Arrays.asList(array)로 감싼 뒤 set은 되는데 add나 remove(0)에서 UnsupportedOperationException이 난다면, 반환된 목록의 크기 제한을 먼저 봐야 한다. 이 목록은 원본 배열과 연결된 고정 크기 뷰다. 기존 칸의 값을 바꿀 수 있지만 칸 수를 늘리거나 줄일 수는 없다.

추가·삭제가 필요하다면 생성 지점을 new ArrayList<>(Arrays.asList(array))로 바꾼다. 반대로 원본 배열과 값을 주고받는 것이 목적이라면 asList를 유지한다. 이때 목록의 set이 배열에도 반영된다는 점까지 함께 선택하는 것이다. 고정 크기와 양방향 연결은 Java 17의 Arrays.asList 계약 (새 탭에서 열림)에 명시돼 있다.

배열과 목록을 함께 출력해 보면

다음은 동작을 확인하려고 따로 만든 예제다. JDK 17과 java 명령이 준비된 빈 폴더에 아래 내용을 AsListCheck.java로 저장한다. 외부 라이브러리나 프로젝트 설정은 필요 없다.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class AsListCheck {
    public static void main(String[] args) {
        String[] array = {"red", "blue"};
        List<String> view = Arrays.asList(array);

        view.set(0, "green");
        System.out.println("view.set: array=" + Arrays.toString(array));
        array[1] = "yellow";
        System.out.println("array[1]: view=" + view);

        try { view.add("black"); }
        catch (UnsupportedOperationException e) {
            System.out.println("view.add: " + e);
        }
        try { view.remove(0); }
        catch (UnsupportedOperationException e) {
            System.out.println("view.remove: " + e);
        }
        System.out.println("after failures: view=" + view + ", array=" + Arrays.toString(array));

        List<String> copy = new ArrayList<>(view);
        copy.set(0, "white");
        copy.add("black");
        System.out.println("copy.add: copy=" + copy + ", size=" + copy.size());
        copy.remove(1);
        System.out.println("copy.remove: copy=" + copy + ", size=" + copy.size());
        System.out.println("copy writes: array=" + Arrays.toString(array));
        array[0] = "purple";
        System.out.println("array[0]: view=" + view + ", copy=" + copy);
    }
}

그 폴더에서 다음 명령을 순서대로 실행한다. 첫 명령으로 JDK가 실행되는지 확인한 뒤 두 번째 명령을 실행한다. .java 파일을 직접 실행하므로 별도의 컴파일 명령은 필요 없다.

java --version
java --class-path . AsListCheck.java

Windows 11의 OpenJDK 17.0.2에서 버전 출력 뒤에 얻은 프로그램 출력은 다음과 같다. 예외 두 개는 프로그램이 잡아서 출력하므로, 뒤의 복사 실험도 계속 실행된다.

view.set: array=[green, blue]
array[1]: view=[green, yellow]
view.add: java.lang.UnsupportedOperationException
view.remove: java.lang.UnsupportedOperationException
after failures: view=[green, yellow], array=[green, yellow]
copy.add: copy=[white, yellow, black], size=3
copy.remove: copy=[white, black], size=2
copy writes: array=[green, yellow]
array[0]: view=[purple, yellow], copy=[white, black]

view.set 뒤 원본 배열의 첫 값이 green으로 바뀌었다. 반대 방향도 같다. 배열의 두 번째 값을 yellow로 바꾸자 목록에서도 바로 보였다. asList가 배열 내용을 별도의 저장 공간으로 복사했다고 생각하면 이 두 줄을 설명할 수 없다.

add와 유효한 인덱스의 remove(0)는 모두 예외를 냈고, 실패 뒤에도 목록과 배열은 [green, yellow]였다. set은 이미 있는 칸을 교체하므로 크기를 바꾸지 않는다. 반면 이 예제의 추가·삭제는 원소 수를 바꾸려 하므로 고정 크기 제한에 걸린다.

크기 변경이 필요하면 목록을 복사한다

예제에서 new ArrayList<>(view)로 만든 목록은 set과 add를 거쳐 [white, yellow, black], 크기 3이 됐다. remove(1) 뒤에는 [white, black], 크기 2가 됐다. 그동안 원본 배열은 [green, yellow]를 유지했다.

마지막에 배열의 첫 값을 purple로 바꾸자 view만 따라 바뀌고 copy는 그대로였다. 복사 시점의 원소를 담은 새 목록이므로 이후 배열의 칸 교체와 연결되지 않는다. ArrayList 문서 (새 탭에서 열림)는 크기 변경을 지원하는 목록이며, 컬렉션을 받는 생성자가 전달된 원소를 순서대로 담는다고 설명한다.

코드를 고를 기준은 다음 두 가지다.

  • 원소 수를 유지하면서 배열과 같은 칸을 읽고 바꿔야 한다면 Arrays.asList(array)를 쓴다.
  • 배열의 칸 변경과 분리하고 원소를 추가·삭제해야 한다면 new ArrayList<>(Arrays.asList(array))로 시작한다.

여기서 복사되는 것은 목록을 구성하는 원소 참조다. 원소 객체 자체를 복제하지는 않는다. 별도로 실행한 StringBuilder[] 예제에서도 복사본과 배열이 같은 원소 객체를 가리켰고, 그 객체에 문자를 붙이자 양쪽에서 바뀐 내용이 보였다. 가변 원소의 내부 상태까지 분리해야 한다면 별도 복사 규칙이 필요하다.

List.of와 같은 변경 불가 목록은 아니다

Arrays.asList를 “불변 목록이라서 실패한다”고 설명하면, 성공한 set과 원본 배열의 변경을 놓친다. List.of의 변경 불가 계약 (새 탭에서 열림)은 추가·삭제뿐 아니라 원소 교체도 금지한다. 별도 실행에서 List.of("red", "blue").set(0, "green") 역시 UnsupportedOperationException을 냈다. 크기를 고정하는 것과 원소 교체까지 막는 것은 다른 선택이다.

다만 모든 remove 호출이 반드시 예외를 내는지로 목록을 판별하지는 않는다. 이번 실행에서 없는 값에 대한 view.remove("missing")은 false를 반환했다. 삭제할 원소가 없어 크기도 바뀌지 않았다. 크기 제한을 확인할 때는 위 예제처럼 실제 원소를 지우는 remove(0)의 결과를 본다.

직접 실행한 범위는 OpenJDK 17.0.2의 합성 배열과 단일 스레드다. Java 21 문서 (새 탭에서 열림)에서도 고정 크기와 원본 연결 계약을 확인했지만, Java 21 실행이나 여러 스레드의 동시 수정은 검증하지 않았다. 자기 코드에서 set마저 UnsupportedOperationException을 낸다면, 전달받은 목록이 실제로 어느 생성 경로에서 만들어졌는지 먼저 확인한다.

← 전체 글 보기