[Java] Java 03 Arrays


Java

Period 01 — 변수 100개 만들 거야?

  1. 변수 여러 개로 다루기 어려운 상황을 알고, 배열이 왜 필요한지 설명할 수 있다.
  2. int[] arr = new int[5];로 배열을 선언 • 초기화하고 arr.length를 활용할 수 있다.
  3. for 루프와 인덱스로 배열을 순화하면서 합계 • 평균을 계산할 수 있다.
  4. 인덱스가 왜 0부터 시작하는지, arr[length]가 왜 오류인지 설명할 수 있다.

배열 선언, 그리고 메모리에서 일어나는 일

  • 🧠 1단계: 변수 선언 (Stack 영역)
    • int[] scores; 코드가 실행되면, 메모리의 Stack 영역에 scores라는 이름의 참조 변수가 생성됨.
    • 이 시점에는 실제 데이터(정수 값)가 들어갈 공간이 물리적으로 존재하지 않으며, 알 수 없는 쓰레기 값이나 null 상태로 머물러 있음.
  • 🧱 2단계: 배열 생성 (Heap 영역)
    • new int[3]; 연산자가 실행되면, 메모리의 Heap 영역에 정수형(int) 데이터 3개를 연속으로 저장할 수 있는 공간이 물리적으로 할당됨.
    • 자바는 이 Heap 영역의 공간을 임의의 값으로 두지 않고, 해당 데이터 타입의 기본값인 0으로 자동 초기화(Clean up)함.
  • 🔗 3단계: 참조값 연결 (대입 연산자 =)
    • 대입 연산자(=)에 의해 Heap 영역에 생성된 배열의 시작 주소(참조값, 예: @x100)가 Stack 영역에 있는 참조 변수 scores에 최종 저장됨.
    • 연결이 완료된 이 시점부터 비로소 참조 변수를 통해 Heap 영역의 배열 데이터에 정상적으로 접근하여 제어할 수 있게 됨.

실습 1 — 점수 5개의 합계 구하기

public class ScoreSum {
    public static void main(String[] args) {
        int[] scores = {92, 78, 85, 61, 99};
        int sum = 0;

        for (int i = 0; i < scores.length; i++) {
            sum += scores[i];   // 인덱스 i의 값을 sum에 더함
        }

        System.out.println("합계 = " + sum);
    }
}

실습 2 — 평균을 계산하라

public class ScoreAvg {
    public static void main(String[] args) {
//        int[] scores = {92, 78, 85, 61, 99};
        int[] scores = {90, 80, 80, 60, 52};
        int sum = 0;

        for (int i = 0; i < scores.length; i++) {
            sum += scores[i];
        }
        double avg = (double) sum / scores.length;
        System.out.println("average = " + avg);
    }
}

잠깐 — arr[5]는 6번째가 아니다

  • ⚠️ 인덱스 번호와 인간의 세기(Count)의 괴리:
    • 일상생활에서 인간은 숫자를 1부터 세기 시작하지만, 자바를 포함한 대부분의 프로그래밍 언어에서 배열의 인덱스는 무조건 0부터 시작함.
    • 따라서 arr[5]라고 명시했을 때 컴퓨터가 가리키는 대상은 ‘5번째 요소’가 아니라 ‘6번째 요소’를 의미함. 이 규칙을 착각하면 프로그램의 논리 구조가 통째로 뒤틀리게 됨.
  • 🧱 0번 인덱스(Zero-based Indexing)의 정밀한 위치 매핑:
    • 배열의 인덱스 번호는 순서나 개수를 뜻하는 것이 아니라, “배열의 첫 번째 시작 주소로부터 몇 칸(Offset) 떨어져 있는가”를 나타내는 물리적 거리 값임.
    • arr[0]: 시작 주소에서 0칸 떨어진 자리 ➡️ 첫 번째 데이터 위치.
    • arr[5]: 시작 주소에서 정수 크기만큼 5칸 뒤로 떨어진 자리 ➡️ 여섯 번째 데이터 위치.
  • 📉 입문자가 반드시 마주치는 치명적인 에러:
    • 만약 크기가 5인 배열(int[] arr = new int[5];)을 만들었다면, 사용할 수 있는 합법적인 인덱스는 arr[0]부터 arr[4]까지 총 5개뿐임.
    • 여기에 존재하지 않는 6번째 요소인 arr[5]를 호출해 데이터에 접근하려고 시도하는 순간, 자바 가상머신(JVM)은 배열의 물리적 경계를 넘어섰다고 판단하여 ArrayIndexOutOfBoundsException 에러를 던지며 프로그램을 강제 종료시킴.

실습 3 — 거꾸로 출력 (반전을 적용)

int[] scores = {92, 78, 85, 61, 99};

// 처음/끝/조건을 어떻게 잡을까?
for (int i = scores.length - 1; i >= 0; i--) {
    System.out.println(scores[i]);
}
$ java ReverseScores
99
61
85
78
92

도전 문제 — 짝수는 몇 개?

