본문 바로가기

Java

[Java] Java Collections Framework 와 List - ArrayList 편

들어가며

Class diagram of Java Collections framework

Java Collections Framework
애플리케이션을 개발할때 다수의 객체를 저장해 두고 필요할 때마다 꺼내서 사용하는 경우가 많은데, 이를 효율적으로 관리할 수 있도록 자료구조(Data Structure)를 활용해 인터페이스와 클래스들을 미리 만들어 놓은 것이 바로 컬렉션 프레임워크이다. 
해당 인터페이스들과 클래스들은 java.util 패키지에서 관리된다.

List 인터페이스 

Diagram for List

위의 이미지는 리스트의 다이어그램을 나타낸 것으로 List 가 extends 하고 있는 인터페이스를 보여준다.

List<E> 는 Collection<E> 를 확장하고, Collection<E> 는 Iterable<T> 를 확장한다.

Collection<E> 인터페이스는 컬렉션 계층의 루트 인터페이스로 자바의 컬렉션은 모두 이를 상속받는다.

 

Iterable<T> 인터페이스는 왜 컬렉션 루트 인터페이스 상위의 인터페이스 일까?
Iterable<T> 인터페이스를 구현하면 향상된 for 문의 대상이 되는데, 자바 컬렉션 프레임워크는 이를 강제하여 iterator를 사용한 반복을 무조건적으로 구현하도록 한 것을 알 수 있다.

 

List Interface 는 객체를 일렬로 늘어놓은 선형 구조로, 객체를 인덱스로 관리하기 때문에 그 형태가 배열과 비슷하다. 자바의 배열은 저장할 수 있는 객체 수가 생성 시에 결정되기 때문에 불특정 다수의 객체를 저장하기에는 문제가 있다.  이를 해결하기 위한 것이 바로 List 이다. 즉, 배열 기능과 동적 크기 할당 기능이 합쳐진 것이 바로 List 이다.

List 컬렉션은 객체 자체를 저장하는 것이 아니라 객체의 번지를 참조한다. 동일한 객체를 중복 저장하는 것이 가능한데, 이때 동일 객체는 동일 번지가 참조된다.

 

List Interface 를 구현한 클래스들에는 ArrayList, LinkedList, Vector 가 있다.

 


ArrayList

Diagram for ArrayList

이제부터 ArrayList 클래스를 하나씩 뜯어보도록 하자.

 


Fields

    @java.io.Serial
    private static final long serialVersionUID = 8683452581122892189L;

    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // 직렬화를 막기 위해 transient 키워드를 붙였다

    private int size;

 

- DEFAULT_CAPACITY : 크기를 설정하지 않고 ArrayList 생성 시, Default 용량은 10이다.

 

- EMPTY_ELEMENTDATA : 기본 크기의 빈 인스턴스에 사용되는 공유 빈 배열 인스턴스. 첫 번째 요소가 추가될 때 늘어날 양을 알기 위해 이를 EMPTY_ELEMENTDATA 와 구별한다.

 

- DEFAULTCAPACITY_EMPTY_ELEMENTDATA :  배열 목록의 요소가 저장되는 배열 버퍼. ArrayList의 용량은 이 어레이 버퍼의 길이이다. 첫 번째 요소를 추가할 때 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA인 빈 ArrayList가  DEFAULT_CAPITY로 확장된다.

 

- elementData : 크기를 설정하지 않고 ArrayList 생성 시, Default 용량은 10이다.

 

- size :  ArrayList의 크기.


