자바 형변환과 부동소수점 오차: 0.1은 처음부터 0.1이 아니었다
자바 char 정수 변환, int 나눗셈 결과 타입, float double 0.1 오차와 IEEE 754, possible lossy conversion 에러, 리터럴 기본 타입까지 — 자바의 정석 2장에서 틀린 문제들의 원인 정리.
목차
『자바의 정석』 2장(변수와 타입). 출력 예상 문제 6개를 풀었는데 결과가 안 좋았다. 문제 1은 절반, 문제 2는 4개 다 틀렸고, 문제 3·4는 아예 못 풀었다.
근데 정리하다 보니 틀린 게 다 두세 개 원인으로 모인다. 채점만 하면 10개인데 고칠 건 3개다. 그 원인들을 적어둔다.
char의 속이 정수라는 걸 몰랐다
문제 2를 4개 다 틀린 원인이 이거 하나였다.
char ch = 'A';
int i = ch;
System.out.println(i); // 내 답: ch → 정답 65
System.out.println(ch + 1); // 내 답: 에러? → 정답 66
System.out.println((char) (ch + 1)); // 내 답: ch1 → 정답 B
System.out.println("" + ch + 1); // 내 답: 에러? → 정답 A1
char를 "문자를 담는 상자"로만 봤다. 실제로는 안에 번호가 들어있다.
'A' = 65 'B' = 66 '0' = 48 '9' = 57
그래서 int i = ch;가 에러가 아니라 65가 되고, ch + 1이 66이 된다. 다시 문자로 보려면 (char)로 되돌려야 B가 나온다.
'0'57에 나란히 있다는 건 나중에 쓰인다. '9'가 48'9' - '0' = 9. 시작점을 빼면 순서 번호가 나온다.
int / int는 int다
int a = 7, b = 2;
System.out.println(a / b); // 내 답: 3.5 → 정답 3
담는 변수가 아니라 나누는 두 값이 결과 타입을 정한다.
7 / 2 → 3 int / int = int (버림, 반올림 아님)
7.0 / 2 → 3.5 한쪽이 double
(double)(7 / 2) → 3.0 ← 이미 3이 된 뒤에 바꿔서 0.5는 이미 없다
마지막이 3.0인 게 포인트다. 한 번 버려진 정보는 나중에 캐스팅해도 안 돌아온다. 괄호 위치가 곧 "언제 바꾸느냐"다.
0.1은 처음부터 0.1이 아니었다
문제 4를 못 풀고 float, double 둘 다 모르겠다고 적었다.
float f = 0.1f;
double d = f;
System.out.println(d); // 0.10000000149011612
처음엔 "double로 옮기면서 오차가 생겼나?"라고 생각했다. 아니었다. BigDecimal로 실제 담긴 값을 꺼내봤다.

단 한순간도 0.1인 적이 없었다. float으로 출력할 땐 자바가 "0.1"로 줄여서 보여준 것뿐이고, double에 옮기니 숨어있던 뒷자리가 드러난 거다.
"그러면 안 그러게 하면 되잖아"가 안 되는 이유
컴퓨터는 2진수만 쓴다. 0.1을 2진수로 쓰면 끝이 안 난다.
0.1 = 0.0001100110011001100110011... (1100 무한 반복)
10진수로 1/3을 쓰려는 것과 같다. 0.333...을 어디서 끊든 1/3이 아니다. 저장 공간이 유한하니 잘라야 하고, 그 잘린 값이 위 캡처의 숫자다.
칸을 늘려도 해결이 안 된다. float(4byte)를 double(8byte)로 늘리면 오차가 작아질 뿐 사라지지 않는다. 무한 반복이니까.
정확히 담기는 소수도 있다
여기가 제일 의외였다. 위 캡처 마지막 세 줄:
0.5 + 0.25 == 0.75 → true
0.1 + 0.2 == 0.3 → false
0.1 + 0.2 = 0.30000000000000004
0.5 = 1/2, 0.25 = 1/4처럼 2의 거듭제곱으로 나눈 수는 2진수로 딱 떨어진다. 그래서 계산도 정확하다.
즉 "소수는 다 부정확"이 아니라 **"2진수로 딱 안 떨어지는 소수가 부정확"**이다. 하필 우리가 자주 쓰는 0.1, 0.2, 0.3이 거기 해당한다.
자바 문제도 아니다. JS·Python·C 전부 같은 IEEE 754 규격을 써서 똑같이 나온다.
리터럴에도 기본 타입이 있다
long big = 10000000000;이 왜 에러인지를 이렇게 적었다.
롱이 인트보다 커서 인트에 안담김
방향이 반대였다. 담는 그릇(long)은 충분하다. 문제는 적어놓은 숫자 자체가 int로 읽히는 거다.
정수 리터럴 10 → 기본 int (long 이면 L)
소수 리터럴 3.14 → 기본 double (float 이면 f)
이걸 몰라서 float f = 3.14;를 두 번이나 그대로 뒀다. 3.14 자체가 이미 double이라 float에 안 들어간다. 3.14f여야 한다.
long에 L 붙이는 거랑 완전히 같은 이야기인데, 정수 쪽만 외우고 소수 쪽은 몰랐다.
possible lossy conversion — 이번 장의 대표 에러
int i = 10;
double d = i; // 된다
int back = d; // 안 된다
"언박싱인가?"라고 답했는데 아니었다. 언박싱은 Integer → int(객체 → 기본형)이고, 이건 기본형끼리의 형변환이다.
byte → short → int → long → float → double
방향대로 = 자동 (손실 없음)
거꾸로 = (타입) 을 직접 써야 함
double(8byte) → int(4byte)는 거꾸로라 소수점을 잃을 수 있다. 그래서 컴파일러가 막는다. 실제로 이렇게 나온다.

뜻은 항상 같다. "큰 그릇에서 작은 그릇으로 옮기려 한다. 정말이면 (타입)으로 직접 말해라." int back = (int) d;가 "잃어도 괜찮다"는 선언이다.
자잘하게 틀린 것들
String이 스택에 저장된다고 적었다. 반대다. 스택에는 주소만, 실제 문자열 객체는 힙에 있다. "참조만 한다"는 감은 맞았는데 어디에 뭐가 있는지를 뒤집어 알고 있었다.
기본형 크기를 세 칸 틀렸다. char는 8byte가 아니라 2byte(0~65535), float은 4byte고 double만 8byte다.
byte 오버플로. byte b = 127; b++; 하면 -128이 된다. 범위 끝에서 1을 더하면 최솟값으로 한 바퀴 돈다. 무서운 건 에러가 안 난다는 거다. 조용히 틀린 값이 된다.
final. 한 번 정하면 못 바꾸게 잠그는 것. "이 값은 안 바뀐다"를 컴파일러가 지키게 하는 거라 사람 기억보다 정확하다.
이번에 배운 방식
BigDecimal로 실제 값을 꺼내본 게 제일 컸다. "오차가 있다"는 설명을 백 번 듣는 것보다 0.100000001490116119384765625를 한 번 눈으로 보는 게 확실했다.
그리고 정확한 경우(0.5 + 0.25 == 0.75 → true)를 같이 확인하고서야 "전부 부정확한 게 아니라 2진수로 안 떨어지는 것만"이라는 게 잡혔다. 틀리는 경우만 보면 규칙이 안 보인다.
더 봐야 할 것
BigDecimal— 돈 계산에 쓴다는 것만 알고 아직 안 써봤다- 힙/스택 제대로 정리 — 클래스와 객체 들어가기 전에 한 번 해야 할 듯
- IEEE 754 내부 표현 — 2진수 변환까지는 아직 안 읽었다