// 문제 정수 7개를 Scanner로 입력받아 길이 7짜리 int 배열에 저장한 후, 그 중 짝수의 개수만 출력하세요

// 조건 반드시 배열을 만들어 저장한 뒤 셀 것 for 루프와 arr.length를 활용할 것 0은 짝수로 취급

import java.util.Scanner;

public class EvenCount {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int[] arr = new int[7];

        System.out.println("Enter 7 integers : ");
        for (int i=0; i<arr.length; i++) {
            arr[i] = sc.nextInt();
        }

        int count = 0;
        for (int i=0; i<arr.length; i++) {
            if (arr[i] % 2 == 0) {
                count += 1;
            }
        }
        System.out.println("Number of even numbers : " + count);
    }
}
Enter 7 integers : 
3 8 12 5 6 11 4
Number of even numbers : 4

Period 02 — 1등은 누구?

  1. 배열을 한 번 순회하며 최댓값을 찾는 알고리즘을 구현할 수 있다.
  2. 값과 인덱스를 동시에 추적해 String[]int[] 평행 배열에서 “1등의 이름”을 출력할 수 있다.
  3. 왜 “1바퀴 비교”로는 정렬이 끝나지 않는지, 중첩 반복이 왜 필요한지 설명할 수 있다.
  4. 선택 정렬(Selection Sort)을 직접 작성하고 동작을 단계별로 추적할 수 있다.

“순회”에서 “알고리즘”으로 — 비교가 등장한다

  • 🔄 단순 출력을 넘어선 데이터 처리:
    • 반복문을 사용해 배열의 모든 요소를 단순히 화면에 순차적으로 출력하는 단계를 넘어, 각 요소의 값을 서로 비교하고 분석하기 시작할 때 비로소 알고리즘(Algorithm)의 영역으로 진입함.
  • 📐 최대값 찾기(Max Value)의 논리:
    • 배열 내에서 가장 큰 값을 찾으려면 기준이 되는 임시 변수(max)를 선언하고, 배열의 첫 번째 값(arr[0])으로 초기화한 뒤 순회를 시작해야 함.
  • ⚡ 실시간 업데이트와 값의 교체:
    • 루프를 돌며 현재 검사 중인 요소의 값(arr[i])이 기존 max보다 크다면, max 변수의 값을 arr[i]로 갱신함. 이 비교 연산의 반복이 알고리즘의 핵심임.

실습 1 — 최고점 출력

public class FindMax {
    public static void main(String[] args) {
        int[] scores = {92, 78, 85, 61, 99};

        int max = scores[0];   // 첫 원소로 초기화

        for (int i = 1; i < scores.length; i++) {
            if (scores[i] > max) {
                max = scores[i];
            }
        }

        System.out.println("최고점 = " + max);
    }
}

실습 2 — 1등의 “이름”까지 출력하라

String[] names  = {"민수", "지우", "예린", "동현", "가영"};
int[]    scores = {92,    99,    85,    61,    78};

int maxIdx = 0;   // 1등 인덱스를 기억

for (int i = 1; i < scores.length; i++) {
    if (scores[i] > scores[maxIdx]) {
        maxIdx = i;
    }
}

System.out.println(
    "1등: " + names[maxIdx] + " (" + scores[maxIdx] + "점)");

실습 3 — 선택 정렬 (Selection Sort)

  • 🔄 최소값 선택과 위치 교환:
    • 정렬되지 않은 데이터 중 가장 작은 값(최소값)을 찾아서, 아직 정렬되지 않은 부분의 맨 첫 번째 위치와 자리를 바꾸는 알고리즘임. 이 과정을 한 칸씩 오른쪽으로 이동하며 반복함.
  • 🧱 확정된 구역과 미확정 구역:
    • 한 번 교환이 일어날 때마다 맨 앞자리부터 차례대로 가장 작은 값이 채워지므로, 정렬이 완료된 ‘확정 구역’이 늘어남. 자연스럽게 남은 ‘미확정 구역’의 크기는 매 루프마다 줄어들게 됨.
  • 📉 시간 복잡도와 효율성:
    • 데이터의 개수가 $N$개일 때, 이중 루프를 돌며 모든 원소를 비교해야 하므로 $O(N^2)$의 시간 복잡도를 가짐. 구현은 직관적이고 간단하지만, 데이터의 양이 많아질수록 성능이 급격히 떨어지는 특성이 있음.
import java.util.Arrays;

public class SelectionSort {
    public static void main(String[] args) {
        int[] a = {5, 1, 4, 2, 8};

        for (int i=0; i<a.length-1; i++) {
            int minIdx = i;
            for (int j=i+1; j<a.length; j++) {
                if (a[j] < a[minIdx]) minIdx = j;
            }

            int t = a[i];
            a[i] = a[minIdx];
            a[minIdx] = t;
        }

        System.out.println(Arrays.toString(a));
    }
}

도전 문제 — 내림차순 정렬 후, 상위 3명 출력

// 문제 names • scores 평행 배열(5명)을 점수 내림차순으로 정렬한 뒤, 상위 3명의 이름과 점수를 출력하세요

