前提
ご存知の方も多いかと思いますが、javaに限らずコンピュータではfloatやdoubleでの数値計算において浮動小数点数を用いているため、以下のように意図する正確な計算結果が得られない場合があります。
これは、計算効率を上げるためコンピュータが10進数から2進数に変換して計算していることが原因です。
小数を2進数に変換すると有限桁では表せないため、結果的に近似計算になるということです。
ref : https://seiai.ed.jp/sys/text/csc/ch12/c12a300.html
double x = 1.21;
double y = 0.1;
double z = x - y;
System.out.println(z); // 1.1099999999999999
問題
計算機シミュレーションなど大規模な計算を行う場合は便利ですが、金額計算など正確な値が必要となる状況においては上のような誤差はできるだけ避けたいです。
例えば異なる通貨を介した計算などで遭遇しがちな問題でしょうか。
1. BigDecimalを使う
doubleの代わりにBigDecimal型を使用することで、正確な結果を出すことができます。
BigDecimalでは浮動小数点数を使わず、整数値と小数点以下を別々で管理することで丸め込みを回避しているそうです。
add, subtract, multiply, divideを使って四則演算が可能です。
BigDecimal x = new BigDecimal("1.21");
BigDecimal y = new BigDecimal("0.1");
BigDecimal z = x.subtract(y);
System.out.println(z); // 1.11
ただ、計算の規模が大きくなったりや変数が増えて複雑化すると可読性もパフォーマンスも下がることは想像できます。
2. 整数計算に持ち込む
事前に小数点以下の桁数が分かっているかつ桁数が大きくなりすぎない場合は整数で計算を行うのが便利です。
具体的には-2147483648 ~ 2147483647に収まるならint, -9223372036854775808 ~ 9223372036854775807に収まるならlongといった感じです。
計算で出現する最大の小数点以下桁数がnの時、全てに$10^n$をかけて計算し最後に$10^n$で割る
以下はn=2の例(1.21 - 0.1
)
int x = 121;
int y = 10;
int z = x - y;
System.out.println((double) z / 100); // 1.11
補足
計算内で割り算がある場合は整数の状態で分母と分子を計算し、できるだけ最後に一回だけ割り算を行うようにすると計算精度が高くなります。割り切れなかった場合の丸め込みの影響を次の計算に及ぼさないためです。当然のように聞こえますが、計算の文脈が長くなってくると途中で割ってしまいがち。
ちょっと強引な例として半径7の球の体積をできるだけ正確に求めたいとします。
V = {\frac{4}{3}}{\pi}r^3 , ({\pi} = {\frac{22}{7}} \fallingdotseq 3.1415929203539825)
- 前から順番に計算
$$1.3333333333333333 \times 3.1415929203539825 \times 7^3 = 1436.755162241888$$ - 割り算を最後に
$$\frac{4 \times 22 \times 7^3}{3 \times 7} = \frac{4312}{3} = 1437.3333333333333$$