java, 안전한 연산을 위하여 - 오버플로우, 부동소수점, NaN, Infinity 문제
오버플로우
- 제한된 메모리를 가진 정수가 그 한계 이상을 초과하여 원치 않는 결과를 발생하는 경우가 있다. 이런 경우를 오버플로우라고 하며 그 예시는 아래 코드와 같다.
- Integer#toBinaryString는 정수를 이진법으로 나타낸 문자열을 리턴한다.
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE)); // 1111111111111111111111111111111
System.out.println(Integer.MAX_VALUE + 1); // -2147483648
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE + 1)); // 10000000000000000000000000000000
- 오버플로우가 발생하더라도 자바에서는 어떤 예외 및 에러 처리를 하지 않는다. 이러한 문제를 방지하기 위하여 Math의 정적 메서드를 활용할 수 있다.
@Test
@DisplayName("덧샘, 정상")
void add(){
final int result = Math.addExact(123, 234);
Assertions.assertThat(result).isEqualTo(357);
}
@Test
@DisplayName("덧셈, 오버플로우")
void add_overflow(){
Assertions.assertThatThrownBy(()->{
Math.addExact(Integer.MAX_VALUE, 1);
}).isInstanceOf(ArithmeticException.class);
}
@Test
@DisplayName("곱셈, 정상")
void multi(){
final int multi = Math.multiplyExact(10, 6);
Assertions.assertThat(multi).isEqualTo(60);
}
@Test
@DisplayName("곱셈, 오버플로우")
void multi_overflow(){
Assertions.assertThatThrownBy(()->{
Math.multiplyExact(Integer.MAX_VALUE/5, 6);
}).isInstanceOf(ArithmeticException.class);
}
unsigned와 long
- 데이타베이스에서 테이블을 만들 때, 보통 pk는 1부터 시작하는 숫자를 사용하곤 한다. pk는 0과 음수가 필요 없으므로 양수만 사용할 수 있도록 해당 칼럼을 정의할 때 unsigned 키워드를 추가한다. 이를 통하여 signed에 대비해 두 배의 메모리를 확보할 수 있다. 이를 통해 한정된 메모리를 가지고 오버플로우의 가능성을 최대한 늦춘다.
- 자바는 unsigned 기능을 제공하나 실질적으로 사용하기 불편하다. 또, 내부 구조를 보면 결국 long을 사용한다. 결과적으로 int에 대한 오버플로우는 특별한 고민 없이 long으로 해결하는 것이 낫다.
String max = Integer.toUnsignedString(Integer.MAX_VALUE);// 2147483647
String maxPlus1 = Integer.toUnsignedString(Integer.MAX_VALUE + 1); // 2147483648
long l = Integer.MAX_VALUE + (long) 1; // 2147483648
// Integer#toUnsignedString 메서드 구조
public static String toUnsignedString(int i) {
return Long.toString(toUnsignedLong(i));
}
부동소수점
- 동일한 값인 0.1을 가진 float과 double이 있다. 십진수에게 있어서 명확한 소수가 이진수의 입장에서는 명확하지 않다.
- 첫 번째 예제인 double을 float으로 다운 캐스팅할 경우 두 개의 값은 간다고 응답한다.
- 두 번재 예제인 float을 double로 프로모션한 경우 두 개의 값은 다르다고 응답한다.
- 왜 이러한 차이가 발생할까?
double d = 0.1d;
float f = 0.1f;
System.out.println("((float)d==f) = " + ((float)d==f)); // true
System.out.println("(d==(double)f) = " + (d == (double) f)); // false
- 십진수에서 1/10은 0.1로 떨어지지만, 1/3은 0.33333으로 영원히 나누어지지 않는 무한 소수이다.
- 이진수에서 1/10은 십진수의 1/3과 같은 무한소수이다.
- 컴퓨터는 이진수 계산만 할 수 있다. 그러므로 0.1을 이진수로 표현하면 무한소수가 된다. 해당 코드는 아래와 같다. 참고로
Long.toBinaryString(Double.doubleToRawLongBits(double d))
형태로 메서드로 출력할 수 있다.
String dToBinary = Long.toBinaryString(Double.doubleToRawLongBits(d));
System.out.println("dToBinary = " + dToBinary); // 11111110111001100110011001100110011001100110011001100110011010
String fToBinary = Integer.toBinaryString(Float.floatToRawIntBits(f));
System.out.println("fToBinary = " + fToBinary); // 111101110011001100110011001101
String fToDToBinary = Long.toBinaryString(Double.doubleToRawLongBits((double) f));
System.out.println("fToDToBinary = " + fToDToBinary); // 11111110111001100110011001100110100000000000000000000000000000
- 결과를 부호 - 지수부 - 가수부로 나누면 다음과 같다.
1-11111101110-01100110011001100110011001100110011001100110011010
1-11101110-011001100110011001101
1-11111101110-01100110011001100110100000000000000000000000000000
- 가수부를 기준으로 double과 float인 첫 번째와 두 번재 결과를 비교하자. float의 반올림을 제외한 ‘01100110011001100110’ 이 일치함을 확인할 수 있다.
- float을 double로 프로모션한 세 번째 예제를 볼 경우 남은 공간을 0으로 할당한다. 그러므로 0으로 떨어지지 않는 이진수의 무한소수 0.1을 double로 변환할 경우 두 개는 다르다고 출력한다. 반대로 double을 float으로 변경할 경우 두 개를 같은 값이라고 출력한다.
- 십진수의 세계의 세계에 사는 우리는 이진법으로 나누기를 하는 컴퓨터의 계산을 신뢰해서는 안된다. 엄격한 소수 계산이 필요한 경우 BigDecimal을 사용한다. cpu가 하드웨어적으로 이진수로 숫자를 처리한다면, BigDecimal은 소프트웨어적으로 10진수를 처리한다.
NaN, Infinity 연산
- 0을 나눌 경우 NaN 혹은 Infinity를 반환한다. 정확하게 / 연산은 Infinity를, %는 NaN으로 응답한다.
System.out.println((double) 5 / (double) 0); // Infinity
System.out.println(Double.isInfinite((double) 5 / (double) 0)); // true
System.out.println((double) 5 % (double) 0); // NaN
System.out.println(Double.isNaN((double) 5 % (double) 0)); // true
참고 https://www.youtube.com/watch?v=vOO-oLS0H68 https://www.youtube.com/watch?v=wI7mVv1GYwA