ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ArrayList는 어떻게 동적으로 크기를 늘릴 수 있을까?
    One Cookie a day 2023. 10. 9. 16:01

    이전에 CS 스터디에서 ArrayList와 LinkedList의 차이에 대해 공부한 적이 있다. 

    • ArrayList
      • 배열과 유사하나 크기를 지정하지 않고 동적으로 값을 삽입하고 삭제할 수 있음
      • 초기 용량이 다 차면 더 큰 ArrayList 생성 후 거기다 복사한다. 원소가 많다면 대략적인 용량을 처음에 설정하는 것이 성능상 좋다. 
      • 조회 
        • 각 데이터의 index 가지고 있고 index를 통해 해당 데이터에 바로 접근 가능 
      • 데이터 삽입, 삭제
        • 가장 마지막 자리에 데이터를 삽입하거나 삭제한다면 빠른 속도가 가능하나, 중간에 데이터를 삽입하거나 삭제할 경우 빈 칸만큼 뒤의 원소를 앞으로 이동시켜야 한다. (위치를 맞춰줘야 함) 
        • 따라서 일반적으로 ArrayList 중간에 원소를 삽입하거나 삭제하는 경우가 많다면 비효율적
        • 만약 원소를 맨 뒤에 원소부터 쭉 삭제하거나 삽입하게 된다면 오히려 Linked List보다 빠르다. 

     

     

    데이터를 삽입할 때, ArrayList는 공간이 부족할 시 자동으로 공간을 확장해준다. 너무 자주 공간을 할당하거나, 한 번에 너무 많은 공간을 할당해버려서 낭비되는 공간이 많아지지 않도록 내부적으로 구현이 되어있다. 

    데이터를 추가할 때 어떤 작동방식을 가지고 있어서 이것이 가능한것일지, Java 6, 7, 8 버전을 통해 조사해보았다. 

     

     

     


    Java 6

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
        private transient Object[] elementData;
    	
        private int size;
        
        // ...
    }

     

    add 

    • capacity 다 찼는지 확인한 후(ensureCapacity) data 넣기 
    • checkForComodification
      • Iterator<E> 인터페이스 구현한 내부 클래스 Itr에 존재하는 final 메소드
        • if (modCount != expectedModCount) throw new ConcurrentModificationException();
    /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
    */
    public boolean add(E e) {
            ensureCapacity(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
    }
    
    
    public void add(int index, E e) {
                rangeCheckForAdd(index);
                checkForComodification();
                parent.add(parentOffset + index, e);
                this.modCount = parent.modCount;
                this.size++;
    }
    
    
    private void rangeCheck(int index) {
            if (index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    private void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

     

    ensureCapacity

    public void ensureCapacity(int minCapacity) {
            modCount++;
            int oldCapacity = elementData.length;
            if (minCapacity > oldCapacity) {
                Object oldData[] = elementData;
                int newCapacity = (oldCapacity * 3)/2 + 1;
                if (newCapacity < minCapacity)
                    newCapacity = minCapacity;
                // minCapacity is usually close to size, so this is a win:
                elementData = Arrays.copyOf(elementData, newCapacity);
            }
    }
    • 먼저 처음의 elementData 길이를 oldCapacity에 저장(data 저장된 길이가 아니고 현재의 배열 크기를 의미한다! 이만큼 넣을 수 있다는뜻)
    • minCapacity(데이터 하나 넣었을 때의 용량 == 최소 필요 길이) > oldCapacity라면 
      • 새로운 용량은 (oldCapacity * 3) / 2 + 1로 계산
      • 만약 새로운 용량보다도 더 큰 길이가 필요하다면 newCapacity를 minCapcity(필요한 최소 길이)로 바꿔준다.
    • new Capacity만큼의 크기의 배열에 기존 데이터를 복사해 넣는다. 
    • 이때 복사되는 Arrays.copyOf는 shallow Copy

     


     

    Java 7

     

    public boolean add(E e) {
            ensureCapacity(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
    }
    
    public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacity(size + 1);  // Increments modCount!!
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
    }
    
    public boolean addAll(Collection<? extends E> c) {
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacity(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
    }
    
    public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacity(size + numNew);  // Increments modCount
    
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
    
            System.arraycopy(a, 0, elementData, index, numNew);
            size += numNew;
            return numNew != 0;
    }

     

    ensureCapacity

    단순히 곱하기 나누기로 계산했던 JDK6에서, 7은 grow 함수 호출을 통해 arrayList의 크기를 늘린다.

     

    grow(int minCapacity)

    • oldCapacity : 현재 데이터 담는 배열 크기
    • newCapacity : 비트 연산을 통해 구함 
      • oldCapacity >> 1 : oldCapacity의 비트를 한칸 오른쪽으로 이동, 빈칸은 최상위 부호와 같은 값으로 채워짐
      • JDK 6에서는 일반 곱셈으로 구하는데, 위의 비트 연산자는 단순 비트값을 옮겨주기만 하면 되기 때문에 CPU 부하를 줄일 수 잇음 
      • 만약 MAX_ARRAY_SIZE보다 크다면(MAX_ARRAY_SIZE : Integer.MAX_VALUE - 8이다) hugeCapacity 호출 후newCapacity로 배열 복사 
        • 이때, minCapacity가 음수 나왔다는 것은 int 범위를 넘어갔다는 의미이므로 OutofMemory 에러 던진다.

     

    public void ensureCapacity(int minCapacity) {
            modCount++;
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

     

     


     

     

    Java 8

     

    기본적으로 7까지는 10이라는 default 크기의 배열이 생겼는데, 8에서는 EMPTY_ELEMENTDATA가 할당이 된다. 

    private static final Object[] EMPTY_ELEMENTDATA = {}; 로 선언이 되어 있다. 

    빈 배열에 처음 원소를 넣게되면, DEFAULT_CAPACITY인 10 크기를 가진 배열로 확장된다. 

    public ArrayList() {
            super();
            this.elementData = EMPTY_ELEMENTDATA;
    }

     

    add

    • ensureCapacityInternal이라는 메소드가 사용되었다. 
      • 배열에 아무것도 없는 초기 상태라면, minCapacity 따라 default를 넣을지, minCapacity 만큼 할당할 지 결정한다. 
      • 그 후 ensureExplicitCapacity 호출
    • ensureExplicitCapacity
      • 앞 버전들에 있는 ensureCapacity와 동일한 흐름으로 간다. (grow 내부에서 비트연산자 사용하는 것, MAX_CAPACITY 등) 
    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
    }
    
    public void add(int index, E element) {
            rangeCheckForAdd(index);
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            System.arraycopy(elementData, index, elementData, index + 1, size - index);
            elementData[index] = element;
            size++;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
            if (elementData == EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
    }

     

     

     


    결론

      Java 6 Java 7  Java 8
    배열 크기
    늘리는 방식 
    ( 현재 배열의 크기 * 3 ) / 2 + 1 shift 연산, oldCapacity >> 1
    사용되는 함수 - add
    - ensureCapacity
    - add
    - ensureCapacity
    - grow
    - hugeCapacity
    - add
    - ensureCapacityInternal
    - ensureExplicitCapacity
    - grow
    - hugeCapacity
    Add 수행 순서  - add
    - ensureCapacity
    넣을 수 있는지 검사 후, 불가능하다면 배열 크기 늘리고 원본 배열 복사 후 넣는다. 
    - add
    - ensureCapacity
    넣을 수 있는지 검사
    - grow
    할당할 수 있는 배열의 최대 크기 넘지 않는지 검사 후 크기 늘려줌
    - hugeCapacity
    MAX_ARRAY_SIZE 보다 크다면 Integer.MAX_VALUE까지만 늘림, 그 이상은 out of memory
    - add 
    - ensureCapacityInternal
    최초값 넣는건지 아닌지 검사
    - ensureExlicitCapacity
    이전 버전의 ensureCapacity와 동일하게 grow, hugeCapacity 호출 
    최초값 크기 10인 배열 빈 배열
    (처음 add 호출시에 할당 크기 결정)

     

     

     

     

    기타 참조 글 

     

Designed by Tistory.