본문 바로가기
공부 기록

자바에서 배열은 클래스인가?

by 타태 2025. 8. 4.

 

배열은 클래스인가?

 

자바에서 배열은 어떻게 다뤄지는지 궁금해졌다.

 

클래스를 꺼내 출력해 보니 [I로 나온다

    public static void main(String[] args) {
        final int[] ints = new int[10];
        System.out.println("ints = " + ints.getClass());
    }
    
    -- ints = class [I

 

IntArray로 검색해 보면 java.lang.Class의 주석에서 언급이 된다.

"[[[I"를 클래스로 읽으면 int [][][]를 로드할 수 있다고 나온다.

package java.lang;

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement,
                              TypeDescriptor.OfField<Class<?>>,
                              Constable {


--> Class<?> intArrayClass = Class.forName("[[[I", false, currentLoader);   // Class of int[][][]


 @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        @SuppressWarnings("removal")
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
        }
        return forName(name, initialize, loader, caller);
    }

                              
}

 

 

설명을 보면 String []은 "[Ljava.lang.String"로 읽을 수 있다고 나온다.

즉, prefix가 "["로 붙은 클래스를 배열로 다루는 규칙을 찾을 수 있다.

 

 

배열은 기본적으로 length 필드와 clone() 메서드를 지원한다.

별도로 컴파일 된 클래스를 찾을 수 없기 때문에 컴파일 후 디어셈블링하여 확인해 본다.

javap -c -v Main.class

-c 각 메서드의 바이트코드(opcode)를 출력 (compile된 명령어 확인)
-v verbose 모드 – 클래스에 대한 모든 세부 정보 출력 (상수 풀, 메서드 시그니처 등 포함)

 

{
  public Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: bipush        10
         2: newarray       int
         4: astore_1
         5: aload_1
         6: arraylength
         7: istore_2
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 5
        line 6: 8
}

 

array의 생성은 위의 2번 newarray 명령어를 이용해 int 타입 배열을 생성하는데, 옆에 있는 타입 (int) 자료형 배열에 공간을 할당하는 역할을 한다

이때 할당할 배열 길이가 스택 상단에 놓여 있어야 하는데 이는 bipush 10에서 상수 10을 스택에 푸시했기 때문에 이를 활용한다.

newarray

 

또한 length는 위의 6번에 나오는 arraylength 명령어로 대치되는데

이는 스택 상단에 위치한 객체를 그 길이로 치환하는 명령어다. 

이는 JVM에서 제공하는 arraylength로 JVM 내부적으로 구현이 되고 있기 때문에 별도 클래스나 필드를 확인할 수 없다.

 

 

arraylength

 


위의 예시에서 보듯이 int 배열을 생성하면 JVM은 미리 선언한 상수만큼의 int를 담을 수 있는 연속적인 힙 메모리 블록을 메모리에 할당한다.

때문에 배열은 처음에 한번 크기를 설정하면 메모리에 할당이 되고 JVM에서 관리하기 때문에 배열을 늘이거나 줄이지 못한다.

배열의 길이를 늘리거나 줄이고 싶다면 새로 메모리를 할당받아서 값을 복사해야 한다.

 

 

728x90

 

 


 

이러한 배열을 좀 더 사용하기 좋게 지원하는 클래스가 ArrayList이다.

 

ArrayList의 필드를 살펴보면, 먼저 DEFAULT_CAPACITY값이 보인다.

이 값을 보면, ArrayList를 만들 때 기본으로 크기를 10으로 설정할 것 같이 보인다.

하지만  ArrayList에서 래핑하고 있는 필드인 elementData를 보면 다른 설명이 적혀있다.

편의상 한글 번역본으로 옮겼다.

