| 연산자 | 의미 | 예 (a=7, b=2) | 주의 |
|---|---|---|---|
+ |
덧셈 / 문자열 연결 | a + b → 9 |
문자열이 끼면 연결로 바뀜 |
- |
뺄셈 | a - b → 5 |
|
* |
곱셈 | a * b → 14 |
오버플로우 주의 |
/ |
나눗셈 | a / b → 3 |
정수끼리면 몫만. 0으로 나누면 ArithmeticException |
% |
나머지 | a % b → 1 |
음수의 나머지는 음수: -7 % 2 → -1 |
+의 이중성은 초보자를 자주 괴롭힙니다.
System.out.println(1 + 2 + "개"); // "3개" — 왼쪽부터 계산, 1+2=3 후 문자열 연결
System.out.println("개수: " + 1 + 2); // "개수: 12" — 문자열 만나는 순간부터 전부 연결
System.out.println("개수: " + (1 + 2)); // "개수: 3"%의 실무 활용: 짝홀 판별(n % 2 == 0), 순환 인덱스((i + 1) % size), 자릿수 추출(n % 10은 일의 자리), N개마다 줄바꿈(i % 5 == 0).
0으로 나누기: 정수 / 0은 ArithmeticException으로 프로그램이 죽지만, 실수 / 0.0은 Infinity 또는 NaN(Not a Number)이 됩니다. 예외가 안 나서 오히려 더 위험합니다.
결과는 항상 boolean입니다.
| 연산자 | 의미 | 예 |
|---|---|---|
== |
같다 | a == b |
!= |
다르다 | a != b |
<, <=, >, >= |
크기 비교 | a >= 18 |
원시 타입은 ==로 값을 비교하지만, 참조 타입은 ==가 주소 비교입니다(01 레슨). 문자열과 객체는 equals를 씁니다. 실수의 == 비교도 오차 때문에 위험합니다. Math.abs(a - b) < 1e-9 같은 허용 오차 비교를 씁니다.
| 연산자 | 의미 | 특징 |
|---|---|---|
&& |
AND (둘 다 참) | 단축 평가: 왼쪽이 false면 오른쪽 안 봄 |
|| |
OR (하나라도 참) | 단축 평가: 왼쪽이 true면 오른쪽 안 봄 |
! |
NOT | |
&, | |
비단축 AND/OR | 항상 양쪽 다 평가 |
^ |
XOR (하나만 참) |
단축 평가(short-circuit evaluation)는 성능 최적화이면서 동시에 안전 장치입니다.
String name = null;
if (name != null && name.length() > 0) { ... }name != null이 false이므로 &&는 오른쪽 name.length()를 실행하지 않습니다. 만약 &를 썼다면 오른쪽도 평가되어 NPE가 납니다. 이 패턴("null 검사를 왼쪽에 두고 &&로 잇기")은 자바 코드 어디에나 있습니다.
같은 원리로 비용이 큰 검사는 오른쪽에 둡니다.
if (cache.contains(key) || database.exists(key)) // 캐시에 있으면 DB 조회 생략정수를 2진수 비트 단위로 다룹니다. 일반 업무 코드에선 드물지만 권한 플래그, 해시 계산, 네트워크 프로토콜, 성능 최적화에서 만납니다.
| 연산자 | 의미 | 예 (a=0b1100=12, b=0b1010=10) |
|---|---|---|
& |
AND | a & b → 0b1000 = 8 |
| |
OR | a | b → 0b1110 = 14 |
^ |
XOR | a ^ b → 0b0110 = 6 |
~ |
NOT (보수) | ~a → -13 |
<< |
왼쪽 시프트 | a << 1 → 24 (×2) |
>> |
오른쪽 시프트 (부호 유지) | a >> 1 → 6 (÷2), -8 >> 1 → -4 |
>>> |
오른쪽 시프트 (부호 무시, 0 채움) | -8 >>> 28 → 15 |
권한 플래그 패턴:
final int READ = 1, WRITE = 2, DELETE = 4; // 각각 비트 하나
int perm = READ | WRITE; // 0b011 = 3
boolean canWrite = (perm & WRITE) != 0; // true
perm |= DELETE; // 권한 추가 → 0b111
perm &= ~WRITE; // 권한 제거 → 0b101| 연산자 | 의미 | 동등 표현 |
|---|---|---|
= |
대입 | |
+=, -=, *=, /=, %= |
연산 후 대입 | a += 3 ≡ a = a + 3 |
++, -- |
1 증가/감소 |
복합 대입은 자동 캐스팅을 포함합니다. byte b = 10; b += 5;는 되지만 b = b + 5;는 오류입니다. +=는 내부적으로 b = (byte)(b + 5)입니다.
전위/후위 증감은 "언제 증가하느냐"가 아니라 "표현식의 값이 무엇이냐"의 차이입니다.
int i = 5;
int a = ++i; // i를 먼저 6으로 만들고, 그 값 6을 a에. a=6, i=6
int j = 5;
int b = j++; // j의 현재 값 5를 b에 주고, 그 뒤 j를 6으로. b=5, j=6혼자 쓰이면(i++; 한 줄) 둘은 완전히 같습니다. 다른 표현식 안에 섞을 때만 다릅니다. 가독성을 위해 섞지 마세요. arr[i++] = x; 같은 코드는 C 시대의 유산이고 코드 리뷰에서 지적됩니다.
조건 ? 참일_때_값 : 거짓일_때_값if-else를 값을 만드는 표현식으로 압축한 것입니다.
String status = score >= 60 ? "합격" : "불합격";
int fee = age < 19 ? 0 : (age >= 65 ? 5000 : 10000); // 중첩 가능하지만 한 번까지만한 줄로 값 하나를 고를 때만 씁니다. 두 번 이상 중첩되거나 각 분기에서 여러 일을 해야 하면 if-else나 switch 표현식이 낫습니다.
전부 외울 필요는 없고 아래 순서와 "헷갈리면 괄호"만 기억하면 됩니다.
| 순위 | 연산자 | 결합 방향 |
|---|---|---|
| 1 | (), [], . , 후위 ++ -- |
→ |
| 2 | 전위 ++ --, !, ~, 단항 -, 캐스팅 (type) |
← |
| 3 | * / % |
→ |
| 4 | + - |
→ |
| 5 | << >> >>> |
→ |
| 6 | < <= > >=, instanceof |
→ |
| 7 | == != |
→ |
| 8 | & |
→ |
| 9 | ^ |
→ |
| 10 | | |
→ |
| 11 | && |
→ |
| 12 | || |
→ |
| 13 | ? : |
← |
| 14 | = += -= ... |
← |
자주 걸리는 함정: &&가 ||보다 높습니다. a || b && c는 a || (b && c)입니다. 비트 연산 &, |는 비교 연산보다 낮습니다. (perm & WRITE) != 0에서 괄호를 빼면 perm & (WRITE != 0)이 되어 컴파일 오류가 납니다.
if (조건) {
// 조건이 true
} else if (다른조건) {
// 앞 조건이 false이고 이 조건이 true
} else {
// 모두 false
}조건은 반드시 boolean이어야 합니다. C처럼 if (count)는 안 됩니다(if (count != 0)). else if 체인은 위에서부터 순서대로 검사하고 처음 맞는 하나만 실행합니다. 그래서 조건의 순서가 중요합니다.
// 잘못된 순서: 95점도 첫 조건에 걸려 "C"가 됨
if (score >= 70) grade = "C";
else if (score >= 80) grade = "B"; // 절대 도달 못 함
else if (score >= 90) grade = "A";
// 올바른 순서: 좁은 조건부터
if (score >= 90) grade = "A";
else if (score >= 80) grade = "B";
else if (score >= 70) grade = "C";중괄호는 항상 씁니다. 한 줄이라도 {} 없이 쓰면 나중에 줄을 추가할 때 사고가 납니다(Apple의 유명한 goto fail 버그가 이것입니다).
얼리 리턴(early return): 예외 상황을 먼저 걸러내고 빠져나가면 중첩이 줄어듭니다.
// 중첩 깊음
if (user != null) {
if (user.isActive()) {
if (user.hasPermission()) {
doWork();
}
}
}
// 얼리 리턴
if (user == null) return;
if (!user.isActive()) return;
if (!user.hasPermission()) return;
doWork();switch (dayOfWeek) {
case 1:
case 7:
System.out.println("주말");
break;
case 2: case 3: case 4: case 5: case 6:
System.out.println("평일");
break;
default:
System.out.println("잘못된 값");
}switch (값)의 값이 case와 일치하는 곳으로 점프한 뒤 아래로 쭉 실행합니다. break를 만나야 멈춥니다. 이것을 fall-through(낙하)라 하며, break를 빼먹으면 다음 case까지 실행되는 유명한 버그의 원인입니다. 위 코드에서 case 1:에 break가 없어서 case 7:로 떨어지는 것은 의도된 fall-through입니다(여러 값 묶기).
switch 가능 타입: byte short char int, String, enum, 그리고 래퍼 클래스. long, double, boolean은 안 됩니다.
옛 switch의 문제는 fall-through 버그와 값을 반환하려면 변수를 밖에 두고 각 case에서 대입해야 한다는 번거로움이었습니다. 새 switch는 둘 다 해결합니다.
String type = switch (dayOfWeek) {
case 1, 7 -> "주말";
case 2, 3, 4, 5, 6 -> "평일";
default -> throw new IllegalArgumentException("잘못된 값: " + dayOfWeek);
};| 특징 | 옛 switch 구문 | 새 switch 표현식 |
|---|---|---|
| 구분자 | case X: |
case X -> |
| fall-through | 있음 (break 필요) | 없음 (한 case만 실행) |
| 여러 값 | case 1: case 7: |
case 1, 7 -> |
| 값 반환 | 불가 (변수 대입) | 가능 (= switch (...)) |
| 모든 경우 처리 | 선택 | 표현식으로 쓰면 필수 (default 또는 enum 전체) |
| 여러 줄 블록의 반환 | - | yield 값; |
yield는 화살표 오른쪽이 블록 {}일 때 그 블록의 값을 내보내는 키워드입니다.
int fee = switch (memberGrade) {
case "VIP" -> 0;
case "GOLD" -> {
int base = 3000;
yield base / 2; // 블록에서는 yield로 값을 내보냄
}
default -> 3000;
};패턴 매칭 switch (JDK 21 정식): 타입에 따라 분기하면서 변수까지 꺼냅니다.
Object payload = 42;
String desc = switch (payload) {
case Integer i when i > 100 -> "큰 정수 " + i;
case Integer i -> "정수 " + i;
case String s -> "문자열 " + s;
case null -> "null";
default -> "기타";
};when은 가드 조건(추가 필터)입니다. 중급 레슨의 sealed/record와 함께 쓰면 진가를 발휘합니다.
어느 것을 쓸까? 새 코드는 무조건 화살표(->) 문법입니다. fall-through 버그가 구조적으로 불가능하고, 표현식으로 값을 바로 만들 수 있습니다. 옛 문법은 기존 코드를 읽을 때만 알면 됩니다.
for (초기화; 조건; 증감) {
본문
}실행 순서: 초기화(1회) → 조건 검사 → (true면) 본문 → 증감 → 조건 검사 → ... → (false면) 종료.
for (int i = 0; i < 5; i++) {
System.out.print(i + " "); // 0 1 2 3 4
}
// i는 여기서 접근 불가 — for 안에서 선언한 변수의 범위는 for 블록까지i < n을 쓰는 이유: 배열 인덱스가 0부터 n-1이기 때문입니다. i <= n은 마지막에 범위 초과 오류를 냅니다. 자바 개발자는 "0부터 시작, 미만으로 끝" 을 몸에 익힙니다.
여러 변수를 다룰 수도 있습니다: for (int i = 0, j = 10; i < j; i++, j--).
for (타입 변수 : 배열또는컬렉션) {
본문
}인덱스 없이 요소를 하나씩 꺼냅니다. "모든 요소를 처음부터 끝까지 읽기만" 할 때는 이것이 정답입니다. 인덱스 실수가 원천 차단됩니다.
int[] prices = {1000, 2500, 3200};
int sum = 0;
for (int price : prices) {
sum += price;
}제약: 인덱스를 알 수 없고, 순회 중 배열 요소를 바꿀 수 없습니다(price = 0;은 복사본만 바꿈). 인덱스가 필요하거나 값을 수정해야 하면 일반 for를 씁니다.
while (조건) { // 조건이 true인 동안 반복. 처음부터 false면 0번 실행
본문
}
do { // 본문을 먼저 1번 실행하고
본문
} while (조건); // 조건 검사. 최소 1번은 실행됨for는 "몇 번 반복할지 알 때", while은 "언제 끝날지 모를 때"(파일 끝까지 읽기, 사용자가 종료할 때까지, 큐가 빌 때까지), do-while은 "메뉴 표시 후 입력 받기"처럼 한 번은 반드시 실행해야 할 때 씁니다.
| 키워드 | 동작 |
|---|---|
break |
가장 안쪽 반복문(또는 switch)을 즉시 탈출 |
continue |
이번 회차의 나머지를 건너뛰고 다음 회차로 |
라벨: for (...) + break 라벨; |
지정한 바깥 반복문까지 탈출 |
continue 라벨; |
지정한 바깥 반복문의 다음 회차로 |
중첩 반복문에서 break는 안쪽 하나만 빠져나옵니다. 바깥까지 나가려면 라벨을 씁니다.
outer:
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == target) {
System.out.println("발견: " + i + "," + j);
break outer; // 두 반복문 모두 탈출
}
}
}라벨은 좀처럼 쓸 일이 없어야 합니다. 라벨이 필요할 정도면 안쪽 반복문을 메서드로 빼서 return하는 편이 읽기 좋습니다.
while (true) { ... } // 관례적인 무한루프
for (;;) { ... } // 같은 뜻서버의 요청 대기, 게임 루프, 콘솔 메뉴처럼 의도적 무한루프는 반드시 안에 break나 return이 있어야 합니다.
의도치 않은 무한루프의 원인 3가지:
while (i < 10) { ... } 에서 i++ 없음for (int i = 10; i > 0; i++) (증가하면서 0보다 크길 기다림)for (double d = 0; d != 1.0; d += 0.1) — 오차 때문에 정확히 1.0이 안 됨컴파일러는 while (true) 뒤에 도달 불가능한 코드가 있으면 오류를 냅니다(unreachable statement). 반대로 말하면 while (true)에 break가 없으면 그 뒤 코드는 쓸 수 없습니다.