// 조건 점수를 정렬할 때 이름도 함께 따라가야 함 (짝이 안 어긋나게) 선택 정렬을 응용 — 한 자리 채울 때마다 두 배열 모두 swap 최종적으로 인덱스 [0], [1], [2]를 출력

import java.util.Arrays;

public class Top3 {
    public static void main(String[] args) {
        String[] names = {"민수", "지우", "예린", "동현", "가영"};
        int[] scores = {92, 99, 85, 61, 78};

        for (int i = 0; i < names.length - 1; i++) {
            int maxIdx = i;
            for (int j = i + 1; j < names.length; j++) {
                if (scores[j] >= scores[maxIdx]) maxIdx = j;
            }

            String t1 = names[i];
            names[i] = names[maxIdx];
            names[maxIdx] = t1;

            int t2 = scores[i];
            scores[i] = scores[maxIdx];
            scores[maxIdx] = t2;
        }

        System.out.println("[Top 3]");
        for (int i = 0; i < 3; i++) {
            System.out.println(
                i+1 + "위: " + names[i] + " (" + scores[i] + "점)");
        }
    }
}

Period 03 — 표를 만들자

  1. 1차원으로는 표현하기 불편한 데이터(표, 격자)에 2차원 배열을 적용해야 함을 안다.
  2. int[][] m = new int[3][4];를 선언하고 중첩 for로 순회할 수 있다.
  3. m.lengthm[0].length의 차이를 메모리 그림으로 설명할 수 있다.
  4. 행마다 길이가 다른 가변 길이 2차원 배열을 만들고 활용할 수 있다.

학생 3명, 과목 3개 — 1차원으로 다루면?

  • ⚠️ 1차원 배열로 다중 데이터를 다룰 때의 한계와 비효율:
    • 학생 3명의 국어, 영어, 수학 점수(총 9개의 데이터)를 하나의 1차원 배열(int[] scores = new int[9];)에 몰아서 저장할 수는 있음.
    • 하지만 데이터가 한 줄로 길게 늘어서기 때문에 “2번째 학생의 영어 점수”처럼 특정 위치의 데이터를 직관적으로 찾아내기가 매우 까다로워짐.
  • 🧱 인덱스 계산 공식의 강제화 (물리적 주소 연산):
    • 1차원 배열에서 행과 열의 개념을 흉내 내려면 개발자가 머릿속으로 공식을 만들어 인덱스를 직접 계산해야 함.
    • 예: (학생 번호 × 과목 개수) + 과목 번호
    • 1번째 학생(인덱스 1)의 2번째 과목(인덱스 2) 점수를 찾으려면 scores[(1 * 3) + 2] 즉, scores[5]를 호출해야 하는 번거로움과 코드 오독의 위험이 항상 뒤따름.
  • 💡 2차원 배열이 등장하게 된 결정적 이유:
    • 데이터의 구조는 본질적으로 ‘행(가로)’과 ‘열(세로)’을 가진 격자 형태인데, 이를 억지로 1차원에 구겨 넣으면 가독성이 극도로 떨어지고 인덱스 계산 착오로 인한 버그가 양산됨.
    • 개념적인 표(Table) 구조를 소스코드에 그대로 직관적으로 투영하여, 복잡한 인덱스 수학 연산 없이 scores[row][col] 형태로 안전하고 명확하게 제어하기 위해 2차원 배열 문법이 반드시 필요함.

2차원 배열 선언과 메모리 구조

  • 🏗️ 2차원 배열의 실체 — ‘배열의 배열’ 구조:
    • 자바에서 2차원 배열은 표(Matrix) 형태로 데이터가 나란히 묶여 있는 격자판 공간이 아님.
    • 실제로는 “1차원 배열들을 요소로 가지는 또 다른 1차원 배열”이며, 메모리상에서는 2단계 이상의 참조 주소 연쇄를 거치는 ‘참조의 참조’ 형태로 설계되어 있음.
  • 🧱 메모리 영역별 정밀 데이터 흐름 (행과 열의 분리):
    • Stack 영역: 2차원 배열 변수(예: int[][] arr)가 생성되며, Heap 영역에 만들어진 ‘행(Row) 제어용 마스터 배열’의 시작 참조값(@x100)을 저장함.
    • Heap 영역 (1단계 - 행 배열): 각 행의 실제 데이터가 담긴 독립된 1차원 배열들의 ‘참조 주소값’을 저장하는 변수방들이 나열됨. (예: arr[0] 방에는 0번째 행 배열의 주소 @x200이, arr[1] 방에는 1번째 행 배열의 주소 @x300이 저장됨)
    • Heap 영역 (2단계 - 열 배열): 최종적으로 실제 정수(int) 데이터가 연속적인 칸으로 저장되는 물리적인 가로 공간임.
  • .length 속성이 반환하는 값의 차이:
    • arr.length: 2차원 배열의 전체 공간 크기가 아니라, 마스터 격인 ‘행(Row)의 개수’를 뜻함.
    • arr[i].length: 해당 $i$번째 행이 가리키고 있는 ‘열(Column)의 개수’를 뜻함.
    • 자바는 이처럼 행마다 열의 길이를 완전히 다르게 구성하는 가변 배열(Ragged Array) 구조가 가능한 이유가 바로 이 메모리 참조 메커니즘 덕분임.

