공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대0/8 완료
  • 00개발 환경과 첫 프로그램
  • 01변수, 데이터 타입, 형변환
  • 02연산자와 제어문
  • 031차원 · 2차원 배열
  • 04클래스, 객체, 생성자, 오버로딩
  • 05접근 제어자와 캡슐화
  • 06메서드 활용 패턴 (초급)
  • 07enum, 패키지와 import, static 과 final
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 초급 › 03 / 8

1차원 · 2차원 배열

섹션 7진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 배열의 선언, 생성, 초기화

java
int[] scores;              // 1. 선언: "int 배열을 가리킬 변수 scores"  (아직 배열 없음, null)
scores = new int[5];       // 2. 생성: 힙에 int 5칸 배열을 만들고 주소를 scores에
scores[0] = 90;            // 3. 사용: 0번 칸에 90 저장

한 번에 쓰면:

java
int[] scores = new int[5];              // 크기 지정, 모든 칸 기본값 0
int[] scores = {90, 85, 77, 92, 68};    // 값 나열 (선언과 동시에만 가능)
int[] scores = new int[]{90, 85, 77};   // 값 나열 (나중에 대입하거나 인자로 넘길 때)

int scores[](C 스타일)도 문법상 허용되지만 자바 관례는 int[] scores입니다. "타입이 int 배열"이라는 뜻이 명확하기 때문입니다.

배열의 핵심 성질 3가지:

성질 설명 결과
고정 크기 생성 시 정한 크기를 바꿀 수 없음 더 필요하면 새 배열을 만들어 복사 (ArrayList가 하는 일)
동일 타입 한 배열에는 한 타입만 int[]에 문자열 불가
0부터 인덱스 첫 칸이 0, 마지막이 length - 1 length번 칸은 없음 → 예외

2.2 기본값

new로 만든 배열의 모든 칸은 타입별 기본값으로 채워집니다. 01 레슨의 필드 기본값과 같습니다.

타입 기본값
int, long, short, byte 0
double, float 0.0
char '' (널 문자)
boolean false
참조 타입 (String, 객체, 배열) null

참조 타입 배열의 함정: String[] names = new String[3]; 을 만들면 문자열 3개가 생기는 것이 아니라 null 3개가 생깁니다. names[0].length()는 NPE입니다. 각 칸에 객체를 직접 넣어 줘야 합니다.

2.3 length와 ArrayIndexOutOfBoundsException

java
int[] arr = new int[5];
arr.length      // 5  (필드. 메서드 아님. 괄호 없음!)
arr[4]          // 마지막 요소
arr[5]          // ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
arr[-1]         // ArrayIndexOutOfBoundsException

length는 필드입니다. String.length()는 메서드라 괄호가 있고, ArrayList.size()도 메서드입니다. 세 가지가 다 달라서 자주 헷갈립니다.

대상 길이 구하기
배열 arr.length
String str.length()
컬렉션 list.size()

왜 범위를 벗어나면 예외인가? C 언어는 배열 밖을 읽어도 그냥 옆 메모리를 읽어 버립니다(버퍼 오버플로우, 보안 사고의 주범). 자바는 모든 배열 접근에 범위 검사(bounds check)를 넣어 밖으로 나가는 순간 예외를 던집니다. 약간의 성능을 내주고 안전을 얻은 것입니다. 예외 메시지에 인덱스와 길이가 모두 나오므로 디버깅이 쉽습니다.

2.4 배열의 메모리 구조

java
int[] a = {10, 20, 30};
text
스택                     힙
┌──────────┐            ┌────────────────────────────┐
│ a │ 0x100│──────────► │ length=3 │ 10 │ 20 │ 30    │  ← 0x100
└──────────┘            └────────────────────────────┘
                          (int 4바이트씩 연속 배치)
  • 배열은 객체이므로 항상 힙에 있습니다. 변수 a에는 주소만 있습니다.
  • 요소들은 메모리에 연속으로 놓입니다. 그래서 a[i]의 주소 = 시작주소 + i × 4바이트로 즉시 계산됩니다. 이것이 배열 인덱스 접근이 O(1)(크기와 무관하게 일정 시간)인 이유이고, ArrayList의 get(i)가 빠른 이유입니다.
  • 배열 객체는 자기 길이를 알고 있습니다(length 필드). 그래서 범위 검사가 가능합니다.

참조 타입 배열은 한 단계 더 들어갑니다.

java
String[] names = {"Kim", "Lee"};
text
스택              힙
┌────────┐       ┌─────────────────┐        ┌───────┐
│ names  │─────► │ 0x200 │ 0x300   │───────►│ "Kim" │ 0x200
└────────┘       └─────────────────┘   └───►│ "Lee" │ 0x300
                  (주소들의 배열)            └───────┘

