배열(Array)
변수는 하나의 데이터만 저장할 수 있습니다. 그러나, 저장해야 할 데이터가 많아지면 그만큼 변수를 사용해야 합니다. 이를 해결하기 위해서 배열을 사용하면 많은 데이터를 손쉽게 처리할 수 있습니다.
배열(Array)은 메모리 상에 원소를 연속하게 배치한 자료구조입니다. 그리고 배열은 같은 탑의 변수를 담을 수 있는 고정 크기의 데이터 구조로, 메모리에 연속적으로 저장됩니다. 배열을 구성하는 각각의 값들을 배열 요소(element)라고 하며, 각 값이 저장되어 있는 위치를 인덱스(index)라고 합니다.
배열은 선언과 동시에 저장할 수 있는 타입이 결정되며, 만약에 다른 타입의 값을 저장하려고 하면 타입이 불일치하다는 컴파일 에러가 발생하게 됩니다. 그리고 배열을 한 번 생성하면 길이를 늘리거나 줄일 수 없습니다. 만약에 길이를 늘리거나 줄이고 싶다면 새로운 배열을 생성하고 기존 배열의 항목을 새 배열에 복사해야 합니다. 이러한 작업은 비용이 많이 들기 때문에 배열의 길이를 처음부터 적절하게 해주는 것이 좋습니다.
배열의 선언과 생성
배열을 선언할 때에는 우선 데이터 타입을 명시적으로 표현하고, 뒤에 대괄호를 사용해서 배열임을 나타내고, 배열의 이름을 작성해줘야 합니다. 배열을 선언하는 것은 배열을 다루기 위한 참조변수를 담을 공간을 만드는 것입니다.
예를 들어, int 타입의 배열 numbers를 선언 및 생성할 때는 다음처럼 작성합니다.
// 배열 선언
int[] numbers;
// 배열 생성
int[] numbers = {1, 2, 3, 4, 5};
배열변수를 이미 선언한 후에는 다른 실행문에서 중괄호를 사용한 배열 생성이 안됩니다.
int[] numbers;
numbers = { ... }; // 컴파일 에러
배열 변수를 미리 선언한 후에 데이터들이 후에 결정될 때는 다음처럼 new 연산자를 사용해서 지정해 주면 됩니다.
numbers = new int[]{1, 2, 3, 4, 5};
나중에 데이터들을 저장할 배열을 미리 만들고 싶다면 new 연산자로 다음처럼 배열 객체를 생성할 수 있습니다.
int[] numbers = new int[5];
//이미 배열 변수가 선언된 경우에도 new 연산자로 배열을 생성할 수 있습니다.
int[] numbers = null;
numbers = new int[5];
new 연산자로 배열 생성 시 초기화되는 값
new 연산자로 배열을 처음 생성할 경우 배열은 자동으로 기본 값으로 초기화됩니다.
분류 | 타입 | 초기화 |
기본 타입(정수) | byte[] | 0 |
char[] | '\u000' | |
short[] | 0 | |
int[] | 0 | |
long[] | 0 | |
기본 타입(실수) | float[] | 0.0F |
double[] | 0.0 | |
기본 타입(논리) | boolean[] | false |
참조 타입 | 클래스[] | null |
인터페이스[] | null |
배열의 인덱스
배열은 인덱스(Index)가 데이터를 저장한 순서대로 0부터 시작하며 1씩 증가됩니다. 이를 이용해서, 배열의 원소에 접근하여 읽거나 저장되는 데 사용됩니다. 배열의 원소들을 초기화하여 배열을 생성하는 것도 가능합니다.
String[] students = {"Kim", "Lee", "Choo", "Ji"};
위 students 배열을 선언하고, "Kim", "Lee", "Choo", "Ji"로 초기화할 수 있습니다. 그리고 배열 이름 뒤에 대괄호에 인덱스를 사용해서 접근할 수 있습니다.
String kim = students[0]; // "Kim"
배열의 인덱스 값은 직접 변경이 가능합니다. 배열 변수명 뒤에 대괄호와 함께 변경할 인덱스 값을 지정하고, 대입 연산자로 새로운 값으로 변경할 수 있습니다.
String[] students = {"Kim", "Lee", "Choo", "Ji"};
log.info("student : {}", students[2]); // "Choo"
students[2] = "Bong";
log.info("student : {}", students[2]); // "Bong"
배열은 참조 타입으로 배열 변수가 참조하는 배열 객체의 주소값을 가지고 있습니다. 배열 변수의 인덱스 값을 변경한다면 해당 인덱스에 저장된 값이 변경되는 것이지, 참조하고 있는 배열 객체의 주소값은 변경되지 않습니다. 즉, 배열 변수의 인덱스 값을 변경한다고 해서 참조하고 있는 배열 객체의 주소값이 변경되지 않습니다. 배열 변수가 참조하는 배열 객체의 주소값은 배열 변수가 선언될 때 한 번 할당하고, 이후에는 변경되지 않습니다. 이 특성은 Java에서 참조 타입에 대한 기본 동작 방식입니다.
배열의 길이[크기]
배열의 크기는 고정되어 있기 때문에, Java에서 배열의 크기를 확인하려면 length() 메서드를 통해 확인할 수 있습니다.
Java에서 배열의 크기 정보를 얻는 length 메서드는 배열 객체의 내부 필드로 구현되어 있습니다. 그리고 배열의 크기는 변경할 수 없으므로 final로 선언되어 있기 때문에, length 필드의 값을 변경을 시도하게 되면 컴파일 오류가 발생합니다.
2차원 배열
다차원 배열이란 2차원 이상의 배열을 의미하며, 배열 요소로 또 다른 배열을 가지는 배열입니다. 즉, 1차원 배열은 배열 요소로 하나의 값을 가지는 배열이라면, 2차원 배열은 배열 요소로 1차원 배열을 가지게됩니다. 메모리 용량이 허용하는 한 제한은 없지만 주로 1차원과 2차원 배열이 많이 사용됩니다.
2차원 배열의 선언과 생성
1차원 배열과 선언하는 방법은 동일합니다. 단순하게 대괄호[]를 하나 더 붙여주면 됩니다.
// 다차원 배열 선언
int[][] numbers;
보편적으로 2차원 배열은 테이블 데이터를 담는 데 사용됩니다.위 int[][] numbers에서 첫 번째 대괄호는 테이블 데이터의 행(row)값 이며, 두 번째 대괄호는 테이블 데이터의 열(column)값 입니다.
2차원 배열 활용
2차원 배열 출력
int[][] numbers = {
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10}.
{11, 12, 13, 14, 15},
{16, 17, 18, 19, 20}
};
// 2중 for문으로 출력
for(int i = 0; i < numbers.length; i++) {
for(int j = 0; j < numbers[i].length; j++) {
System.out.println(numbers[i][j]);
}
}
// Arrays.deepToString 메서드 출력
System.out.println(Arrays.deepToString(numbers));
// 위 2중 for문과 Arrays.deepToString() 둘 다 같은 결과를 출력합니다.
// [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15], [16, 17, 18, 19, 20]
2차원 배열 비교
String[][] students1 = {
{"이태균", "이지윤", "안효승"},
{"이인균", "김창훈", "김예찬"}
};
String[][] students2 = {
{"이태균", "이지윤", "안효승"},
{"이인균", "김창훈", "김예찬"}
};
String[][] students3 = {
{"박형준"},
{"박원범", "홍창모", "유세종"}
};
return(Arrays.deepEquals(students1, students2); // true
return(Arrays.deepEquals(students1, students3); // false
가변 배열
이차원 배열이 테이블 형태라고 하지만 반드시 행과 열이 균등할 필요가 없습니다. 자바에서 다차원 배열은 마지막 차수의 길이를 다르게 할 수 있기 때문에 각 요소로 들어가는 1차월 배열의 길이 달라도 이차원 배열을 생성할 수 있습니다. 그렇게 생성할 배열을 가변 배열이라고 합니다.
int[][] numbers = {
{1, 2, 3, 4, 5},
{6, 7},
{8, 9, 10},
{11, 12, 13},
{14, 15, 16, 17, 18, 19, 20}
};
배열의 메모리
배열 변수는 참조 변수에 속하기 때문에 객체에 해당합니다. 그렇기 때문에 힙 영역에 생성되며, 배열 변수는 힙 영역에 배열 객체를 참조하게 됩니다. 만약 참조할 배열 객체가 없으면 배열 객체는 null로 초기화됩니다. 배열 변수가 null을 가진 상태에서 인덱스로 값을 읽거나 저장하게 되면 NPE가 발생합니다. 그렇기 때문에 배열을 사용하기 전에는 배열을 생성하고 배열 변수가 참조하는 상태여야 합니다.
Java의 배열은 메모리에 연속적으로 저장됩니다. 배열의 각 원소는 해당 데이터 타입의 크기만큼 메모리 공간을 차지하고, 인덱스 값에 따라 순서대로 배치됩니다. 배열의 첫 번째 원소 1은 메모리 주소 0x1000에서 4바이트의 공간을 차지하고, 다음 원소들도 순서대로 메모리 공간에 저장됩니다. Java에서 배열로 선언된 변수와 해당 변수의 인덱스는 메모리의 스택 영역에 저장됩니다. 스택은 함수 호출 시 사용되는 지역 변수들과 함께 사용되며, 변수와 인덱스들도 함수 내의 지역 변수로 취급됩니다.
예시
변수 numbers와 해당 배열의 각 인덱스들은 메모리의 스택 영역에 저장됩니다. 이때, numbers 변수는 배열의 첫 번째 원소를 가리키는 포인터 역할을 합니다.
주소 | 0x2000 | 0x2004 | 0x2008 | 0x200C | 0x2010 | 0x2014 |
값 | 0x1000 | 1 | 2 | 3 | 4 | 5 |
변수 numbers는 메모리 주소 0x2000에 저장되며, 해당 변수 값으로는 배열의 첫 번째 원소를 가리키는 포인터인 0x1000에 저장됩니다. 이후에는 각 인덱스들의 값이 순서대로 저장되며, 인덱스 값은 변수 numbers에 저장된 포인터를 기준으로 offset을 계산해서 참조하게 됩니다.
배열 변수와 변수에 저장된 데이터가 서로 다른 영역에 저장되는 이유?
배열 변수와 해당 변수에 저장된 데이터가 서로 다른 영역에 저장되는 이유는 자바가 객체지향 언어이기 때문입니다. 배열 변수는 참조 변수로 취급되며, 해당 변수가 가리키는 배열 데이터는 힙 영역에 저장됩니다. 힙 영역은 객체가 생성될 때 동적으로 할당되며, 객체의 크기와 수명은 프로그램의 실행 도중에 결정됩니다. 배열 데이터가 힙 영역에 저장되는 이유는 배열이 객체이기 때문입니다. 배열은 다른 객체와 마찬가지로 인스턴스로 생성되며, 생성된 인스턴스는 힙 영역에 할당됩니다. 배열의 각 원소는 힙 영역에 연속적으로 할당되며, 해당 주소는 배열 변수의 값으로 참조됩니다. 반면에, 배열 변수는 메모리의 스택 영역에 저장됩니다. 스택은 함수 호출 시 사용되는 지역 변수들과 함께 사용되며 변수의 수명은 함수 실행 중에만 유지됩니다. 배열 변수는 해당 배열을 가리키는 포인터 역할을 하며, 해당 배열이 생성된 힙 영역의 주소를 참조합니다. 따라서, 배열 변수와 해당 변수에 저장된 데이터가 서로 다른 영역에 저장되는 이유는 배열이 객체이기 때문입니다. 배열 데이터를 힙 영역에 저장함으로서 객체 지향 프로그래밍에서 필요한 다양한 특성을 구현할 수 있습니다.
배열 데이터를 힙 영역에 저장함으로써, 배열 변수는 참조 변수로 취급되며 배열 데이터는 객체로 취급됩니다. 이는 다형성을 구현하는 데 유용하며, 다양한 객체를 배열로 다룰 수 있게 됩니다. 예를 들어, 여러 종류의 도형 객체를 배열로 다루는 경우, 모든 도형 객체를 배열의 원소로 추가할 수 있습니다. 이 배열은 다형성을 통해 각 도형 객체의 고유한 특성을 유지하면서도 하나의 변수로 다룰 수 있습니다. 또한, 힙 영역에 배열 데이터를 저장함으로써 동적 메모리 할당이 가능해집니다. 배열의 크기를 실행 중에 결정할 수 있으므로, 프로그램의 유연성을 높일 수 있습니다. 또한, 힙 영역은 가비지 컬렉터에 의해 자동으로 관리되므로, 메모리 누수나 불필요한 메모리 사용을 방지할 수 있습니다.