요약

배열은 같은 종류의 값을 번호 붙은 칸에 줄지어 넣어 둔 것이다. 아파트 한 동의 우편함처럼, 이름 하나와 번호만 있으면 어느 칸이든 바로 열 수 있다. 값이 백 개든 만 개든 변수 하나와 반복문으로 다룰 수 있다. 대신 칸 수는 만들 때 정해져 나중에 늘릴 수 없고, 번호는 0부터라 마지막 칸의 번호가 칸 수보다 1 작다. 없는 번호를 열면 프로그램이 멈춘다.

예시로 보기

학생 30명의 점수를 score0, score1, … 하고 변수 30개에 담으면 합을 낼 때 변수 이름을 30번 적어야 한다. 학생이 한 명 늘면 코드를 고쳐야 한다. 배열에 담으면 이름은 score 하나이고 번호만 바뀐다.

          score 배열
        +----+----+----+----+----+-----+----+
        | 83 | 90 | 87 | 93 | 78 | ... | 75 |
        +----+----+----+----+----+-----+----+
 인덱스    0    1    2    3    4   ...   29

배열은 값들을 메모리의 이어진 자리에 나란히 두고 값마다 번호를 붙인 것이다. 이 번호를 인덱스라 부른다. 대괄호 [ ] 안에 인덱스를 적어 그 칸의 값을 읽거나 넣는다. score[0]은 83이고, score[1] = 95;는 둘째 칸에 95를 넣는다. 30칸이면 번호는 0부터 29까지다.

검증: 값 목록과 new로 만들기, 타입별 기본값 표, 범위 밖 인덱스의 예외 메시지, 2차원 배열의 길이와 합 432, new String[3]의 null, 복사 방법 셋과 얕은 복사, 향상된 for의 합 436과 칸을 못 고치는 것, java Sum 10 20의 합 30, 카드 C1·C3 — 10_java-array_impl.java

선언과 생성

변수 선언

배열 변수를 선언하는 모양은 둘이다.

int[] numbers1;    // 타입 뒤에 대괄호 (보통 이쪽을 쓴다)
int numbers2[];    // 변수 이름 뒤에 대괄호

배열은 참조 타입이다. 배열 자체는 힙 영역에 만들어지고, 배열 변수에는 힙에 있는 배열의 주소가 들어간다. 가리킬 배열이 없으면 null로 둘 수 있다. 변수를 선언하는 것만으로는 배열이 생기지 않는다.

값 목록으로 만들기

넣을 값을 이미 알면 중괄호 안에 늘어놓는다.

int[] score = {83, 90, 87, 93, 78};
  스택               힙
  score [ 5번지 ] --> 5번지: [83][90][87][93][78]
                             [0] [1] [2] [3] [4]

선언과 값 넣기를 따로 할 때는 중괄호 앞에 new 타입[]을 붙인다. 타입은 배열 변수의 타입과 같아야 한다.

int[] score;
score = new int[] {83, 90, 87, 93, 78};   // 된다
score = {83, 90, 87, 93, 78};             // 컴파일 오류

new로 만들기

값은 나중에 넣고 자리만 먼저 잡을 때는 new 타입[길이]를 쓴다. 이렇게 만든 배열의 칸은 기본값으로 저절로 채워진다.

int[] score = new int[30];   // 30칸, 모두 0
배열 타입 칸의 기본값
byte[], short[], int[] 0
long[] 0L
char[] '\u0000' (0번 글자)
float[], double[] 0.0F, 0.0
boolean[] false
클래스·인터페이스 타입 배열 (String[] 등) null

길이

배열의 칸 수는 배열.length로 읽는다. 괄호가 없다1. 길이가 n이면 인덱스는 0부터 n - 1까지다. 5칸 배열에서 score[5]를 읽으면 ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5로 멈춘다. 한번 만든 배열의 길이는 바꿀 수 없다1.

for (int i = 0; i < score.length; i++) {   // i <= 가 아니라 i <
    sum += score[i];
}

다차원 배열

배열의 칸에 다시 배열이 들어 있는 것이 다차원 배열이다. 두 반의 점수를 담아 본다. 대괄호를 차원 수만큼 붙이고, 중괄호를 차원 수만큼 겹친다.

