자바는 타입에 엄격한 언어예요. 그래서 서로 다른 자료형끼리 대입하거나 계산하려면 타입을 맞춰 주는 작업이 필요해요. 이걸 형변환(타입 캐스팅)이라고 부릅니다. 규칙만 잡으면 어렵지 않아요. 천천히 따라오세요.
5/2)의 함정과 실수 결과를 얻는 방법을 알아요.char와 int가 서로 변환된다는 걸('A'==65) 이해해요.지난 장(CH 03)에서 int, double, char 같은 자료형(타입)을 배웠어요. 각 타입은 담을 수 있는 값의 종류와 크기가 정해져 있어요. 자바는 타입이 다르면 그냥 섞어 쓰지 못하게 막는 언어예요. 그래서 서로 다른 타입끼리 대입하거나 계산할 때는 타입을 하나로 맞춰 줘야 하는데, 그 작업이 바로 형변환(type conversion / type casting)이에요.
int는 작은 컵, double은 큰 컵이에요.아래 코드처럼 타입이 안 맞으면 자바는 컴파일 단계에서 에러를 내거나, 규칙에 따라 변환을 요구해요. 그래서 형변환은 선택이 아니라 "타입이 다를 때 반드시 거치는 관문"이에요.
double d = 10; // int 값 10 → double로 자동 변환 (작은→큰, OK)
int i = 3.9; // ❌ 컴파일 에러! double(큰) → int(작은)은 자동으로 안 됨
int j = (int) 3.9; // ✅ (int)로 "강제로 바꿀게"라고 선언 → j는 3
int i = 3.9;처럼 타입이 안 맞으면 아예 번역이 안 돼서 실행조차 못 해요. 무서워 보이지만, 사실은 실수를 일찍 잡아 주는 고마운 안전장치예요.자동 형변환(묵시적 형변환, implicit conversion)은 작은(좁은) 타입의 값을 큰(넓은) 타입에 담을 때 자바가 알아서 자동으로 해주는 변환이에요. 내가 아무것도 안 써도 되고, 값이 손실되지 않아요. 작은 그릇의 물을 큰 그릇에 붓는 거라 넘칠 일이 없거든요.
타입의 "크기 순서"는 대략 아래 방향이에요. 오른쪽으로 갈수록 더 넓은 타입이라, 이 방향으로는 자동 변환이 됩니다.
예를 들어 int 값을 double 변수에 대입하면, 자바가 자동으로 double로 바꿔서 담아 줘요. 굳이 (double)을 붙이지 않아도 됩니다.
int n = 10;
double d = n; // int → double 자동 변환. d 는 10.0
System.out.println(d); // 10.0 (정수 10이 실수 10.0으로)
double d2 = 10; // 리터럴 10(int)도 자동으로 10.0 이 됨
long big = 100; // int 100 → long 으로 자동 변환
float f = 5; // int → float 자동 변환. f 는 5.0
double d3 = f; // float → double 자동 변환 (더 넓은 실수로)
10, 3.9, 'A'처럼요. 소수점이 없는 10은 int 리터럴, 10.0은 double 리터럴로 취급돼요. 위 double d2 = 10;에서 10이 바로 int 리터럴이고, 이게 자동으로 10.0이 되는 거예요.10을 실수 10.0으로 바꿔도 값 자체는 그대로예요. 오히려 표현할 수 있는 범위가 더 넓어졌죠. 작은 컵의 물을 큰 컵에 부으면 물의 양은 안 변하고, 컵만 커진 것과 같아요.
그래서 자동 형변환은 안전(safe)합니다. 자바가 굳이 나에게 확인받지 않고 알아서 처리해 주는 이유예요.
int → double)은 자동, 값 손실 없음. 아무것도 안 써도 됩니다.강제 형변환(명시적 캐스팅, explicit casting)은 큰(넓은) 타입의 값을 작은(좁은) 타입에 담을 때 쓰는 방법이에요. 큰 컵의 물을 작은 컵에 부으면 넘칠 수 있죠? 그래서 자바는 자동으로 해주지 않고, 내가 변수 앞에 (바꿀타입)을 붙여서 "손실이 나도 괜찮아, 강제로 바꿀게"라고 직접 선언하게 합니다.
double d = 3.9;
int i = (int) d; // double → int 강제 캐스팅
System.out.println(i); // 3 ← 소수점 아래(.9)는 반올림이 아니라 그냥 "버림"!
int x = (int) 3.99; // 3 (0.99 도 무조건 버림)
int y = (int) 9.0; // 9
(int)3.9는 4가 아니라 3입니다. 소수점 아래를 그냥 잘라내는 것이지 반올림하는 게 아니에요. 이건 초보자가 가장 많이 헷갈리는 부분이니 꼭 기억하세요.강제 캐스팅에는 두 가지 손실이 생길 수 있어요. 하나는 방금 본 소수점 버림이고, 다른 하나는 값이 작은 타입의 표현 범위를 넘어설 때 생기는 오버플로우(overflow)예요.
// (1) 소수점 손실
int a = (int) 12.75; // 12 (.75 버려짐)
// (2) 오버플로우 — 범위를 벗어나면 "엉뚱한 값"이 됨
// byte 는 -128 ~ 127 까지만 표현 가능한데, 300은 그 범위를 넘음!
byte b = (byte) 300; // 예상: 300 → 실제: 44 (완전히 다른 값!)
System.out.println(b); // 44
byte는 -128 ~ 127까지 256칸만 표현할 수 있어요. 여기에 300을 억지로 넣으면, 마치 시계 바늘이 12를 넘으면 다시 1로 돌아가듯 값이 한 바퀴 돌아 엉뚱한 숫자(44)가 돼요. (300 - 256 = 44)
💡 핵심은 "정확한 계산"이 아니라, 작은 타입에 큰 값을 강제로 넣으면 값이 완전히 망가질 수 있다는 감각이에요. 그래서 강제 캐스팅은 "내가 범위를 확인했다"는 책임이 따라옵니다.
(타입)을 붙였다면, "값이 잘려도 괜찮다"는 걸 내가 확인했다는 뜻이 됩니다.int i = (int) d; 를 실행해도 원래의 d(double)는 그대로 3.9예요. 캐스팅은 그 순간 계산에 쓸 임시 값을 만들어 i에 담는 것이지, d 자체를 정수로 바꿔치기하는 게 아니에요.
double d = 3.9;
int i = (int) d;
System.out.println(i); // 3 (i 에 담긴 값)
System.out.println(d); // 3.9 (d 는 그대로!)
비유하자면, 큰 컵(d)의 물을 복사해서 작은 컵(i)에 옮겨 담은 거예요. 원본 컵은 여전히 가득 차 있죠.
형변환은 대입할 때만이 아니라 계산(연산)할 때도 몰래 일어나요. 그런데 이걸 모르면 "분명히 맞게 짰는데 결과가 이상한" 일을 겪게 돼요. 대표적인 게 정수 나눗셈이에요.
5 / 2는 우리가 기대하는 2.5가 아니라 2예요!
int a = 5 / 2;
System.out.println(a); // 2 ← 2.5 가 아님! (정수÷정수=정수, .5 버림)
System.out.println(7 / 2); // 3 (3.5 → 3)
System.out.println(10 / 3); // 3 (3.33... → 3)
System.out.println(1 / 2); // 0 (0.5 → 0) ← 특히 조심!
그럼 2.5라는 실수 결과를 얻으려면 어떻게 할까요? 계산에 참여하는 둘 중 하나 이상을 실수(double)로 만들어 주면 돼요. 방법은 두 가지예요.
(double)5 / 2 처럼 하나를 double로 바꾸면, 다른 한쪽도 자동으로 double로 승격돼서 실수 나눗셈이 됩니다..0을 붙여 5.0 / 2 처럼 처음부터 double로 쓰면 돼요.
💡 (double)은 나누기 전에 한쪽 타입을 바꾸는 거예요. (double)(5/2)처럼 괄호를 통째로 씌우면 너무 늦어요 — 이미 5/2가 정수 2로 계산된 뒤라 결과는 2.0이 됩니다.
System.out.println((double) 5 / 2); // 2.5 ✅ 나누기 전에 5를 double로
System.out.println(5.0 / 2); // 2.5 ✅ 5.0 은 처음부터 double
System.out.println(5 / 2.0); // 2.5 ✅ 한쪽만 double이어도 됨
System.out.println((double)(5 / 2)); // 2.0 ❌ 이미 5/2=2 된 뒤라 소용없음
System.out.println(5 / 2); // 2 ❌ 둘 다 정수라 정수 나눗셈
int + double은 int가 double로 승격돼서, 결과도 double이 됩니다.
int i = 3;
double d = 2.0;
System.out.println(i + d); // 5.0 (int 3 → double 3.0 으로 승격 후 계산)
System.out.println(10 + 3.5); // 13.5 (int + double → double)
long L = 5;
System.out.println(L + 2); // 7 (long + int → long, 결과 7)
마지막으로 자주 나오는 특별한 짝, char와 int예요. 자바에서 문자는 사실 내부적으로 숫자(유니코드 번호)로 저장돼요. 그래서 char는 int와 자유롭게 넘나들 수 있어요.
char는 문자를 이 번호로 저장하기 때문에, 'A'의 정체가 곧 숫자 65인 거예요.'A'는 내부적으로 숫자 65예요(유니코드/아스키). 'B'는 66, 'C'는 67 … 이렇게 이어져요.char를 int와 계산하면 숫자로 취급되고, 반대로 숫자를 (char)로 캐스팅하면 그 번호의 문자가 나와요.
💡 'A' + 1은 문자열 "A1"이 아니라 정수 66이에요(65+1). 다시 문자 'B'로 보려면 (char)('A' + 1)처럼 char로 캐스팅해야 해요.
System.out.println('A' == 65); // true ('A' 의 정체는 65)
System.out.println('A' + 1); // 66 (65 + 1, 정수로 계산됨!)
System.out.println((char) 65); // A (65번 문자 → 'A')
System.out.println((char)('A' + 1)); // B (66 → 'B')
char c = 'A';
int code = c; // char → int 자동 변환. code 는 65
5/2 == 2(정수 나눗셈), ② (int)3.9 == 3(반올림 아님, 버림), ③ 'A'+1 == 66(문자가 아니라 정수). 이 셋만 확실히 잡아도 실수를 크게 줄일 수 있어요.| 구분 | 자동(묵시적) 형변환 | 강제(명시적) 캐스팅 |
|---|---|---|
| 방향 | 작은 타입 → 큰 타입 (예: int → double) | 큰 타입 → 작은 타입 (예: double → int) |
| 쓰는 법 | 아무것도 안 씀 (자바가 알아서) | 변수 앞에 (타입) 을 붙임 |
| 값 손실 | 없음 (안전) 🟢 | 있을 수 있음 (소수점 버림·오버플로우) 🔴 |
| 예시 | double d = 10; → 10.0 | int i = (int)3.9; → 3 |
| 비유 | 작은 컵 → 큰 컵 (안 넘침) | 큰 컵 → 작은 컵 (넘칠 수 있음) |
| 상황 | 결과 | 이유 / 손실 여부 |
|---|---|---|
double d = 10; | 10.0 | int→double 자동, 손실 없음 |
int i = (int)3.9; | 3 | 강제, 소수점 버림(손실) |
(byte)300 | 44 | 강제, 범위 초과 → 오버플로우 |
5 / 2 | 2 | 정수÷정수=정수, .5 버림 |
(double)5 / 2 | 2.5 | 한쪽 double 승격 → 실수 나눗셈 |
5.0 / 2 | 2.5 | double 리터럴로 실수 나눗셈 |
'A' + 1 | 66 | char→int(65), 정수로 계산 |
(char)65 | 'A' | int→char, 65번 문자 |
int→double). 안 써도 되고 값 손실 없음.(타입)을 붙임. (int)3.9==3(반올림 아닌 버림), (byte)300은 오버플로우로 엉뚱한 값.5/2==2. 실수 결과는 (double)5/2 또는 5.0/2 → 2.5.int + double → double).char는 사실 숫자: 'A'==65, 'A'+1==66(정수), (char)65=='A'.