배열 칸에는 문자열이 아니라 문자열의 주소가 들어 있습니다. 이 구조 때문에 얕은 복사/깊은 복사 문제가 생깁니다(2.8).

2.5 배열과 반복문

가장 흔한 패턴 네 가지입니다.

java
int[] arr = {5, 3, 8, 1};

// 1. 인덱스 순회: 인덱스가 필요하거나 값을 바꿀 때
for (int i = 0; i < arr.length; i++) {
    arr[i] *= 2;
}

// 2. 향상된 for: 읽기만 할 때
int sum = 0;
for (int v : arr) {
    sum += v;
}

// 3. 역순
for (int i = arr.length - 1; i >= 0; i--) { ... }

// 4. 최댓값 찾기: 첫 요소로 초기화
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i];
}

최댓값을 0으로 초기화하면 모든 값이 음수일 때 틀립니다. arr[0] 또는 Integer.MIN_VALUE로 시작합니다.

2.6 2차원 배열 — 배열의 배열

자바에는 진짜 2차원 배열이 없습니다. "배열을 담는 배열"이 있을 뿐입니다.

java
int[][] grid = new int[3][4];      // 3행 4열
grid[1][2] = 7;                    // 1행 2열
grid.length                        // 3 (행 수)
grid[0].length                     // 4 (0행의 열 수)
text
grid ──► ┌───────┐
         │ 0x10  │──► [0, 0, 0, 0]
         │ 0x20  │──► [0, 0, 7, 0]     ← grid[1]
         │ 0x30  │──► [0, 0, 0, 0]
         └───────┘

grid는 int[] 3개의 주소를 담은 배열이고, 각 grid[i]가 실제 int 배열입니다. grid[1]은 그 자체로 int[] 타입의 값이고 독립된 객체입니다.

정방(직사각) 배열:

java
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
};

가변(jagged) 배열: 행마다 길이가 다를 수 있습니다. "배열의 배열"이기 때문에 가능합니다.

java
int[][] triangle = new int[3][];    // 행만 만들고 열은 나중에
triangle[0] = new int[1];
triangle[1] = new int[2];
triangle[2] = new int[3];
// 또는
int[][] jagged = {{1}, {2, 3}, {4, 5, 6}};

가변 배열의 순회는 반드시 grid[i].length를 써야 합니다. grid[0].length를 모든 행에 쓰면 범위 초과가 납니다.

java
for (int i = 0; i < grid.length; i++) {
    for (int j = 0; j < grid[i].length; j++) {     // 행마다 길이 확인
        ...
    }
}
// 향상된 for 중첩
for (int[] row : grid) {
    for (int cell : row) { ... }
}

2.7 Arrays 유틸리티

java.util.Arrays에는 배열 작업의 대부분이 이미 구현되어 있습니다. 직접 짜기 전에 여기부터 봅니다.

메서드 기능 예
Arrays.toString(arr) 1차원 배열을 [1, 2, 3] 형태 문자열로 디버깅 필수
Arrays.deepToString(arr2d) 다차원 배열을 문자열로 [[1, 2], [3, 4]]
Arrays.sort(arr) 오름차순 정렬 (원본 변경) 원시 타입은 듀얼 피벗 퀵정렬
Arrays.sort(arr, from, to) 일부 구간만 정렬
Arrays.fill(arr, val) 모든 칸을 val로 초기화, 리셋
Arrays.copyOf(arr, newLen) 새 길이의 복사본 (늘리면 기본값 채움, 줄이면 잘림) ArrayList 확장의 원리
Arrays.copyOfRange(arr, from, to) [from, to) 구간 복사
Arrays.equals(a, b) 요소가 모두 같은지 a == b는 주소 비교
Arrays.deepEquals(a, b) 다차원 배열 내용 비교
Arrays.binarySearch(arr, key) 정렬된 배열에서 이진 탐색 정렬 안 되어 있으면 결과 무의미
Arrays.asList(...) 배열을 고정 크기 List로 원시 타입 배열은 의도대로 안 됨
Arrays.stream(arr) 스트림으로 변환 Arrays.stream(arr).sum()

