Java 프로그램은 기본적으로 main() 메서드의 시작 중괄호부터 끝 중괄호까지 위에서 아래로 한 줄씩 실행된다.
public class Main {
public static void main(String[] args) {
System.out.println("첫 번째 실행");
System.out.println("두 번째 실행");
System.out.println("세 번째 실행");
}
}
하지만 실제 프로그램은 언제나 모든 코드를 같은 순서로 실행하지 않는다.
사용자의 나이에 따라 다른 문장을 출력하거나, 로그인 여부에 따라 접근을 허용해야 할 수 있다. 상품 목록을 모두 출력하려면 같은 작업을 여러 번 반복해야 한다.
이처럼 프로그램의 실행 순서를 조건에 따라 나누거나 특정 코드를 반복하도록 만드는 문법을 제어문이라고 한다.
Java의 제어문은 크게 다음과 같이 구분할 수 있다.
제어문
├─ 조건문
│ ├─ if
│ └─ switch
│
└─ 반복문
├─ for
├─ while
└─ do-while
반복문이나 switch의 실행을 제어하기 위해 break와 continue도 함께 사용한다.
조건문은 조건식의 결과에 따라 서로 다른 코드를 실행한다.
Java의 조건식에는 반드시 true 또는 false를 결과로 만드는 표현식이나 boolean 타입 변수가 와야 한다.
int age = 20;
boolean isAdult = age >= 19;
if (isAdult) {
System.out.println("성인입니다.");
}
Java에서는 숫자 0이나 1을 조건식으로 사용할 수 없다.
int number = 1;
// if (number) {
// 컴파일 오류
// }
C나 JavaScript와 달리 Java의 if 조건에는 반드시 boolean 결과가 필요하다.
if문은 조건식이 true일 때 중괄호 내부의 실행문을 실행한다.
if (조건식) {
조건식이 true일 때 실행할 코드
}
예를 들어 사용자의 점수가 60점 이상일 때 합격을 출력할 수 있다.
public class Main {
public static void main(String[] args) {
int score = 75;
if (score >= 60) {
System.out.println("합격입니다.");
}
System.out.println("프로그램을 종료합니다.");
}
}
score >= 60이 true이므로 "합격입니다."가 출력된다.
실행문이 한 줄뿐이라면 중괄호를 생략할 수 있다.
if (score >= 60)
System.out.println("합격입니다.");
그러나 코드가 추가될 때 실수하기 쉽기 때문에 중괄호를 항상 작성하는 것이 안전하다.
if (score >= 60) {
System.out.println("합격입니다.");
}
다음 코드는 들여쓰기만 보면 두 문장이 모두 조건에 포함된 것 같지만, 실제로는 첫 번째 문장만 if문의 영향을 받는다.
if (score >= 60)
System.out.println("합격입니다.");
System.out.println("축하합니다.");
"축하합니다."는 조건과 관계없이 항상 실행된다.
들여쓰기는 사람이 코드를 읽기 쉽게 만들 뿐, Java의 실행 범위를 결정하지 않는다. 실행 범위는 중괄호가 결정한다.
조건식이 true인 경우와 false인 경우에 각각 다른 코드를 실행하려면 else를 사용한다.
if (조건식) {
조건식이 true일 때 실행
} else {
조건식이 false일 때 실행
}
public class Main {
public static void main(String[] args) {
int score = 55;
if (score >= 60) {
System.out.println("합격입니다.");
} else {
System.out.println("불합격입니다.");
}
}
}
두 블록 중 하나만 실행된다.
조건이 여러 개라면 else if를 사용한다.
if (조건식1) {
실행문 A
} else if (조건식2) {
실행문 B
} else {
실행문 C
}
점수에 따라 등급을 출력하는 프로그램을 만들어보자.
public class Main {
public static void main(String[] args) {
int score = 85;
char grade;
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else if (score >= 60) {
grade = 'D';
} else {
grade = 'F';
}
System.out.println("학점: " + grade);
}
}
조건은 위에서부터 순서대로 검사하며, 처음으로 true가 된 블록만 실행한다.
따라서 조건의 순서가 매우 중요하다.
int score = 95;
if (score >= 60) {
System.out.println("D 이상");
} else if (score >= 90) {
System.out.println("A");
}
95는 첫 번째 조건인 score >= 60을 만족하므로 두 번째 조건은 검사하지 않는다.
범위를 판별하는 조건은 일반적으로 큰 값부터 작은 값 순서로 작성한다.
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
}
if문 내부에 또 다른 if문을 작성할 수 있다.
public class Main {
public static void main(String[] args) {
int age = 20;
boolean hasTicket = true;
if (age >= 19) {
if (hasTicket) {
System.out.println("입장할 수 있습니다.");
} else {
System.out.println("티켓이 필요합니다.");
}
} else {
System.out.println("성인만 입장할 수 있습니다.");
}
}
}
다만 중첩이 너무 깊어지면 코드를 이해하기 어려워진다.
다음과 같이 논리 연산자를 활용해 조건을 합칠 수 있다.
if (age >= 19 && hasTicket) {
System.out.println("입장할 수 있습니다.");
}
switch문은 하나의 변수나 표현식의 결과를 여러 값과 비교할 때 사용한다.
switch (변수 또는 표현식) {
case 값1:
실행문 A;
break;
case 값2:
실행문 B;
break;
default:
실행문 C;
}
요일 번호를 입력받아 요일을 출력하는 예제를 살펴보자.
public class Main {
public static void main(String[] args) {
int day = 3;
switch (day) {
case 1:
System.out.println("월요일");
break;
case 2:
System.out.println("화요일");
break;
case 3:
System.out.println("수요일");
break;
case 4:
System.out.println("목요일");
break;
case 5:
System.out.println("금요일");
break;
default:
System.out.println("주말입니다.");
}
}
}
기존 형태의 switch문에서 break는 현재 switch문을 종료한다.
break를 작성하지 않으면 일치하는 case 이후의 코드까지 계속 실행된다. 이를 fall-through라고 한다.
public class Main {
public static void main(String[] args) {
int number = 1;
switch (number) {
case 1:
System.out.println("첫 번째");
case 2:
System.out.println("두 번째");
case 3:
System.out.println("세 번째");
}
}
}
실행 결과는 다음과 같다.
첫 번째
두 번째
세 번째
의도적으로 여러 case를 함께 처리하는 경우가 아니라면 break를 빠뜨리지 않아야 한다.
기존 switch문에서는 여러 case를 이어 작성할 수 있다.
public class Main {
public static void main(String[] args) {
int month = 2;
switch (month) {
case 12:
case 1:
case 2:
System.out.println("겨울");
break;
case 3:
case 4:
case 5:
System.out.println("봄");
break;
case 6:
case 7:
case 8:
System.out.println("여름");
break;
default:
System.out.println("가을");
}
}
}
최근 Java에서는 화살표 ->를 사용하는 switch 표현식을 사용할 수 있다.
switch 표현식은 다음과 같은 장점이 있다.
switch 자체가 값을 반환할 수 있다.case만 실행하므로 break가 필요 없다.public class Main {
public static void main(String[] args) {
int score = 85;
char grade = switch (score / 10) {
case 10, 9 -> 'A';
case 8 -> 'B';
case 7 -> 'C';
case 6 -> 'D';
default -> 'F';
};
System.out.println("학점: " + grade);
}
}
화살표 형태의 switch는 Java 14부터 정식 문법으로 사용할 수 있다.
한 case에서 여러 문장을 실행하고 값을 반환하려면 중괄호와 yield를 사용한다.
public class Main {
public static void main(String[] args) {
String membership = "GOLD";
int discountRate = switch (membership) {
case "VIP" -> {
System.out.println("VIP 회원입니다.");
yield 20;
}
case "GOLD" -> {
System.out.println("GOLD 회원입니다.");
yield 10;
}
default -> {
System.out.println("일반 회원입니다.");
yield 0;
}
};
System.out.println("할인율: " + discountRate + "%");
}
}
yield는 switch 표현식의 결과값을 반환한다.
메서드에서 사용하는 return과 비슷해 보이지만, yield는 현재 switch 표현식의 값을 결정한다.
| 구분 | if문 | switch문 |
|---|---|---|
| 주요 용도 | 범위와 복합 조건 검사 | 하나의 값에 따른 분기 |
| 조건 표현 | 비교·논리 연산 가능 | 일치하는 값 중심 |
| 범위 조건 | 적합 | 일반적으로 부적합 |
| 복합 조건 | 적합 | 상대적으로 제한적 |
| 가독성 | 조건이 적으면 좋음 | 경우의 수가 많을 때 좋음 |
다음과 같은 범위 조건에는 if가 적절하다.
if (score >= 90) {
System.out.println("A");
}
다음과 같이 값이 명확히 나뉘는 경우에는 switch가 적절하다.
switch (menuNumber) {
case 1:
System.out.println("회원 조회");
break;
case 2:
System.out.println("회원 등록");
break;
}
반복문은 조건을 만족하는 동안 특정 코드를 반복 실행한다.
Java의 대표적인 반복문은 다음과 같다.
forwhiledo-while일반적으로 반복 횟수를 알고 있다면 for, 특정 조건이 만족될 때까지 반복한다면 while을 사용한다.
for문은 초기화식, 조건식, 증감식으로 구성된다.
for (초기화식; 조건식; 증감식) {
반복할 실행문
}
실행 순서는 다음과 같다.
1. 초기화식
2. 조건식 검사
3. 실행문 실행
4. 증감식 실행
5. 다시 조건식 검사
1부터 5까지 출력하는 예제를 살펴보자.
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
}
}
int i = 1은 반복이 시작될 때 한 번만 실행된다.
i <= 5가 true인 동안 반복하며, 한 번 반복할 때마다 i++가 실행된다.
public class Main {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println("합계: " + sum);
}
}
for문의 초기화식에서 선언한 변수는 for문 내부에서만 사용할 수 있다.
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
// System.out.println(i);
// 컴파일 오류
반복이 끝난 뒤에도 변수가 필요하다면 반복문 밖에서 선언해야 한다.
int i;
for (i = 0; i < 5; i++) {
System.out.println(i);
}
System.out.println("반복 종료 후 i: " + i);
반복문 내부에 또 다른 반복문을 작성할 수 있다.
구구단을 출력하는 예제를 살펴보자.
public class Main {
public static void main(String[] args) {
for (int dan = 2; dan <= 9; dan++) {
System.out.println("[" + dan + "단]");
for (int number = 1; number <= 9; number++) {
System.out.printf(
"%d × %d = %d%n",
dan,
number,
dan * number
);
}
System.out.println();
}
}
}
바깥쪽 반복문이 한 번 실행될 때마다 안쪽 반복문은 처음부터 끝까지 실행된다.
while문은 조건식이 true인 동안 반복한다.
while (조건식) {
실행문
}
public class Main {
public static void main(String[] args) {
int number = 1;
while (number <= 5) {
System.out.println(number);
number++;
}
}
}
while문에서는 증감식을 직접 작성해야 한다.
number++;
증감이나 조건을 변경하는 코드를 빠뜨리면 무한 반복이 발생할 수 있다.
int number = 1;
while (number <= 5) {
System.out.println(number);
// number가 변하지 않으므로 무한 반복
}
반복 횟수를 정확히 모르고 특정 조건이 만족될 때까지 반복해야 할 때 유용하다.
사용자가 0을 입력할 때까지 숫자를 계속 더하는 예제다.
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int sum = 0;
while (true) {
System.out.print("숫자를 입력하세요. 종료는 0: ");
int number = Integer.parseInt(scanner.nextLine());
if (number == 0) {
break;
}
sum += number;
}
System.out.println("입력한 숫자의 합: " + sum);
scanner.close();
}
}
do-while문은 실행문을 먼저 한 번 실행한 뒤 조건을 검사한다.
do {
실행문
} while (조건식);
조건이 처음부터 false여도 실행문이 최소 한 번 실행된다.
public class Main {
public static void main(String[] args) {
int number = 10;
do {
System.out.println(number);
number++;
} while (number < 5);
}
}
조건 number < 5는 처음부터 false지만 10이 한 번 출력된다.
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int menu;
do {
System.out.println("1. 조회");
System.out.println("2. 등록");
System.out.println("0. 종료");
System.out.print("메뉴 선택: ");
menu = Integer.parseInt(scanner.nextLine());
if (menu < 0 || menu > 2) {
System.out.println("올바른 메뉴를 입력하세요.");
}
} while (menu < 0 || menu > 2);
System.out.println("선택한 메뉴: " + menu);
scanner.close();
}
}
입력을 반드시 한 번 받아야 하므로 do-while문이 잘 어울린다.
| 반복문 | 특징 | 적합한 상황 |
|---|---|---|
for | 초기화·조건·증감이 한곳에 있음 | 반복 횟수를 알고 있을 때 |
while | 조건을 먼저 검사 | 종료 시점을 조건으로 판단할 때 |
do-while | 실행 후 조건 검사 | 최소 한 번 실행해야 할 때 |
동일한 작업을 어떤 반복문으로도 구현할 수 있지만, 상황에 가장 자연스러운 문법을 선택하는 것이 좋다.
break는 반복문이나 switch문의 실행을 즉시 종료한다.
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
if (i == 5) {
break;
}
System.out.println(i);
}
}
}
실행 결과는 다음과 같다.
1
2
3
4
i가 5가 되는 순간 반복문이 종료된다.
continue는 현재 반복에서 남은 코드를 건너뛰고 다음 반복으로 이동한다.
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue;
}
System.out.println(i);
}
}
}
짝수일 때 출력을 건너뛰므로 홀수만 출력된다.
1
3
5
7
9
break
→ 반복문 전체를 종료
continue
→ 현재 반복만 건너뛰고 다음 반복 진행
| 구분 | break | continue |
|---|---|---|
| 현재 반복 | 중단 | 중단 |
| 다음 반복 | 실행하지 않음 | 계속 실행 |
| 반복문 전체 | 종료 | 유지 |
반복문이 중첩된 상태에서 일반적인 break를 사용하면 가장 가까운 반복문 하나만 종료한다.
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) {
break;
}
System.out.println("i = " + i + ", j = " + j);
}
}
}
}
바깥쪽 반복문까지 한 번에 종료하려면 라벨을 사용할 수 있다.
public class Main {
public static void main(String[] args) {
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i == 2 && j == 2) {
break outer;
}
System.out.println("i = " + i + ", j = " + j);
}
}
System.out.println("반복 종료");
}
}
라벨은 메모에 적힌 다음 형태처럼 작성한다.
outer:
for (...) {
for (...) {
break outer;
}
}
라벨을 과도하게 사용하면 코드 흐름을 추적하기 어려워질 수 있다. 가능하면 메서드 분리나 조건 재구성을 먼저 고려하는 것이 좋다.
조건문과 반복문을 함께 활용해 간단한 회원 관리 메뉴를 만들어보자.
import java.util.Scanner;
public class MemberMenu {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println();
System.out.println("===== 회원 관리 =====");
System.out.println("1. 회원 조회");
System.out.println("2. 회원 등록");
System.out.println("3. 회원 삭제");
System.out.println("0. 종료");
System.out.print("메뉴를 선택하세요: ");
int menu = Integer.parseInt(scanner.nextLine());
switch (menu) {
case 1:
System.out.println("회원 정보를 조회합니다.");
break;
case 2:
System.out.println("새 회원을 등록합니다.");
break;
case 3:
System.out.println("회원을 삭제합니다.");
break;
case 0:
System.out.println("프로그램을 종료합니다.");
scanner.close();
return;
default:
System.out.println("올바른 메뉴 번호를 입력하세요.");
}
}
}
}
이 예제에서는 다음 개념이 사용되었다.
while (true)를 이용한 반복 메뉴switch를 이용한 메뉴 분기return을 이용한 프로그램 종료Scanner를 이용한 입력Integer.parseInt()를 이용한 문자열 변환int number = 1;
// if (number) {
// 사용할 수 없음
// }
if (number == 1) {
System.out.println("1입니다.");
}
=과 ==는 다르다int number = 10; // 대입
boolean result = number == 10; // 비교
break와 반복문의 break두 경우 모두 현재 제어 구조를 종료하지만 종료 대상이 다르다.
switch 안의 break: 해당 switch 종료break: 가장 가까운 반복문 종료while과 do-whilewhile (조건식) {
실행문
}
while은 조건을 먼저 검사한다.
do {
실행문
} while (조건식);
do-while은 실행문을 먼저 실행하므로 최소 한 번 실행된다.
조건문이 지나치게 중첩되면 읽기 어렵기 때문에, 실패 조건을 먼저 처리하고 메서드를 종료하는 가드 클로즈 방식을 활용할 수 있다.
public static void registerUser(String name, int age) {
if (name == null || name.isBlank()) {
System.out.println("이름이 올바르지 않습니다.");
return;
}
if (age < 0) {
System.out.println("나이가 올바르지 않습니다.");
return;
}
System.out.println("회원을 등록합니다.");
}
다음과 같이 여러 단계로 중첩하는 것보다 읽기 쉽다.
if (name != null) {
if (!name.isBlank()) {
if (age >= 0) {
System.out.println("회원을 등록합니다.");
}
}
}
반복문 내부에서 컬렉션을 순회할 때 요소를 직접 삭제하면 오류가 발생할 수 있으므로, 이후 컬렉션을 학습할 때 Iterator나 removeIf() 같은 방법도 함께 알아두는 것이 좋다.
if문과 switch문이 있다.if문의 조건식은 반드시 boolean 결과를 만들어야 한다.else if 조건은 위에서부터 순서대로 검사한다.switch문이 유용하다.break가 필요 없다.yield로 값을 반환한다.for문이 적합하다.while문이 적합하다.do-while문은 조건과 관계없이 최소 한 번 실행된다.break는 반복문 전체를 종료한다.continue는 현재 반복만 건너뛰고 다음 반복을 진행한다.학생 30명의 점수를 저장해야 한다고 가정해보자.
배열을 사용하지 않는다면 다음과 같이 변수를 여러 개 선언해야 한다.
int score1 = 90;
int score2 = 80;
int score3 = 100;
데이터가 30개, 100개, 1,000개로 늘어나면 각각의 변수를 직접 선언하고 처리하는 방식은 현실적이지 않다.
이 문제를 해결하기 위해 같은 타입의 데이터를 하나로 묶어 관리하는 자료구조가 배열이다.
int[] scores = {90, 80, 100};
하지만 배열을 제대로 이해하려면 먼저 Java의 기본 타입과 참조 타입, 그리고 Stack과 Heap 메모리의 관계를 알아야 한다.
Java의 데이터 타입은 기본 타입과 참조 타입으로 구분된다.
기본 타입 변수는 정수, 실수, 문자, 논리값을 직접 저장한다.
int age = 20;
double height = 175.5;
char grade = 'A';
boolean isStudent = true;
Java의 기본 타입은 다음 8개다.
byte
short
int
long
float
double
char
boolean
참조 타입 변수는 객체 자체가 아니라 객체를 참조하는 값을 저장한다.
대표적인 참조 타입은 다음과 같다.
StringString name = "김자바";
int[] scores = {90, 80, 100};
개념적으로 다음과 같이 이해할 수 있다.
Stack 영역 Heap 영역
name ───────────→ "김자바"
scores ───────────→ [90][80][100]
참조 타입 변수에는 객체를 찾아갈 수 있는 참조값이 저장된다.
실제 JVM 구현에서 참조값이 반드시 사람이 볼 수 있는 메모리 주소 그 자체라고 단정할 수는 없다. 입문 단계에서는 객체의 위치를 찾아가기 위한 값이라고 이해하면 된다.
Java 프로그램이 실행되면 JVM은 목적에 따라 메모리를 여러 영역으로 나누어 사용한다.
입문 단계에서 중요한 영역은 다음 세 가지다.
JVM 메모리
├─ Method Area
├─ Heap
└─ JVM Stack
메서드 영역에는 JVM이 읽은 클래스 관련 정보가 저장된다.
대표적으로 다음과 같은 정보가 포함된다.
정확한 내부 구현은 JVM 버전과 구현체에 따라 달라질 수 있지만, 클래스 단위 정보가 관리되는 영역이라고 이해할 수 있다.
Heap에는 new 연산자 등을 통해 생성된 객체와 배열이 저장된다.
int[] scores = new int[3];
String name = new String("Java");
위 코드에서 배열과 String 객체는 Heap에 생성된다.
더 이상 어떤 변수에서도 참조하지 않는 객체는 Garbage Collector의 정리 대상이 된다.
메서드가 호출될 때마다 Stack에 프레임이 생성된다.
프레임에는 해당 메서드에서 사용하는 로컬 변수와 연산 중간값 등이 저장된다.
public static void main(String[] args) {
int age = 20;
int[] scores = new int[3];
}
개념적으로 다음과 같이 표현할 수 있다.
JVM Stack Heap
main() 프레임
┌────────────────┐
│ age = 20 │
│ scores = 참조값 │ ───────────→ [0][0][0]
└────────────────┘
메서드 실행이 끝나면 해당 프레임도 Stack에서 제거된다.
기본 타입에 ==를 사용하면 값을 비교한다.
int first = 10;
int second = 10;
System.out.println(first == second);
참조 타입에 ==를 사용하면 두 변수가 같은 객체를 참조하는지 비교한다.
int[] first = {1, 2, 3};
int[] second = first;
int[] third = {1, 2, 3};
System.out.println(first == second);
System.out.println(first == third);
실행 결과는 다음과 같다.
true
false
first와 second는 같은 배열을 참조하지만, third는 내용이 같더라도 별도로 생성된 배열을 참조한다.
first ──┐
├──→ [1][2][3]
second ──┘
third ──────→ [1][2][3]
참조 타입 변수는 객체를 참조하지 않는 상태를 나타내기 위해 null을 가질 수 있다.
String name = null;
int[] scores = null;
변수가 null인지 == 또는 !=로 확인할 수 있다.
if (name == null) {
System.out.println("이름이 없습니다.");
}
null 상태인 참조 변수를 통해 객체의 필드, 메서드 또는 배열 요소에 접근하면 NullPointerException이 발생한다.
public class Main {
public static void main(String[] args) {
String text = null;
System.out.println(text.length());
}
}
text가 참조하는 String 객체가 없기 때문에 length()를 호출할 수 없다.
배열도 마찬가지다.
int[] numbers = null;
numbers[0] = 10;
배열 객체가 생성되지 않았기 때문에 요소에 접근할 수 없다.
다음처럼 객체가 존재하는지 먼저 확인해야 한다.
if (text != null) {
System.out.println(text.length());
}
문자열 리터럴이 동일하면 Java는 문자열 상수 풀에 있는 같은 객체를 재사용할 수 있다.
String first = "Java";
String second = "Java";
System.out.println(first == second);
이 경우 true가 나올 가능성이 아니라, 동일한 문자열 리터럴은 일반적으로 같은 문자열 풀 객체를 참조하므로 true가 나온다.
반면 new 연산자로 생성하면 새로운 객체가 만들어진다.
String first = new String("Java");
String second = new String("Java");
System.out.println(first == second);
실행 결과는 false다.
문자열의 내용은 equals()로 비교해야 한다.
System.out.println(first.equals(second));
결과는 true다.
문자열 비교에
==를 사용하면 생성 방식에 따라 결과가 달라질 수 있으므로 내용 비교에는 항상equals()를 사용해야 한다.
배열은 같은 타입의 여러 데이터를 하나의 연속된 구조로 관리하는 자료구조다.
각 데이터에는 0부터 시작하는 인덱스가 부여된다.
scores
index 0 1 2
┌────┬────┬─────┐
value │ 90 │ 80 │ 100 │
└────┴────┴─────┘
Java 배열의 특징은 다음과 같다.
배열 변수는 다음 두 방식으로 선언할 수 있다.
int[] scores;
int scores2[];
두 문법 모두 가능하지만 Java에서는 타입 뒤에 대괄호를 작성하는 첫 번째 방식을 권장한다.
int[] scores;
이 방식은 변수의 타입이 int 배열이라는 점을 더 분명히 보여준다.
int number; // int 타입
int[] numbers; // int[] 타입
배열 변수를 선언하는 것만으로 배열 객체가 생성되는 것은 아니다.
int[] numbers = null;
이 상태에서는 사용할 배열이 존재하지 않는다.
배열을 선언하면서 값 목록을 제공할 수 있다.
int[] scores = {90, 80, 100};
String[] names = {"김자바", "이자바", "박자바"};
배열 길이는 입력한 값의 개수에 따라 결정된다.
System.out.println(scores.length);
결과는 3이다.
다음 코드는 컴파일 오류가 발생한다.
int[] scores;
// scores = {90, 80, 100};
// 컴파일 오류
중괄호만 사용하는 값 목록 방식은 배열 변수를 선언하는 문장에서만 사용할 수 있다.
나중에 값을 결정해야 한다면 new를 명시해야 한다.
int[] scores;
scores = new int[]{90, 80, 100};
배열의 길이만 알고 있고 값은 나중에 저장하려면 new 연산자를 사용한다.
int[] scores = new int[3];
기본 형식은 다음과 같다.
타입[] 변수 = new 타입[길이];
이미 변수를 선언했다면 생성문을 분리할 수 있다.
int[] scores;
scores = new int[3];
new로 배열을 생성하면 각 요소는 타입에 맞는 기본값으로 자동 초기화된다.
| 요소 타입 | 기본값 |
|---|---|
byte, short, int, long | 0 |
float, double | 0.0 |
char | '\u0000' |
boolean | false |
| 참조 타입 | null |
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] numbers = new int[3];
boolean[] results = new boolean[3];
String[] names = new String[3];
System.out.println(Arrays.toString(numbers));
System.out.println(Arrays.toString(results));
System.out.println(Arrays.toString(names));
}
}
실행 결과는 다음과 같다.
[0, 0, 0]
[false, false, false]
[null, null, null]
배열 요소는 인덱스를 이용해 읽거나 변경한다.
int[] scores = new int[3];
scores[0] = 90;
scores[1] = 80;
scores[2] = 100;
System.out.println(scores[0]);
배열의 첫 번째 요소는 인덱스 0이다.
첫 번째 요소 → 0
두 번째 요소 → 1
세 번째 요소 → 2
배열의 범위를 벗어난 인덱스에 접근하면 ArrayIndexOutOfBoundsException이 발생한다.
int[] scores = new int[3];
scores[3] = 100;
길이가 3인 배열에서 사용할 수 있는 인덱스는 0, 1, 2다.
마지막 인덱스는 다음과 같이 구할 수 있다.
int lastIndex = scores.length - 1;
배열의 길이는 length 필드로 확인한다.
int[] scores = {90, 80, 100};
System.out.println(scores.length);
String의 길이는 메서드이므로 괄호를 사용하지만, 배열의 length는 필드이므로 괄호가 없다.
String text = "Java";
System.out.println(text.length());
System.out.println(scores.length);
length()length배열의 모든 요소를 처리할 때는 for문과 length를 함께 사용한다.
public class Main {
public static void main(String[] args) {
int[] scores = {90, 80, 100};
for (int i = 0; i < scores.length; i++) {
System.out.printf(
"%d번째 점수: %d%n",
i,
scores[i]
);
}
}
}
조건식에는 배열의 마지막 인덱스가 아니라 길이를 사용한다.
i < scores.length
<=를 사용하면 범위를 벗어난 인덱스에 접근하게 된다.
// i <= scores.length 사용 금지
public class Main {
public static void main(String[] args) {
int[] scores = {90, 80, 100, 70, 85};
int sum = 0;
for (int i = 0; i < scores.length; i++) {
sum += scores[i];
}
double average = (double) sum / scores.length;
System.out.println("합계: " + sum);
System.out.println("평균: " + average);
}
}
배열 길이를 5처럼 직접 작성하지 않고 scores.length를 사용하면 배열 크기가 변경되어도 코드를 수정할 필요가 없다.
향상된 for문은 배열이나 컬렉션의 요소를 처음부터 끝까지 순서대로 읽을 때 사용한다.
for (요소타입 변수 : 배열) {
실행문
}
public class Main {
public static void main(String[] args) {
int[] scores = {90, 80, 100};
for (int score : scores) {
System.out.println(score);
}
}
}
각 반복마다 배열 요소가 score 변수에 대입된다.
향상된 for문에서는 현재 요소의 인덱스를 직접 알 수 없다.
for (int score : scores) {
System.out.println(score);
}
인덱스가 필요하다면 일반 for문을 사용한다.
for (int i = 0; i < scores.length; i++) {
System.out.println(i + ": " + scores[i]);
}
또한 기본 타입 배열을 향상된 for문으로 순회하며 반복 변수 값을 변경해도 원본 배열은 바뀌지 않는다.
public class Main {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
for (int number : numbers) {
number = number * 10;
}
for (int number : numbers) {
System.out.println(number);
}
}
}
출력은 여전히 1, 2, 3이다.
원본 요소를 변경하려면 인덱스로 접근해야 한다.
for (int i = 0; i < numbers.length; i++) {
numbers[i] *= 10;
}
배열 변수를 그대로 출력하면 사람이 읽기 어려운 문자열이 나온다.
int[] numbers = {1, 2, 3};
System.out.println(numbers);
배열 내용을 간단히 확인하려면 Arrays.toString()을 사용한다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
System.out.println(Arrays.toString(numbers));
}
}
실행 결과는 다음과 같다.
[1, 2, 3]
Arrays 클래스를 사용하려면 import가 필요하다.
import java.util.Arrays;
메모에 있는 "1234567890" 문자열 예제를 완성해보자.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String original = "1234567890";
char[] characters = new char[original.length()];
int[] numbers = new int[original.length()];
for (int i = 0; i < original.length(); i++) {
characters[i] = original.charAt(i);
numbers[i] = characters[i] - '0';
}
System.out.println(Arrays.toString(characters));
System.out.println(Arrays.toString(numbers));
}
}
실행 결과는 다음과 같다.
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
characters의 요소는 문자이고 numbers의 요소는 숫자다.
numbers[i] = characters[i] - '0';
문자 '0'부터 '9'까지는 유니코드상 연속된 값을 가지므로 '0'을 빼면 실제 숫자를 얻을 수 있다.
'3' - '0'
→ 51 - 48
→ 3
문자열을 문자 배열로 바꾸는 작업은 API를 사용해 간단히 처리할 수도 있다.
char[] characters = original.toCharArray();
Java 배열은 한 번 생성되면 길이를 늘리거나 줄일 수 없다.
int[] numbers = new int[3];
각 요소의 값은 변경할 수 있다.
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
그러나 네 번째 요소를 추가할 수는 없다.
// numbers[3] = 40;
// 범위 초과
더 큰 배열이 필요하다면 새로운 배열을 만들고 기존 값을 복사해야 한다.
int[] oldNumbers = {10, 20, 30};
int[] newNumbers = new int[5];
for (int i = 0; i < oldNumbers.length; i++) {
newNumbers[i] = oldNumbers[i];
}
크기가 자주 변해야 한다면 배열보다 ArrayList가 더 적합하다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] original = {10, 20, 30};
int[] copied = new int[5];
for (int i = 0; i < original.length; i++) {
copied[i] = original[i];
}
System.out.println(Arrays.toString(copied));
}
}
실행 결과는 다음과 같다.
[10, 20, 30, 0, 0]
Java가 제공하는 System.arraycopy()를 사용하면 배열의 일부 또는 전체를 복사할 수 있다.
System.arraycopy(
원본배열,
원본시작인덱스,
대상배열,
대상시작인덱스,
복사할요소개수
);
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] original = {"Java", "Spring", "Vue"};
String[] copied = new String[5];
System.arraycopy(
original,
0,
copied,
0,
original.length
);
System.out.println(Arrays.toString(copied));
}
}
실행 결과는 다음과 같다.
[Java, Spring, Vue, null, null]
메서드 이름은 arrayCopy가 아니라 소문자 c를 사용한 다음 형태다.
System.arraycopy()
배열 전체를 새로운 길이로 복사할 때는 Arrays.copyOf()가 간결하다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] original = {10, 20, 30};
int[] copied = Arrays.copyOf(original, 5);
System.out.println(Arrays.toString(copied));
}
}
실행 결과는 다음과 같다.
[10, 20, 30, 0, 0]
두 번째 인수는 새 배열의 길이다.
Arrays.copyOf(원본배열, 새로운길이);
참조 타입 배열을 복사할 때는 배열 요소가 참조하는 객체를 새로 만드는 것이 아니라 참조값이 복사된다.
public class Main {
public static void main(String[] args) {
StringBuilder[] original = {
new StringBuilder("Java")
};
StringBuilder[] copied = new StringBuilder[1];
System.arraycopy(original, 0, copied, 0, 1);
copied[0].append(" Programming");
System.out.println(original[0]);
System.out.println(copied[0]);
}
}
실행 결과는 두 배열 모두 다음 문자열을 출력한다.
Java Programming
배열 객체는 서로 다르지만 내부 요소가 같은 StringBuilder 객체를 참조하기 때문이다.
original ──→ [참조값] ──┐
├─→ StringBuilder 객체
copied ──→ [참조값] ──┘
이와 같이 객체 자체가 아닌 참조값만 복사하는 방식을 얕은 복사라고 한다.
Java의 다차원 배열은 배열이 다른 배열을 참조하는 구조다.
2차원 배열은 다음과 같이 선언하고 생성한다.
int[][] scores = new int[2][3];
첫 번째 길이는 행, 두 번째 길이는 열로 이해할 수 있다.
scores
├─ scores[0] → [0][0][0]
└─ scores[1] → [0][0][0]
값을 저장할 때는 두 개의 인덱스를 사용한다.
scores[0][0] = 90;
scores[0][1] = 80;
scores[0][2] = 100;
scores[1][0] = 70;
scores[1][1] = 85;
scores[1][2] = 95;
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
중첩 반복문으로 모든 요소를 출력할 수 있다.
public class Main {
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
for (int row = 0; row < matrix.length; row++) {
for (int column = 0;
column < matrix[row].length;
column++) {
System.out.print(matrix[row][column] + " ");
}
System.out.println();
}
}
}
matrix.length는 행 배열의 길이고, matrix[row].length는 해당 행의 열 길이다.
Java의 2차원 배열은 각 행의 길이가 서로 다를 수 있다.
int[][] scores = new int[2][];
scores[0] = new int[2];
scores[1] = new int[3];
메모리 구조는 다음과 같다.
scores
├─ scores[0] → [0][0]
└─ scores[1] → [0][0][0]
값 목록으로도 만들 수 있다.
int[][] numbers = {
{1, 2},
{3, 4, 5},
{6}
};
이를 계단식 배열 또는 가변 길이 배열이라고 부르기도 한다.
참조 타입 배열의 요소에는 객체의 참조값이 저장된다.
String[] languages = new String[3];
languages[0] = "Java";
languages[1] = "Java";
languages[2] = new String("Java");
관계는 다음과 같이 볼 수 있다.
languages[0] ──┐
├──→ 문자열 풀의 "Java"
languages[1] ──┘
languages[2] ─────→ new String("Java")
따라서 다음 결과가 나온다.
System.out.println(languages[0] == languages[1]);
System.out.println(languages[0] == languages[2]);
System.out.println(languages[0].equals(languages[2]));
true
false
true
main() 메서드의 매개변수는 문자열 배열이다.
public static void main(String[] args)
프로그램을 실행할 때 전달한 값들이 args 배열에 저장된다.
public class Main {
public static void main(String[] args) {
System.out.println("전달된 값의 개수: " + args.length);
for (String argument : args) {
System.out.println(argument);
}
}
}
예를 들어 다음과 같이 실행했다고 가정하자.
java Main Java Spring 100
args 배열은 다음 값을 가진다.
args[0] = "Java"
args[1] = "Spring"
args[2] = "100"
명령행 인수를 전달하지 않아도 일반적으로 args는 null이 아니라 길이가 0인 배열이다.
데이터가 몇 가지 한정된 값만 가질 수 있다면 열거 타입을 사용할 수 있다.
요일은 다음 일곱 값 중 하나만 가진다.
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
문자열로 관리하면 오타가 발생할 수 있다.
String today = "SUNDEY";
열거 타입을 사용하면 허용된 값만 저장할 수 있다.
Week.java 파일을 생성한다.
public enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
열거 상수는 관례적으로 모두 대문자와 언더바를 사용한다.
public class Main {
public static void main(String[] args) {
Week today = Week.FRIDAY;
if (today == Week.FRIDAY) {
System.out.println("오늘은 금요일입니다.");
}
}
}
열거 상수는 정해진 하나의 객체이므로 ==를 사용해 안전하게 비교할 수 있다.
switch와도 잘 어울린다.
public class Main {
public static void main(String[] args) {
Week today = Week.SATURDAY;
String message = switch (today) {
case MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY -> "평일입니다.";
case SATURDAY, SUNDAY -> "주말입니다.";
};
System.out.println(message);
}
}
배열, 반복문, 조건문을 함께 활용해 점수 통계를 계산해보자.
import java.util.Arrays;
public class ScoreManager {
public static void main(String[] args) {
int[] scores = {85, 92, 78, 100, 66};
int sum = 0;
int maximum = scores[0];
int minimum = scores[0];
for (int score : scores) {
sum += score;
if (score > maximum) {
maximum = score;
}
if (score < minimum) {
minimum = score;
}
}
double average = (double) sum / scores.length;
System.out.println("점수: "
+ Arrays.toString(scores));
System.out.println("합계: " + sum);
System.out.printf("평균: %.2f%n", average);
System.out.println("최고 점수: " + maximum);
System.out.println("최저 점수: " + minimum);
}
}
실행 결과는 다음과 같다.
점수: [85, 92, 78, 100, 66]
합계: 421
평균: 84.20
최고 점수: 100
최저 점수: 66
int[] numbers;
배열 변수만 선언한 상태다.
numbers = new int[3];
실제 배열 객체를 생성하고 참조값을 변수에 저장한 상태다.
길이가 3인 배열의 인덱스는 0, 1, 2다.
int length = numbers.length;
int lastIndex = numbers.length - 1;
numbers.length
text.length()
배열은 필드, 문자열은 메서드다.
int[] first = {1, 2, 3};
int[] second = first;
새로운 배열이 만들어진 것이 아니다. 두 변수가 같은 배열을 참조한다.
second[0] = 100;
System.out.println(first[0]);
결과도 100이다.
독립된 배열을 만들려면 실제 복사가 필요하다.
int[] second = Arrays.copyOf(first, first.length);
배열은 크기가 고정되어 있고 같은 타입의 값을 빠르게 순차 처리하는 데 적합하다.
다음과 같은 경우 배열이 잘 어울린다.
데이터의 추가와 삭제가 자주 발생한다면 ArrayList 같은 컬렉션이 더 적합하다.
List<String> names = new ArrayList<>();
배열의 내용을 비교할 때도 ==를 사용하면 안 된다.
int[] first = {1, 2, 3};
int[] second = {1, 2, 3};
System.out.println(first == second);
결과는 false다.
요소의 내용을 비교하려면 Arrays.equals()를 사용한다.
import java.util.Arrays;
System.out.println(Arrays.equals(first, second));
2차원 이상 배열의 내용을 비교할 때는 Arrays.deepEquals()를 사용한다.
Arrays.deepEquals(firstMatrix, secondMatrix);
2차원 배열을 출력할 때는 Arrays.deepToString()을 사용할 수 있다.
System.out.println(Arrays.deepToString(matrix));
==는 같은 객체를 참조하는지 비교한다.equals() 또는 배열 비교 API를 사용해야 한다.null을 가질 수 있다.null인 변수로 객체를 사용하면 NullPointerException이 발생한다.length - 1이다.new로 생성한 배열 요소는 타입별 기본값으로 초기화된다.for문은 인덱스가 필요할 때 사용하고, 향상된 for문은 요소를 순서대로 읽을 때 편리하다.Arrays.toString()으로 배열 내용을 쉽게 출력할 수 있다.System.arraycopy(), Arrays.copyOf() 등으로 복사할 수 있다.String[] args에는 프로그램 실행 시 전달한 명령행 인수가 저장된다.