실습 1 — 4 x 6 좌석표 출력

int rows = 4;
int cols = 6;
String[][] seats = new String[rows][cols];

// 좌석 이름 채우기: A1, A2 …, B1 …
for (int r=0; r < rows; r++) {
    for (int c=0; c < cols; c++) {
        char row = (char)('A' + r);
        seats[r][c] = row + String.valueOf(c+1);
    }
}

// 출력
for (int r=0; r < rows; r++) {
    for (int c=0; c < cols; c++) {
        System.out.print(seats[r][c] + " ");
    }
    System.out.println();
}

실습 2 — 학생 x 과목 평균

int[][] m = {
    {90, 80, 70},   // 학생 0
    {60, 85, 75},   // 학생 1
    {100, 95, 88}   // 학생 2
};

// 학생별 평균 (행 단위)
for (int r=0; r < m.length; r++) {
    int sum = 0;
    for (int c=0; c < m[r].length; c++) {
        sum += m[r][c];
    }
    System.out.println("학생"+r+" 평균: " + sum/3.0);
}

// 과목별 평균 (열 단위) — for의 순서가 뒤집힌다
for (int c=0; c < m[0].length; c++) {
    int sum = 0;
    for (int r=0; r < m.length; r++) {
        sum += m[r][c];
    }
    System.out.println("과목"+c+" 평균: " + sum/3.0);
}

실습 3 — 계단형(jagged) 별 찍기

int n = 5;

// ① 가변 길이 2차원 배열 만들기
char[][] grid = new char[n][];
for (int r=0; r < n; r++) {
    grid[r] = new char[r+1];   // 행마다 다른 크기!
}

// ② 별 채우기
for (int r=0; r < grid.length; r++) {
    for (int c=0; c < grid[r].length; c++) {
        grid[r][c] = '*';
    }
}

// ③ 출력
for (int r=0; r < grid.length; r++) {
    for (int c=0; c < grid[r].length; c++) {
        System.out.print(grid[r][c]);
    }
    System.out.println();
}

도전 문제 — 5x5 행렬의 두 대각선 합 구하기

// 문제 5x5 정수 행렬을 만들고(자유롭게 초기화 OK), 주대각선(좌상 → 우하)과 반대각선(우상 → 좌하)의 합을 각각 출력하세요.

// 조건 중첩 for를 쓰되, “왔다 갔다” 두 번 도는 코드보다 한 번에 둘 다 더하는 것이 더 좋다. 중앙 칸은 두 대각선에 모두 속함 → 출력 시 어떻게 처리할지 생각

public class Diagonal {
    public static void main(String[] args) {
        int[][] diagonal = {
                {1, 2, 3, 4, 5},
                {6, 7, 8, 9, 10},
                {11, 12, 13, 14, 15},
                {16, 17, 18, 19, 20},
                {21, 22, 23, 24, 25}
        };
        int main_diagonal_sum = 0, sub_diagonal_sum = 0;

        for (int i=0; i<diagonal.length; i++) {
            for (int j=0; j<diagonal[i].length; j++) {
                if (i == j) main_diagonal_sum += diagonal[i][j];
                else if (i + j == 4) sub_diagonal_sum += diagonal[i][j];
            }
        }
        sub_diagonal_sum += diagonal[diagonal.length/2][diagonal[0].length/2];

        System.out.println("주대각선 합: " + main_diagonal_sum);
        System.out.println("반대각선 합: " + sub_diagonal_sum);
    }
}


Period 04 — 같은 코드를 두 번 쓰지 마라

  1. 메서드가 왜 필요한지 — 중복 제거와 의미 단위 분리의 가치를 설명할 수 있다.
  2. static 반환타입 이름(매개변수) 구조로 메서드를 정의 • 호출할 수 있다.
  3. 반환 타입과 return이 어긋날 때 컴파일러가 거절하는 이유를 안다.
  4. 1 • 2 • 3교시의 합계 • 평균 • 정렬 코드를 메서드로 분리해 main을 짧게 유지할 수 있다.

