본문으로 건너뛰기
김신건의 로그

[Java] ArrayList

· 수정 · 📖 약 2분 · 786자/단어 #java #collection #data-structure #list #array
ArrayList, java.util.ArrayList, Java ArrayList, 어레이리스트, ArrayList class

정의

java.util.ArrayList동적 배열 (dynamic array) 로 구현된 List. 내부적으로 Object[] 를 들고 다니며, 용량이 부족해지면 새 배열로 옮긴다.

Java 의 가장 흔한 컬렉션 구현체. 인덱스 기반 접근이 압도적으로 빠르고, 캐시 친화적이라 LinkedList 보다 실측이 빠른 경우가 대부분이다.

시각화

내부 구조

public class ArrayList<E> extends AbstractList<E> implements List<E>, ... {
    transient Object[] elementData;   // 백킹 배열
    private int size;                 // 실제 원소 수
    private static final int DEFAULT_CAPACITY = 10;
}

세 개의 핵심 변수.

  • elementData: 실제 데이터가 들어있는 배열. 길이가 곧 capacity.
  • size: 사용 중인 슬롯 수. size <= elementData.length 가 항상 성립.
  • DEFAULT_CAPACITY: 첫 add 시 할당되는 초기 크기 (10).

빈 ArrayList 의 lazy 초기화

List<Integer> list = new ArrayList<>();  // elementData = {}, 빈 배열
list.add(1);                              // 이 순간 처음으로 길이 10 배열 할당

new ArrayList<>() 만 호출했을 때는 실제 배열을 만들지 않는다. 첫 add 에서 비로소 길이 10 의 배열이 잡힌다. 메모리 절약을 위한 lazy 전략.

용량 확장 (grow())

size == elementData.length 인 상태에서 add 가 호출되면 새 배열을 만들고 옮긴다.

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = ArraysSupport.newLength(
        oldCapacity,
        minCapacity - oldCapacity,    // 최소 증가량
        oldCapacity >> 1              // 선호 증가량 = 50%
    );
    return elementData = Arrays.copyOf(elementData, newCapacity);
}

기본 정책: 1.5 배 (oldCapacity + oldCapacity >> 1). 즉 10 → 15 → 22 → 33 → 49 → 73 → …

IMPORTANT

미리 크기를 알면 new ArrayList<>(initialCapacity) 로 생성. 매 grow() 마다 Arrays.copyOf (= O(n) 복사) 가 발생하므로, 큰 데이터를 채울 때 차이가 크다.

주요 연산 비용

메서드시간 복잡도설명
get(int i)O(1)elementData[i] 직접 접근
set(int i, E e)O(1)인덱스 자리만 교체
add(E e)amortized O(1)size 슬롯에 대입, capacity 부족하면 1.5x 확장
add(int i, E e)O(n)i 이후 원소를 모두 한 칸씩 오른쪽으로 shift
remove(int i)O(n)i 이후 원소를 모두 왼쪽으로 shift
remove(Object o)O(n)indexOf 검색 + shift
contains(Object o)O(n)선형 검색
indexOf(Object o)O(n)선형 검색
size(), isEmpty()O(1)size 필드

amortized O(1) 의 의미

매 add 가 O(1) 이 아니라 평균적으로 O(1) 이라는 뜻. n 개 원소를 추가하는 비용을 따져보면:

용량 확장 비용 합 = 1 + 1.5 + 2.25 + ... ≈ 3n (등비급수 합 ≤ n / (1-1.5/2.25) ≈ 3n)
총 비용 = n (단순 대입) + 3n (확장) ≈ 4n
평균 = O(1)

함정

1. remove(int) vs remove(Object) 모호성

List<Integer> 에 대해서는 둘이 다른 의미.

List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1);                  // 인덱스 1 의 원소 제거 → [10, 30]
list.remove(Integer.valueOf(1)); // 값 1 을 찾아서 제거 → 변화 없음 (1 이 없음)

오토박싱이 위 둘을 헷갈리게 만든다. 명시적으로 Integer.valueOf(x) 를 권장.

2. fail-fast iterator

List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer x : list) {
    list.add(4);  // ConcurrentModificationException
}

iterator 의 expectedModCountArrayList.modCount 와 다르면 즉시 예외. 단일 스레드여도 발생.

3. subList() 는 뷰일 뿐

List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> sub = list.subList(1, 4);  // [2, 3, 4]
sub.set(0, 99);                          // sub = [99, 3, 4]
// list 는?
System.out.println(list);                // [1, 99, 3, 4, 5] ← 원본도 바뀐다

독립 사본이 필요하면 new ArrayList<>(sub).

4. thread-safe 가 아니다

여러 스레드가 동시에 수정하면 데이터 손상, 무한 루프, ArrayIndexOutOfBoundsException 가 발생할 수 있다. 동시성이 필요하면 CopyOnWriteArrayList 또는 Collections.synchronizedList.

메모리 사용량

  • 객체 헤더 (~16B) + Object[] 참조 (4-8B) + size (4B) + modCount (4B) → 약 32-40B
  • elementData: capacity 만큼의 참조 슬롯 (4-8B × capacity)
  • 비어있는 슬롯 (capacity - size) 도 메모리 차지. trimToSize() 로 회수 가능.
ArrayList<Integer> list = new ArrayList<>(1000);
list.add(1); list.add(2);  // 998 슬롯이 null 로 낭비됨
list.trimToSize();          // elementData = new Object[2]

언제 ArrayList 가 답인가

  • 인덱스 접근이 자주 일어남 (랜덤 read)
  • 끝에 추가가 대부분 (스택, 누적 버퍼)
  • 이미 정렬돼서 보관, 이진 탐색 (Collections.binarySearch)
  • 단일 스레드 또는 외부 동기화

언제 다른 구현이 더 나은가

  • 앞/중간 삽입·삭제가 빈번 + 인덱스 접근 거의 없음LinkedList (드물게)
  • 수정이 거의 없는 공유 데이터CopyOnWriteArrayList
  • 불변List.of(...) / List.copyOf(...)

참고

이 글의 용어 (9개)
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] ConcurrentModificationExceptionjava
정의 (CME) 는 가 컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 ( ). 이름과 달리 반드시 동시성 (멀티스레드) 문제일 때만 발생하는 것이 아니다. 단일…
[Java] CopyOnWriteArrayListjava
정의 는 쓰기 시 배열 전체를 복사 하는 thread-safe 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 으로 직렬화한다. (JSR-166) 의 컬렉션. 읽기 압도적 다,…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] LinkedListjava
정의 는 양방향 연결 리스트 (doubly linked list) 로 구현된 이자 . 각 원소는 와 포인터를 가진 노드로 보관된다. 와 같은 List 이지만, 메모리 레이아웃과 …
[Java] Listjava
정의 는 순서가 있고 중복이 허용 되는 컬렉션을 표현하는 인터페이스. 각 원소는 0 부터 시작하는 정수 인덱스로 접근 가능. 인터페이스를 확장하며, 의 모든 성질을 물려받는다. …
[Java] Objectjava
정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
Geometry Basic: 벡터, CCW, 외적, 내적algorithm
정의 기하 알고리즘의 밑바탕. 대부분의 2D/3D 기하 문제는 벡터 연산, 내적/외적, CCW 판정 세 가지의 조합으로 풀립니다. 벡터 2D 벡터: 두 점 $A = (ax, ay…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기