1. 소개

이 사용방법(예제)에서는 Java에서 숫자 데이터 유형의 오버플로 및 언더 플로를 살펴 봅니다.

우리는 더 이론적 인 측면에 대해 더 깊이 파고 들지 않을 것입니다. 우리는 Java에서 발생하는 경우에만 집중할 것입니다.

먼저 정수 데이터 유형을 살펴본 다음 부동 소수점 데이터 유형을 살펴 보겠습니다. 두 가지 모두에 대해 오버플로 또는 언더 플로가 발생하는시기를 감지하는 방법도 살펴볼 것입니다.

2. 오버플로와 언더 플로

간단히 말해, 오버플로 및 언더 플로는 변수의 선언 된 데이터 유형 범위를 벗어난 값을 할당 할 때 발생합니다.

(절대) 값이 너무 크면 오버플로라고하고 값이 너무 작 으면 언더 플로라고합니다. 

우리가 값을 대입 할 때의 예에서 살펴 보자 (10) 1000  (A 11000 형의 변수에 제로) INT 또는 더블 . Java int 또는 double 변수에 대한 값이 너무 커서 오버플로가 발생합니다.

두 번째 예로서 double  유형의 변수에 10 -1000(0에 매우 가까운)을 할당하려고한다고 가정 해 보겠습니다 . 이 값은 Java 이중 변수에 비해 너무 작아서 언더 플로가 발생합니다.

이러한 경우 Java에서 어떤 일이 발생하는지 자세히 살펴 보겠습니다.

3. 정수 데이터 유형

Java의 정수 데이터 유형은 byte (8 비트), short (16 비트), int (32 비트) 및 long  (64 비트)입니다.

여기서는 int 데이터 유형 에 중점을 둘 것 입니다. 최소값과 최대 값이 다르다는 점을 제외하고는 다른 데이터 유형에도 동일한 동작이 적용됩니다.

Java에서 int 유형의 정수는 음수 또는 양수일 수 있습니다. 즉, 32 비트로 -2 31  ( -2147483648 )에서 2 31 -1 ( 2147483647 ) 사이의 값을 할당 할 수 있습니다 .

래퍼 클래스 IntegerInteger.MIN_VALUE  및  Integer.MAX_VALUE 값을 보유하는 두 개의 상수를 정의합니다 .

3.1.

int 유형 의 변수 m정의하고 너무 큰 값 (예 : 21474836478 = MAX_VALUE + 1) 을 할당하려고하면 어떻게됩니까?

이 할당의 가능한 결과는 m이 정의되지 않거나 오류가 발생하는 것입니다.

둘 다 유효한 결과입니다. 그러나, 자바에서의 값 m이 될 것이다 -2147483648 (최소 값). 반면에 -2147483649 ( = MIN_VALUE – 1 ) 의 값을 지정하려고하면 m2147483647 (최대 값)이됩니다. 이 동작을 정수 순환이라고합니다.

이 동작을 더 잘 설명하기 위해 다음 코드 스 니펫을 고려해 보겠습니다.

int value = Integer.MAX_VALUE-1;
for(int i = 0; i < 4; i++, value++) {
    System.out.println(value);
}

오버플로를 보여주는 다음 출력이 표시됩니다.

2147483646
2147483647
-2147483648
-2147483647

4. 정수 데이터 유형의 언더 플로 및 오버플로 처리

Java는 오버 플로우가 발생할 때 예외를 발생시키지 않습니다. 그렇기 때문에 오버플로로 인한 오류를 찾기가 어려울 수 있습니다. 대부분의 CPU에서 사용 가능한 오버플로 플래그에 직접 액세스 할 수도 없습니다.

그러나 가능한 오버플로를 처리하는 방법에는 여러 가지가 있습니다. 이러한 가능성 중 몇 가지를 살펴 보겠습니다.

4.1. 다른 데이터 유형 사용

2147483647 보다 큰 값 (또는 -2147483648 보다 작은 값)을 허용 하려면 단순히 long 데이터 유형 또는 BigInteger를 대신 사용할 수 있습니다 .

long 유형의 변수 도 오버플로 될 수 있지만 최소값과 최대 값이 훨씬 더 크고 대부분의 상황에서 충분할 수 있습니다.

BigInteger 의 값 범위 는 JVM에서 사용 가능한 메모리 양을 제외하고는 제한되지 않습니다.

BigInteger로 위의 예를 다시 작성하는 방법을 살펴 보겠습니다 .

BigInteger largeValue = new BigInteger(Integer.MAX_VALUE + "");
for(int i = 0; i < 4; i++) {
    System.out.println(largeValue);
    largeValue = largeValue.add(BigInteger.ONE);
}