/**
 * 기본 초기 용량.
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 비어 있는 ArrayList 인스턴스에서 공유되는 빈 배열 인스턴스.
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 기본 크기를 갖는 비어 있는 인스턴스에서 공유되는 빈 배열 인스턴스.
 * 이 배열은 EMPTY_ELEMENTDATA와 구분되며, 첫 번째 요소가 추가될 때
 * 얼마만큼 용량을 늘릴지 판단하는 데 사용된다.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * ArrayList의 요소들이 저장되는 배열 버퍼.
 * ArrayList의 용량(capacity)은 이 배열 버퍼의 길이로 정의된다.
 * elementData가 DEFAULTCAPACITY_EMPTY_ELEMENTDATA인 비어 있는 ArrayList는
 * 첫 번째 요소가 추가될 때 DEFAULT_CAPACITY만큼 확장된다.
 */
transient Object[] elementData; // 중첩 클래스에서 접근을 단순화하기 위해 private 아님

 

 

즉, 생성 시 크기가 정해지지 않은 경우, 주어진 크기가 빈 배열과 같은 경우( 0인 경우 )에는 빈 배열을 사용하고, 이후에 첫 요소가 추가될 때 기본적으로 크기를 DEFAULT_CAPACITY 값인 10씩 늘린다는 것을 알 수 있다.

 

 

이를 다시 확인하기 위해 ArrayList의 세 종류의 생성자를 보겠다.

