Java | Java 標準ライブラリ:BigDecimal の必要性

Java Java
スポンサーリンク
スポンサーリンク

なぜ 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);  // これが実は良くない
Java

0.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点です。

タイトルとURLをコピーしました