[Java] Java 03 Arrays

- Period 01 — 변수 100개 만들 거야?
- Period 02 — 1등은 누구?
- Period 03 — 표를 만들자
- Period 04 — 같은 코드를 두 번 쓰지 마라
- Period 05 — 같은 이름, 다른 일
- Period 06 — 메서드 호출의 비밀
- Period 07 — 종합 실습 (학생 성적 관리 v0)
Period 01 — 변수 100개 만들 거야?
- 변수 여러 개로 다루기 어려운 상황을 알고, 배열이 왜 필요한지 설명할 수 있다.
int[] arr = new int[5];로 배열을 선언 • 초기화하고arr.length를 활용할 수 있다.- for 루프와 인덱스로 배열을 순화하면서 합계 • 평균을 계산할 수 있다.
- 인덱스가 왜
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번째 요소’를 의미함. 이 규칙을 착각하면 프로그램의 논리 구조가 통째로 뒤틀리게 됨.
- 일상생활에서 인간은 숫자를 1부터 세기 시작하지만, 자바를 포함한 대부분의 프로그래밍 언어에서 배열의 인덱스는 무조건
- 🧱 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에러를 던지며 프로그램을 강제 종료시킴.
- 만약 크기가 5인 배열(
실습 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등은 누구?
- 배열을 한 번 순회하며 최댓값을 찾는 알고리즘을 구현할 수 있다.
- 값과 인덱스를 동시에 추적해
String[]•int[]평행 배열에서 “1등의 이름”을 출력할 수 있다. - 왜 “1바퀴 비교”로는 정렬이 끝나지 않는지, 중첩 반복이 왜 필요한지 설명할 수 있다.
- 선택 정렬(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차원으로는 표현하기 불편한 데이터(표, 격자)에 2차원 배열을 적용해야 함을 안다.
int[][] m = new int[3][4];를 선언하고 중첩 for로 순회할 수 있다.m.length와m[0].length의 차이를 메모리 그림으로 설명할 수 있다.- 행마다 길이가 다른 가변 길이 2차원 배열을 만들고 활용할 수 있다.
학생 3명, 과목 3개 — 1차원으로 다루면?
- ⚠️ 1차원 배열로 다중 데이터를 다룰 때의 한계와 비효율:
- 학생 3명의 국어, 영어, 수학 점수(총 9개의 데이터)를 하나의 1차원 배열(
int[] scores = new int[9];)에 몰아서 저장할 수는 있음. - 하지만 데이터가 한 줄로 길게 늘어서기 때문에 “2번째 학생의 영어 점수”처럼 특정 위치의 데이터를 직관적으로 찾아내기가 매우 까다로워짐.
- 학생 3명의 국어, 영어, 수학 점수(총 9개의 데이터)를 하나의 1차원 배열(
- 🧱 인덱스 계산 공식의 강제화 (물리적 주소 연산):
- 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) 데이터가 연속적인 칸으로 저장되는 물리적인 가로 공간임.
- Stack 영역: 2차원 배열 변수(예:
- ⚡
.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 — 같은 코드를 두 번 쓰지 마라
- 메서드가 왜 필요한지 — 중복 제거와 의미 단위 분리의 가치를 설명할 수 있다.
static 반환타입 이름(매개변수)구조로 메서드를 정의 • 호출할 수 있다.- 반환 타입과
return이 어긋날 때 컴파일러가 거절하는 이유를 안다. - 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)가 발생한다.
- 메서드 선언부에 지정한 반환 타입(Return Type)과 메서드 바디 내부에서
- 🛑 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 — 같은 이름, 다른 일
parameter(정의)와argument(호출)의 차이를 구분할 수 있다.- 같은 이름의 메서드를 매개변수 개수 • 타입을 달리해 여러 개 작성할 수 있다 (오버로딩)
- 컴파일러가
ambiguous오류를 내는 상황을 직접 만들고 원인을 설명할 수 있다. - 오버로딩을 언제 쓰고 언제 피할지 판단할 수 있다 (가독성 우선).
매개변수(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)을 호출하면 정수형 데이터10과20이 양쪽 메서드의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 메서드를 모두 작성하세요:
- int max(int a, int b)
- int max(int a, int b, int c)
- 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장의 Stack/Heap 메모리 모델을 다시 그려, 6교시의 토대로 활용한다.
- 기본형 매개변수가 메서드 안에서 변경되어도 밖에 영향 없음을 메모리로 설명할 수 있다.
- 배열을 인자로 넘기면 원소가 바뀌는 이유를 “참조값의 복사”로 설명할 수 있다.
- 메서드 안에서
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)
- 요구사항을 읽고 필요한 메서드들을 식별 • 설계할 수 있다.
- 2장 메뉴 루프 + 1~6교시 메서드를 결합해 동작하는 콘솔 앱을 완성한다.
- “학생 1명 추가” 같은 요구사항이 왜 배열만으로는 불편한지 직접 체험 • 논증할 수 있다.
- 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라는 클래스 객체를 사용한다면 새로운 학생이 추가되거나 제거해야할 때 객체만 제거하면 되기 때문에 관리가 매우 편할 것 같다.