System.out.println(arr)를 하면 [I@1b6d3586 같은 것이 나옵니다. 배열은 toString을 재정의하지 않아 "타입@해시" 형식이 출력됩니다. [I는 int 배열이라는 뜻입니다. 반드시 Arrays.toString을 쓰세요.

2.8 배열 복사 — 얕은 복사와 깊은 복사

java
int[] original = {1, 2, 3};
int[] alias = original;          // 복사 아님! 같은 배열을 가리키는 두 번째 이름
alias[0] = 99;
System.out.println(original[0]); // 99

=는 주소를 복사합니다. 진짜 복사는 새 배열을 만들어 요소를 옮겨야 합니다.

방법 코드 특징
반복문 for (...) copy[i] = src[i]; 원리 이해용
clone() int[] copy = src.clone(); 간단, 같은 길이
Arrays.copyOf Arrays.copyOf(src, src.length) 길이 변경 가능
System.arraycopy System.arraycopy(src, 0, dst, 0, len) 가장 빠름, 기존 배열에 덮어씀

여기서 "얕은 복사(shallow copy)"의 문제가 나옵니다. 위 방법들은 모두 한 단계만 복사합니다. 원시 타입 배열은 값이 복사되니 완전히 독립됩니다. 하지만 참조 타입 배열이나 2차원 배열은 주소만 복사되어 안쪽 객체를 공유합니다.

java
int[][] src = {{1, 2}, {3, 4}};
int[][] shallow = src.clone();       // 바깥 배열만 새로 만듦
shallow[0][0] = 99;
System.out.println(src[0][0]);       // 99 !! 안쪽 배열 {1,2}는 공유됨
shallow[0] = new int[]{7, 8};        // 바깥 배열의 칸을 바꾸는 건 독립적
System.out.println(src[0][0]);       // 여전히 99 (src[0]는 원래 안쪽 배열)
text
src ─────► [ 0xA, 0xB ]         0xA ──► [99, 2]
shallow ─► [ 0xA, 0xB ] (새 배열)      0xB ──► [3, 4]
           두 바깥 배열이 같은 안쪽 배열을 가리킴

깊은 복사(deep copy)는 안쪽까지 재귀적으로 새로 만듭니다.

java
int[][] deep = new int[src.length][];
for (int i = 0; i < src.length; i++) {
    deep[i] = src[i].clone();        // 각 행을 따로 복사
}

객체 배열(Person[])도 마찬가지입니다. clone()하면 Person 객체들은 공유됩니다. 깊은 복사가 필요하면 각 객체를 새로 만들어야 합니다. 실무에서 "복사본을 수정했는데 원본도 바뀌었다"는 버그의 90%는 이 얕은 복사입니다.

2.9 배열을 메서드에 넘기기

배열은 참조 타입이므로 메서드에 넘기면 주소가 복사됩니다. 메서드 안에서 요소를 바꾸면 호출한 쪽의 배열도 바뀝니다.

java
static void doubleAll(int[] arr) {
    for (int i = 0; i < arr.length; i++) arr[i] *= 2;
}
int[] data = {1, 2, 3};
doubleAll(data);            // data는 {2, 4, 6}

이것은 장점(큰 배열을 복사 없이 넘김)이자 위험(의도치 않은 수정)입니다. 원본을 지키려면 메서드 안에서 복사본을 만들거나, 호출자가 복사본을 넘깁니다. 04 레슨의 call by value에서 원리를 더 다룹니다.

가변 인자(varargs): int... nums는 메서드가 임의 개수의 인자를 배열로 받게 합니다.

java
static int sum(int... nums) {           // nums는 int[]
    int s = 0;
    for (int n : nums) s += n;
    return s;
}
sum();          // 0
sum(1, 2, 3);   // 6
sum(new int[]{4, 5});  // 9

2.10 배열의 한계와 ArrayList 예고

배열의 한계 ArrayList의 해결
크기 고정 자동으로 늘어남 (내부적으로 Arrays.copyOf로 1.5배 확장)
중간 삽입/삭제 시 직접 이동 add(i, x), remove(i) 제공
원시 타입 저장 가능 래퍼 클래스만 (Integer, 약간 느림)
길이 length 필드 size() 메서드
유틸이 Arrays에 분리 메서드가 객체에 붙어 있음

배열이 필요한 곳: 크기가 정해져 있고 성능이 중요할 때, 다차원 데이터, 원시 타입 대량 저장. 그 외 대부분은 ArrayList입니다. 하지만 ArrayList를 제대로 쓰려면 배열을 먼저 알아야 합니다.

핵심 원리
  • 2.1 배열의 선언, 생성, 초기화
  • 2.2 기본값
  • 2.3 length와 ArrayIndexOutOfBoundsException
  • 2.4 배열의 메모리 구조
  • 2.5 배열과 반복문
  • 2.6 2차원 배열 — 배열의 배열
  • 2.7 Arrays 유틸리티
  • 2.8 배열 복사 — 얕은 복사와 깊은 복사
  • 2.9 배열을 메서드에 넘기기
  • 2.10 배열의 한계와 ArrayList 예고
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제