main이 점점 길어진다 — 절차지향의 한계

  • 📉 코드의 비대화와 가독성 저하:
    • 프로그램의 규모가 커질수록 main 메서드 내부에 모든 비즈니스 로직, 배열 제어, 계산 공식이 순차적으로 채워지면서 코드가 수천 줄로 비대해짐.
    • 이로 인해 소스코드 전체의 유기적인 흐름과 맥락을 한눈에 파악하는 것이 사실상 불가능해짐.
  • ⚠️ 데이터와 로직의 분리 및 수정의 공포:
    • 데이터(배열)는 프로그램 상단에 선언되어 있고 이를 처리하는 연산 로직은 하단에 길게 늘어서 있어, 데이터 구조가 아주 미세하게만 바뀌어도 하단의 관련 로직들을 수작업으로 일일이 찾아 고쳐야 함.
    • 이 과정에서 한 곳을 수정하면 연동된 다른 예측 불가능한 구역이 무너지는 연쇄 논리 버그의 주요 원인이 됨.
  • 💡 패러다임의 전환 필요성 (객체지향으로의 도약):
    • 단순히 코드를 위에서 아래로 순차 실행하는 전통적인 절차지향(Procedural) 아키텍처는 규모의 확장성 측면에서 명확한 한계에 봉착함.
    • 따라서 밀접한 연관성을 가진 ‘데이터’와 그 데이터를 처리하는 ‘코드(메서드)’를 하나의 독립적인 유닛으로 단단하게 묶어서 관리하는 객체지향(Object-Oriented) 패러다임으로 구조를 완전히 전환해야 함.

DRY 원칙 : Don’t Repeat Yourself. 같은 코드는 한 곳에. 메서드는 그 도구이다


메서드의 6가지 부품

자바의 메서드는 기능을 수행하는 단순한 코드 덩어리가 아니라, 정밀하게 정의된 6가지 핵심 부품(요소)의 결합체이다.

  • 1. 제어자 (Modifier)
    • public static과 같이 메서드의 접근 범위와 성격을 규정하는 부품이다.
  • 2. 반환 타입 (Return Type)
    • 메서드가 실행을 끝내고 호출한 곳으로 돌려줄 데이터의 종류를 명시하는 부품이다. 반환 값이 없다면 void를 사용한다.
  • 3. 메서드 이름 (Method Name)
    • 메서드의 역할을 직관적으로 나타내는 식별자이다. 동사로 시작하는 낙타 표기법(camelCase)의 사용을 권장한다.
  • 4. 매개변수 목록 (Parameter List)
    • 외부로부터 메서드 내부로 값을 전달받기 위한 변수들의 선언 구역이다. 소괄호 () 내부에 선언한다.
  • 5. 반환 데이터 성격 일치 (Return Match)
    • 반환 타입과 정확히 일치해야한다.
  • 6. 메서드 바디 (Method Body)
    • 중괄호 {} 구역을 의미한다. 메서드가 실제로 수행해야 하는 비즈니스 로직이 구현되는 핵심 공간이다.

반환 타입과 return은 한 몸 — 어긋나면 컴파일러가 거절한다

  • 🎯 반환 타입과 반환 데이터의 일치성
    • 메서드 선언부에 지정한 반환 타입(Return Type)과 메서드 바디 내부에서 return 키워드 뒤에 명시하는 실제 데이터의 타입은 완벽하게 일치해야 한다.
    • 만약 반환 타입을 int로 선언했다면 return 문은 반드시 정수형 데이터를 반환해야 하며, 이를 위반할 경우 컴파일 에러(Type Mismatch)가 발생한다.
  • 🛑 void 타입과 return의 생략 및 제어
    • 반환 타입이 void인 메서드는 실행 후 호출처로 돌려줄 값이 없다는 의미이므로 원칙적으로 return 문을 생략할 수 있다. 이 경우 메서드 바디의 중괄호 {} 끝을 만나면 자동으로 호출처로 복귀한다.
    • 다만 void 메서드 내부에서도 특정 조건에서 메서드 실행을 조기에 종료하고 빠져나가고 싶을 때는 뒤에 값을 적지 않고 단독으로 return; 문을 사용하여 실행 흐름을 끊을 수 있다.
  • ⚙️ 모든 실행 경로에서의 return 보장 규칙
    • 반환 타입이 void가 아닌 특정 데이터 타입(예: int, boolean 등)으로 정의된 메서드는 어떤 조건과 상황을 만나더라도 반드시 return 문이 실행되어 값을 반환하는 구조를 갖추어야 한다.
    • 메서드 내부에 조건문(if-else)이나 반복문이 존재할 경우, 모든 분기(Path)의 끝에 각각 적절한 return 문이 매설되어 있어야만 자바 컴파일러의 검증을 통과할 수 있다.

도전 문제 — 소수 판별 메서드 boolean isPrime(int n)

// 문제 static boolean isPrime(int n) 메서드를 작성하세요. 소수면 true, 아니면 false를 반환합니다.

// 조건 0과 1은 소수가 아님 → false 2부터 n-1까지 어떤 수로도 안 나누어떨어지면 소수 main에서 2~20까지 호출해 결과 출력

public class PrimeCheck {
    public static void main(String[] args) {
        for (int i=2; i < 21; i++) {
            System.out.print(i+": ");
            if (isPrime(i)) System.out.println("true");
            else System.out.println("false");
        }
    }

    static boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i=2; i*i <= n; i++) {
            if (n % i == 0) return false;
        }
        return true;
    }
}

Period 05 — 같은 이름, 다른 일

  1. parameter(정의)와 argument(호출)의 차이를 구분할 수 있다.
  2. 같은 이름의 메서드를 매개변수 개수 • 타입을 달리해 여러 개 작성할 수 있다 (오버로딩)
  3. 컴파일러가 ambiguous오류를 내는 상황을 직접 만들고 원인을 설명할 수 있다.
  4. 오버로딩을 언제 쓰고 언제 피할지 판단할 수 있다 (가독성 우선).