Constructors

    /**
     * @param  initialCapacity  the initial capacity of the list
     * @throws 지정된 초기 용량이 음수이면 IllegalArgumentException 예외를 발생시킨다.
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }
    
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * @param c ArrayList로 대체될 요소를 가진 컬렉션
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

 

- ArrayList() : 초기 용량이 10인 빈 ArrayList 를 생성한다.

 

- ArrayList(int initialCapacity) :  ArrayList 생성시 initialCapacity를 인자로 설정한다면 해당 용량을  초기 용량으로 하는 빈ArrayList 를 생성한다.

 

- ArrayList(Collection<? extends E> c) : 컬렉션의 반복자가 반환하는 순서대로 컬렉션 요소를 포함하는 List 를 생성한다.


Methods

ArrayList는 대략 60개 정도의 메소드가 존재하기 때문에, ArrayList의 특징과 연관되는 대표적인 메소드 몇가지만 살펴보도록 하자.

 

1. add method

 

    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

    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);
        elementData[index] = element;
        size = s + 1;
    }
    
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

    private Object[] grow() {
        return grow(size + 1);
    }

 

add(E e)  add(int index, E element) 메소드를 중심적으로 살펴보자. 

 

add(E e)

내부적으로 private 메소드를 이용해 해당 ArrayList의 size 와 ArrayList 의 요소를 담고있는 배열 elementData 의 길이를 비교한 후, 같다면 역시 private 메소드 grow()를 사용해 ArrayList의 용량을 1 늘린다. ArrayList 에 요소를 추가한 후, size 를 1 증가시켜 현재 ArrayList 의 size 를 최신값으로 유지한다.

 

add(int index, E element) 

먼저 rangeCheckForAdd 매소드를 통해 삽입하고자 하는 인덱스의 범위를 체크하고, 문제가 없다면 add(E e, Object[] elementData, int s) 메소드와 유사한 로직을 실행한다. 한가지 다른 점은 배열 복사를 이용한다는 것이다. 배열은 한번 생성하면  크기 변경이 불가능하기 때문에 더 많은 저장공간이 필요하다면 더 큰 배열을 만들고 이전 배열의 항목들을 복사해야 한다.

이때 System.arraycopy 메소드를 활용해 새로운 요소를 삽입할 인덱스를 기준으로 한칸씩 미룬다.

 

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 
src : 원본 배열
srcPos : 원본 배열에서 복사할 항목의 시작 인덱스
dest : 새 배열
destPos : 새 배열에서 붙여넣을 시작 인덱스
length : 복사할 개수

 

 

 

2. remove method

 

    public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        found: {
            if (o == null) {
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
        fastRemove(es, i);
        return true;
    }

    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

 

이번에는 remove(Object o) 메소드를 간단히 살펴보자.

현재 ArrayList 에 있는 모든 요소를 돌며 해당 요소와 일치하는 위치의 인덱스를 가져온 후, 역시 System.arraycopy 메소드를 활용해 

새로운 삭제된 배열을 복사 생성해 낸다. 위의 add(int index, E element) 메소드와 같이 비싼 녀석이라는 것을 알 수 있다.

 

 

 

3. get method

 

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }

 

get(int index) 메소드는 설명할 것도 없다. 인덱스를 통해  데이터를 가져오는 것을 아주 잘 확인할 수 있다.

 

 


ArrayList 정리

ArrayList 는 List 인터페이스의 구현 클래스로, ArrayList 에 객체를 추가하면 객체가 인덱스로 관리된다. 일반 배열과 ArrayList 는 인덱스로 객체를 관리한다는 점에서 유사하지만 동적으로 배열의 크기를 조정한다는 점에서 다르다. 즉, ArrayList 는 Object[] 배열을 사용해 내부 구현을 동적으로 관리하는 배열에 지나지 않는다.

위의 추가, 삭제, 검색에 관련된 대표적인 메소드 몇가지만 살펴보더라도 ArrayList 가 어떤 특징을 가지고 있는지 확인할 수 있다. 배열의 끝에 객체를 추가하고 인덱스를 통한 검색에서는 상당히 빠른 성능을 보여주지만, 해당 인덱스 위치에 객체를 삽입하거나, 객체를 삭제하는 것은 좋지 않은 성능을 보여준다. 따라서 인덱스를 통한 검색이나 배열 끝에 객체를 추가하는 경우에 ArrayList 를 사용하는 것이 권장된다.

 


참고 자료 :https://st-lab.tistory.com/142