int[][] scores = {
    {80, 92, 96},    // 0번 반
    {76, 88}         // 1번 반
};
  스택                 힙
  scores [ 5번지 ] --> 5번지: [ 10번지 ][ 30번지 ]        <- 1차원: 반의 배열
                                  |         |
                                  v         v
                       10번지: [80][92][96]   30번지: [76][88]   <- 2차원: 점수 배열

표처럼 생겼지만 메모리에서는 표가 아니다. 바깥 배열의 칸마다 안쪽 배열의 주소가 들어 있다. 그래서 안쪽 배열끼리 길이가 달라도 된다.

식 값 뜻
scores.length 2 반의 수
scores[0].length 3 0번 반의 학생 수
scores[1].length 2 1번 반의 학생 수
scores[0][2] 96 0번 반 2번 학생

new로 만들 때도 대괄호를 차원 수만큼 쓴다. new int[2][3]은 길이 3인 배열 둘을 만들고 모든 칸을 0으로 채운다.

참조 타입 배열

기본 타입 배열은 칸에 값이 그대로 들어간다. 참조 타입 배열은 칸마다 객체의 주소가 들어간다.

String[] strArray = new String[3];   // 칸 셋, 모두 null
strArray[0] = "Java";
strArray[1] = "C++";
strArray[2] = "C#";
  스택 영역              힙 영역
  strArray [ 5번지 ] --> 5번지: [ 10번지 ][ 20번지 ][ 30번지 ]     <- String[] 배열
                                    |         |         |
                                    v         v         v
                                 "Java"     "C++"     "C#"         <- String 객체들

new String[3]은 문자열 셋을 만들지 않는다. 주소를 넣을 칸 셋만 만든다. 값을 넣기 전에 strArray[0].length()를 부르면 NullPointerException이다.

main의 String[] args. main() 메서드의 괄호 안에는 늘 String[] args가 있어야 한다. 프로그램을 실행할 때 클래스 이름 뒤에 적은 글자들이 문자열 배열로 묶여 이 변수에 들어온다.

java Sum 10 20      ->   args = { "10", "20" }

args[0]은 수 10이 아니라 문자열 "10"이다. 더하려면 Integer.parseInt(args[0])으로 바꾼다(타입 변환). 뒤에 아무것도 적지 않고 실행하면 args는 null이 아니라 길이 0인 배열이다1.

복사와 향상된 for

복사. 배열 변수를 다른 변수에 넣으면 주소만 베껴진다. 배열은 여전히 하나다(참조 타입과 메모리 영역). 칸까지 따로 가진 배열이 필요하면 새 배열을 만들어 값을 옮긴다. 길이를 못 바꾸므로 더 큰 배열이 필요할 때도 이렇게 한다2.

방법 코드 (src는 {1, 2, 3}) 결과
for로 한 칸씩 for (int i = 0; i < src.length; i++) c[i] = src[i]; (c는 new int[5]) [1, 2, 3, 0, 0]
System.arraycopy(원본, 원본 시작, 대상, 대상 시작, 개수) System.arraycopy(src, 0, c, 1, 3); [0, 1, 2, 3, 0]
Arrays.copyOf(원본, 새 길이) int[] c = Arrays.copyOf(src, 5); [1, 2, 3, 0, 0]

세 방법 모두 칸에 든 것을 베낀다. 참조 타입 배열이나 2차원 배열은 칸에 주소가 들어 있으므로 주소가 베껴진다. 바깥 배열은 둘이 되지만 안쪽 배열과 객체는 같이 쓴다(얕은 복사). int[][] m2 = Arrays.copyOf(m, 2); m2[0][0] = 99;를 하면 m[0][0]도 99다2.

향상된 for. 인덱스 없이 배열의 칸을 처음부터 끝까지 하나씩 꺼낸다2.

int sum = 0;
for (int s : score) {    // score의 칸을 차례로 s에 베껴 넣는다
    sum += s;
}

s는 칸의 사본이다. 그래서 s를 고쳐도 배열은 바뀌지 않는다. 칸을 고치려면 인덱스로 도는 보통의 for를 쓴다.

