Language/Java

[Java] ArrayList 파헤쳐 보기

Deveun 2021. 10. 6. 02:40

Java 개발에서 Collection의 데이터 타입을 사용하면서 내부 코드를 볼 생각을 하지는 않았었는데, 직접 ArrayList 클래스를 살펴보며 어떻게 동작하는지 공부해보니 흥미로운 부분이 많았다. 여기서는 ArrayList의 생성자, 삽입/삭제/초기화/정렬 메소드 중심으로 정리해보려 한다.

 

Resizable-array implementation of the List

 

ArrayList란 Size가 변하는 배열을 말한다. 그리고 다음과 같은 몇가지 특징을 가진다.

 o 중간에 데이터를 삽입, 삭제하는 것이 가능

 o Reference Data Type만 넣을 수 있음

 o Thread Safe 하지 않음

 

 

그럼 코드를 한 번 살펴보자.

o 인스턴스 변수

: ArrayList는 값을 담고있는 Object배열과 실제 존재하는 element의 갯수를 담는 size를 인스턴스 변수로 가진다.

transient Object[] elementData; 
private int size;

 

o 생성자

: default는 { }, 생성자로 데이터 개수를 입력받을 시 new Object[initialCapacity] 로 element 배열 생성

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA; // {}
        } else {//...}
    }

 

o 데이터 삽입

:  List 인터페이스를 구현한 add() 메소드로 데이터를 추가하는 경우는 두가지 케이스로 나뉜다. 

 

(1) 데이터의 맨 끝에 삽입하는 경우

   private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow(); // 배열의 크기 증가시키기
        elementData[s] = e;
        size = s + 1;
    }

 

(2) 특정 index에 삽입하는 경우

    public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow(); // 배열의 크기 증가시키기
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index); // index위치 부터의 데이터를 index+1 위치부터 시작하도록 바꾸어줌
        elementData[index] = element;
        size = s + 1;
    }

 

여기서 주의할 부분은 grow() 메소드이다. ArrayLisr의 elementData는 배열이기 때문에 제한된 length를 가지는데, 실제 값이 들어있는 size가 이와 같아지게 되면 배열의 크기를 증가시키는 작업이 필요하다. 

 

* grow()

: 이는 내부적으로 다음과 같이 현재 size/2만큼 length를 증가시키고 이전 elements들을 copy한다.

    private Object[] grow(int minCapacity) {
    	//생략..
        return elementData = Arrays.copyOf(elementData, newCapacity);
    }

 

맨 뒤에 데이터를 추가하는 경우에는 size위치에, index가 주어진 경우에는 기존에 index~end 까지의 데이터를 index+1~end+1 위치로 옮긴 뒤에 해당 위치에 값을 넣어주고 size를 증가시킨다.

 

o 데이터 삭제

: List 인터페이스에 선언된 메소드들을 구현한 부분이다.

 

(1) 특정 인덱스의 데이터를 삭제하는 경우

--> remove() 메소드

: fastRemove() 에서 System.arraycopy(es, i + 1, es, i, newSize - i); 를 수행한 뒤, 삭제하는 element값을 return한다.

 

(2) 모든 데이터를 삭제하는 경우

--> clear() 메소드

: 모든 elementData를 모두 순회하며 값을 null로 바꿔준다.

 

o 정렬

: List 인터페이스에 선언된 메소드를 구현하고 있는데, Arrays.sort() 메소드를 통해 정렬하고 있다.

그렇다면 한 번 더 들어가서 Arrays.sort()는 어떻게 구현되어져 있을까?

    public static <T> void sort(T[] a, int fromIndex, int toIndex,
                                Comparator<? super T> c) {
       //...
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, fromIndex, toIndex, c);
            else
                TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
      
      // 생략...
    }

이는 개발자가 system.property 값을 설정하여 LegacyMergeSort를 사용하거나 merge sort보다 최적화된 TimSort를 사용하도록 구현되어있다. 여기서는 자세한 해당 정렬 방법은 생략하고, TimSort가 사용되는 것만 확인하고 넘어가자. 

 

 

이 외에 get(), set() 같은 메소드는 구현이 너무 간단하여 설명을 생략하였다.

추가로 ArrayList 내부에는 훨씬 더 많은 메소드들이 존재하지만, 주요 메소드만 정리해보았으니 개발하면서 한번씩 ArrayList에서 사용하는 메소드들은 실제로 해당 소스파일에서 확인해보는 것도 좋을 것 같다.