ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4. 자바 배열과 반복문 (3) 중첩 배열
    자바(Java) 강의 2019. 3. 9. 17:42

    이번 포스팅을 이용해서 더 복잡한 형태의 배열에 대해 이야기 해 보려고 한다. 특별히, 이중 배열을 이용해 중첩 배열(multi-dimensional array)에 대해 중점적으로 다뤄본다.

    예상 독자

    목표

    • 중첩 배열 (multi-dimensional array)
    • 중첩 배열 선언/초기화 방법

    중첩 배열 (multi-dimensional arrray)

     영화관 예매 페이지를 만든다고 가정 해 보자. 영화관의 좌석을 어떻게 표현 할 것인가? 보통 영화관의 좌석을 어떻게 되어 있는가? A행 3열, J행 2열 등등 행과 열로 좌석을 구분 할 것이다. 이전 포스트에서 다뤘던 배열을 이용해 이를 구현하려면 어떻게 하겠는가? 예를들어서 간단하게 좌석은 5개의 행이 있고 각 행마다 2개의 좌석이 존재한다고 쳐 보자. 


    위와 같은 형태의 영화관의 좌석은 아래와 같이 표현 할 수 있을 것이다. 각각의 네모를 배열의 '엘리멘트(element)'라고 부른다.

    public class Main {
    public static void main(String[] args) {
    char seat0[] = new char[2];
    char seat1[] = new char[2];
    char seat2[] = new char[2];
    char seat3[] = new char[2];
    char seat4[] = new char[2];

    }
    }

    하지만 요즘 영화관 좌석이 5개밖에 없는 영화관이 어디 있겠는가? 이를 한번 10개로 늘려보라. 또, 영화관이 아니라 100*1000명을 수용해야하는 스타디움이라면 어떻게 하겠는가? A, B, C, D, E,.. AA, AB.. 이렇게 계속해서 배열 변수를 선언하겠는가? 이 패턴 어디서 많이 보지 않았는가? 그렇다 배열을 처음 시작했을 때, 우리는 같은 문제에 대해 고민하고 있었다. 연속적인 무언가를 할당하기 위해 배열을 사용했는데, 이제는 배열 자체를 연속적으로 할당하고 싶은 것이다. 가능 할까? 가능하다. 배열의 배열(이중 배열)을 이용해 표현 할 수 있다. 위처럼  char[5][2]인 배열을 생성 해 보자. 이해를 돕기위해 값을 넣어 생성 한다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{ // 바깥 배열
    {'a', 'b'}, // seat[0] // 내부 배열
    {'c', 'd'}, // seat[1] // 내부 배열
    {'e', 'f'}, // seat[2] // 내부 베열
    {'g', 'h'}, // seat[3] // 내부 배열
    {'i', 'j'}, // seat[4] // 내부 배열
    };

    }
    }

    이전 포스트에서 {} <- 얘를 이용하면 배열 초기화 할 수 있다는 사실을 배웠다. 또 {}를 이용하면 컴퓨터가 배열의 크기를 계산 할 수 있기 때문에 크기를 적어주지 않아도 된다고 했다. 중첩 배열은, 배열 안에 또 배열이 있는 것이라고 했다. 그래서 { } <- 하나의 배열 초기화 안에 배열 5개를 넣어 보았다. 그림으로 치면 아래와 같다.


    seat은 배열 하나를 접근 할 수 있게 해주는 변수이다. seat이 접근 할 수 있는 배열의 크기는 5개이다. 따라서 seat[0], seat[1], seat[2], seat[3], seat[4]로 접근 한다. seat[0]~seat[4]이 세로로 되어있지만 이거는 공간상의 제약때문에 그렇다. 얘네는 배열이다. 이전 포스트에서 했던 그 배열이랑 같은 배열이다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a' /* 0 */, 'b' /* 1 */}, // seat[0]
    {'c' /* 0 */, 'd' /* 1 */}, // seat[1]
    {'e' /* 0 */, 'f' /* 1 */}, // seat[2]
    {'g' /* 0 */, 'h' /* 1 */}, // seat[3]
    {'i' /* 0 */, 'j' /* 1 */}, // seat[4]
    };

    System.out.println(seat.length);
    }
    }
    실행 결과: 5

    다시 말하지만 seat이 접근 할 수 있는 배열의 크기는 5이기 때문에 length를 출력하면 5가 나온다.

    seat의 각각의 엘리멘트(element-파란 네모하나)안에는 또 배열이 있다. 각 엔리멘트안의 배열은 크기가 2이고 문자형(char)을 넣을 수 있다. 이제 중첩 배열의 내부를 어떻게 접근하는지 알아보자.

     잠깐! 화살표가 무엇인가? 화살표는 메모리 주소를 뜻한다. 네모 하나는 실제 값을 넣을 수 있는 메모리 공간을 뜻한다. 네모+화살표의 의미는 해당 네모(메모리)의 값이 누군가의 주소라는 것이다. 그 누군가가 여기서는 배열이다. 즉 왼편의 네모+화살표는 어떤 배열이 존재하는 곳의 주소를 갖고있다. 주소를 갖고있으므로 필요 할 때 마다 그 주소를 찾아가(접근) 값을 가져온다. 이런 '주소'의 개념을 자바에서는 '레퍼런스'라고 부른다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a' /* 0 */, 'b' /* 1 */}, // seat[0]
    {'c' /* 0 */, 'd' /* 1 */}, // seat[1]
    {'e' /* 0 */, 'f' /* 1 */}, // seat[2]
    {'g' /* 0 */, 'h' /* 1 */}, // seat[3]
    {'i' /* 0 */, 'j' /* 1 */}, // seat[4]
    };

    System.out.println(seat[0]);
    }
    }
    실행 결과: ab

    seat[0]을 출력 해 보면 ab가 출력되는 것을 확인 할 수 있다. (모든 배열이 이렇게 배열 전체를 출력하지 않는다는 것 이전 포스트에서 설명 했었다. 배열의 값 전체를 출력하는 것은 char 배열 뿐이라는 것을 잊지말자.) 위에서 각 슬롯의 안은 배열의 크기가 2이라고 했다. seat[0] = { 'a', 'b' } 라는 사실을 확인 했다. 이제 아래를 출력 해 보자.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a' /* 0 */, 'b' /* 1 */}, // seat[0]
    {'c' /* 0 */, 'd' /* 1 */}, // seat[1]
    {'e' /* 0 */, 'f' /* 1 */}, // seat[2]
    {'g' /* 0 */, 'h' /* 1 */}, // seat[3]
    {'i' /* 0 */, 'j' /* 1 */}, // seat[4]
    };

    System.out.println(seat[0][0]);
    }
    }
    실행 결과: a

    어떻게 된 것인가?

    seat[0]에는 또 다른 배열이 들어있다. 이 배열은 { 'a', 'b' }이다 이 배열의 0번 인덱스의 값은 a이다. 다시말해 seat[0]의 0번 인덱스의 값은 a이다. { 'a', 'b' }의 1번 인덱스에 있는 값은 몇인가? b이다. { 'a', 'b' }는 어디에 할당되어있나? seat[0]에 할당 되어 있다. 따라서, 

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a' /* 0 */, 'b' /* 1 */}, // seat[0]
    {'c' /* 0 */, 'd' /* 1 */}, // seat[1]
    {'e' /* 0 */, 'f' /* 1 */}, // seat[2]
    {'g' /* 0 */, 'h' /* 1 */}, // seat[3]
    {'i' /* 0 */, 'j' /* 1 */}, // seat[4]
    };

    System.out.println(seat[0][1]);
    }
    }
    실행 결과: b

    b를 출력한다. seat[0]에는 또 다른 배열이 들어있으므로 이 내부 배열의 크기도 출력 할 수 있어야 한다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a' /* 0 */, 'b' /* 1 */}, // seat[0]
    {'c' /* 0 */, 'd' /* 1 */}, // seat[1]
    {'e' /* 0 */, 'f' /* 1 */}, // seat[2]
    {'g' /* 0 */, 'h' /* 1 */}, // seat[3]
    {'i' /* 0 */, 'j' /* 1 */}, // seat[4]
    };

    System.out.println(seat[0].length);
    }
    }
    실행 결과: 2

    이제 'seat[0]이 가리키는 배열'의 의미가 조금 이해가 되는가?

    이번에는 다른 질문을 해보자. i를 출력하려면 어떻게 해야 하는가? i는 seat의 몇번 인덱스의 배열에 들어있는가? 그렇다 4번 인덱스의 배열에 들어있다. 4번 인덱스의 배열 { 'i', 'j' }의 몇번째 값인가? 0번째 값이다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a' /* 0 */, 'b' /* 1 */}, // seat[0]
    {'c' /* 0 */, 'd' /* 1 */}, // seat[1]
    {'e' /* 0 */, 'f' /* 1 */}, // seat[2]
    {'g' /* 0 */, 'h' /* 1 */}, // seat[3]
    {'i' /* 0 */, 'j' /* 1 */}, // seat[4]
    };

    System.out.println(seat[4]);
    System.out.println(seat[4][0]);
    }
    }
    실행 결과: ij i

    혼자서 인덱스를 찾아냈다면 잘 한 것이다.

    연습하기 :

    public class Main {
    public static void main(String[] args) {
    char matrix[][] = new char[][]{
    {'a', 'b', 'c', 'd', 'e'},
    {'f', 'g', 'h', 'i', 'j'},
    {'k', 'l', 'm', 'n', 'o'}
    };
    }
    }

    • 위의 matrix는 크기가 몇인 배열인가? 
    • matrix[0]은 크기가 몇인 배열인가?
    • matrix[1][3]은 무슨 값을 출력하는가? 그냥 출력해보지 말고 직접 구해봐라.
    • matrix[][] <- 대괄호 두개에 무슨 값을 넣어야 g가 출력 되는가?

    여기까지 했다면 다음으로 넘어가도 된다. 만약 이해가 안된다면 다시 읽고, 다른 책도 읽어보고, 다른 블로그도 검색해가면서 중첩 배열을 꼭 이해하라. 그리고 다음으로 넘어가라. 위의 중첩 배열의 개념을 모르면 앞으로 계속 이해가 안될것이다.

    중첩 배열 선언/초기화 방법

    여러분이 중첩 배열을 이해한다고 생각하고, 이제 중첩 배열의 선언 방법에 대해 이야기 하겠다. 배열에도 선언 방법이 여러가지 있었다. 중첩 배열도 결국 배열일 뿐이기 때문에 그냥 배열의 선언 방법이랑 같다고 생각하면 된다. 

    첫번째는 위에서 했던 것 처럼 값으로 초기화 하는 것이다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a', 'b'},
    {'c', 'd'},
    {'e', 'f'},
    {'g', 'h'},
    {'i', 'j'},
    };
    }
    }

    또는

    public class Main {
    public static void main(String[] args) {
    char[][] seat = {
    {'a', 'b'},
    {'c', 'd'},
    {'e', 'f'},
    {'g', 'h'},
    {'i', 'j'},
    };
    }
    }

    이전 포스트에서 값으로 초기화하면 크기를 넣어 줄 필요가 없다고 했다. 중첩 배열도 마찬가지이다. 초기화를 해주면 이미 얼마만큼의 메모리가 필요한지 알기 때문에 크기를 넣어 줄 필요가 없다. 그렇다면 넣을 값을 모를 때는 어떻게 하겠는가? 크기를 넣어주면 된다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[5][2];
    }
    }

    이렇게 하면 코드 실행시 컴퓨터가, '어? 얘는 크기가 5인 배열이고, 그 배열의 엘리먼트 안에는 각각 크기가 2인 char형 배열이 들어가므로 (5*4 bytes)*( 2*2 bytes) = 80바이트의 공간을 할당해야겠다'하고 그 크기를 할당한다. 2bytes는 char의 크기이다. 그럼 4 bytes는 무엇인가? 위에서 각 seat[0]~seat[5]는 내부 배열의 주소를 저장한다고 말했던게 기억 나는가? 그 주소의 크기가 4 byte이다 (컴퓨터에 따라 다르다, 어떤 주소는 8 byte이다. 여기선 4라고 치자.)

    하지만 내부배열의 크기는 꼭 이 배열 선언시에 지정 될 필요는 없다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[5][];
    }
    }

    이렇게 하면 코드 실행시 컴퓨터가, '어? 얘는 크기가 5인 배열이고, 그 배열의 엘리먼트는 char형 배열이다.' 여기까지만 안다. 그러면 적어도 이후에 생성될 내부 배열의 주소를 저장 할 수 있게 5 * 4 bytes = 20바이트의 공간을 생성한다. 그리고 이후에 누군가 내부 배열을 초기화하면, 초기화 된 배열의 주소를 할당하면 끝인것이다.

    다시말해 이렇게 나중에 주소(레퍼런스)를 저장하기 위해 위와 같이 배열을 메모리에 생성한다. 그리고 나서 우리는 아래처럼 나중에 배열을 생성 할 수 있다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[5][];

    seat[0] = new char[]{ 'a', 'b' };
    System.out.println(seat[0][0]);
    System.out.println(seat[0][1]);
    }
    } 실행 결과: a b




    그렇다면 아직 초기화 하지 않은 다른 엘리멘트(seat[1] ~ seat[4])에는 뭐가 들어갈까? 모르겠으면 해보자!

    public class Main {
    public static void main(String[] args) {
    char[][] seat= new char[5][];

    seat[0] = new char[]{'a', 'b'};
    System.out.println(seat[1]);
    }
    }
    실행 결과: Exception in thread "main" java.lang.NullPointerException at java.io.Writer.write(Writer.java:127) at java.io.PrintStream.write(PrintStream.java:503) at java.io.PrintStream.print(PrintStream.java:653) at java.io.PrintStream.println(PrintStream.java:792) at Main.main(Main.java:6)

    NullPointerException이란 예외(Exception)가 났다! 이게 무엇 뜻인가? 즉 System.out.println이 화면에 seat[1]을 출력하려고 했으나, seat[1]아무것도 존재하지 않기 때문에(null) 에러가 났다는 것이다. 이를 통해 초기화를 해 주지 않으면 그 값은 null이고 (null은 아무것도 없다는 뜻이다.) 따라서 에러가 났다 라고 이해 할 수 있다.

    public class Main {
    public static void main(String[] args) {
    char[][] seat= new char[5][];

    seat[0] = new char[]{'a', 'b'};
    seat[1] = new char[]{'c', 'd', 'e'};
    System.out.println(seat[0].length);
    System.out.println(seat[1].length);
    }
    }
    실행 결과: 2 3

    이렇게 내부 배열을 당장 초기화하지 않아도 된다는 특징 때문에, 내부 배열의 크기가 달라도 상관이 없다. seat[0]은 크기가 2인 배열이었지만, seat[1]는 크기가 3인 배열이다. 이제 이 배열을 아래처럼 생겼을 것이다.

    초기화로 선언하면 다음과 같다. 

    public class Main {
    public static void main(String[] args) {
    char[][] seat = new char[][]{
    {'a', 'b'},
    {'c', 'd', 'e'},
    };
    }
    }

     중첩 배열은 처음 프로그래밍을 배우는 사람들의 첫 난관이기도 하다. 약간 이해하기 어려운 개념들이 있지만 인덱스만 잘 찾을 수 있으면 넘어가도 된다. 중첩 배열을 이해하는데 시간이 좀 걸릴 수 있으니, 책도 읽어보고 다른 블로그도 보면서 천천히 나아가길 바란다. 이 포스트에서는 이중배열만 했지만, 같은 개념으로 3중배열 4중배열도 가능하다. 다음 포스트에서는 중첩 배열을 반복문에서 이용 해 보도록 하겠다.

    다음 포스트: 4. 자바 배열과 반복문 (4) 중첩 반복문

    댓글

f.software engineer @ All Right Reserved