int[] twice = {1, 2, 3};
for (int v : twice) { v = v * 2; }                               // 그대로 [1, 2, 3]
for (int i = 0; i < twice.length; i++) { twice[i] *= 2; }        // [2, 4, 6]

활용

  • 읽기만 하고 인덱스가 필요 없으면 향상된 for, 칸을 고치거나 인덱스를 써야 하면 보통의 for를 쓴다.
  • 흔한 실수: i <= arr.length로 돈다. 마지막에 없는 칸을 열어 ArrayIndexOutOfBoundsException이다.
  • 흔한 실수: 문자열의 길이는 s.length()(괄호 있음), 배열의 길이는 arr.length(괄호 없음)다. 섞어 쓰면 컴파일 오류다.
  • 흔한 실수: 2차원 배열을 Arrays.copyOf로 복사하고 완전히 따로라고 믿는다. 안쪽 배열도 하나씩 복사해야 한다.
  • 길이를 실행 중에 늘려야 하는 자료에는 배열 대신 ArrayList 같은 컬렉션을 쓴다1.

연결

과목별 관점

컴파일러 (3-2학기). 컴파일러에서는 같은 배열을 “메모리에 어떤 모양으로 놓이는가”로 본다. 언어마다 놓는 법이 다르다. C 방식과 Java 방식을 나란히 놓으면 두 가지가 더 보인다.

  • 길이는 원소 앞에 적혀 있다. C 방식 배열은 원소만 메모리에 연달아 놓는다. Java 방식 배열은 원소를 연달아 놓는 것은 같고, 맨 앞에 길이를 적은 칸이 하나 더 붙는다. 위에서 score.length로 길이를 읽을 수 있고 범위 밖 인덱스를 실행 중에 잡아낼 수 있는 것은 이 칸이 있어서다. C 방식에는 그 칸이 없어 배열만 보고는 길이를 알 수 없다.
  • 2차원 배열은 덩어리가 여럿이다. A[m][n]을 C 방식으로 놓으면 0번 줄, 1번 줄, …의 원소를 한 덩어리로 잇는다. Java 방식은 위의 scores 그림 그대로다. 바깥 배열에 길이 \(m\)과 안쪽 배열의 주소 \(m\)개가 있고, 안쪽 배열은 저마다 따로 떨어진 덩어리이며 각자 맨 앞에 자기 길이를 갖는다. 안쪽 배열마다 길이 칸이 따로 있으니 scores[0].length와 scores[1].length가 달라도 된다.

원소를 놓을 번지를 타입마다 정해진 눈금에 맞추는 이야기는 데이터 정렬과 패딩에 있다.

자료구조 (1-2학기). 자료구조에서는 위의 두 놓는 법에 이름을 붙이고 드는 메모리를 센다. 자바 방식은 배열의 배열(array of arrays)이고, C의 int a[3][4]는 가로줄을 차례로 이어 붙인 행 우선 배치(row-major mapping)다. 여기서 세 가지가 더 보인다.

  • 덤으로 드는 자리. 3줄 4칸을 배열의 배열로 놓으면 메모리 덩어리가 넷(길이 3, 4, 4, 4)이고, 줄 주소를 담는 배열만큼 자리가 더 든다. 주소 하나를 4바이트로 치면 줄 수 × 4 = 12바이트다. 한 덩어리로 놓으면 시작 주소와 한 줄의 칸 수만 적어 두면 되어 8바이트다. 이 셈에는 위에서 말한 길이 칸은 들어 있지 않다.
  • 위치 식. 한 덩어리에서는 x[i][j]가 \(ic + j\)번 칸이다. \(c\)는 한 줄의 칸 수다. 3줄 4칸에서 x[2][1]은 \(2 \times 4 + 1 = 9\)번 칸이다3. 배열의 배열에서는 이 식을 쓰지 않고 x[i]에 적힌 주소를 한 번 따라간다. 대신 한 덩어리는 표 전체만큼 긴 빈자리가 통째로 있어야 한다.
  • 언어별 목록. Java, C#, Scala, Swift는 줄 주소의 배열을 쓴다. C, C++, Pascal은 행 우선 한 덩어리이고, Fortran, MATLAB, R은 세로줄부터 놓는 열 우선 한 덩어리다. C에서도 포인터의 배열을 직접 만들면 자바와 같은 모양이 된다3.

