Java List - 직접 구현해보기 2

이전에 작성한 배열의 문제점은 다음과 같았다.

  • 배열 길이를 동적으로 확장 없음
    • Out of Bounds 오류 발생

이전 코드에 이어서 배열 길이를 동적으로 확장하는 것을 만들어 개선한다.


CList

확장하는 신규 메소드를 먼저 만든다.

grow

private void grow() {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity * 2;

    // 배열을 새로 만들고
    // 기존 배열을 새로운 배열로 복사
/*

    Object[] newArr = new Object[newCapacity];
    for (int i = 0; i < elementData.length; i++) {
        newArr[i] = elementData[i];
    }
*/

    // 깊은 복사 새로운 배열
    // 참조 변경
    elementData = Arrays.copyOf(elementData, newCapacity);
    
}
  • grow() 메소드는 새로운 길이의 배열을 생성하고 기존 배열에 들어있는 값을 새 배열로 복사한다.

add - 기존 로직 변경

public void add(Object e) {
    // 코드 추가
    if (size == elementData.length) {
        grow();
    }

    elementData[size] = e;
    size++;
}
  • 기존 로직에서 size 가 배열 전체에 도달하면 데이터 추가 시 오류가 발생한다.
  • grow() 호출하여 기존 배열을 새로운 배열로 만들고 2배 크기로 늘린다.
    • Arrays.copyOf 깊은 복사의 새 배열로 손쉽게 알아서 값을 넣고 배열이 복사된다.

Main.class

CList list = new CList();
System.out.println("** 데이터 추가하기 **");
System.out.println(list);
list.add("a");
System.out.println(list);
list.add("b");
System.out.println(list);
list.add("c");
System.out.println(list);
list.add("c");
System.out.println(list);

System.out.println("** 메소드 기능 사용하기 **");
System.out.println("list.size() = " + list.size());
System.out.println("list.get(2) = " + list.get(2));
System.out.println("list.indexOf('c') = " + list.indexOf('c'));
System.out.println("list.set(2, 'z') = " + list.set(2, 'z'));
System.out.println("== 범위초과 ==");
list.add("d");
list.add("k");
list.add("k");
list.add("k");
list.add("k");
list.add("i");
System.out.println(list);

System.out.println("Crash?");
list.add("i");
System.out.println(list);

** 데이터 추가하기 **
[], size = 0, capacity = 10
[a], size = 1, capacity = 10
[a, b], size = 2, capacity = 10
[a, b, c], size = 3, capacity = 10
[a, b, c, c], size = 4, capacity = 10
** 메소드 기능 사용하기 **
list.size() = 4
list.get(2) = c
list.indexOf('c') = -1
list.set(2, 'z') = c
== 범위초과 ==
[a, b, z, c, d, k, k, k, k, i], size = 10, capacity = 10
Crash?
[a, b, z, c, d, k, k, k, k, i, i], size = 11, capacity = 20

크래쉬가 발생되지 않고 정상적으로 capacity 배열의 크기가 늘어난 것을 볼 수 있다.

grow()

Object[] elementData
size = 2, capacity = 2
+---+---+
| a | b | <- 1. add("c") 
+---+---+
x001


2. Create New Object
Object[capacity * 2]
size = 2, capacity = 4
+---+---+-------+-------+
| a | b | null  | null  | <- 3. x001 Array Copy Data
+---+---+-------+-------+
x002
  • add() 호출한 이후 size 가 배열 크기인 capacity 같다면 확장하는 작업을 수행한다.
  • grow() 메소드는 다음과 같은 역할을 지녔다.
    • 기존 2배 큰 배열 생성
    • 새로운 배열로 기존 배열 값 복사 (깊은 복사)
      • Arrays.copyOf(기존배열, 새길이)로 편리한 배열복사를 지원한다.
    • 참조 값 변경함

grow() 참조 변경

보기

size = 2, capacity = 2
+---+---+
| a | b |
+---+---+
elementData = x001


size = 2, capacity = 4
+---+---+-------+-------+
| a | b | null  | null  |
+---+---+-------+-------+
Arrays.copyOf = x002
  • 보기의 멤버 필드 elementData는 x001 를 참조하고 있었다.
  • elementData 참조 값을 새 배열인 Arrays.copyOf의 x002 힙메모리로 바꿔준다.
  • 기존 배열의 x001 는 아무도 참조하지 않으므로 GC의 대상이 된다.

grow() 배열의 크기

grow 배열은 왜 배수로 증가하도록 했을까

  • 데이터가 하나 추가될때마다 배열의 크기가 1씩 증가하면 복사 연산 비용이 매우 커진다.
  • 또 한 새 배열로 기존 데이터를 복사하는 과정이 길어지므로 줄이는 것이 좋다. 반면 배열의 크기를 너무 크게 증가하면 사용되지 않는 메모리 낭비가 발생되므로 점진적으로 확장한다.
  • 다른 언어에서도 List 구현에는 2배 또는 50%만 배수로 커지도록 하였다.