dsc-sookmyung / 2023-01-Effective-Java-Study

이펙티브 자바 공부하는 스터디입니다
2 stars 3 forks source link

Item60. 정확한 답이 필요하다면 float와 double은 피하라 #55

Open Mingadinga opened 1 year ago

Mingadinga commented 1 year ago

정확한 답이 필요한 계산에는 float나 double은 피하라. 대신 BigDecimal이나 int, long을 사용하라. BigDecimal은 비즈니스 계산에 편리하지만 API가 불편하고 성능 저하가 발생한다. 만약 성능이 중요하고 숫자가 너무 크지 않다면 int(9자리 십진수)나 long(18자리 십진수)을 사용하자. 18자리를 넘어가면 BigDecimal을 사용해야한다.

float, double의 부정확성

float와 double은 과학, 공학 계산용으로 설계되었다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 근사치로 계산하도록 설계되었다. 따라서 정확한 결과가 필요할 때는 사용하면 안된다! 특히 금융 관련 계산과는 맞지 않는다. 0.1 혹은 10의 음의 제곱을 표현할 수 없기 때문이다.

아래 코드는 1달러로 10센트, 20센트, 30센트, … , 1달러 짜리의 사탕을 순서대로 구입하는 코드이다. 원래 의도는 1달러로 10센트, 20센트, 30센트, 40센트 총 4개의 사탕을 구입하고 잔돈은 0이어야한다. 그러나 double을 사용하여 부동소수점 연산을 사용하여 의도한 것과 다른 결과가 나온다.

public class Change {
    // 부동소수타입(double) 사용
    public static void main(String[] args) {
        double funds = 1.00;
        int itemsBought = 0;
        for (double price = 0.10; price <= funds; price += 0.10) {
            funds -= price;
            itemsBought++;
        }
        System.out.println(itemsBought + " items bought.");
        System.out.println("Change: $" + funds);
    }
}

/* 실제 출력 결과
3 items bought.
Change: $0.3999999999999999
 */

/* 의도한 결과
4 items bought.
Change: $0
 */

image

BigDecimal 사용

double 타입을 BigDecimal로 교체했다. BigDecimal 생성자에 문자열을 넘겼다. 계산 시 부정확한 값이 사용되는걸 막기 위한 조치다.

import java.math.BigDecimal;

public class BigDecimalChange {
    public static void main(String[] args) {
        final BigDecimal TEN_CENTS = new BigDecimal(".10");

        int itemsBought = 0;
        BigDecimal funds = new BigDecimal("1.00"); // 문자열 생성자
        for (BigDecimal price = TEN_CENTS;
             funds.compareTo(price) >= 0;
             price = price.add(TEN_CENTS)) {
            funds = funds.subtract(price);
            itemsBought++;
        }
        System.out.println(itemsBought + " items bought.");
        System.out.println("Money left over: $" + funds);
    }
}

/* 출력 결과
4 items bought.
Money left over: $0.00
 */

정확한 계산을 할 수 있지만, 문제는 기본 타입보다 API가 훨씬 불편하고 성능이 느리다는 단점이 있다.

int나 long 사용

기본 타입으로 int나 long을 사용할 수도 있다. 하지만 기존에 비해 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야한다는 단점이 있다. 만약 가능하다면, 소수점을 다룰 필요가 없도록 기본 단위를 작게 해서 사용할 수 있다. 아래 예제에서는 기본 단위를 달러가 아닌 센트로 변경하여 소수점을 다루지 않도록 했다.

public class IntChange {
    public static void main(String[] args) {
        int itemsBought = 0;
        int funds = 100;
        for (int price = 10; funds >= price; price += 10) {
            funds -= price;
            itemsBought++;
        }
        System.out.println(itemsBought + " items bought.");
        System.out.println("Cash left over: " + funds + " cents");
    }
}

/* 출력 결과
4 items bought.
Cash left over: 0 cents
 */

참고 : double → long

[2417번: 정수 제곱근](https://www.acmicpc.net/problem/2417)

public class Main {

    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    public static void main(String[] args) throws IOException {
        long target = Long.parseLong(br.readLine());
        System.out.println(sqrt(target));
    }

    static long sqrt(long n){
        long start = 0;
        long end = n;
        long result = 0;

        while(start <= end) {
            long mid = (start + end) / 2;
//            if (n <= (long) Math.pow(mid, 2)){ // 오답
                            if (n <= Math.pow(mid, 2)){ // 정답
                end = mid - 1;
            } else {
                start = mid+1;
            }
            result = mid;
        }
        return result;
    }
}

image