2014/10/31 このエントリーをはてなブックマークに追加 はてなブックマーク - 【そんなBigDecimalで大丈夫か】Javaシステムの計算処理について

【そんなBigDecimalで大丈夫か】Javaシステムの計算処理について






Twitterの話題に乗っかって、内容をまとめてみようと思います。


私自身の考えではなく、様々な意見を整理したいなという試みです。僕には知識はありません。
B○zzNews並みに頑張ろうと思います。





基本的に、Javaを中心として、その周辺についても少し触れるぐらいのアプローチで書こうと思います。
ポイントとしては、以下の3点です。
※無意識に利用している部分(基礎的で普段意識しないところ)ではありますが、とても重要な話です。

1.Javaではあたり前のように計算処理にはBigDecimalが使われている
2.ではなぜ、BigDecimalではないといけないのか
3.どういった計算ならBigDecimalを利用するのか/利用しないのか



※本記事は演算誤差についてあまり厳密に的確な内容ではない可能性があります。
Java内の演算処理方法としてベターな方向の提示を目的としていますが、「誤差」や「精度」などに関して、
誤解を招く可能性があり、申し訳ないです。
BigDecimal以外もそうですが、100%誤差の無いものではなく、 それぞれの言語仕様、型仕様、小数点数方式、有効桁数、実行環境etc...によって精度は変わってくるという認識です。


精度などについては言及せず、以下のURL参照という形でお願いします。


参考:浮動小数点数の限界
BigDecimal 基本編
Javaプログラマーのためのjava.math.BigDecimalまとめ














話題としては以下のような事柄が上がりました



・浮動小数点における演算誤差
・Javaの通貨クラス
・型とnullの扱い(null safety)
   派生してJavaAPサーバーでのJava8使用
   パフォーマンス(オートボクシング、nullチェック、型変換)
・銀行丸め(金融丸め)
    C#は銀行丸めがデフォルト Javaは四捨五入となっている
・演算子オーバーロード
・COBOLに誤差はない?










浮動小数点は誤差が出てしまうもの、というのが大前提です。
基本的な事は押さえておく必要があります。
>浮動小数点数型と誤差


・整数値だけのclosedなスコープなら誤差は出ない

BigDecimalを特に利用しなくても誤差が出ない例です。
全てが整数の世界であればint値などで演算しても誤差は出ないはずです。
例えば人員数の単純な加算など。

これに人員あたりの粗利など変動する利益率などが含まれてくるドメインとなると
BigDecimalを使わざるを得ないでしょう。




・doubleで少数を考慮して処理しても大きな桁だと誤差が出る


例えば消費税計算。
12000 * 1.08 小数点以下が発生しないよう、税別価格の100未満を切り捨てる、という考え方です。
下2桁が0であれば今の税率なら影響しないのではないか。と考えられますが、そうではないようです。
大きい金額の消費税計算の場合誤差が出てしまいます。


「0.08, 1.08, 0.10, 1.10 あたりは二進の浮動小数点数で正確に表せないので、
100京円くらいの商品を扱うと、1円の桁に誤差が出てしまいそうです。」とのこと。

@yusukeさんのが実際に試したところ、誤差が出ていました。



JavaにはJava1.4から通貨クラスが標準であります。
java.util.Currency

糞っぽいコードですが、このように利用出来ます。


import java.util.Currency;
import java.util.Locale;

public class CurrencySample {
    public static void main(final String[] args) {
        // Localeを引数にしてインスタンス取得
        final Currency current  = Currency.getInstance(Locale.JAPAN);
        // 通貨記号を出力
        System.out.println(current.getSymbol());
        System.out.println("デフォルトの小数点けた数:" + current.getDefaultFractionDigits());
    }
}

使う利点としては当然、国際化対応が容易であること、ISO 4217通貨コードに準拠した情報であることなどがあります。






この件に関していうと、演算の誤差などの精度の問題ではなく、Javaの型とNull安全性の話です。

・プリミティブのラッパー型を使うよりはプリミティブ型を使う方が良い
-> オートボクシングのコストが高い。nullチェックが必要になる。
・プリミティブが使えない場合(DB側でnull許容など)の場合、型は注意が必要
-> JavaSE8基準で考えると、OptionalとOptional派生型(OptionalLong、OptionalInt、OptionalDouble)を使うのがnull安全。
※ただし、JavaSE8に対応している実行環境が現在少なく、商業利用は現実的でない。(現状、商用ではwildflyぐらい。weblogicもでるとの噂。非商用ではglassfish)



したがって、プリミティブが使えない、かつJavaSE8が使えない想定で考えるとnullチェック、オートボクシングは避けられない。
ただし、JavaEE環境であれば、プレゼンテーション層、もしくはビジネスロジック層などのレイヤーでのnullチェック、および層間変換のみで
対応出来難しいものではない。はず(私としてはどのプロジェクトでもやってる認識。そこがブレるとも得るけど)。

