Java 배열 완전 정복 — 선언부터 Arrays 유틸리티까지

Java 배열의 선언·생성·초기화·접근·순회부터 Arrays 유틸리티 클래스까지, 힙 메모리 구조와 흔한 함정을 포함해 배열의 모든 것을 정리한다

· 6 min read · PALDYN Team

지난 글에서 패턴 매칭 switch로 타입 기반 분기를 선언적으로 처리하는 방법을 살펴봤다. 이번에는 Java에서 가장 기초적이면서도 뜻밖에 함정이 많은 **배열(Array)**을 처음부터 끝까지 다진다. 배열은 동일한 타입의 값을 연속된 메모리 공간에 나란히 저장하는 자료구조로, Java의 모든 컬렉션 프레임워크가 내부적으로 배열을 활용한다. 제대로 이해하면 ArrayList, HashMap 같은 고수준 컬렉션을 쓸 때도 성능 예측이 훨씬 쉬워진다.

배열이란 무엇인가

배열은 고정 크기의 동일 타입 데이터를 담는 객체다. 선언 시점에는 참조 변수만 만들어지고, new 키워드로 실제 메모리를 할당해야 한다. 생성된 배열은 **힙(Heap)**에 위치하며 스택의 참조 변수가 그 주소를 가리킨다.

배열 구조와 힙 메모리

배열의 핵심 특성 세 가지를 먼저 기억해 두자.

  • 0-based 인덱스: 첫 요소는 arr[0], 마지막은 arr[arr.length - 1]
  • 고정 크기: 한 번 생성하면 크기를 바꿀 수 없다. 크기 변경이 필요하면 ArrayList를 쓴다
  • 기본값 자동 초기화: int[]0, boolean[]false, 객체 배열은 null로 초기화된다

선언, 생성, 초기화

배열을 만드는 방법은 세 가지다.

// 방법 1: 선언 후 new로 생성
int[] scores = new int[5];      // [0, 0, 0, 0, 0]

// 방법 2: 선언과 동시에 리터럴 초기화
int[] primes = {2, 3, 5, 7, 11};

// 방법 3: new와 리터럴 동시 사용 (메서드 인수 전달 시 유용)
printArray(new int[]{10, 20, 30});

// 객체 배열
String[] names = new String[3]; // [null, null, null]
names[0] = "Alice";

int arr[] 처럼 변수 뒤에 []를 붙이는 C 스타일 문법도 허용되지만, Java 컨벤션은 **타입 뒤에 []**를 붙인다.

배열 접근과 순회

int[] arr = {10, 20, 30, 40, 50};

// 인덱스 직접 접근 — O(1)
int first = arr[0];   // 10
int last  = arr[arr.length - 1]; // 50

// 인덱스가 필요할 때: 전통 for
for (int i = 0; i < arr.length; i++) {
    System.out.printf("[%d] = %d%n", i, arr[i]);
}

// 인덱스가 불필요할 때: enhanced for (for-each)
for (int v : arr) {
    System.out.println(v);
}

arr.length필드이므로 괄호가 없다. String.length()와 혼동하지 않도록 주의하자.

Arrays 유틸리티 클래스

java.util.Arrays는 배열 조작에 필요한 거의 모든 기능을 정적 메서드로 제공한다.

import java.util.Arrays;

int[] arr = {5, 3, 1, 4, 2};

// 정렬 (Dual-Pivot Quicksort, O(n log n))
Arrays.sort(arr);                    // [1, 2, 3, 4, 5]

// 이진 탐색 — 반드시 정렬 후 사용
int idx = Arrays.binarySearch(arr, 3); // 인덱스 2 반환, 없으면 음수

// 복사
int[] copy = Arrays.copyOf(arr, arr.length);         // 전체 복사
int[] slice = Arrays.copyOfRange(arr, 1, 4);         // [2, 3, 4]

// 내용 비교 (== 대신 사용)
System.out.println(Arrays.equals(arr, copy));        // true

// 전체 동일 값으로 채우기
Arrays.fill(arr, 0);                                 // [0, 0, 0, 0, 0]

// 디버깅용 문자열 변환
System.out.println(Arrays.toString(arr));            // [0, 0, 0, 0, 0]

역순 정렬이 필요하면 기본형 배열(int[])을 직접 쓸 수 없다. 래퍼 타입 배열로 변환해야 한다.

Integer[] boxed = {5, 3, 1, 4, 2};
Arrays.sort(boxed, Comparator.reverseOrder()); // [5, 4, 3, 2, 1]

배열 복사 주의: 얕은 복사 vs 깊은 복사

int[] a = {1, 2, 3};
int[] b = a;          // 얕은 복사 — 같은 힙 객체를 가리킴

b[0] = 99;
System.out.println(a[0]); // 99 ← a도 바뀐다!

// 진짜 복사(독립 배열)
int[] c = Arrays.copyOf(a, a.length);
c[0] = 0;
System.out.println(a[0]); // 99 ← a 불변

객체 배열의 경우 Arrays.copyOf는 여전히 내부 객체의 참조만 복사한다(얕은 복사). 객체 자체를 독립적으로 복사하려면 별도 로직이 필요하다.

흔한 런타임 예외

배열 연산 패턴과 주의사항

예외원인
ArrayIndexOutOfBoundsException인덱스가 0 미만이거나 length 이상
NullPointerException선언만 하고 초기화하지 않은 배열에 접근
NegativeArraySizeExceptionnew int[-1]처럼 음수 크기로 생성

배열 vs ArrayList

배열은 성능이 뛰어나지만 크기가 고정된다. 일반적으로 다음 기준으로 선택한다.

  • 배열: 크기가 확정적이고 성능이 중요한 경우 (예: 알고리즘 문제, 내부 버퍼)
  • ArrayList: 동적 추가·삭제가 필요한 경우

ArrayList도 내부는 배열이다. 용량 초과 시 1.5배 크기로 새 배열을 할당하고 복사한다는 사실을 알면 성능 예측이 쉬워진다.

정리

배열은 Java에서 가장 원시적인 자료구조지만, 힙 할당 방식과 참조 의미론을 정확히 이해하면 더 복잡한 컬렉션도 자신 있게 다룰 수 있다. 핵심은 세 가지다: 0-based 인덱스, 고정 크기, 그리고 = 대입은 참조 복사라는 사실.


다음 글: Java 다차원 배열 완전 정복


읽어주셔서 감사합니다. 😊