/**
 * 지정된 초기 용량을 가진 비어 있는 리스트를 생성합니다.
 *
 * @param  initialCapacity  리스트의 초기 용량
 * @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);
    }
}

 

초기화 시점에 인자로 크기가 주어졌을 경우, 크기가 0보다 크면 해당 크기만큼의 배열을, 크기가 0이면 빈 배열을, 0보다 작으면 예외를 발생한다.

 

 

/**
 * 지정된 컬렉션의 요소를 포함하는 리스트를 생성합니다.
 * 컬렉션의 반복자(iterator)에 의해 반환되는 순서대로 요소들이 리스트에 담깁니다.
 *
 * @param c 이 리스트에 추가할 요소들을 담고 있는 컬렉션
 * @throws NullPointerException 지정된 컬렉션이 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 {
        // 빈 배열로 대체합니다.
        elementData = EMPTY_ELEMENTDATA;
    }
}

 

Collection을 상속하는 인자를 받는 경우 이 데이터를 복사하여 새로운 AraayList를 만든다.

 

/**
 * 초기 용량이 10인 비어 있는 리스트를 생성합니다.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

크기 값 없이 생성하는 경우 초기 값인 10의 크기를 갖는 배열을 초기화한다.

 

 

그런데 잠깐, 

/**
 * 기본 크기를 갖는 비어 있는 인스턴스에서 공유되는 빈 배열 인스턴스.
 * 이 배열은 EMPTY_ELEMENTDATA와 구분되며, 첫 번째 요소가 추가될 때
 * 얼마만큼 용량을 늘릴지 판단하는 데 사용된다.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};


위에서 확인했던 DEFAULTCAPACITY_EMPTY_ELEMENTDATA는 분명 크기가 0이었는데 왜 EMPTY_ELEMENTDATA와는 다르며 초기 용량이 10인 비어 있는 리스트라고 하는 걸까?

EMPTY_ELEMENTDATA 사용자가 new ArrayList(0)처럼 초기 용량을 0으로 명시했을 때 사용하는 진짜 빈 배열
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 사용자가 new ArrayList() (기본 생성자)를 호출했을 때 사용하는 의미상 ‘초기 용량 10’ 예약용 빈 배열

 

 

EMPTY_ELEMENTDATA는 정말 크기가 0인 빈 배열을 의미하며, 명시적으로 grow()를 호출하지 않으면 배열의 크기를 늘리지 않는다.

이와 달리  DEFAULTCAPACITY_EMPTY_ELEMENTDATA는 처음에는 빈 배열로, 크기가 0인 배열을 만들지만 첫 요소가 추가될 때 10으로 초기화하기 때문에 의미상 EMPTY_ELEMENTDATA와 구분되며 10의 초기 크기를 갖는다고 말한다.

 

 

그럼 의식의 흐름대로 요소를 추가하는 로직을 살펴보자

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

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

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

    private Object[] grow() {
        return grow(size + 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)];
        }
    }
}

 

ArrayList에서는 add() 메서드로 요소를 추가하다가 배열의 마지막 인덱스에 값이 추가되면 grow()를 호출한다.

이때 기존의 크기를 oldCapacity에 저장하고 확장에 필요한 최소 크기(add()인 경우 기존 크기 +1)와 oldCapacity >> 1의 값 중 더 큰 값을 새로운 크기로 할당한다.

 

우측 시프트 연산은 2진수를 오른쪽으로 한 칸씩 미는 것이기 때문에 값을 2로 나눈 것과 같다.

때문에 oldCapacity >> 1 값은 기존 크기의 절반이다.

 

새로운 크기를 결정하는 코드는 ArraysSupport 클래스에서 볼 수 있다.

package jdk.internal.util;

public class ArraysSupport{

public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // preconditions not checked because of inlining
        // assert oldLength >= 0
        // assert minGrowth > 0

        int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
            return prefLength;
        } else {
            // put code cold in a separate method
            return hugeLength(oldLength, minGrowth);
        }
    }
}

 

즉, 최소치와 기존 크기의 절반 중 큰 값을 기존 크기에 더한 값이 SOFT_MAX_ARRAY_LENGTH보다 같거나 작으면 그대로 반환, SOFT_MAX_ARRAY_LENGTH보다 그거나 prefLength가 0과 같거나 작으면 hugeLength를 호출한다.

 

private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError(
                "Required array length " + oldLength + " + " + minGrowth + " is too large");
        } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
            return SOFT_MAX_ARRAY_LENGTH;
        } else {
            return minLength;
        }
    }

 

 

 

이때 기존 길이와 최소증가분을 더했을 때 어떻게 0보다 작은 값이 나올 수 있는지 궁금해진다.

그 대답은 수를 더한 결과가 Integer.MAX_VALUE를 초과하면, 결과는 음수로 wrap-around(overflow) 된다고 한다.

즉, 정수 값을 더했는데 음수가 된다면 Integer.MAX_VALUE를 초과한 것이라고 판단하는 것이다.

 

이런 경우가 어떻게 발생할 수 있을까?

처음부터 허용치를 넘는 값을 생성하려고 하면 발생할 수 있어 보인다.

ArrayList<Object> list = new ArrayList<>(Integer.MAX_VALUE - 10);
for (int i = 0; i < Integer.MAX_VALUE - 10; i++) {
    list.add(new Object());
}
list.add(new Object());  // 이 시점에 grow() → newLength() 호출됨

 

SOFT_MAX_ARRAY_LENGTH의 값에 대한 설명은 주석에 나와 있다.

/**
 * 배열 확장 연산에서 적용되는 소프트 최대 배열 길이입니다.
 * 일부 JVM(예: HotSpot)에서는 Integer.MAX_VALUE에 가까운 크기의 배열을
 * 생성하려고 하면, 충분한 힙 메모리가 있더라도 다음과 같은 예외가 발생할 수 있습니다:
 *
 *     OutOfMemoryError("Requested array size exceeds VM limit")
 *
 * 실제 최대 배열 크기는 JVM 구현에 따라 다를 수 있으며,
 * 객체 헤더 크기 같은 구현 세부사항에 의존할 수 있습니다.
 * 이 소프트 최대값은 그러한 구현 한계보다 작도록 보수적으로 설정되어 있으며,
 * 현실적으로 마주칠 수 있는 모든 제한보다 작도록 선택된 값입니다.
 */
public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

 

 


 

배열의 크기가 고정되어 있고, 인덱스를 기반으로 직접 접근하는 경우에는 배열을 사용하는 것이 더 간단하고 빠르다.

메모리 오버헤드가 없고, 요소 타입이 원시형이면 autoboxing 비용도 피할 수 있다는 장점이 있다.

 

하지만 우리가 개발하는 애플리케이션에서는 대부분의 경우 배열의 크기를 사전에 정하기 어렵고, 가변적인 경우가 많다.

때문에 고수준의 자동화 API를 제공하는 ArrayList를 사용하는 것이 더 안전하고 유지보수가 쉬운 선택이 될 수 있겠다.

 

 

참고자료

graalvm-jdk-21

Chapter 6. The Java Virtual Machine Instruction Set

반응형

댓글