매개변수(Parameter) vs 인자(Argument) — 헷갈리지 마라

  • 🏗️ 매개변수 (Parameter): 정의 시점의 변수
    • 메서드를 정의(선언)할 때 소괄호 () 안에 선언하는 변수이다.
    • 외부에서 어떤 값이 들어올지 모르는 상태에서 데이터를 받아내기 위해 자리를 비워두는 틀(껍데기) 역할을 한다.
  • 🧱 인자 (Argument): 호출 시점의 실제 값
    • 메서드를 호출할 때 실제로 전달하는 구체적인 값(데이터)이다.
    • 매개변수가 준비해 둔 메모리 공간에 물리적으로 복사되어 들어가는 진짜 내용물에 해당한다.
  • ⚡ 값의 복사 메커니즘 (Call by Value)
    • 인자로 전달한 변수 자체가 넘어가는 것이 아니라 변수에 들어있는 값만 복사되어 매개변수로 전달된다.
    • 따라서 메서드 내부에서 매개변수의 값을 변경해도 호출처에 있는 원본 변수의 값은 전혀 영향을 받지 않는다.

오버로딩(Overloading) — 같은 이름, 다른 시그니처

  • 🏗️ 오버로딩(Overloading)의 정의
    • 자바에서 한 클래스 내에 이미 존재하는 메서드와 이름이 동일한 메서드를 여러 개 정의하는 것을 의미한다.
    • 하나의 이름으로 유사한 기능을 수행하는 메서드들을 통합 관리할 수 있어 코드의 일관성과 가독성을 높여준다.
  • 🧱 메서드 시그니처(Method Signature)의 개념
    • 컴파일러가 각각의 메서드를 고유하게 식별하기 위해 사용하는 기준이다.
    • 메서드 시그니처는 ‘메서드 이름’과 ‘매개변수의 형태(타입, 개수, 순서)’로 구성된다.
    • 반환 타입(Return Type)이나 접근 제어자는 시그니처에 포함되지 않으므로, 이를 바꾸는 것만으로는 오버로딩이 성립하지 않는다.
  • ⚡ 오버로딩의 성립 조건과 규칙
    • 필수 조건: 메서드의 이름이 완전히 같아야 하고, 매개변수의 목록(타입, 개수, 순서 중 하나 이상)은 반드시 달라야 한다.
    • 만약 메서드 이름과 매개변수 구조가 모두 같은데 반환 타입만 다르게 선언하면, 컴파일러는 중복 정의로 판단하여 컴파일 에러(Duplicate Method)를 발생시킨다.

실습 1 — print 메서드 3종 오버로딩

public class PrintOverload {
    public static void main(String[] args) {
        print(42);
        print("hello");
        print(3, 7);
    }
    static void print(int n) {
        System.out.println("정수: "+n);
    }
    static void print(String s) {
        System.out.println("문자열: "+s);
    }
    static void print(int a, int b) {
        System.out.println("쌍: "+a+", "+b);
    }
}

실습 2 — avg 오버로딩 (2개 • 3개 • 배열)

public class AvgOverload {
    public static void main(String[] args) {
        System.out.println(avg(80, 90));
        System.out.println(avg(80, 90, 100));

        int[] scores = {92, 78, 85, 61, 99};
        System.out.println(avg(scores));
    }

    static double avg(int a, int b) {
        return (a + b) / 2.0;
    }
    static double avg(int a, int b, int c) {
        return (a + b + c) / 3.0;
    }
    static double avg(int[] arr) {
        int s = 0;
        for (int x : arr) s += x;
        return (double) s / arr.length;
    }
}

컴파일러가 “모르겠다”고 손을 든다 — Ambiguous!

  • 🏗️ 모호한 매칭 (Ambiguous Method Call)
    • 오버로딩된 메서드들 중 호출 시점에 넘겨준 인자와 일치하는 메서드가 둘 이상 존재하여, 컴파일러가 어떤 메서드를 실행해야 할지 결정하지 못하는 상태를 의미한다.
  • 🧱 자동 형변환(Promotion)의 함정
    • 예컨대 print(int a, double b)print(double a, int b)가 오버로딩되어 있을 때, print(10, 20)을 호출하면 정수형 데이터 1020이 양쪽 메서드의 double 매개변수로 각각 자동 형변환이 가능해지면서 모호성이 발생한다.
  • ⚡ 해결책: 명시적 형변환 (Casting)
    • 컴파일러의 모호성을 해결하려면 호출 시점에 인자를 명시적으로 형변환하여 타깃 메서드를 확정해주어야 한다.
    • print(10, (double)20)과 같이 작성하면 첫 번째 메서드가 명확하게 매칭되어 컴파일 에러가 해결된다.
// 이 두 개를 만들고 ...

static void show(int a, double b) {
    System.out.println("A: int, double");
}

