なぜ BigDecimal が「わざわざ」必要になるのか(全体像)
BigDecimal は、ざっくり言うと
「お金や桁数が重要な数値を、ちゃんと正確に扱うためのクラス」
です。
初心者が最初にハマるポイントはここです。
「double でいいじゃん。double って小数も扱えるんでしょ?」
たしかに double は小数を扱えます。でも、double は “だいたいの値” を高速に扱うための型であって、
“きっちりした値” を扱うには向いていません。
お金の計算、税計算、集計処理、金融・会計系などでは、
「0.1 の誤差」すらバグになります。
この「誤差問題」を避けるために登場するのが BigDecimal です。
double(浮動小数点)の「誤差問題」を体で感じる
一見ちゃんと計算できているように見えるが…
とりあえず、次のコードを見てください。
public class DoubleSample {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println("0.1 + 0.2 = " + c);
}
}
Java実行すると、環境によりますが多くの場合こうなります。
0.1 + 0.2 = 0.30000000000000004
「0.3」じゃない。
「0.30000000000000004」という、微妙にズレた値が出ます。
なぜこんなことが起きるのか(直感的な説明)
double は「2進数ベース」の仕組みで小数を表します。
ところが「0.1」「0.2」のような 10 進数の小数は、
2 進数ではきれいに表現できないものがたくさんあります。
たとえば、10 進数で
1/3 = 0.3333…
と「無限に続く小数」になってしまうのと似ています。
2 進数の世界では、0.1 や 0.2 が「無限に続く」感じになってしまうので、
コンピュータ内部では「近いところで丸めた近似値」を使います。
その結果、
0.1 も 0.2 も「本当の値」ではなく「近似値」
近似値同士を足すから、結果も「ちょっとずれた値」になる
という現象が起こります。
double は、「だいたい合っていればいい」
グラフ描画、物理シミュレーション、統計などには便利ですが、
「1 円でも狂ったらダメ」な世界には向きません。
お金の計算に double を使うと何がマズいか
税計算・合計でズレていく
たとえば、1 個 0.1 円の商品を 10 個買ったとします(極端な例ですが)。
double price = 0.1;
double total = 0.0;
for (int i = 0; i < 10; i++) {
total += price;
}
System.out.println("合計 = " + total);
Java理屈では 1.0 になるはずですが、実際には
合計 = 0.9999999999999999
のような結果になることがあります。
これをそのまま「金額」として表示したり、
さらに税計算・割引計算・合計繰り返し…とやっていくと、
本来あるはずの 1 円がどこかで行方不明になったり、
逆に 1 円多くなったりします。
現場でこれをやると、「会計システムとしてアウト」です。
BigDecimal の役割:10 進数を「桁どおり」に正確に扱う
10 進数として「そのままの桁」で持つ
BigDecimal は、「10 進数としての桁」をそのまま持ちます。
double→ 2 進数の近似表現(中身はだいたい)BigDecimal→ 10 進数の正確な表現(中身はきっちり)
という違いです。
たとえば、さっきの 0.1 + 0.2 を BigDecimal でやってみましょう。
import java.math.BigDecimal;
public class BigDecimalSample {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal c = a.add(b);
System.out.println("0.1 + 0.2 = " + c.toString());
}
}
Java結果はこうなります。
0.1 + 0.2 = 0.3
ぴったり 0.3。
10 進数の桁をそのまま扱っているので、「0.1」も「0.2」も誤差なしに表現できます。
文字列コンストラクタを使うのが重要(ここはかなり大事)
BigDecimal を作るときに、うっかりこう書くと危険です。
BigDecimal a = new BigDecimal(0.1); // これが実は良くない
Java0.1 の時点で、もう double として誤差を含んだ値になってしまっているので、
その誤差付きの値を元に BigDecimal が作られてしまいます。
正解は「文字列で渡す」です。
BigDecimal a = new BigDecimal("0.1"); // これなら 0.1 を 10 進数として正確に扱える
Java実務でお金を扱うときは、ここはほぼ「必須ルール」と言っていいです。
BigDecimal でお金計算をするとどう変わるか
さっきの 0.1 × 10 の例を BigDecimal で
先ほどの「0.1 円 × 10 個」を BigDecimal でやってみます。
BigDecimal price = new BigDecimal("0.1");
BigDecimal total = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) {
total = total.add(price);
}
System.out.println("合計 = " + total.toString());
Java結果はきちんと
合計 = 1.0
になります。
「足していったら 0.9999… になった」みたいなことは起きません。
お金の世界で必要なのはまさにこういう「一切の誤差がない計算」です。
小数桁数をしっかり管理できる(scale の概念)
BigDecimal は、「小数点以下が何桁か(scale)」を意識して扱えます。
お金なら「小数第2位(0.01 円単位)まで」と決めることが多いです。
例えば、税込価格の計算。
BigDecimal price = new BigDecimal("100"); // 100 円
BigDecimal taxRate = new BigDecimal("0.1"); // 10%
BigDecimal tax = price.multiply(taxRate); // 10.0
BigDecimal total = price.add(tax); // 110.0
System.out.println(total); // 110.0
Javaさらに、四捨五入や切り捨てをしっかり制御したい場合には、setScale(桁数, 丸め方) で「何桁でどう丸めるか」を指定できます。
「じゃあ全部 BigDecimal にすればいいの?」に対する答え
何でもかんでも BigDecimal は逆にしんどい
BigDecimal はとても正確ですが、その代わりに
- 計算が遅い(内部で複雑な処理をしている)
- 記法が長い(
+演算子が使えず、add,subtract,multiply,divideを呼ぶ必要がある)
というデメリットもあります。
例えば、
BigDecimal a = new BigDecimal("1.23");
BigDecimal b = new BigDecimal("4.56");
BigDecimal c = a.add(b).multiply(new BigDecimal("2"));
Javaのように、全部メソッド呼びになります。double でやるよりはるかにコードが重くなるし、読みづらくもなります。
使い分けの大雑把な基準
初心者向けにざっくり線を引くと、
お金・料金・税額など、「1円も狂ってはいけない」計算
→ BigDecimal を積極的に使う
グラフ、統計、ゲーム内の座標、センサー値など、「多少の誤差はどうでもいい」計算
→ double で十分
というイメージで OK です。
金融・会計系、請求書、売上集計のようなコードに double が出てきたら、
「本当にこれでいいの?」と一度立ち止まるクセをつけておくと安全です。
まとめ:BigDecimal の「必要性」をあなたの中でどう位置づけるか
ここまでを、初心者向けに一言でまとめるとこうです。
double は「速くて便利だけど、誤差を含む小数」BigDecimal は「遅くて面倒だけど、誤差なしで扱える小数」
だから、
誤差が許されない(特にお金)ところでは BigDecimal が「必要」になる
誤差があってもいいところでまで BigDecimal を使うと、コードもパフォーマンスも重くなる
特に重要なのは、
- 0.1 + 0.2 のような計算が
doubleでは正確でないことを、自分の目で一度確認しておく - BigDecimal を使うなら、
new BigDecimal("0.1")のように文字列で初期化するクセをつける
この2点です。
