사용자 점수 3개를 저장한다면 변수 세 개로도 가능하다.
int score1 = 80;
int score2 = 90;
int score3 = 75;
하지만 데이터가 100개라면 변수 100개를 만들 수는 없다. 같은 타입의 데이터를 여러 개 관리할 때 사용하는 구조가 배열(array) 이다.
int[] scores = new int[3];
두 부분으로 나눠보자.
int[] scores;
int 배열을 참조할 변수 scores를 선언한다.
scores = new int[3];
길이가 3인 실제 배열 객체를 생성한다.
배열은 참조형이므로 변수 자체에 모든 요소가 직접 들어가는 것이 아니라 생성된 배열을 가리키는 참조가 저장된다.
int[] scores = new int[3];
System.out.println(scores[0]);
int 배열의 요소는 기본값 0으로 초기화된다.
타입에 따라 기본값이 다르다.
00.0boolean: falsenull지역 변수와 달리 배열의 요소는 생성 시 기본값으로 초기화된다.
배열은 0부터 시작한다.
scores[0] = 80;
scores[1] = 90;
scores[2] = 75;
길이가 3인 배열의 인덱스 범위는 0~2다.
scores[3] = 100;
이 코드는 실행 시 ArrayIndexOutOfBoundsException이 발생한다.
int[] scores = {80, 90, 75};
값이 이미 정해져 있다면 이 방식이 훨씬 간결하다.
배열의 장점은 반복문과 결합했을 때 크게 드러난다.
int[] scores = {80, 90, 75};
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
배열 길이를 직접 숫자로 작성하지 않고 length를 사용하는 것이 중요하다.
scores.length
배열 크기가 바뀌어도 반복문을 수정할 필요가 없기 때문이다.
int[] scores = new int[3];
이 배열의 길이는 생성 후 3으로 고정된다.
런타임에 길이를 결정할 수는 있다.
int size = scanner.nextInt();
int[] scores = new int[size];
하지만 일단 배열을 생성한 뒤 같은 배열의 길이를 늘리거나 줄일 수는 없다.
데이터 개수가 계속 바뀌어야 한다면 나중에 ArrayList 같은 컬렉션을 사용하게 된다.
배열의 모든 요소를 처음부터 끝까지 읽기만 한다면 for-each가 편하다.
int[] scores = {80, 90, 75};
for (int score : scores) {
System.out.println(score);
}
기존 for문보다 인덱스 관련 코드가 줄어든다.
하지만 인덱스 자체가 필요하다면 일반 for문이 더 적합하다.
for (int i = 0; i < scores.length; i++) {
System.out.println(i + "번째 점수: " + scores[i]);
}
int[] responseTimes = {120, 250, 180, 300};
int sum = 0;
for (int responseTime : responseTimes) {
sum += responseTime;
}
double average = (double) sum / responseTimes.length;
System.out.println("합계: " + sum);
System.out.println("평균: " + average);
배열은 단순히 데이터를 저장하는 데서 끝나지 않고 반복 처리의 기반이 된다.
행과 열 구조의 데이터를 표현할 때 2차원 배열을 사용할 수 있다.
int[][] visits = {
{120, 150, 170},
{90, 110, 130}
};
접근할 때는 두 개의 인덱스를 사용한다.
System.out.println(visits[0][1]);
첫 번째 행의 두 번째 값인 150이 출력된다.
for (int row = 0; row < visits.length; row++) {
for (int column = 0; column < visits[row].length; column++) {
System.out.print(visits[row][column] + " ");
}
System.out.println();
}
중요한 점은 다음 두 길이가 의미가 다르다는 것이다.
visits.length
행의 개수다.
visits[row].length
해당 행의 열 개수다.
Java의 2차원 배열은 실제로는 배열을 요소로 가지는 배열이므로 행마다 길이가 다를 수도 있다.
int[][] data = {
{1, 2},
{3, 4, 5},
{6}
};
int[] prices = {32000, 18000, 45000, 27000};
int min = prices[0];
int max = prices[0];
for (int price : prices) {
if (price < min) {
min = price;
}
if (price > max) {
max = price;
}
}
배열을 배우면 반복문과 조건문을 실제 데이터 처리에 연결할 수 있다.
int[] prices = new int[3];
prices는 참조형 변수다. 실제 요소들이 변수 안에 통째로 들어 있는 것이 아니라, 생성된 배열 객체를 가리키는 참조값(reference value) 이 저장된다.
이 때문에 배열 변수를 다른 변수에 대입하면 요소 전체를 복사하는 것이 아니라 참조값이 복사된다.
int[] a = {10, 20, 30};
int[] b = a;
b[0] = 999;
System.out.println(a[0]); // 999
a와 b가 같은 배열을 참조하기 때문이다. 이 개념은 나중에 객체와 메서드 값 전달을 이해할 때 다시 등장한다.
int[] values = {1, 2, 3};
System.out.println(values);
보통 다음과 비슷한 문자열이 나온다.
[I@6d03e736
이 값을 실제 메모리 주소라고 단정하면 안 된다. 배열이 상속받는 Object.toString() 계열 표현은 클래스 정보와 객체의 identity hash code에 기반한 문자열 표현이며 JVM의 실제 메모리 주소를 보여주는 계약이 아니다.
배열 내용을 확인하려면 다음처럼 출력한다.
System.out.println(java.util.Arrays.toString(values));
[1, 2, 3]
int size = scanner.nextInt();
int[] values = new int[size];
사용자 입력을 이용해 실행 중에 배열 길이를 결정할 수 있다. 이것을 보고 "배열 크기가 동적으로 바뀐다"고 오해하기 쉽다.
하지만 new int[size]가 실행되어 하나의 배열이 생성된 뒤에는 그 배열의 length를 바꿀 수 없다.
실행 중 크기 결정 가능 ✅
생성된 배열 자체의 크기 변경 ❌
데이터 개수가 계속 늘고 줄어야 한다면 ArrayList 같은 컬렉션을 배우게 된다.
다음은 모두 배열을 만드는 코드다.
int[] a = new int[3];
int[] b = new int[]{10, 20, 30};
int[] c = {10, 20, 30};
마지막 축약 문법은 선언과 동시에 사용할 때 특히 편하다.
int[] values;
// values = {10, 20, 30}; // 이런 형태는 사용할 수 없음
values = new int[]{10, 20, 30};
변수 이름이 각각 다르면 반복문으로 접근하기 어렵다.
int user1 = 10;
int user2 = 20;
int user3 = 30;
배열로 묶으면 인덱스만 바꾸며 반복할 수 있다.
int[] users = {10, 20, 30};
for (int i = 0; i < users.length; i++) {
System.out.println(users[i]);
}
배열의 핵심 학습 포인트는 "여러 데이터를 저장한다"보다 동일한 규칙으로 반복 처리할 수 있게 만든다는 데 있다.
int[][] metrics = new int[2][3];
겉으로는 표처럼 보이지만 Java에서는 각 행이 다시 하나의 배열이다.
그래서 행마다 길이가 달라도 된다.
int[][] logsPerDay = {
{10, 20},
{30, 40, 50},
{60}
};
이 구조에서는 항상 array[0].length가 모든 행의 길이라고 가정하면 안 된다.
for (int row = 0; row < logsPerDay.length; row++) {
for (int column = 0; column < logsPerDay[row].length; column++) {
System.out.println(logsPerDay[row][column]);
}
}
다음처럼 임의의 숫자로 시작하면 데이터 범위에 따라 버그가 생길 수 있다.
int min = 0;
모든 데이터가 양수라면 min은 계속 0으로 남는다.
배열이 비어 있지 않다는 전제라면 첫 번째 요소를 초기값으로 쓰는 방식이 안전하다.
int min = values[0];
int max = values[0];
for (int value : values) {
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
상품 이름과 가격을 각각 배열로 관리한다고 해보자.
String[] productNames = new String[10];
int[] productPrices = new int[10];
같은 인덱스끼리 하나의 상품이라는 규칙을 개발자가 직접 지켜야 한다.
productNames[0] ↔ productPrices[0]
productNames[1] ↔ productPrices[1]
데이터가 복잡해질수록 이런 병렬 배열은 관리하기 어렵다. 이후 객체지향을 배우면 이름과 가격을 Product 객체 하나로 묶고, 개수가 유동적이면 ArrayList<Product> 같은 컬렉션으로 확장할 수 있다.
0부터 시작한다.ArrayIndexOutOfBoundsException이 발생한다.length를 사용하면 크기에 종속되지 않는 반복 코드를 만들 수 있다.for문이 편리하다.