다음 출력이 표시됩니다.

2147483647
2147483648
2147483649
2147483650

출력에서 볼 수 있듯이 여기에는 오버플로가 없습니다. Java의 BigDecimalBigInteger 기사 에서는 BigInteger 를 더 자세히 다룹니다 .

4.2. 예외 발생

더 큰 값을 허용하지 않거나 오버플로가 발생하는 것을 원하지 않고 대신 예외를 던지고 싶은 상황이 있습니다.

Java 8부터 정확한 산술 연산을 위해 메소드를 사용할 수 있습니다. 먼저 예를 살펴 보겠습니다.

int value = Integer.MAX_VALUE-1;
for(int i = 0; i < 4; i++) {
    System.out.println(value);
    value = Math.addExact(value, 1);
}

정적 메서드 addExact () 는 일반적인 추가를 수행하지만 작업 결과 오버플로 또는 언더 플로가 발생하면 예외가 발생합니다.

2147483646
2147483647
Exception in thread "main" java.lang.ArithmeticException: integer overflow
	at java.lang.Math.addExact(Math.java:790)
	at baeldung.underoverflow.OverUnderflow.main(OverUnderflow.java:115)

addExact () 외에도 Java 8 Math 패키지는 모든 산술 연산에 해당하는 정확한 메소드를 제공합니다. 이러한 모든 메소드 List은 Java 문서참조하십시오 .

또한 다른 데이터 유형으로 변환하는 동안 오버플로가 발생하면 예외를 발생시키는 정확한 변환 방법이 있습니다.

long 에서 int 로의 변환 :

public static int toIntExact(long a)

BigInteger 에서 int 또는 long으로 변환하려면 다음을 수행하십시오 .

BigInteger largeValue = BigInteger.TEN;
long longValue = largeValue.longValueExact();
int intValue = largeValue.intValueExact();

4.3. Java 8 이전

정확한 산술 방법이 Java 8에 추가되었습니다. 이전 버전을 사용하는 경우 이러한 방법을 직접 만들 수 있습니다. 이를위한 한 가지 옵션은 Java 8에서와 동일한 방법을 구현하는 것입니다.

public static int addExact(int x, int y) {
    int r = x + y;
    if (((x ^ r) & (y ^ r)) < 0) {
        throw new ArithmeticException("int overflow");
    }
    return r;
}

5. 정수가 아닌 데이터 유형

정수가 아닌 유형 float  및 double 은 산술 연산과 관련하여 정수 데이터 유형과 동일한 방식으로 작동하지 않습니다.

한 가지 차이점은 부동 소수점 숫자에 대한 산술 연산으로 인해 NaN이 발생할 수 있다는 것입니다 . Java의 NaN에 대한 전용 기사가 있으므로이 기사에서  더 자세히 살펴 보지 않겠습니다. 또한 Math 패키지 에는 정수가 아닌 유형에 대한 addExact  또는 multiplyExact 와 같은 정확한 산술 메서드가 없습니다 .

Java 는 부동 소수점이중 데이터 유형에 대해 IEEE 표준 부동 소수점 산술 (IEEE 754)  을 따릅니다 . 이 표준은 Java가 부동 소수점 숫자의 오버플로 및 언더 플로를 처리하는 방식의 기초입니다.

아래 섹션에서는 이중 데이터 유형 의 오버플로 및 언더 플로와 이러한 유형이 발생하는 상황을 처리하기 위해 할 수있는 작업에 중점을 둘 것 입니다.

5.1. 과다

정수 데이터 유형의 경우 다음을 예상 할 수 있습니다.

assertTrue(Double.MAX_VALUE + 1 == Double.MIN_VALUE);

그러나 부동 소수점 변수의 경우에는 해당되지 않습니다. 다음은 사실입니다.

assertTrue(Double.MAX_VALUE + 1 == Double.MAX_VALUE);

이중 값에는 제한된 수의 유효 비트있기 때문 입니다. double 값의 값을 1만큼만 늘리면 중요한 비트를 변경하지 않습니다. 따라서 값은 동일하게 유지됩니다.

변수의 중요한 비트 중 하나를 늘리도록 변수 값을 늘리면 변수의 값은 INFINITY가됩니다 .

assertTrue(Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);

NEGATIVE_INFINITY 음의 값의 경우 :

