빌드노트
Math.abs를 호출해도 Integer.MIN_VALUE는 음수다
Math.abs(int)는 Integer.MIN_VALUE에서 같은 음수를 반환한다. JShell로 호출 전후의 long 변환과 absExact를 비교하고, 결과 범위를 넓힐지 int 범위 초과를 거부할지 고른다.
int 값을 Math.abs(n)으로 바꿔 인덱스나 크기에 쓰는 코드라면, 입력이 Integer.MIN_VALUE일 때를 확인해야 한다. 이 값에서는 예외 없이 음수가 그대로 나온다. 모든 int 입력의 절댓값을 보관하려면 호출 전에 long으로 넓히고, 결과가 int 범위를 넘어서는 입력을 거부하려면 Math.absExact(n)을 쓴다. 받는 변수만 long으로 바꾸는 것으로는 해결되지 않는다.
-7과 0에서는 보이지 않는 차이
JDK가 설치되어 있고 터미널에서 jshell을 실행할 수 있으면 다음 비교를 할 수 있다. 뒤에서 쓰는 absExact까지 실행하려면 JDK 15 이상이 필요하다. 이번에 직접 실행한 환경은 Windows 11, OpenJDK·JShell 17.0.2다. 실제 업무 데이터 대신 고정 정수만 사용했다.
먼저 터미널에서 JShell을 연다. --no-startup은 시작 스크립트를 불러오지 않는 옵션이다. JDK 17 JShell 명령 문서 (새 탭에서 열림)
jshell --no-startup
이어서 아래 코드를 붙여 넣는다. 같은 입력을 그대로 계산한 abs, 계산 뒤에 long으로 바꾼 after, 계산 전에 바꾼 before를 한 줄에서 비교한다.
for (int n : new int[] {-7, 0, Integer.MIN_VALUE}) {
System.out.printf("n=%d | abs=%d | after=%d | before=%d%n",
n, Math.abs(n), (long) Math.abs(n), Math.abs((long) n));
}
직접 얻은 출력에서 JShell 프롬프트를 제외하면 다음 세 줄이다.
n=-7 | abs=7 | after=7 | before=7
n=0 | abs=0 | after=0 | before=0
n=-2147483648 | abs=-2147483648 | after=-2147483648 | before=2147483648
-7은 세 방식 모두 7이 되고 0은 그대로 0이다. 하지만 마지막 줄에서는 after도 음수다. 결과의 타입을 나중에 바꾸어도 계산 결과는 복구되지 않았다. 반면 before는 필요한 절댓값을 얻었다. 이 비교에서 원하는 값은 항상 양수가 아니라 0 이상인 값이다.
int에는 2,147,483,648이 없다
Java 17 언어 명세의 정수 범위 (새 탭에서 열림)에 따르면 int는 -2_147_483_648부터 2_147_483_647까지다. 음수 쪽에 값이 하나 더 있다. 가장 작은 값의 수학적 절댓값인 2_147_483_648은 int에 담을 수 없다. Math.abs(int)의 계약 (새 탭에서 열림)도 이 입력에서는 같은 음수를 반환한다고 명시한다.
따라서 먼저 확인할 것은 abs에 들어가는 인자의 타입이다. n이 int면 Math.abs(n)은 int를 반환한다. long magnitude = Math.abs(n);이라고 적어도 호출은 그대로다. 호출 뒤의 int → long 변환은 이미 나온 음수를 보존한다. 이는 정수 확대 변환 규칙 (새 탭에서 열림)에 따른 동작이다.
모든 int 입력의 절댓값을 허용할 코드라면 long magnitude = Math.abs((long) n);으로 바꾼다. 인자를 먼저 long으로 바꾸므로 long을 반환하는 메서드가 선택된다. 위 출력의 before가 이 경우다. 모든 int 입력의 절댓값은 최대 2_147_483_648이므로 long 범위에 들어간다. 이 결론은 정수 범위에서 도출한 것이며, 전체 입력을 열거한 실험 결과는 아니다.
int 범위를 유지해야 하면 예외로 거부한다
호출하는 API나 저장 형식이 계속 int를 요구한다면, 2_147_483_648을 받아들이는 해결은 맞지 않는다. Math.absExact(int) (새 탭에서 열림)는 Java 15부터 제공되며, 반환 타입은 int다. 절댓값이 그 범위를 넘는 Integer.MIN_VALUE에서 ArithmeticException을 던진다.
같은 JShell에서 앞서 비교한 세 입력을 이번에는 absExact에 한 줄씩 넘겨 본다.
Math.absExact(-7)
Math.absExact(0)
Math.absExact(Integer.MIN_VALUE)
앞의 두 식은 각각 7과 0을 반환했다. 마지막 식은 다음 예외로 실패했다. 스택 위치는 생략했다.
| Exception java.lang.ArithmeticException: Overflow to represent absolute value of Integer.MIN_VALUE
이 정책을 고르면 범위를 넘는 입력을 호출 경계에서 오류로 처리해야 한다. 예외를 잡아 원래 입력을 그대로 돌려주면 음수가 다시 다음 계산으로 넘어간다. absExact를 찾을 수 없다는 컴파일 오류가 나오면 먼저 사용 중인 JDK 버전을 확인한다. Java 15 미만에는 이 API가 없으므로, 업그레이드할 수 없다면 n == Integer.MIN_VALUE를 명시적으로 검사해 거부한 뒤 나머지 입력에 Math.abs(n)을 적용한다. 구버전 실행 자체는 이번에 검증하지 않았다.
또한 long으로 계산한 뒤 강제로 int에 넣는 것은 범위 검사가 아니다. 별도 실행에서 (int) Math.abs((long) Integer.MIN_VALUE)는 다시 -2147483648이 됐다. 정수 축소 변환 규칙 (새 탭에서 열림)에 따라 값의 일부가 버려지기 때문이다. 결과를 실제로 보관할 타입까지 정한 뒤 두 방법 중 하나를 선택해야 한다.
입력이 long이면 경계도 long으로 옮겨 간다
int를 먼저 넓히는 방법은 원래 입력이 int일 때의 해결이다. 입력부터 long이면 Long.MIN_VALUE의 절댓값은 long에도 들어가지 않는다. 같은 JShell에서 다음 두 식으로 확인할 수 있다.
Math.abs(Long.MIN_VALUE)
Math.absExact(Long.MIN_VALUE)
직접 실행했을 때 첫 식은 -9223372036854775808을 반환했고, 둘째 식은 ArithmeticException을 던졌다. Math.abs(long) (새 탭에서 열림)과 Math.absExact(long) (새 탭에서 열림)의 계약에 해당한다. 이 값까지 받아야 한다면 long 반환 타입 자체를 다시 정해야 한다.
마지막으로 절댓값이 0 이상이라는 사실만으로 유효한 인덱스나 허용 가능한 크기가 되지는 않는다. 인덱스는 대상 길이보다 작은지, 크기는 코드가 허용하는 상한 안인지 별도로 확인해야 한다. absExact도 그 용도의 상한까지 검사하지는 않는다. 여기서 직접 비교한 것은 JDK 17.0.2에서 고정한 정수 입력과 캐스트 위치, 반환 타입, 예외의 차이다. 실제 데이터의 발생 빈도, 난수 분포, 해시 보안이나 성능은 검증하지 않았다.