static void show(double a, int b) {
    System.out.println("B: double, int");
}
// 이렇게 호출하면?

show(1, 2);   // 둘 다 int 인자
// 컴파일 결과

Ambiguous.java:5: error: reference to show is ambiguous
  both method show(int,double) in Ambiguous
  and method show(double,int) in Ambiguous match
    show(1, 2);
    ^

도전 문제 — max 메서드 3종을 직접 만들고 모두 호출하라

// 문제 다음 3종의 max 메서드를 모두 작성하세요:

  1. int max(int a, int b)
  2. int max(int a, int b, int c)
  3. int max(int[] arr)

// 조건 main에서 셋 다 호출해 결과 확인 3번은 빈 배열이면? — 어떻게 처리할지 고민 2번은 1번을 재사용해도 OK (메서드가 메서드를 호출!)

public class MaxOverload {
    public static void main(String[] args) {
        System.out.println("max(5, 8) = "+max(5, 8));
        System.out.println("max(3, 7, 2) = "+max(3, 7, 2));
        System.out.println("max({9, 1, 4, 7, 5}) = "+max(new int[]{9, 1, 4, 7, 5}));
    }
    static int max(int a, int b) {
        return (a > b) ? a : b;
    }
    static int max(int a, int b, int c) {
        return max(max(a, b), c);
    }
    static int max(int[] arr) {
        int m = arr[0];
        for (int i=1; i < arr.length; i++) {
            m = max(m, arr[i]);
        }
        return m;
    }
}

Period 06 — 메서드 호출의 비밀

  1. 1장의 Stack/Heap 메모리 모델을 다시 그려, 6교시의 토대로 활용한다.
  2. 기본형 매개변수가 메서드 안에서 변경되어도 밖에 영향 없음을 메모리로 설명할 수 있다.
  3. 배열을 인자로 넘기면 원소가 바뀌는 이유를 “참조값의 복사”로 설명할 수 있다.
  4. 메서드 안에서 arr = new int[]{...}로 배열을 갈아치워도 밖에 영향 없는 이유를 설명할 수 있다.

기본형 매개변수 (Primitive Parameter): 값이 복사된다

  • 🏗️ 값의 직접 저장과 독립적 메모리 할당
    • 자바에서 int, double, boolean 등 기본형(Primitive Type)으로 매개변수를 선언하면, Stack 영역에 실제 데이터 값을 직접 저장하는 독립적인 메모리 공간이 생성된다.
  • 🧱 Call by Value (값의 복사) 메커니즘의 적용
    • 메서드를 호출할 때 인자로 넘겨주는 원본 변수의 실제 값(Value) 자체가 복사되어 매개변수로 전달된다.
    • 이는 데이터의 주소가 아닌 ‘순수한 값의 사본’이 넘어가는 구조이다.
  • ⚡ 호출처 원본 데이터 보호 (부수 효과 방지)
    • 메서드 내부(바디)에서 매개변수의 값을 변경하거나 재할당하더라도, 이는 Stack 영역에 복사된 별도의 사본 공간 안에서만 일어나는 변화이다.
    • 따라서 메서드 실행이 종료된 후 호출처로 돌아갔을 때, 인자로 전달했던 외부 원본 변수의 값은 전혀 바뀌지 않고 그대로 유지된다.
static void swap(int a, int b) {
    int t = a;
    a = b;
    b = t;
    // 메서드 안에서는 분명 바뀜
}

public static void main(...) {
    int x = 1, y = 2;
    swap(x, y);
    System.out.println(x+", "+y);
    // 출력: 1, 2  ← ?!
}

자바는 항상 값 전달입니다. 그 값이 참조일 뿐.

  • 🏗️ Call by Value(값 전달) 패러다임의 일관성
    • 자바는 매개변수 전달 방식으로 오직 ‘Call by Value(값에 의한 전달)’만을 채택하고 있다.
    • 기본형 변수이든 참조형 변수이든 관계없이, 메서드를 호출할 때 인자로 넘어가는 것은 변수가 가진 메모리 공간의 ‘내용물(값)’ 그 자체이다. 자바에서 변수 자체를 넘기는 ‘Call by Reference’ 방식은 존재하지 않는다.
  • 🧱 참조형 변수 전송의 물리적 실체
    • 참조형 변수(객체나 배열)를 메서드의 인자로 전달할 때, 변수 내부에 들어있는 값은 객체 자체가 아니라 ‘객체가 위치한 Heap 영역의 메모리 주소(참조값)’이다.
    • 따라서 메서드 호출 시 이 주소값의 사본이 매개변수로 복사되어 전달된다. 결과적으로 호출처의 원본 참조 변수와 메서드의 매개변수는 메모리상에서 동일한 객체를 가리키게 된다.
  • ⚡ 참조값 복사가 가져오는 효과와 구별점
    • 멤버 변수 변경 가능: 매개변수를 통해 동일한 주소의 객체에 접근할 수 있으므로, 메서드 내부에서 param.value = 20;과 같이 객체의 내부 데이터를 변경하면 호출처의 원본 객체도 변경된다.
    • 변수 자체의 재할당 불가능: 메서드 내부에서 매개변수에 새로운 객체를 대입(param = new Data();)하더라도, 이는 복사된 주소방의 값을 바꾼 것일 뿐이므로 호출처의 원본 변수가 가리키는 객체는 전혀 변하지 않는다.

