2차원 배열과 향상된 for
행과 열로 가입자 수를 배치하고 가변 길이 행을 안전하게 순회하며 지역별 가입 현황을 집계합니다.
한 지역의 채널별 가입자 수는 1차원 배열로 충분하지만, 여러 지역을 함께 비교하려면 행과 열이 있는 표가 편합니다.
자바의 2차원 배열은 거대한 한 덩어리 표가 아니라 배열을 원소로 가지는 배열입니다.
이 구조를 이해하면 직사각형 표뿐 아니라 행마다 길이가 다른 배열도 안전하게 다룰 수 있습니다.
2차원 배열의 구조
public final class RectangularGrid {
public static void main(String[] args) {
int[][] signupCounts = new int[2][3];
signupCounts[0][0] = 20;
signupCounts[0][1] = 30;
signupCounts[0][2] = 40;
signupCounts[1][0] = 15;
signupCounts[1][1] = 25;
signupCounts[1][2] = 35;
for (int row = 0; row < signupCounts.length; row++) {
for (int column = 0; column < signupCounts[row].length; column++) {
System.out.print(signupCounts[row][column] + " ");
}
System.out.println();
}
}
}20 30 40
15 25 35signupCounts.length는 바깥 배열의 길이, 즉 행 수 2입니다.
signupCounts[row].length는 선택한 행의 안쪽 배열 길이, 즉 그 행의 열 수 3입니다.
signupCounts[1][2]를 읽을 때 먼저 둘째 행 배열을 고르고, 그 배열의 셋째 원소를 고릅니다.
인덱스는 두 단계 모두 0부터 시작합니다.
다음 두 선언은 같은 직사각형 모양을 만듭니다.
int[][] created = new int[2][3];
int[][] literal = {
{0, 0, 0},
{0, 0, 0}
};그러나 변수 created가 직접 숫자 여섯 개를 담는다고 생각하면 안 됩니다.
바깥 배열은 두 개의 int[] 참조를 담고, 각 참조가 길이 3의 숫자 배열을 가리킵니다.
그래서 행 하나를 다른 배열로 교체할 수도 있습니다.
created[1] = new int[] {7, 8};이 순간 전체 모양은 첫 행 길이 3, 둘째 행 길이 2인 가변 길이 배열이 됩니다.
가변 길이 행
public final class RaggedArrayBug {
public static void main(String[] args) {
int[][] signupCounts = {
{20, 30, 40},
{15},
{25, 35}
};
for (int row = 0; row < signupCounts.length; row++) {
for (int column = 0; column < signupCounts[0].length; column++) {
System.out.println(signupCounts[row][column]);
}
}
}
}20
30
40
15
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1안쪽 반복 조건이 늘 signupCounts[0].length를 사용해 첫 행의 길이 3을 모든 행에 강요했습니다.
둘째 행에는 원소가 하나뿐이므로 column이 1이 되는 순간 예외가 납니다.
현재 행을 선택한 뒤 signupCounts[row].length를 사용해야 합니다.
for (int row = 0; row < signupCounts.length; row++) {
for (int column = 0; column < signupCounts[row].length; column++) {
System.out.println(signupCounts[row][column]);
}
}가변 길이 배열은 잘못된 배열이 아닙니다.
지역마다 운영하는 가입 채널 수가 다르거나 좌석 행마다 좌석 수가 다를 때 자연스러운 모델입니다.
문제는 실제 구조를 확인하지 않고 직사각형이라고 단정하는 데 있습니다.
향상된 for 순회
인덱스 자체가 필요하지 않고 모든 값을 한 번씩 읽기만 한다면 향상된 for가 의도를 더 선명하게 드러냅니다.
public final class EnhancedForMemberGrid {
public static void main(String[] args) {
int[][] signupCounts = {
{20, 30, 40},
{15},
{25, 35}
};
int total = 0;
for (int[] region : signupCounts) {
for (int value : region) {
total += value;
}
}
System.out.println("total=" + total);
}
}total=165바깥 반복에서 region은 현재 행 배열을 가리키고, 안쪽 반복에서 value는 현재 원소 값을 복사해 받습니다.
value = 0으로 바꾸어도 원본 배열 원소는 바뀌지 않습니다.
값을 수정하려면 인덱스로 접근해야 합니다.
for (int row = 0; row < signupCounts.length; row++) {
for (int column = 0; column < signupCounts[row].length; column++) {
signupCounts[row][column] += 5;
}
}따라서 선택 기준은 간단합니다.
위치를 출력하거나 특정 원소를 교체해야 하면 기본 for, 모든 값을 읽어 합계처럼 축약하면 향상된 for가 적합합니다.
무조건 짧은 문법을 고르는 것이 아니라 작업에 필요한 정보가 무엇인지 먼저 판단합니다.
행 출력 리팩터링
행마다 별도의 반복문을 작성하면 행을 추가할 때 출력 코드도 추가해야 합니다.
for (int column = 0; column < signupCounts[0].length; column++) {
System.out.println(signupCounts[0][column]);
}
for (int column = 0; column < signupCounts[1].length; column++) {
System.out.println(signupCounts[1][column]);
}바깥 반복을 도입하면 행 수가 바뀌어도 로직은 그대로입니다.
이것이 여기서의 리팩터링입니다.
관찰되는 결과는 유지하면서 구조를 바꾸어 중복과 변경 지점을 줄입니다.
리팩터링 전후에 작은 입력의 출력과 합계를 비교해야 값 누락이나 중복 집계를 발견할 수 있습니다.
지역별 가입 현황
행은 지역, 열은 가입 채널로 정하면 같은 열이 같은 채널을 의미합니다.
표의 축을 코드 가까이에 적어 두면 숫자의 의미를 잃지 않습니다.
public final class RegionalSignupGrid {
public static void main(String[] args) {
String[] regions = {"Seoul", "Busan", "Daejeon"};
String[] channels = {"Web", "Mobile", "Partner"};
int[][] signupCounts = {
{40, 20, 0},
{30, 0, 50},
{45, 25, 35}
};
if (regions.length != signupCounts.length) {
throw new IllegalStateException("region count differs");
}
int grandTotal = 0;
for (int row = 0; row < signupCounts.length; row++) {
if (signupCounts[row].length != channels.length) {
throw new IllegalStateException("channel count differs at row " + row);
}
int regionTotal = 0;
for (int value : signupCounts[row]) {
regionTotal += value;
}
grandTotal += regionTotal;
System.out.println(regions[row] + "=" + regionTotal);
}
for (int column = 0; column < channels.length; column++) {
int channelTotal = 0;
for (int row = 0; row < signupCounts.length; row++) {
channelTotal += signupCounts[row][column];
}
System.out.println(channels[column] + "=" + channelTotal);
}
System.out.println("all=" + grandTotal);
}
}Seoul=60
Busan=80
Daejeon=105
Web=115
Mobile=45
Partner=85
all=245지역 합계는 한 행을 고정하고 열을 움직입니다.
채널 합계는 한 열을 고정하고 행을 움직입니다.
같은 표라도 질문에 따라 순회 방향이 달라집니다.
0은 해당 지역에서 그 채널로 가입한 회원이 없다는 실제 집계값입니다.
아직 집계하지 않은 칸과 구분해야 한다면 별도의 입력 여부 배열이 필요합니다.
이 모델은 지역과 채널 수가 고정된 보고서에 알맞습니다.
회원 한 명씩 계속 추가하는 서비스라면 이후 장에서 배우는 객체와 컬렉션이 더 자연스럽습니다.
데이터 모양을 먼저 정하고 배열 차원을 정해야 합니다.
연습 문제
RegionalSignupGrid의 표를 사용해 각 행의 합계를 구하고, 가입자 수가 가장 많은 지역을 출력하세요.
모든 행이 비어 있는 입력은 없다고 가정하되, 행마다 열 길이가 달라도 동작하게 만드세요.
해설 보기
public final class BestSignupRegion {
public static void main(String[] args) {
String[] regions = {"Seoul", "Busan", "Daejeon"};
int[][] signupCounts = {
{40, 20},
{30, 50, 10},
{45}
};
int bestRow = -1;
int bestTotal = -1;
for (int row = 0; row < signupCounts.length; row++) {
int total = 0;
for (int value : signupCounts[row]) {
total += value;
}
if (total > bestTotal) {
bestTotal = total;
bestRow = row;
}
}
System.out.println(regions[bestRow] + "=" + bestTotal);
}
}Busan=90행마다 안쪽 반복문으로 합계를 계산하고, 지금까지 가장 큰 합계와 행 번호를 함께 갱신합니다.
합계가 0 이상이라는 입력 조건을 사용해 bestTotal을 -1로 시작했습니다.
같은 합계 계산이 여러 곳에서 반복되기 시작하면 ch3-6에서 메서드로 분리합니다.