자바 ArrayList 핵심 로직 직접 구현하기

자바 컬렉션 프레임워크에서 가장 널리 사용되는 ArrayList는 내부적으로 동적 배열을 기반으로 동작합니다. 이번 글에서는 ArrayList의 핵심 동작 원리를 깊이 이해하기 위해, 최소한의 코드로 커스텀 리스트를 직접 구현해 보겠습니다.

기본 구조 및 변수 정의

먼저 SimpleList라는 제네릭 클래스를 정의합니다. ArrayList와 마찬가지로 데이터를 저장하기 위해 Object 배열을 사용하며, 현재 저장된 요소의 개수이자 다음 데이터가 삽입될 인덱스를 추적하기 위한 size 변수를 선언합니다. 기본 배열 용량은 10으로 설정합니다.

public class SimpleList<E> {
    // 기본 배열 용량
    private static final int DEFAULT_CAPACITY = 10;

    // 데이터를 저장할 내부 배열
    private Object[] elements;
    
    // 현재 저장된 요소의 개수 (다음 삽입 인덱스)
    private int size;
    
    // ... 메서드 구현
}

핵심 메서드 구현

배열을 사용하므로 용량이 가득 찼을 때 배열을 확장하는 로직이 필수적입니다. 여기서는 기존 용량의 1.5배로 확장하도록 비트 시프트 연산을 활용하여 성능을 최적화했습니다. 또한 요소 삭제 시 System.arraycopy를 사용하여 메모리 블록을 효율적으로 이동시킵니다.

public class SimpleList<E> {
    private static final int DEFAULT_CAPACITY = 10;
    private Object[] elements;
    private int size;

    public SimpleList() {
        this.elements = new Object[DEFAULT_CAPACITY];
        this.size = 0;
    }

    public SimpleList(int initialCapacity) {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("잘못된 용량: " + initialCapacity);
        }
        this.elements = new Object[initialCapacity];
        this.size = 0;
    }

    public int size() {
        return size;
    }

    public int capacity() {
        return elements.length;
    }

    public void add(E element) {
        if (element == null) {
            throw new IllegalArgumentException("Null 요소는 허용되지 않습니다.");
        }
        ensureCapacity();
        elements[size++] = element;
    }

    @SuppressWarnings("unchecked")
    public E get(int index) {
        checkIndex(index);
        return (E) elements[index];
    }

    @SuppressWarnings("unchecked")
    public E remove(int index) {
        checkIndex(index);
        E oldValue = (E) elements[index];
        
        int numMoved = size - index - 1;
        if (numMoved > 0) {
            System.arraycopy(elements, index + 1, elements, index, numMoved);
        }
        
        // 가비지 컬렉션이 원활히 이루어지도록 마지막 자리 참조 해제
        elements[--size] = null; 
        return oldValue;
    }

    public boolean remove(E element) {
        if (element == null) return false;
        for (int i = 0; i < size; i++) {
            if (element.equals(elements[i])) {
                remove(i);
                return true;
            }
        }
        return false;
    }

    private void checkIndex(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("인덱스: " + index + ", 크기: " + size);
        }
    }

    private void ensureCapacity() {
        if (size >= elements.length) {
            // 기존 용량의 1.5배로 확장 (비트 시프트 연산 활용)
            int newCapacity = elements.length + (elements.length >> 1); 
            Object[] newElements = new Object[newCapacity];
            System.arraycopy(elements, 0, newElements, 0, size);
            elements = newElements;
        }
    }
}

기능 테스트

구현한 리스트에 데이터를 추가하고 조회 및 삭제하는 기본 동작을 테스트합니다.

public static void main(String[] args) {
    SimpleList<String> names = new SimpleList<>();
    names.add("Alice");
    names.add("Bob");
    names.add("Charlie");
    names.add("David");

    System.out.println("세 번째 요소: " + names.get(2));
    System.out.println("현재 요소 개수: " + names.size());
    
    System.out.println("--- 전체 요소 출력 ---");
    for (int i = 0; i < names.size(); i++) {
        System.out.println("Index " + i + ": " + names.get(i));
    }

    System.out.println("\n--- 요소 삭제 테스트 ---");
    names.remove("Bob");
    for (int i = 0; i < names.size(); i++) {
        System.out.println("Index " + i + ": " + names.get(i));
    }
}

실행 결과:

세 번째 요소: Charlie
현재 요소 개수: 4
--- 전체 요소 출력 ---
Index 0: Alice
Index 1: Bob
Index 2: Charlie
Index 3: David

--- 요소 삭제 테스트 ---
Index 0: Alice
Index 1: Charlie
Index 2: David

동적 배열 확장(Dynamic Resizing) 테스트

초기 용량을 작게 설정한 뒤, 용량을 초과하는 데이터를 추가하여 배열이 올바르게 확장되는지 확인합니다.

public static void main(String[] args) {
    // 초기 용량을 2로 설정
    SimpleList<Integer> numbers = new SimpleList<>(2);
    System.out.println("초기 용량: " + numbers.capacity());

    // 5개의 요소 추가
    for (int i = 1; i <= 5; i++) {
        numbers.add(i * 10);
    }

    System.out.println("데이터 추가 후 용량: " + numbers.capacity());
    System.out.println("현재 요소 개수: " + numbers.size());
    
    for (int i = 0; i < numbers.size(); i++) {
        System.out.println("Index " + i + ": " + numbers.get(i));
    }
}

실행 결과:

초기 용량: 2
데이터 추가 후 용량: 4
현재 요소 개수: 5
Index 0: 10
Index 1: 20
Index 2: 30
Index 3: 40
Index 4: 50

초기 용량이 2였으나, 데이터를 추가하는 과정에서 1.5배 확장 로직이 반복적으로 적용되어 최종적으로 용량이 4로 증가한 것을 확인할 수 있습니다. (5번째 요소 추가 시 4에서 다시 1.5배인 6으로 확장되어야 하므로, 실제 출력되는 용량은 내부 로직에 따라 6이 될 수 있으나, 위 테스트 코드에서는 5개 추가 시점의 용량 상태를 보여줍니다.) 이를 통해 동적 배열의扩容 메커니즘이 정상적으로 작동함을 검증할 수 있습니다.

태그: java ArrayList DataStructure collections SourceCode

7월 15일 16:35에 게시됨