도전 문제 — 두 사실을 출력으로 직접 증명하라

// 문제 하나의 main 안에서 두 메서드를 호출하고, 같은 배열에 대한 결과를 한 번에 비교해서 출력하세요.

import java.util.Arrays;

public class Proof {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};

        System.out.println(Arrays.toString(arr));
        modify(arr);
        System.out.println(Arrays.toString(arr));
        replace(arr);
        System.out.println(Arrays.toString(arr));

    }
    static void modify(int[] a) {
        System.out.println("modify 호출 ...");
        a[0] = 99;
    }
    static void replace(int[] a) {
        System.out.println("replace 호출 ...");
        a = new int[]{99, 99, 99};
    }
}

Period 07 — 종합 실습 (학생 성적 관리 v0)

  1. 요구사항을 읽고 필요한 메서드들을 식별 • 설계할 수 있다.
  2. 2장 메뉴 루프 + 1~6교시 메서드를 결합해 동작하는 콘솔 앱을 완성한다.
  3. “학생 1명 추가” 같은 요구사항이 왜 배열만으로는 불편한지 직접 체험 • 논증할 수 있다.
  4. v0의 한계를 4장 / 5장이 어떻게 해결할지 예측할 수 있다.

import java.util.Scanner;

public class GradeApp {
    static final int N = 5;
    static Scanner sc = new Scanner(System.in);

    public static void main(String[] args) {
        String[] names = new String[N];
        int[] scores = new int[N];
        input(names, scores);

        while (true) {
            System.out.println(
                "\n=== 학생 성적 관리 v0 ===\n" +
                "1) 전체 2) 평균 3) 1등 4) 정렬 0) 종료");
            System.out.print("선택: ");
            int sel = sc.nextInt();

            switch (sel) {
                case 1 -> printAll(names, scores);
                case 2 -> System.out.printf(
                        "평균 = %.1f%n", average(scores));
                case 3 -> {
                    int i = findTop(scores);
                    System.out.printf("1등: %s (%d점)%n", names[i], scores[i]);
                }
                case 4 -> {
                    sortDesc(names, scores);
                    printAll(names, scores);
                }
                case 0 -> { System.out.println("종료"); return; }
                default -> System.out.println("잘못된 입력");
            }
        }
    }
    static void input(String[] names, int[] scores) {
        for (int i=0; i < names.length; i++) {
            System.out.print("이름: ");
            names[i] = sc.next();
            System.out.print("점수: ");
            scores[i] = sc.nextInt();
        }
    }
    static void printAll(String[] names, int[] scores) {
        System.out.println("no.    이름 점수");
        for (int i=0; i < names.length; i++) {
            System.out.printf("%-8d %s %d%n", i+1, names[i], scores[i]);
        }
    }
    static double average(int[] scores) {
        int sum = 0;
        for (int x : scores) sum += x;
        return (double) sum / scores.length;
    }
    static int findTop(int[] scores) {
        int maxIdx = 0;
        for (int i=1; i < scores.length; i++) {
            if (scores[i] > scores[maxIdx]) maxIdx = i;
        }
        return maxIdx;
    }
    static void sortDesc(String[] names, int[] scores) {
        int n = scores.length;
        for (int i=0; i < n - 1; i++) {
            int maxidx = i;
            for (int j = i + 1; j < n; j++) {
                if (scores[j] > scores[maxidx]) maxidx = j;
            }

            int ti = scores[i];
            scores[i] = scores[maxidx];
            scores[maxidx] = ti;

            String ts = names[i];
            names[i] = names[maxidx];
            names[maxidx] = ts;
        }
    }
}

도전 — 메뉴 “5) 학생 추가”를 만들어보라

Q1. 배열 길이가 고정이라 어떤 불편이 생겼나요?

배열 길이가 고정이면, 시스템에서 그 길이를 초과해서 요소가 생겨 추가해야 하는 일이 발생 했을 때 배열을 늘릴 수 없기에 불편함이 생긴다.

Q2. 두 배열(names, scores)을 동시에 관리하는 게 왜 위험한가요?

만약 배열을 정렬하려고 할 때 실수로 한 곳만 정렬하고 나머지를 정렬하지 않았을 경우 데이터에 큰 오류가 생긴다.

또한, 데이터를 수정할 때 하나가 아닌 두 개의 배열을 각각 수정해야 하는 것이 불편할 수 있다.

Q3. 만약 “Student”라는 새 타입이 있다면 어떻게 달라질까요?

배열을 사용하지 않고 Student라는 클래스 객체를 사용한다면 새로운 학생이 추가되거나 제거해야할 때 객체만 제거하면 되기 때문에 관리가 매우 편할 것 같다.




© 2017. by isme2n

Powered by aiden