모든 자바 프로그램은 자바 가상 머신(JVM)을 통해서 실행됩니다.
자바 프로그램이 실행되면, JVM은 운영 체제로부터 해당 프로그램을 수행할 수 있도록 필요한 메모리를 할당받습니다.
이렇게 할당받은 메모리를 JVM은 용도에 따라 다음과 같이 구분하여 관리합니다.
메소드(method) 영역은 자바 프로그램에서 사용되는 클래스에 대한 정보와 함께 클래스 변수(static variable)가 저장되는 영역입니다.
JVM은 자바 프로그램에서 특정 클래스가 사용되면 해당 클래스의 클래스 파일(*.class)를 읽어들여, 해당 클래스에 대한 정보를 메소드 영역에 저장합니다.
힙(heap) 영역은 자바 프로그램에서 사용되는 모든 인스턴스 변수가 저장되는 영역입니다.
JVM은 자바 프로그램에서 new 키워드를 사용하여 인스턴스가 생성되면, 해당 인스턴스의 정보를 힙 영역에 저장합니다.
힙 영역은 메모리의 낮은 주소에서 높은 주소의 방향으로 할당됩니다.
스택(stack) 영역은 자바 프로그램에서 메소드가 호출될 때 메소드의 스택 프레임이 저장되는 영역입니다.
JVM은 자바 프로그램에서 메소드가 호출되면, 메소드의 호출과 관계되는 지역 변수와 매개변수를 스택 영역에 저장합니다.
이렇게 스택 영역은 메소드의 호출과 함께 할당되며, 메소드의 호출이 완료되면 소멸합니다.
이렇게 스택 영역에 저장되는 메소드의 호출 정보를 스택 프레임(stack frame)이라고 합니다.
스택 영역은 푸시(push) 동작으로 데이터를 저장하고, 팝(pop) 동작으로 데이터를 인출합니다.
이러한 스택은 후입선출(LIFO, Last-In First-Out) 방식에 따라 동작하므로, 가장 늦게 저장된 데이터가 가장 먼저 인출됩니다.
스택 영역은 메모리의 높은 주소에서 낮은 주소의 방향으로 할당됩니다.
배열(array)은 같은 타입의 변수들로 이루어진 유한 집합으로 정의할 수 있습니다.
배열을 구성하는 각각의 값을 배열 요소(element)라고 하며, 배열에서의 위치를 가리키는 숫자를 인덱스(index)라고 합니다.
자바에서 인덱스는 언제나 0부터 시작하며, 0을 포함한 양의 정수만을 가질 수 있습니다.
배열은 같은 종류의 데이터를 많이 다뤄야 하는 경우에 사용할 수 있는 가장 기본적인 자료 구조입니다.
배열은 선언되는 형식에 따라 1차원 배열, 2차원 배열뿐만 아니라 그 이상의 다차원 배열로도 선언할 수 있습니다.
하지만 현실적으로 이해하기가 쉬운 2차원 배열까지가 많이 사용됩니다.
1차원 배열은 가장 기본적인 배열로 다음과 같은 문법에 따라 선언합니다.
<문법>
1. 타입[] 배열이름;
2. 타입 배열이름[];
타입은 배열 요소로 저장되는 변수의 타입을 명시합니다.
배열 이름은 배열이 선언된 후에 배열에 접근하기 위해 사용됩니다.
※ 참고 : 자바에서는 배열을 만들기 위해 위의 두 가지 방법을 모두 사용할 수 있지만, 될 수 있으면 첫 번째 방법만을 사용하는 것이 좋습니다.
위와 같이 선언된 배열은 new 키워드를 사용하여 실제 배열로 생성할 수 있습니다.
<문법>
배열이름 = new 타입[배열길이];
배열의 길이는 해당 배열이 몇 개의 배열 요소를 가지게 되는지 명시합니다.
또한, 다음과 같이 배열의 선언과 생성을 동시에 할 수도 있습니다.
<문법>
타입[] 배열이름 = new 타입[배열길이];
자바에서는 이러한 배열도 모두 객체이므로, 각각의 배열은 모두 자신만의 필드와 메소드를 가지고 있습니다.
다음 예제는 int형 데이터를 3개 저장할 수 있는 배열을 선언과 동시에 생성하고 있습니다.
<예제>
int[] grade1 = new int[3]; // 길이가 3인 int형 배열의 선언 및 생성
int[] grade2 = new int[3]; // 길이가 3인 int형 배열의 선언 및 생성
grade1[0] = 85; // 인덱스를 이용한 배열의 초기화
grade1[1] = 65;
grade1[2] = 90;
grade2[0] = 85; // 배열의 길이보다 적은 수의 배열 요소만 초기화
for (int i = 0; i < grade1.length; i++) {
System.out.print(grade1[i] + " "); // 인덱스를 이용한 배열로의 접근
}
for (int i = 0; i < grade2.length; i++) {
System.out.print(grade2[i] + " "); // 인덱스를 이용한 배열로의 접근
}
결과
85 65 90 85 0 0
위의 예제처럼 0부터 시작하는 인덱스(index)를 이용하면 각각의 배열 요소에 따로 접근할 수 있습니다.
또한, 배열 grade2처럼 배열의 길이보다 적은 수의 배열 요소만을 초기화할 경우, 나머지 배열 요소들은 배열의 타입에 맞게 자동으로 초기화될 것입니다.
하지만 다음 예제처럼 해당 배열의 길이를 초과하는 인덱스를 사용하면, ArrayIndexOutOfBounds 예외가 발생할 것입니다.
<예제>
int[] grade = new int[3]; // 길이가 3인 int형 배열의 선언 및 생성
grade[0] = 85; // 인덱스를 이용한 배열의 초기화
grade[1] = 65;
grade[2] = 90;
System.out.print(grade[4]); // ArrayIndexOutOfBounds 예외 발생
위의 예제에서 사용한 length는 배열의 길이를 저장하고 있는 배열 객체의 필드입니다.
자바에서는 변수와 마찬가지로 배열도 선언과 동시에 초기화할 수 있습니다.
다음과 같이 괄호({})를 사용하여 초깃값을 나열한 것을 초기화 블록(initialization block)이라고 합니다.
자바에서는 이러한 초기화 블록을 이용하여 배열을 선언과 동시에 초기화할 수 있습니다.
초기화 블록을 이용한 배열의 초기화 방법은 다음과 같습니다.
<문법>
1. 타입[] 배열이름 = {배열요소1, 배열요소2, ...};
2. 타입[] 배열이름 = new 타입[]{배열요소1, 배열요소2, ...};
위의 두 가지 초기화 방법은 완전히 같은 결과를 반환하며, 초기화 블록에 맞춰 자동으로 배열의 길이가 설정됩니다.
하지만 다음과 같은 경우에는 첫 번째 방법이 아닌 두 번째 방법만을 사용하여 초기화해야 합니다.
배열의 선언과 초기화를 따로 진행해야 할 경우
메소드의 인수로 배열을 전달하면서 초기화해야 할 경우
<예제>
int[] grade1 = {70, 90, 80}; // 배열의 선언과 동시에 초기화할 수 있음.
int[] grade2 = new int[]{70, 90, 80}; // 배열의 선언과 동시에 초기화할 수 있음.
int[] grade3;
// grade3 = {70, 90, 80}; // 이미 선언된 배열을 이 방법으로 초기화하면 오류가 발생함.
int[] grade4;
grade4 = new int[]{70, 90, 80}; // 이미 선언된 배열은 이 방법으로만 초기화할 수 있음.
위의 예제처럼 초기화 블록의 타입과 배열의 타입은 반드시 일치해야 합니다.
다음 예제는 앞선 예제와 같은 배열을 선언과 동시에 초기화 블록으로 초기화하는 예제입니다.
<예제>
int[] grade = new int[]{85, 65, 90}; // 길이가 3인 int형 배열을 선언과 동시에 초기화
for (int i = 0; i < grade.length; i++) {
System.out.print(grade[i] + " "); // 인덱스를 이용한 배열로의 접근
}
결과
85 65 90
다음 예제는 배열 요소의 합과 평균을 구하는 예제입니다.
<예제>
int[] grade = new int[]{85, 65, 90}; // 길이가 3인 int형 배열을 선언과 동시에 초기화
int sum = 0;
for (int i = 0; i < grade.length; i++) {
sum += grade[i];
}
System.out.println("모든 과목에서 받은 점수의 합은 " + sum + "입니다.");
System.out.println("이 학생의 평균은 " + (sum / grade.length) + "입니다.");
결과
모든 과목에서 받은 점수의 합은 240입니다.
이 학생의 평균은 80입니다.
다차원 배열(multi-dimensional array)
다차원 배열이란 2차원 이상의 배열을 의미하며, 배열 요소로 또 다른 배열을 가지는 배열을 의미합니다.
즉, 2차원 배열은 배열 요소로 1차원 배열을 가지는 배열이며,
3차원 배열은 배열 요소로 2차원 배열을 가지는 배열이고,
4차원 배열은 배열 요소로 3차원 배열을 가지는 배열인 것입니다.
2차원 배열이란 배열의 요소로 1차원 배열을 가지는 배열입니다.
자바에서는 2차원 배열을 나타내는 타입을 따로 제공하지 않습니다.
대신에 1차원 배열의 배열 요소로 또 다른 1차원 배열을 사용하여 2차원 배열을 나타낼 수 있습니다.
따라서 자바에서 2차원 배열은 다음과 같은 문법으로 선언할 수 있습니다.
<문법>
1. 타입[][] 배열이름;
2. 타입 배열이름[][];
3. 타입[] 배열이름[];
타입은 배열 요소로 저장되는 변수의 타입을 설정합니다.
배열 이름은 배열이 선언된 후에 배열에 접근하기 위해 사용됩니다.
다음 그림은 2차원 배열을 이해하기 쉽도록 도식적으로 표현한 그림입니다.
<예제>
int[][] arr = new int[2][3];
int k = 10;
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
arr[i][j] = k; // 인덱스를 이용한 초기화
k += 10;
}
}
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
결과
10 20 30
40 50 60
위의 예제에서 사용된 arr는 2차원 배열이며, arr[i]는 arr의 각 배열 요소로 1차원 배열이 됩니다.
따라서 arr.length는 2차원 배열인 arr의 배열 요소의 총 개수를 반환하며, arr[i].length는 arr의 각 배열 요소인 1차원 배열이 가지고 있는 배열 요소의 총 개수를 반환하게 됩니다.
1차원 배열과 마찬가지로 2차원 배열도 선언과 동시에 초기화할 수 있습니다.
자바에서는 2차원 배열의 모든 요소를 좀 더 직관적으로 초기화할 수 있습니다.
<문법>
타입 배열이름[열의길이][행의길이] = {
{배열요소[0][0], 배열요소[0][1], ...},
{배열요소[1][0], 배열요소[1][1], ...},
{배열요소[2][0], 배열요소[2][1], ...},
...
};
다음 예제는 앞서 살펴본 예제를 2차원 배열의 초기화 형태로 초기화하는 예제입니다.
<예제>
int[][] arr = {
{10, 20, 30},
{40, 50, 60}
};
결과
10 20 30
40 50 60
자바에서는 2차원 배열을 생성할 때 열의 길이를 명시하지 않음으로써, 행마다 다른 길이의 배열을 요소로 저장할 수 있습니다.
이렇게 행마다 다른 길이의 배열을 저장할 수 있는 배열을 가변 배열(dynamic array)이라고 합니다.
<예제>
int[][] arr = new int[3][];
arr[0] = new int[2];
arr[1] = new int[4];
arr[2] = new int[1];
위의 예제처럼 배열을 생성할 때 두 번째 첨자를 생략하면 가변 배열을 만들 수 있습니다.
또한, 가변 배열도 초기화 블록을 사용하여 배열을 선언과 동시에 초기화할 수 있습니다.
다음 예제는 앞선 예제와 같은 가변 배열을 선언과 동시에 초기화 블록으로 초기화하는 예제입니다.
int[][] arr = {
{10, 20},
{10, 20, 30, 40},
{10}
};
결과
10 20
10 20 30 40
10
자바에서 배열은 한 번 생성하면 그 길이를 변경할 수 없습니다.
따라서 더 많은 데이터를 저장하기 위해서는 더욱 큰 배열을 만들고, 이전 배열의 데이터를 새로 만든 배열로 복사해야 합니다.
이러한 배열의 복사를 위해 자바에서는 다음과 같이 여러 가지 방법을 제공합니다.
System 클래스의 arraycopy() 메소드
Arrays 클래스의 copyOf() 메소드
Object 클래스의 clone() 메소드
for 문과 인덱스를 이용한 복사
이 중에서 가장 좋은 성능을 보이는 것은 배열의 복사만을 위해 만들어진 arraycopy() 메소드입니다.
하지만 현재 배열의 복사를 위해 가장 많이 사용되는 메소드는 좀 더 유연한 방식의 copyOf() 메소드입니다.
arraycopy(), copyOf() 메소드와 for 문을 이용한 복사는 배열의 길이를 마음대로 늘일 수 있습니다.
하지만 clone() 메소드는 이전 배열과 같은 길이의 배열밖에 만들 수 없습니다.
다음 예제는 다양한 방법으로 배열을 복사하는 예제입니다.
<예제>
int[] arr1 = new int[]{1, 2, 3, 4, 5};
int newLen = 10;
// 1. System 클래스의 arraycopy() 메소드
int[] arr2 = new int[newLen];
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
// 2. Arrays 클래스의 copyOf() 메소드
int[] arr3 = Arrays.copyOf(arr1, 10);
// 3. Object 클래스의 clone() 메소드
int[] arr4 = (int[])arr1.clone();
// 4. for 문과 인덱스를 이용한 복사
int[] arr5 = new int[newLen];
결과
1 2 3 4 5 0 0 0 0 0
1 2 3 4 5 0 0 0 0 0
1 2 3 4 5
1 2 3 4 5 0 0 0 0 0
JDK 1.5부터는 배열과 컬렉션의 모든 요소를 참조하기 위한 Enhanced for 문이라는 반복문이 새롭게 추가됩니다.
이 반복문은 배열과 컬렉션 프레임워크에서 유용하게 사용됩니다.
자바에서 Enhanced for 문은 다음과 같은 문법으로 사용합니다.
<문법>
for (타입 변수이름 : 배열이나컬렉션이름) {
배열의 길이만큼 반복적으로 실행하고자 하는 명령문;
}
Enhanced for 문은 명시한 배열이나 컬렉션의 길이만큼 반복되어 실행됩니다.
루프마다 각 요소는 명시한 변수의 이름으로 저장되며, 명령문에서는 이 변수를 사용하여 각 요소를 참조할 수 있습니다.
다음 예제는 Enhanced for 문을 사용하여 각 배열 요소의 값을 출력하는 예제입니다.
<예제>
int[] arr = new int[]{1, 2, 3, 4, 5};
for (int e : arr) {
System.out.print(e + " ");
}
결과
1 2 3 4 5
하지만 Enhanced for 문은 요소를 참조할 때만 사용하는 것이 좋으며, 요소의 값을 변경하는 작업에는 적합하지 않습니다.
다음 예제는 for 문과 Enhanced for 문을 이용하여 모든 배열 요소에 10을 더하는 예제입니다.
int[] arr1 = new int[]{1, 2, 3, 4, 5};
int[] arr2 = new int[]{1, 2, 3, 4, 5};
for (int i = 0; i < arr1.length; i++) {
① arr1[i] += 10; // 각 배열 요소에 10을 더함.
}
for (int e : arr2) {
② e += 10; // 각 배열 요소에 10을 더함.
}
결과
11 12 13 14 15
1 2 3 4 5
위 예제의 ①번 라인에서는 for 문을 이용하여 각 배열 요소에 10을 더하고 있습니다.
이렇게 for 문을 사용하면, 각 배열 요소의 값을 손쉽게 변경할 수 있습니다.
②번 라인에서는 Enhanced for 문을 이용하여 각 배열 요소에 10을 더하고 있습니다.
하지만 실행 결과를 살펴보면, 원본 배열에는 아무런 변화가 없음을 알 수 있습니다.
이렇게 Enhance for 문 내부에서 사용되는 배열 요소는 배열 요소 그 자체가 아닌 배열 요소의 복사본입니다.
따라서 Enhance for 문에서 배열 요소의 값을 변경하여도 원본 배열에는 아무런 영향을 주지 못하게 됩니다.