RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

JDKバージョンとBigDecimalの挙動について

お金の計算など正確にJavaで計算をするうえで欠かせないBigDecimalですが、
一部JDKバージョンで挙動に変更が入っていました。
この改修により問題に直面してしまったため備忘録がてら挙動をまとめることにしました。

BigDecimalの値保持について

まず、本題に入る前にBigDecimalはどのように値を保持しているかを見てみましょう。

BigDecimalは以下の要素を保持しています。

  • intCompact
    • 数値の仮数部を保持する
  • intVal
    • BigDecimalのスケーリングされていない値
  • precision
    • 保持している仮数部の桁数
  • scale
    • 少数のスケール

では実際に見てみましょう。

BigDecimal bigDecimal1 = new BigDecimal("3.14e+25");
BigDecimal bigDecimal2 = new BigDecimal("31400000000000000000000000");

BigDecimalの内部値例

bigDecimal1は指数表現を指定し作成されているためintCompactには314、precisionには3、scaleには-23が格納されています。 また、bigDecimal1は指数表記なのでintValに値は保持されていません。

bigDecimal2は通常の数値表記を指定し作成されているためintCompactにはLong最低値、intValにはスケーリングされていない値、precisionには26が格納されています。

このようにBigDecimalは同じ数値でありながらも内部の値が異なることがあるというパターンは存在します。

BigDecimalから値の抽出

それでは本題です。
先ほどの例で見た通りBigDecimalの値保持には2つのパターンがありました。
これらのtoStringすると以下のようになります。

bigDecimal1 : 3.14E+25
bigDecimal2 : 31400000000000000000000000

当然BigDecimal作成時に指定したものになりますね。

ここで例えば、この文字列を画面表示に使用したい場合について考えてみます。
3.14E+25のように表示しても数値自体は誤りではないですが、
金額の表示などであれば非常にわかりづらいですよね。

ではbigDecimal2は良いとしてもbigDecimal1はどうすればよいでしょうか。

誤った表記変換方法

指数表記になってしまったBigDecimalを通常の数値表記の戻す方法として誤っている例を挙げてみます。
何を使用するかというとBigDecimalのメソッドのmovePointRight(0)です。
movePointRightは小数点を右に指定した分だけずらすというメソッドです。

つまり右に小数点を0個ずらすということです。

BigDecimal bigDecimal1 = new BigDecimal("3.14e+25").movePointRight(0);
BigDecimal bigDecimal2 = new BigDecimal("31400000000000000000000000").movePointRight(0);

JDKのバージョンごとの結果は以下の通りになりました。

JDKバージョン bigDecimal1 bigDecimal2
8 31400000000000000000000000 31400000000000000000000000
11 31400000000000000000000000 31400000000000000000000000
17 3.14E+25 31400000000000000000000000
21 31400000000000000000000000 31400000000000000000000000

なんとJDK17だけbigDecimal1が指数表記のままとなってしまっています。

※以下は使用したJDKディストリビューションとバージョンです。

JDK バージョン
Amazon Corretto 8 1.8.0_412
Amazon Corretto 11 11.0.23
Amazon Corretto 17 17.0.11
Amazon Corretto 21 21.0.3

正しい文字列を取得する方法

movePointRight(0)を使用したBigDecimalをtoStringで表示させた際にJDK17のときのみ
指数表記になってしまうということがわかりました。
では正しい値を取り出す方法はあるのでしょうか。

もちろん存在します。
BigDecimalのtoPlainStringメソッドです。
これを使用すると保持している内部データが異なっていても通常の数値表現(文字列)を返してくれます。
このメソッドはJDK 5から実装されているので問題になっていないJDKバージョンでも積極的に使用したいですね。

まとめ

JDKバージョンによってBigDecimalが動作が一部異なるという現象に遭遇し今回詳しく調べて見ました。
JDK17でのみ挙動が異なっており、最新のLTSであるJDK21では発生していないので
今後修正される可能性もあるかもしれません。
しかし、Java側で通常の数値表記を取り出すメソッドが用意されているので今後はそれを使用したほうが良さそうです。
BigDecimalを使用する場面というのは間違うことが許されないような数値を扱う場面が殆どかと思います。
今後の更新も要チェックですね。

Copyright © RAKUS Co., Ltd. All rights reserved.