두 놓는 법의 코드와 위치 식의 확인은 행 우선 배치와 열 우선 배치에 있다.

확인 문제

C1 int[][] t = new int[3][]; t[0] = new int[] {1}; t[1] = new int[] {1, 2};를 실행한 뒤 t.length, t[1].length, t[2]의 값은? t[2].length를 읽으면 어떻게 되는가?

답: 3, 2, null. t[2].length는 NullPointerException.
이유: new int[3][]은 주소 칸 셋짜리 바깥 배열만 만든다. 참조 타입 칸의 기본값은 null이다. 0번과 1번에만 안쪽 배열을 넣었으므로 2번은 null로 남는다.

C2 new String[3]을 만든 직후 strArray[0].length()를 부르면 왜 예외가 나는가? new int[3]을 만든 직후 arr[0] + 1은 왜 괜찮은가?

답: String[]의 칸에는 주소가 들어가고 기본값이 null이다. 가리키는 문자열이 없는데 .length()를 물어 NullPointerException이다. int[]의 칸에는 값이 그대로 들어가고 기본값이 0이라 0 + 1 = 1이다.
이유: 참조 타입 배열을 만드는 것은 칸을 만드는 것이지 객체를 만드는 것이 아니다.

C3 “향상된 for로 배열의 모든 칸을 두 배로 만들 수 있다”가 틀림을 코드로 보여라. 맞게 고친 코드도 쓰라.

답: int[] a = {1, 2, 3}; for (int v : a) { v = v * 2; } 뒤에 a는 [1, 2, 3] 그대로다. 고치면 for (int i = 0; i < a.length; i++) { a[i] = a[i] * 2; }이고 a는 [2, 4, 6]이 된다.
이유: 향상된 for의 변수는 칸의 값을 베껴 받은 사본이다. 사본을 고쳐도 칸은 그대로다.

C4 int[] src = {1, 2, 3};의 내용을 따로 가진 배열을 만들려 한다. ① int[] a = src; ② int[] b = Arrays.copyOf(src, 3); ③ int[] c = new int[3]; System.arraycopy(src, 0, c, 0, 3); 가운데 맞는 것은? 틀린 것은 왜 틀렸는가?

답: ②와 ③.
이유: ①은 주소만 베껴 a와 src가 같은 배열을 가리킨다. a[0] = 100;을 하면 src[0]도 100이다. ②와 ③은 새 배열을 만들고 값을 옮긴다.

C5 int[][] scores = {{80, 92, 96}, {76, 88}};가 Java 방식으로 메모리에 놓일 때 덩어리는 몇 개이고 길이를 적은 칸은 몇 개인가? 2줄 3칸짜리 배열을 C 방식으로 놓으면 덩어리는 몇 개이고 길이 칸은 몇 개인가?

답: Java 방식: 덩어리 셋(바깥 배열 하나, 안쪽 배열 둘), 길이 칸 셋(바깥 2, 안쪽 3과 2). C 방식: 덩어리 하나(원소 여섯이 줄 순서로 이어진다), 길이 칸은 없다.
이유: Java 방식은 배열마다 맨 앞에 자기 길이를 적는다. 바깥 배열의 칸에는 원소가 아니라 안쪽 배열의 주소가 든다. C 방식은 한 덩어리라서 줄마다 길이가 다른 배열을 이 모양으로는 만들 수 없다.

  1. 예외 메시지와 값은 구현 코드로, 컴파일 오류는 JDK 21에서 직접 컴파일해 확인했다. Java Language Specification §10.3, §10.7에 있다. ↩ ↩2 ↩3 ↩4

  2. 값은 구현 코드로 확인했다. Java Language Specification §14.14.2(향상된 for), System.arraycopy와 Arrays.copyOf의 API 문서에 있다. ↩ ↩2 ↩3

  3. 09_row-major-mapping_impl.c로 확인했다. ↩ ↩2