assertTrue(Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

정수와 달리 랩 어라운드가 없지만 오버플로의 두 가지 다른 가능한 결과를 볼 수 있습니다. 값이 동일하게 유지되거나 특수 값인 POSITIVE_INFINITY 또는 NEGATIVE_INFINITY 중 하나를 얻습니다 .

5.2. 언더 플로

double의 최소값에 대해 정의 된 두 개의 상수 인 MIN_VALUE  (4.9e-324) 및 MIN_NORMAL (2.2250738585072014E-308)입니다.

부동 소수점 산술을위한 IEEE 표준 (IEEE 754)  은 이들 간의 차이점에 대한 자세한 내용을 설명합니다.

부동 소수점 숫자에 대해 최소값이 필요한 이유에 대해 중점적으로 살펴 보겠습니다.

이중 우리 만 값을 나타내는 비트의 수가 제한된 값으로 임의로 작게 할 수 없다.

Java SE 언어 사양의 유형, 값 및 변수대한 장에서는 부동 소수점 유형이 표시되는 방법을 설명합니다. double 의 이진 표현에 대한 최소 지수 -1074 로 제공됩니다 . 즉, double이 가질 수있는 가장 작은 양의 값은 4.9e-324와 같은 Math.pow (2, -1074) 입니다.

결과적으로  Java 에서 double 의 정밀도는 0과 4.9e-324 사이의 값 또는 음수 값에 대해 -4.9e-3240 사이의 값을 지원하지 않습니다 .

그렇다면 double 유형의 변수에 너무 작은 값을 할당하려고하면 어떻게됩니까 ? 예를 살펴 보겠습니다.

for(int i = 1073; i <= 1076; i++) {
    System.out.println("2^" + i + " = " + Math.pow(2, -i));
}

출력 포함 :

2^1073 = 1.0E-323
2^1074 = 4.9E-324
2^1075 = 0.0
2^1076 = 0.0

너무 작은 값을 할당하면 언더 플로가 발생하고 결과 값은 0.0 (양수 0)이됩니다.
마찬가지로 음수 값의 경우 언더 플로 값은 -0.0 (음수 0)이됩니다.

6. 부동 소수점 데이터 유형의 언더 플로 및 오버플로 감지

오버플로는 양수 또는 음수 무한대가되고 양수 또는 음수 0에서 언더 플로가 발생하므로 정수 데이터 유형과 같은 정확한 산술 방법이 필요하지 않습니다. 대신 이러한 특수 상수를 확인하여 오버플로 및 언더 플로를 감지 할 수 있습니다.

이 상황에서 예외를 던지고 싶다면 도우미 메서드를 구현할 수 있습니다. 지수를 어떻게 구할 수 있는지 살펴 보겠습니다.

public static double powExact(double base, double exponent) {
    if(base == 0.0) {
        return 0.0;
    }
    
    double result = Math.pow(base, exponent);
    
    if(result == Double.POSITIVE_INFINITY ) {
        throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY");
    } else if(result == Double.NEGATIVE_INFINITY) {
        throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY");
    } else if(Double.compare(-0.0f, result) == 0) {
        throw new ArithmeticException("Double overflow resulting in negative zero");
    } else if(Double.compare(+0.0f, result) == 0) {
        throw new ArithmeticException("Double overflow resulting in positive zero");
    }

    return result;
}

이 메서드에서는 Double.compare () 메서드를 사용해야합니다 . 일반 비교 연산자 ( <> )는 양수와 음수 0을 구분하지 않습니다.

7. 포지티브 및 네거티브  제로

마지막으로 양수 및 음수 0 및 무한대를 사용할 때주의해야하는 이유를 보여주는 예를 살펴 보겠습니다.

시연 할 몇 가지 변수를 정의 해 보겠습니다.

double a = +0f;
double b = -0f;

양수와 음수 0 은 같은 것으로 간주 되기 때문입니다 .

assertTrue(a == b);

양의 무한대와 음의 무한대는 다른 것으로 간주됩니다.

assertTrue(1/a == Double.POSITIVE_INFINITY);
assertTrue(1/b == Double.NEGATIVE_INFINITY);

그러나 다음 주장은 정확합니다.

assertTrue(1/a != 1/b);

그것은 우리의 첫 번째 주장과 모순되는 것 같습니다.

8. 결론

이 기사에서는 오버 플로우와 언더 플로우가 무엇인지, Java에서 어떻게 발생할 수 있는지, 정수와 부동 소수점 데이터 유형의 차이점을 살펴 보았습니다.

또한 프로그램 실행 중에 오버플로 및 언더 플로를 감지하는 방법도 살펴 보았습니다.

평소처럼 전체 소스 코드는 Github에서 사용할 수 있습니다 .