また、オートボクシングやnullチェックはそれほど処理負荷にならない事が多く、パフォーマンステスト時に様々な原因で
ボトルネックになっている処理がある(局所的な、またはモジュールコア部分)ことが多いようです。






銀行丸めとは、金融業界特有の少数値の丸め方です。
一般的には四捨五入、切り捨て、切り上げという者が浸透していると思いますが、
銀行では特有の丸め処理を行っています。
N桁で丸める場合
第N桁が偶数なら 5以下は切り捨て。それ以外は切り上げ。
第N桁が奇数なら 5未満は切り捨て。それ以外は切り上げ
、というものです。


参考>銀行丸めと四捨五入
参考>Wikipedia - 端数処理 1.3.3 最近接偶数への丸め


C#ではデフォルトで銀行丸めが採用されているらしいです。

おそらくこの辺りのことだと思います
http://jeanne.wankuma.com/tips/csharp/math/round.html







演算子オーバーロードは古くはC++(だと思います。)やVBにもあるものです。
Kotlin、Groovyなどでも言語仕様としてとりこまれています。

・C++の演算子オーバーロード
・Kotlinの演算子オーバーロード
・Groovyの演算子オーバーロード


演算子オーバーロードの何が嬉しいかというと、Javaで言うところのプリミティブ型でないValueObjectに関しても、
「+」「-」「*」「/」といった演算子を用いて計算出来る点です。(間違ってたらご指摘ください。。。。)


Javaにおいて、BigDecimalのどのメソッドがどの演算かパッと分からなくなる、、、なんて事もないわけです。




COBOLはpacked-decimalという型があるらしく、この型で演算すると誤差が出ない?そうです。


COBOL利用技術のご紹介-第4回- 算術演算の精度








3つの前提を振り返りまとめてみます。



1.Javaではあたり前のように計算処理にはBigDecimalが使われている
2.ではなぜ、BigDecimalではないといけないのか 3.どういった計算ならBigDecimalを利用するのか/利用しないのか



BigDecimalを使うのはセオリー化しており、ローリスクではある。
しかし、整数演算に限られたスコープであれば、BigDecimalに限定する必要は無い。
BigDecimalを使わなければいけないのは小数の演算処理の場合必須と考えてよいと思います。


加えて、派生した話題について(まとめられているか分かりませんが。。)。


型を決定することは設計レベルで重要であり、パフォーマンス低下、生産性低下につながる可能性がある。
また、null安全性を考慮しておかないと、思わぬバグの温床が出来上がる。
実際のプロジェクトの構成に基づき、最適解を出していくことが重要。


その他、Java以外にも様々な言語があり、利便性があるというところです。


つまり、みんなKotlinやろう!!!
















2 件のコメント:

  1. 話題が多岐に渡ったやりとりを記事にまとめてくださって, ありがとうございます.
    いくつか補足コメントをします.

    ・int の誤差について

    Java の int (や他の整数型) に収まる演算で誤差は発生しませんが, ラップアラウンドという処理が発生することはあります.
    一例を挙げると 2147483647 + 1 の結果は -2147483648 になります.
    2147483647 は Integer.MAX_VALUE なので 8 bit 符号付き整数の最大値ですが, その値を越えると -2147483648 (= Integer.MIN_VALUE) という 8 bit 符号付き整数の最小値になります.
    最大値から最小値へぐるっと回ってしまいます.

    詳しくは以下のページを参照してください.

    http://stackoverflow.com/questions/5131131/what-happens-when-you-increment-an-integer-beyond-its-max-value
    https://www.jpcert.or.jp/java-rules/num00-j.html

    ・丸めモードについて

    Java の BigDecimal の divide メソッドは (オーバーロードされて) いくつかあり,
    その中に int roundingMode や RoundingMode roundingMode という引数を取るものがあります.

    http://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html#divide-java.math.BigDecimal-int-
    http://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html#divide-java.math.BigDecimal-java.math.RoundingMode-

    そこに与える値によって丸めモードを変えることができます.
    説明は RoundingMode Enum の Javadoc を読みましょう.

    http://docs.oracle.com/javase/8/docs/api/java/math/RoundingMode.html

    ・演算子オーバーロード

    Python にだってあるもん.

    ・型設計について

    結局は使う ORM によって決まるんだろうなぁ, と思ったり.

    返信削除
    返信
    1. >cocoatomoさん
      補足のコメントありがとうございます!

      型の精度や最小値、最大値など本記事で触れていない部分を補足してくださってありがとうございますm(_ _)m

      丸めに関してもそうですね。Javaにおいて、丸めモードを引数で変えるという事も重要ですね。

      Pythonも、、、勉強します!

      型設計のORMが関わっているんじゃないかと言う点ですが、私もそういう想定で話を書いていたつもりだったんですが、言葉足らずでした。フォローありがとうございます。

      削除