빌드노트
long에 넣었는데 int 곱셈 결과가 음수다
int 두 값의 곱은 long에 대입하기 전에 이미 넘칠 수 있다. 곱셈 뒤 캐스트와 곱셈 전 승격의 실제 출력, 작은 값과 경계값을 비교하고 Math.multiplyExact의 int·long 반환 타입에 따라 허용할 결과 범위를 고르는 방법을 설명한다.
Java에서 int 두 값을 곱해 long 변수에 넣었는데 음수가 나오면, 곱셈식에 들어간 두 값의 타입부터 확인한다. 둘 다 int면 곱셈도 int로 끝난 뒤 long에 저장된다. 결과를 (long) (a * b)로 감싸도 이미 넘친 값은 복구되지 않는다. 더 큰 곱을 받아야 한다면 (long) a * b처럼 곱셈 전에 한쪽을 바꾼다.
반대로 결과가 반드시 int 범위 안이어야 한다면 Math.multiplyExact(a, b)로 범위를 넘는 입력을 거절할 수 있다. 두 방법은 허용하는 결과 범위가 다르다.
같은 입력에서 캐스트 위치를 바꿔 본다
JDK 17 이상이 설치되어 있고 터미널에서 java를 실행할 수 있어야 한다. 빈 작업 폴더에 다음 내용을 MultiplyDemo.java로 저장한다. 외부 라이브러리는 필요 없다.
public class MultiplyDemo {
public static void main(String[] args) {
int a = 50_000, b = 50_000;
long assigned = a * b;
long castAfter = (long) (a * b);
long promoted = (long) a * b;
System.out.println("assigned = " + assigned);
System.out.println("castAfter = " + castAfter);
System.out.println("promoted = " + promoted);
try {
long exact = Math.multiplyExact(a, b);
System.out.println("exact = " + exact);
} catch (ArithmeticException e) {
System.out.println("exact = " + e.getClass().getSimpleName());
}
System.out.println("exactLong = " + Math.multiplyExact((long) a, (long) b));
}
}
파일을 저장한 폴더에서 실행한다. JDK 17의 소스 파일 실행 방식 (새 탭에서 열림)을 쓰므로 별도의 컴파일 명령은 필요 없다.
java MultiplyDemo.java
Windows 11의 OpenJDK 17.0.2에서 직접 얻은 출력이다. 운영 장애 로그가 아니라 고정 정수로 따로 실행한 결과다.
assigned = -1794967296
castAfter = -1794967296
promoted = 2500000000
exact = ArithmeticException
exactLong = 2500000000
수학적으로 50_000 × 50_000은 2_500_000_000이다. int의 최댓값 2_147_483_647보다 크다. 첫 두 줄은 이 범위를 넘긴 뒤의 값을 그대로 담았고, 세 번째 줄은 곱셈 자체를 long으로 수행했다.
출력이 다르다면 먼저 a와 b가 둘 다 int인지, 값이 각각 50_000인지 확인한다. 피연산자를 이미 long으로 바꿨다면 비교 조건이 달라진다. java를 찾지 못하거나 파일을 열 수 없다는 오류라면 아직 이 계산을 실행한 상태가 아니므로 JDK 설치와 현재 폴더, 파일 이름부터 확인한다.
저장할 타입이 곱셈 타입을 바꾸지는 않는다
Java 17 언어 명세의 곱셈 규칙 (새 탭에서 열림)은 두 피연산자에 숫자 승격을 적용하고, 그 타입으로 곱셈한다고 정한다. 이 예제에서 각 식의 타입은 다음과 같다.
a * b의 식 타입은int다.long assigned는 계산이 끝난int값을 받아long으로 넓힌다.(long) (a * b)전체의 식 타입은long이지만, 괄호 안의 곱셈은 여전히int다.(long) a * b는 곱하기 전에a가long이 된다. 다른 피연산자b도long으로 승격되어 곱셈 결과의 타입이long이다.
마지막 변환은 숫자 승격 규칙 (새 탭에서 열림)에 따른다. 두 입력이 이 예제처럼 int와 long이면 long으로 맞춰 연산한다. 둘 다 캐스트할 필요는 없다.
int 곱셈의 오버플로는 자동으로 예외가 되지 않는다. 곱셈의 오버플로 규칙 (새 탭에서 열림)에 따라 수학적 곱의 하위 32비트가 결과로 남는다. 이후 int를 long으로 바꾸면 정수 확대 변환 규칙 (새 탭에서 열림)이 그 int 값을 그대로 보존한다. 그래서 첫 두 줄의 음수가 long에도 남는다.
작은 값으로는 이 차이가 가려졌다. 별도 경계 실험에서 1_000 × 2_000은 세 방식 모두 2_000_000이었고, multiplyExact(int, int)도 같은 값을 반환했다. 46_340 × 46_340까지는 2_147_395_600으로 일치했다. 바로 다음 정수인 46_341을 제곱하자 int 곱셈은 -2_147_479_015, 곱셈 전 승격은 2_147_488_281이 됐으며 multiplyExact(int, int)는 예외를 던졌다.
음수만 찾아도 충분하지 않다. 같은 실험에서 65_536 × 65_536의 int 결과는 0, Integer.MAX_VALUE × Integer.MAX_VALUE의 결과는 1이었다. 곱셈 전에 승격했을 때의 실제 값은 각각 4_294_967_296, 4_611_686_014_132_420_609였다. 결과의 부호만으로 오버플로 여부를 판별할 수 없다.
허용할 결과 범위에 맞춰 고른다
두 int의 곱을 long으로 보관할 목적이라면 long total = (long) a * b;로 계산한다. 두 int 값만 한 번 곱하는 경우에는 수학적 곱이 long 범위 안에 들어간다. 이는 정수 범위로부터 계산할 수 있는 성질이다. int의 절댓값은 최대 2의 31제곱이므로 곱의 절댓값은 최대 2의 62제곱이다.
int 범위를 넘는 곱을 실패로 처리할 목적이라면 Math.multiplyExact(a, b)를 쓴다. a와 b가 int이므로 반환 타입도 int인 메서드가 선택된다. 예제처럼 받는 변수를 long exact로 선언해도 바뀌지 않는다. 이번 입력은 곱셈 호출에서 예외가 나므로 그 변수에 값을 저장하지 못했다. 이 동작은 multiplyExact(int, int)의 계약 (새 탭에서 열림)에 명시되어 있다.
long 범위를 기준으로 실패시킬 목적이라면 Math.multiplyExact((long) a, (long) b)를 쓴다. 이 호출의 반환 타입은 long이다. 예제의 2_500_000_000은 허용하므로 int 상한을 검사하는 용도로는 사용할 수 없다. API가 검사하는 것은 선택한 정수 타입의 범위다. 별도의 입력 상한이 있다면 그 조건도 따로 검사해야 한다.
long 입력까지 다루면 long 곱셈에도 한계가 있다. 추가 실행에서 Long.MAX_VALUE * 2L은 예외 없이 -2가 됐고, 같은 두 값을 multiplyExact(long, long) (새 탭에서 열림)에 넘기자 ArithmeticException이 났다. 따라서 더 큰 입력을 받는 코드에서는 long으로 바꾸는 조치와 범위를 넘으면 실패시키는 조치를 함께 검토한다.
여기서 직접 확인한 범위는 OpenJDK 17.0.2의 고정 정수 입력, 두 값의 곱, 캐스트 위치와 두 multiplyExact 반환 타입의 차이다. 다른 JDK 버전, 금융 계산, 성능은 검증하지 않았다. 실제 코드에서는 곱셈 직전의 피연산자 타입과 허용할 결과 범위를 확인해 수정할 식을 고르면 된다.