자바 식의 결과 타입

10^6 개 중 중복 없이 2개를 조합하는 경우의 수를 계산하는 코드를 다음과 같이 작성했다.
하지만 실행해보면 완전 엉뚱한 값이 반환된다.

1
2
3
int a = Math.pow(10, 6);
long b = a * (a - 1) / 2;
System.out.println(b); //-364189984라는 값이 나온다.

음수가 나오는 것으로 봐서는 타입에서 감당할 수 있는 범위를 벗어난 값을 표현하려고 해서 오버플로우가 발생했음을 추측할 수 있다.
하지만 10^6 * (10^6 - 1) / 2는 499999500000이다.
long은 8바이트(64비트)로 최대 2^63까지 저장할 수 있다. 즉 b에는 충분히 계산값을 저장할 수 있어야 한다.

하지만 왜 오버플로우가 발생했을까?
이를 위해서는 자바의 식 결과 타입을 어떻게 결정하는 지 알아야 한다.

일단 식 (expression)은 하나의 결과 값을 반환하는 코드로 변수, 연산자, 함수 호출 등으로 이뤄져있다.
1 + 2, int a = 1 이런 코드들이 모두 식이다.
식의 반환 타입은 식의 구성 요소들에 따라 달라진다.

여기서 우리가 작성했던 코드를 다시 보자.
문제가 됐었던 부분은 바로 이 부분이다.

1
long b = a * (a - 1) / 2;

여기서 코드는 한 줄이지만 여러 식으로 분해할 수 있다.

  1. a * (a - 1) -> 이 식도 여러 식으로 나눌 수 있겠지만 생략.
  2. {결과 값} / 2
  3. long b = {결과 값}

여기서 1번식은 어떤 타입을 반환할까? 산술 연산은 서로 다른 타입이 연산될 경우 피연산자를 일반적인 타입으로 변환하고 연산을 진행한다.
하지면 1번식은 모두 int 타입만 사용되고 있다. 즉 1번 식의 반환 값은 int 타입이다.
int형은 4바이트(32비트)로 최대 20억의 값만 저장할 수 있다. 그러나 1번식의 결과값은 999999000000이다. 그래서 식의 결과를 제대로 저장하지 못하고 오버플로우가 발생했다.

이런 문제를 해결하려면 식의 피연산자의 타입을 long으로 캐스팅하면 된다.

1
2
long b = (long) a * (a - 1) / 2;
long b = a * (long) (a - 1) / 2;

하지만 다음과 같이 식 밖에서 캐스팅을 하면 안된다. 이미 식이 수행되고 나서 캐스팅은 의미가 없기 때문이다.

1
long b = (long) (a * (a - 1)) / 2;
Share