숫자 변환과 정밀도
서로 다른 숫자 타입을 계산하면 Java는 정해진 승격 규칙에 따라 공통 타입을 선택합니다.
작은 타입끼리 계산해도 결과가 int가 되는 규칙은 초반에 자주 혼동을 만듭니다.
넓은 타입으로의 변환은 대체로 자동이지만 좁은 타입으로의 변환은 정보가 사라질 수 있어 명시적 캐스트가 필요합니다.
캐스트는 값을 안전하게 만드는 검사가 아니라 손실을 감수하겠다는 표시입니다.
이 절에서는 변환 전후에 무엇이 보존되는지 확인합니다.
이항 연산은 먼저 타입을 승격한다
byte + byte의 결과가 byte가 아니라 int인 이유는 산술 연산이 최소 int 정밀도에서 이루어지기 때문입니다.
대입 대상 타입이 작다고 계산 타입까지 작아지지는 않습니다.
확대 변환도 모든 값을 정확히 보존하지는 않는다
int를 long으로 옮기면 정확하지만 큰 long을 double로 바꾸면 정밀도를 잃을 수 있습니다.
범위가 넓다는 사실과 모든 정수를 정확히 표현한다는 사실은 다릅니다.
NumericConversions.java에서는 캐스팅을 나눗셈 전후 어디에 두는지와 안전한 정수 축소가 각각 다른 결과를 만듭니다. 각 출력 전에 피연산자 타입을 먼저 적어 보고 값을 예측해 보세요.
public class NumericConversions {
public static void main(String[] args) {
int total = 7;
int count = 2;
System.out.println("integer=" + total / count);
System.out.println("decimal=" + (double) total / count);
long safe = 2_000_000_000L;
System.out.println(Math.toIntExact(safe));
try {
Math.toIntExact(5_000_000_000L);
} catch (ArithmeticException error) {
System.out.println("out of int range");
}
}
}앞의 두 줄은 캐스팅 시점이 나눗셈 결과를 바꾼다는 점을, 뒤의 두 줄은 Math.toIntExact가 안전한 축소와 범위 초과를 구분한다는 점을 보여 줍니다. long → double 정밀도 손실은 아래 비교 코드의 large와 approximate를 각각 출력해 이어서 확인할 수 있습니다.
integer=3
decimal=3.5
2000000000
out of int range실수 대입 전에 이미 정수 나눗셈을 했다면 버린 소수 부분은 되돌아오지 않습니다.
숫자 변환은 “더 큰 타입인가?”만으로 안전성을 결정할 수 없습니다.
byte + byte도 산술 전에int로 승격됩니다.int → long은 모든 값을 보존하지만long → double은 큰 정수의 낮은 자릿수를 잃을 수 있습니다.long → int처럼 범위를 줄이는 변환은 명시적 캐스팅이 있어도 상위 비트가 사라집니다.
따라서 변환 전에는 범위, 변환 뒤에는 값의 정확성을 각각 확인해야 합니다.
축소 변환은 범위를 먼저 검사한다
(int) longValue는 상위 비트를 버릴 수 있습니다.
Math.toIntExact는 범위를 벗어나면 예외를 내므로 조용한 손실을 원하지 않을 때 더 명확합니다.
축소 캐스팅은 컴파일러에게 손실 가능성을 알고 있다는 표시일 뿐 안전 검사가 아닙니다. int 범위를 벗어난 long은 하위 비트만 남으므로, 캐스팅 전에 범위를 검사하거나 Math.toIntExact로 실패를 명시하는 편이 낫습니다.
byte left = 10;
byte right = 20;
int sum = left + right; // 작은 정수도 산술 전에 int로 승격long large = 9_007_199_254_740_993L;
double approximate = large; // 자동 변환되지만 정확도 손실 가능
// int narrowed = large; // 컴파일 오류: 명시적 축소가 필요나눗셈은 피연산자 타입이 결과를 정한다
정수끼리 나누면 소수 부분이 먼저 버려지고 그 뒤 double에 대입해도 복구되지 않습니다.
계산 전에 피연산자 하나를 실수로 바꿔야 합니다.
금액은 이진 부동소수점과 분리한다
정확한 십진 반올림 규칙이 필요한 값은 문자열에서 BigDecimal로 만들고 스케일과 반올림 방식을 명시합니다.
double을 거쳐 만들면 이미 생긴 근사 오차가 함께 들어옵니다.
new BigDecimal(0.1)은 이미 근삿값인 double을 받아 오차까지 보존합니다. new BigDecimal("0.1")이나 BigDecimal.valueOf(0.1)로 바꿔 결과를 비교하면 금액 계산에서 입력 경로가 왜 중요한지 확인할 수 있습니다.
숫자 변환에서는 문법상 가능한가보다 범위와 정밀도 가운데 무엇이 사라질 수 있는지를 먼저 확인해야 합니다.