Javaの金額計算で学んだこと


前置き

プロジェクトで使っているのはJava8です。

事の発端

Javaで金額計算が絡むコーディングをしていて、画面に表示される値が

299.999999999999988897769753748434595763683319091796875000

のように微妙にずれる事象に遭遇しました。
恥ずかしながら、ちゃんと(?)金額を計算するようなロジックを初めて書いたので、
なぜそうなるのかといった理由も調べながら進めました。

犯人探し

金額に対してパーセンテージを掛けるような処理の前段で

// どこかのmodelから取得したパーセンテージと金額
double d = 0.3;
BigDecimal bd = new BigDecimal("1000");
// 計算
BigDecimal bd2 = bd.multiply(new BigDecimal(d));
System.out.println(bd2); // 299.999999999999988897769753748434595763683319091796875000

doubleの引数をコンストラクタに渡してBigDecimalインスタンスを生成していたんですね。
DBからごにょごにょ取得した結果、いろいろあってServiveクラスで使用しているmodelのフィールドがdouble型で定義されていました。

なんでずれるの?

コード上は10進数で定義していますが、内部では2進数で扱っているため、
2進数で循環小数になるような値(例えばdoubleの0.3)は計算をすると丸めが発生、つまり誤差が発生します。


byリファレンス

こういうのもだめ

double d = 3.3 / 1.1 // 3になってほしいけど2.9999999999999996

ここでBigDecimalさんの使い方を再確認(汗)

とりあえずリファレンスを読んでみました。
https://docs.oracle.com/javase/jp/8/docs/api/java/math/BigDecimal.html

 - add
 - subtract
 - multiply
 - divide
 などのメソッドを用いて四則演算します。
RoundingModeで四捨五入/切り捨て/切り上げの指定をします。
今回のロジックはそこまで複雑じゃなかったのでややこしいメソッドは使わなかったです。

だめなのと良いの

// 毎回インスタンス生成、効率が悪い
BigDecimal b1 = new BigDecimal(10); //だめ
// キャッシュを使うvalueOf()
BigDecimal b2 = BigDecimal.valueOf(10); // 良い

// ∵2進数で循環小数になるものは誤差が発生する
BigDecimal b3 = new BigDecimal(0.3); // だめ
// 文字列から生成する
BigDecimal b4 = new BigDecimal("0.3"); // 良い
// doubleの値をBigDecimalに変換したいとき
BigDecimal b5 = BigDecimal.valueOf(0.3); // (なるべくならこの変換が発生する実装にはしたくないけど)
  • BigDecimal#ROUND_xxxx系はJava9以降でDeprecatedです。RoundingModeを使いましょう(今のプロジェクトではJava8を使っていますが、バージョンアップを見据えてそのあたりも意識しようと思います)。

参考URL

http://javazuki.com/articles/bigdecimal-usage.html#_roundingmode
https://qiita.com/TKR/items/52635175654b9b818b89

2019/11/12 自分用メモ

大小比較(compareTo())が毎回わからなくなる件

BigDecimal three = BigDecimal.valueOf(3);
BigDecimal five = BigDecimal.valueOf(5);

int hoge  = three.compareTo(five); // -1 
int piyo = three.compareTo(three); // 0
int fuga = five.compareTo(three); // 1