Java

[Java] ArrayList 내부 구조 파헤치기(크기, 메모리 구조, 내부 동작 과정)

하부루 2024. 7. 30. 17:52

1. ArrayList란?

ArrayList는 Java 컬렉션 프레임워크의 일부로, 가변 길이 배열을 구현한 클래스

ArrayList는 순차적으로 데이터를 저장하며, 저장된 요소는 인덱스를 통해 접근할 수 있음

  • 동적 배열
    • ArrayList는 동적으로 크기가 조정되는 배열
    • 요소를 추가하거나 제거할 때 크기가 자동으로 조정
  • 인덱스 접근
    • 배열처럼 인덱스를 사용해 요소에 접근할 수 있음
  • 중복 허용
    • ArrayList는 중복된 요소를 허용
  • 비동기적
    • ArrayList는 기본적으로 비동기적
    • 멀티스레드 환경에서 동기화를 지원하지 않음
// 기본 생성
ArrayList<String> list = new ArrayList<>();

// 초기 용량을 지정해 생성
ArrayList<Integer> list = new ArrayList<>(10);
 
// 주요 메서드
add(E e) : 요소를 리스트에 추가
add(int index, E element) : 특정 위치에 요소를 추가
get(int index) : 특정 위치의 요소를 반환
set(int index, E element) : 특정 위치의 요소를 새로운 요소로 대체

remove(int index) : 특정 위치의 요소를 제거
remove(Object o) : 특정 요소를 리스트에서 제거

size() : 리스트의 요소 개수를 반환
clear() : 리스트의 모든 요소를 제거
isEmpty() : 리스트가 비어있는지 확인합
contains(Object o) : 특정 요소가 리스트에 포함되어 있는지 확인

2. ArrayList의 크기

  • ArrayList는 내부적으로 요소를 저장하기 위해 배열을 사용
  • 초기에는 고정된 크기의 배열(10)이 생성되며, 요소가 추가됨에 따라 크기가 변경
  • ArrayList에 요소가 추가되어 현재 크기를 초과하면, ArrayList는 더 큰 배열을 할당하고, 기존의 요소를 새로운 배열로 복사 함
ArrayList<Integer> arrList = new ArrayList<>(10);
→ ArrayList 생성시 크기를 10으로 지정했기때문에 초기 크기는 10

ArrayList<Integer> arrList = new ArrayList<>();
→ ArrayList 생성시 크기를 지정하지 않았지만 초기 크기는 10

* private static final int DEFAULT_CAPACITY = 10;
ArrayList 설정 중에 초기 크기 설정하는 상수

크기가 10인 ArrayList에 요소를 더 추가하면 grow() 메서드를 통해서
ArrayList의 크기는 11이 되는게 아닌 * 1.5의 값인 15가 됨
arrList = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
null, null, null, null, null}
//ArrayList 추가 용량 지정 메서드
private void grow(int minCapacity) {

    // 현재 배열의 용량
    int oldCapacity = elementData.length;
    
    // 1.5배 증가된 새로운 용량 계산
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 새로운 용량의 배열로 복사
    elementData = Arrays.copyOf(elementData, newCapacity);
}


3. ArrayList의 메모리 구조

  • ArrayList의 내부 배열은 Heap 영역에 할당
  • 배열 참조는 ArrayList 객체가 Stack 영역에서 유지
  • 요소는 배열의 index를 통해 접근 가능


4. ArrayList의 주요 메서드 동작 과정

1. get( )

ex) numberList.get(2);

  • numberList에서 인덱스 2번값 호출
  • checkIndex( ) 메서드로 현재 호출한 인덱스 번호가 전체 크기를 초과하는지 체크
  • elementData( ) 메서드로 호출한 인덱스 값 리턴

2. set( )

ex) nameList.set(2, “홍길동”);

  • nameList에서 인덱스 2번값에 “홍길동”을 삽입
  • checkIndex( ) 메서드로 현재 호출한 인덱스 번호가 전체 크기를 초과하는지 체크
  • elementData( ) 메서드로 호출한 인덱스 값을 oldValue에 저장
  • elementData[index] 로 새로 입력한 “홍길동”을 삽입해 데이터 변경
  • oldValue를 리턴

3. add( )

exampleList.add(5, “홍길동”);

public void add(int index, E element) {
    // 1. 인덱스 범위 확인
    rangeCheckForAdd(index);

    // 2. 수정 횟수 증가 (ConcurrentModificationException을 방지하기 위함)
    modCount++;

    // 3. 현재 리스트의 크기를 저장
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length) {
        // 4. 현재 크기가 배열의 길이와 같으면 배열 크기 증가
        elementData = grow();
    }

    // 5. 요소 복사: index 위치부터 끝까지 요소를 한 칸씩 오른쪽으로 이동
    System.arraycopy(elementData, index, elementData, index + 1, s - index);

    // 6. 새로운 요소 삽입
    elementData[index] = element;

    // 7. 리스트 크기 증가
    size = s + 1;
}

exampleList.add(“홍길동”);

  • modCount++로 수정 횟수 증가
  • 다른 add( ) 메서드 호출 (추가할 데이터, 배열, 사이즈가 매개변수인 add( ) 메서드)
  • 배열의 크기를 +1 증가시키고 배열의 마지막 요소에 새로운 값을 넣음

4. remove( )

example.remove(2);

  • checkIndex 메서드로 인덱스 번호가 유효한지 체크
  • 삭제할 인덱스의 값을 oldValue에 저장
  • fastRemove 메서드 호출
  • modCount++로 수정 횟수 증가
  • arraycopy 메서드로 현재 수정된 인덱스 이후의 값들을 한 칸씩 왼쪽으로 이동
  • es[size = newSize] = null 로 마지막 요소를 null로 변경

example.remove(”홍길동”);