자동차 주행 거리계가 999999에서 1킬로미터를 더 가면 000000으로 돌아간다. 자바의 정수도 칸이 정해져 있어서, 가장 큰 값에 1을 더하면 오류 없이 가장 작은 값으로 돌아간다. 더하기·비교·대입 같은 연산 기호는 수학과 거의 같게 읽히지만, 이렇게 수학과 어긋나는 곳이 몇 군데 있다. 정수끼리 나누면 소수점이 사라지고, 소수 계산은 끝자리가 조금 틀리며, 0으로 나누면 정수는 프로그램이 멈추고 실수는 멈추지 않은 채 이상한 값이 된다.
예시로 보기
수학에서는 127 + 1 = 128이다. 자바의 byte는 −128부터 127까지만 담는다. 그러면 127에 1을 더할 때 어떻게 될까?
byte b = 127;
b++;
System.out.println(b); // -128
오류도 경고도 없이 −128이 된다. byte의 값들은 양 끝이 이어진 고리처럼 돈다.
+1로 넘으면(오버플로) 맨 왼쪽으로
+---------------------------------------------+
v |
-128 <-> -127 <-> ... <-> 0 <-> ... <-> 126 <-> 127
| ^
+---------------------------------------------+
-1로 넘으면(언더플로) 맨 오른쪽으로
타입이 담을 수 있는 가장 큰 값을 넘는 것을 오버플로, 가장 작은 값 아래로 내려가는 것을 언더플로라 부른다. int도 같다. 2147483647 + 1은 -2147483648이다.
0.29999999999999993, 0으로 나누기 표, 건너뛰는 &&, 복합 대입, 삼항, 우선순위), 카드 C1~C4 — 04_java-operators_impl.java연산자 한눈에
연산 기호를 연산자, 연산에 쓰이는 값을 피연산자라 부른다. x + 1에서 +가 연산자, x와 1이 피연산자다.
부호와 증감
| 식 | 뜻 |
|---|---|
+x |
부호를 그대로 둔다 |
-x |
부호를 바꾼다 |
++x |
x를 1 올리고, 올린 값을 쓴다 |
--x |
x를 1 내리고, 내린 값을 쓴다 |
x++ |
지금 값을 먼저 쓰고, 그 뒤에 1 올린다 |
x-- |
지금 값을 먼저 쓰고, 그 뒤에 1 내린다 |
++가 앞에 붙느냐 뒤에 붙느냐는 다른 계산과 섞일 때만 차이가 난다.
int a = 10;
int b = a++ + ++a; // b = 22, a = 12
| 순서 | 한 일 | 식에 들어간 값 | a |
|---|---|---|---|
| 1 | a++: 10을 쓰고 올린다 |
10 | 11 |
| 2 | ++a: 올리고 쓴다 |
12 | 12 |
| 3 | 10 + 12 | 22 | 12 |
산술
더하기 +, 빼기 -, 곱하기 *, 나누기 /, 나머지 %의 다섯이다. 수학과 다른 곳은 나눗셈이다1.
| 식 | 값 | 까닭 |
|---|---|---|
7 / 2 |
3 | 정수끼리 나누면 몫만 남는다 |
7 % 2 |
1 | 나머지 |
7 / 2.0 |
3.5 | 한쪽이 실수면 실수로 계산한다 |
-7 / 2 |
−3 | 0 쪽으로 버린다 |
-7 % 2 |
−1 | 나머지의 부호는 나뉘는 수를 따른다 |
정확한 계산
사과 1개를 0.1개짜리 조각 7개만큼 덜어 내면 0.3개가 남아야 한다.
int apple = 1;
double pieceUnit = 0.1;
int number = 7;
double result = apple - number * pieceUnit;
System.out.println("사과 1개에서 남은 양: " + result); // 0.29999999999999993
0.1은 2진수로 딱 떨어지지 않는다. 10진수에서 1/3이 0.333…으로 끝나지 않는 것과 같다. double은 가장 가까운 값을 대신 적고, 그 작은 차이가 계산에 묻어 나온다(부동소수점). 정확해야 하는 계산은 정수로 바꿔서 한다. 사과를 조각 10개로 세면 10 - 7 = 3조각이고, 마지막에 한 번만 10으로 나누면 0.3이다1.
0으로 나누기
왼쪽이 정수이고 오른쪽이 정수 0이면 프로그램이 ArithmeticException을 내며 멈춘다. 한쪽이라도 실수면 멈추지 않고 결과가 무한대(Infinity)나 수가 아님(NaN, Not a Number)이 된다.
| 식 | 결과 |
|---|---|
5 / 0 |
ArithmeticException: / by zero |
5 % 0 |
ArithmeticException |
5 / 0.0 |
Infinity |
-5 / 0.0 |
-Infinity |
5 % 0.0 |
NaN |
0.0 / 0.0 |
NaN |
멈추지 않는 쪽이 더 위험하다. 이 값은 뒤의 계산을 모두 물들인다. Infinity + 2는 Infinity, NaN + 2는 NaN이다. 그래서 /나 %를 한 뒤 결과를 확인하고 다음 계산으로 간다.
double z = x / y;
if (Double.isInfinite(z) || Double.isNaN(z)) {
System.out.println("값을 계산할 수 없다");
} else {
System.out.println(z + 2);
}
z == Double.NaN으로 확인하면 안 된다. NaN은 자기 자신과 비교해도 같지 않다. NaN == NaN은 false다1.
비교
같은지(==, !=)와 크기(<, <=, >, >=)를 따져 boolean 값 true나 false를 낸다. 조건문과 반복문의 조건에 주로 쓴다.
| 식 | 뜻 |
|---|---|
a == b |
두 값이 같은가 |
a != b |
두 값이 다른가 |
a > b, a >= b |
크다, 크거나 같다 |
a < b, a <= b |
작다, 작거나 같다 |
문자열을 비교할 때는 == 대신 equals()를 쓴다. 이유는 String에 있다. 실수도 ==로 비교하면 어긋난다. 0.1 == 0.1f는 false다. float의 0.1과 double의 0.1은 서로 다른 근삿값이다1.
논리
boolean 값끼리 계산한다.
| 연산 | 기호 | true가 되는 때 |
|---|---|---|
| 그리고(AND) | && 또는 & |
둘 다 true |
| 또는(OR) | \|\| 또는 \| |
하나라도 true |
| 둘 중 하나만(XOR) | ^ |
하나는 true, 하나는 false |
| 아니다(NOT) | ! |
원래 값이 false |
&&와 &는 답이 같다. 다른 점은 뒤쪽을 계산하느냐다. &&는 앞이 false면 답이 이미 false로 정해졌으므로 뒤를 아예 계산하지 않는다. &는 늘 둘 다 계산한다. ||와 |도 같다1.
int n = 0;
boolean ok = (n != 0) && (10 / n > 1); // false. 10 / n은 계산하지 않는다
boolean ng = (n != 0) & (10 / n > 1); // ArithmeticException
대입
=는 오른쪽 값을 왼쪽 변수에 넣는다. “같다”가 아니다. 오른쪽에는 리터럴, 변수, 다른 식이 올 수 있다.
| 종류 | 식 | 같은 뜻 |
|---|---|---|
| 단순 대입 | x = 5 |
|
| 복합 대입 | x += 5 |
x = x + 5 |
x -= 5, x *= 5, x /= 5, x %= 5 |
x = x - 5 등 |
|
x &= 5, x \|= 5, x ^= 5 |
x = x & 5 등 |
|
x <<= 2, x >>= 2, x >>>= 2 |
x = x << 2 등 |
복합 대입에는 강제 변환이 숨어 있다. byte b = 10;일 때 b = b + 5;는 컴파일 오류지만 b += 5;는 된다. b = (byte) (b + 5);와 같기 때문이다1. 편하지만 넘쳐도 알려 주지 않는다. b가 15일 때 b += 120;을 하면 −121이 된다.
삼항
피연산자가 셋이다. ? 앞의 조건이 true면 : 앞의 값을, false면 : 뒤의 값을 고른다.
조건식 ? 참일 때의 값 : 거짓일 때의 값
int score = 85;
char grade = (score > 90) ? 'A' : ((score > 80) ? 'B' : 'C'); // 'B'
계산 순서
한 식에 연산자가 여럿이면 순위가 높은 것부터 한다. 곱셈과 나눗셈이 덧셈과 뺄셈보다 먼저이고, >와 < 같은 비교가 &&보다 먼저다. 순위가 같으면 대부분 왼쪽부터 한다.
| 순위 | 연산자 | 순위가 같을 때 |
|---|---|---|
| 높음 | 증감 ++ --, 부호 + -, 비트 ~, 논리 ! |
오른쪽부터 |
산술 * / % |
왼쪽부터 | |
산술 + - |
왼쪽부터 | |
시프트 << >> >>> |
왼쪽부터 | |
비교 < > <= >= instanceof |
왼쪽부터 | |
비교 == != |
왼쪽부터 | |
논리 & |
왼쪽부터 | |
논리 ^ |
왼쪽부터 | |
논리 \| |
왼쪽부터 | |
논리 && |
왼쪽부터 | |
논리 \|\| |
왼쪽부터 | |
조건 ? : |
오른쪽부터 | |
| 낮음 | 대입 = += -= 등 |
오른쪽부터 |
1 + 2 * 3 // 7 곱셈 먼저
10 - 3 - 2 // 5 같은 순위는 왼쪽부터: (10 - 3) - 2
a = b = 5 // 대입은 오른쪽부터: a = (b = 5)
x > 0 && y < 0 // (x > 0) && (y < 0)
true || false && false // true &&가 먼저: true || (false && false)
a ? b : c ? d : e // 삼항은 오른쪽부터: a ? b : (c ? d : e)
삼항을 왼쪽부터 묶는다고 보면 답이 달라질 수 있다. a, c, d, e가 true이고 b가 false일 때 오른쪽부터 묶으면 false이고, 왼쪽부터 묶은 (a ? b : c) ? d : e는 true다. 자바는 false를 낸다1.
순서가 헷갈리면 괄호를 친다. 괄호 안이 늘 먼저다.
활용
- 넘치면 안 되는 계산은 처음부터
long으로 한다.1000000 * 1000000은int끼리라 −727,379,968이 되고,1000000L * 1000000은 1,000,000,000,000이다1. - 넘칠 때 멈추게 하고 싶으면
Math.addExact(a, b)를 쓴다. 넘치면ArithmeticException을 낸다1. - 돈 계산처럼 틀리면 안 되는 소수 계산은 가장 작은 단위(원, 센트)의 정수로 한다.
- 흔한 실수: 조건에
==대신=를 쓴다.if (x = 5)는int를 조건 자리에 둔 것이라 자바에서는 컴파일 오류다1. - 흔한 실수: 평균을
sum / count로 내어 소수점을 잃는다.
연결
- 선수: 변수와 기본 타입, 타입 변환
- 비트 단위 연산자(
&,|,^,~, 시프트): 비트 연산 - 비교와 논리 연산자가 쓰이는 곳: 제어문
- 127 다음이 −128인 이유: 정수의 2진 표현과 2의 보수
- 0.1이 딱 떨어지지 않는 이유,
NaN과Infinity의 비트 모양: 부동소수점 - AND, OR, XOR, NOT의 진리표: 명제와 논리 연산
자주 하는 오해
아니다. 자바의 정수 계산은 범위를 넘어도 멈추지 않고 반대쪽 끝에서 이어진 값을 낸다. 0으로 나누면 프로그램이 멈추니까 넘칠 때도 멈출 것 같지만, 정수에서 멈추는 경우는 0으로 나눌 때뿐이다. Integer.MAX_VALUE + 1은 −2,147,483,648이고 프로그램은 그대로 계속 돈다. 확인하는 법: System.out.println(Integer.MAX_VALUE + 1);을 돌려 본다. 넘칠 때 멈추게 하려면 Math.addExact처럼 확인해 주는 메서드를 따로 불러야 한다1.
과목별 관점
어셈블리어 (2-1학기). 자바에서 byte 127에 1을 더하면 아무 표시 없이 −128이 된다. 같은 덧셈을 CPU 명령으로 쓰면 mov al,7Fh 다음에 add al,1이고, 8비트 레지스터 AL은 80h(−128)가 된다. 결과는 자바와 같다. 다른 점은 CPU가 덧셈을 할 때마다 1비트짜리 표시(플래그) 둘을 함께 남긴다는 것이다.
- 올림 플래그(CF). 같은 비트를 부호 없는 수로 볼 때 결과가 범위를 벗어났으면 1이 된다.
- 넘침 플래그(OF). 부호 있는 수로 볼 때 결과가 범위를 벗어났으면 1이 된다. 127 + 1에서는 OF가 1이다.
CPU는 그 비트를 프로그래머가 부호 있는 수로 쓰는지 없는 수로 쓰는지 구별하지 못해 두 표시를 늘 다 남긴다. 그러니 “넘쳤다”는 사실은 기계에 남아 있다. 자바 언어가 그 표시를 프로그램에 보여 주지 않을 뿐이다. 위의 Math.addExact는 넘쳤는지를 대신 확인해 예외로 알려 준다2. 두 플래그가 정해지는 규칙은 산술 연산과 상태 플래그에, 어느 쪽을 읽어야 하는지는 올림 플래그와 넘침 플래그 비교에 있다.
확인 문제
C1 int c = 3; int r = c++ * 2 + --c;를 실행한 뒤 r과 c의 값은?
답: r = 9, c = 3.
이유: c++는 3을 쓰고 c를 4로 올린다. 3 * 2 = 6. --c는 c를 3으로 내리고 3을 쓴다. 6 + 3 = 9. c++에서 4를 쓴다고 보면 11이라는 오답이 나온다.
C2 다음 넷 가운데 프로그램이 멈추는 것은? 멈추지 않는 것은 결과를 쓰라. ① 5 / 0 ② 5 / 0.0 ③ 5 % 0.0 ④ 0 / 5
답: ①만 멈춘다(ArithmeticException). ② Infinity ③ NaN ④ 0.
이유: 정수를 정수 0으로 나눌 때만 예외다. 한쪽이 실수면 Infinity나 NaN이 된다. ④는 0을 나누는 것이라 문제없다.
C3 1 - 7 * 0.1이 0.3이 아니라 0.29999999999999993인 이유를 쓰고, 0.3을 정확히 얻는 방법을 쓰라.
답: 0.1은 2진수로 끝없이 이어져 double에 근삿값으로 들어간다. 그 오차가 곱셈과 뺄셈을 거쳐 드러난다. 정수로 바꿔 계산한다. 사과 1개를 10조각으로 세어 (10 - 7) / 10.0을 하면 0.3이다.
이유: 정수 계산에는 오차가 없다. 나눗셈을 마지막에 한 번만 하면 오차가 쌓일 틈이 없다.
C4 int w = 2;일 때 -w * w + 10 % 3 << 1의 값을 구하라. 괄호를 쳐서 계산 순서를 보여라.
답: −6. (((-w) * w) + (10 % 3)) << 1.
이유: 부호 -가 가장 먼저라 -2. 다음 *와 %: -2 * 2 = -4, 10 % 3 = 1. 다음 +: −3. 시프트가 가장 늦다. -3 << 1 = -6. 시프트를 덧셈보다 먼저 하면 −2라는 오답이 나온다.
C5 byte b = 127; b++;와 같은 계산을 CPU 명령 mov al,7Fh / add al,1로 했다. 올림 플래그(CF)와 넘침 플래그(OF)는 각각 얼마인가? AL에 FFh를 넣고 1을 더하면 두 플래그는 어떻게 되는가? 자바의 byte 계산이 넘쳤는지를 말해 주는 쪽은 어느 플래그인가?
답: 7Fh + 1: CF = 0, OF = 1. FFh + 1: CF = 1, OF = 0. byte의 넘침을 말해 주는 쪽은 OF다.
이유: byte는 부호 있는 수다. 127 + 1 = 128은 −128~127 밖이라 OF가 켜진다. FFh는 byte로 −1이고 −1 + 1 = 0은 범위 안이라 OF가 꺼져 있다. 이때 켜지는 CF는 FFh를 255로 읽었을 때의 판정이라 byte 계산과는 상관없다2.