[Java]BigDecimalのキャストとか

テーマ:
自分の歌が迷子。

どうも、唯野です。


たまにはプログラマーっぽいことを。

今回はBigDecimalについて。

Javaっす。

Javaってなんぞ?ってひとはとじてください。
読んでもわけがわからないと思います。


たぶんJavaで金額計算とか、まるめ云々といった際には必ず使用するBigDecimal。

はじめからBigDecimalで宣言しているものをそのまま計算する分には問題ないですが、
たとえば先人が金額計算なのにdoubleで宣言していた場合、
BigDecimalにキャストしないといけない!
金額計算でdoubleで宣言するなという話ですが、
全て一気にリファクタというか変更するにはコストがかかりすぎるので、
これ以降はBigDecimalの波にしていこう!という感じのシチュエーションです。

なぜ金額計算でdoubleを使用してはいけないのか、ググれば出てくるし、どの入門書にも書いてあると思いますが、
自分の情報整理もかねて簡単に書きます。


浮動小数点数で計算すると、どうしても誤差を生じる場合があります。

浮動小数点数とは仮数部と指数部で表現する形式です。
http://pc.nikkeibp.co.jp/pc21/special/gosa/eg4.shtml
ここにわかりやすく書いてあります。

で、浮動小数点で計算をぐいぐい計算を行うと桁落ちとか情報落ちが発生します。

■桁落ち
桁落ちは有効数字が減ってしまうことを言います。
有効数字って言うのは信頼できる値?って感じでしょうか。

たとえばお手持ちの電卓で1000÷3とたたいてみてください。
自分の持っている電卓では333.333333333と表示されます。
有効数字が12桁の電卓だからですね。
この場合、333.333333333の一番最後の3の次が3なのか、0なのかはわかりません。
ただ、少なくとも有効数字が12桁の間だけは保証しますよ。
みたいな感じでしょうか。
これが有効数字4桁の電卓(と呼べるのか?)では333.3としか表示されません。

こういったものが有効数字ですね。
理系でなくても高校で化学とかとってた人は有効数字○桁で解答せよ
みたいな問題があったと思います。

1000を3で割ったものを有効数字は5桁で解答せよ。
といったものがあった場合、
333.33が解答になるわけです。

なんとなく有効数字ってものがわかるでしょうか



で、本題の桁落ち。
桁落ちは範囲についていろいろな解釈があるようですが、
基本的にはこの有効数字が計算を行うと小さくなってしまうことを言います。
123.456-123.345 = 0.111
有効数字が6桁から3桁に減っています。


何が問題なのか。べつに問題がありそうにはありません。

コンピュータで計算する場合結構問題があります。
浮動小数点型というのは、表現できる最大の精度が存在します。
1000/3=333.3333....以下無限 みたいな表現は出来ないわけです。


と、なると、
ルート999=31.6069612585...
ルート998=31.5911379978...
有効数字7桁で計算すると、最後の値は丸められるので
ルート999→31.60696
ルート998→31.59113

ルート999-ルート998=0.01583
で有効数値が4桁まで落ちてしまいます。

コンピュータは有効数字7桁なんだからといって
0.1583000*10^-1
と仮数部が7桁になるように0を埋めます。


ただし、本当に0.1283の後ろの0は0なのか?というわけです。

有効数字10で計算した場合、
ルート999-ルート998=0.01582326

0.1582326000*10^-1
となります。

はい、答えがすっごい微々たる物ですが、かわりますね。


値としては微々たる物ですが、これに*100000000とかした場合、ずいぶん変わってしまいます。

こんな感じで有効数字という概念がつきまとう
浮動小数点では正確な数値の計算はできません。



■情報落ち
有効数字7桁の状態で以下の計算を行った場合どうなるでしょうか
777777.7+1.111111

答えは
777778.8。

777778.811111にはなりません。

なぜならば有効数字が7桁なので、1.111111の一部の情報が切り捨てられてしまいます。

これは解りやすく答えが変わってしまいますね。


■もっと簡単な例。
浮動少数点型は、2進数で、仮数部と指数部で何とかして値を表現して計算を行います。

そのため
0.1の表現が出来ません。
0.1+0.2が0.30000000000000004
とかなってしまいます。

0.1は2進数で表現すると
0.0001100110011001100110011001100110011001100110011001101.....無限
これを10進数に直すと
0.1000000000000000055511151231257827021181583404541015625
となり、そもそも0.1ではなくなってしまいます。

こんなことで
0.1+0.2=0.30000000000000004みたくなっちゃうわけですね


で、まぁ、通常の計算とかなら別に誤差程度なので構わないのですが、金額とかなるとまぁ大変!
金額計算がずれてしまうわけですね。

BigDecimalはその点うまいこと計算してくれるので、ずれることなく計算できるというわけです。

このずれない理由はまた次回。
また長くなっちゃうからね。



■本題。doubleをBigDecimalにキャストする

BigDecimalのコンストラクタに
BigDecimal(double val)
というコンストラクタが存在します。
http://docs.oracle.com/javase/jp/6/api/java/math/BigDecimal.html


じゃあ、
double a = 0.1;
BigDecimal newA = new BigDecimal(a);
でいいのかということです。


これ、System.out.println(new BigDecimal(a));
とかやってみるとわかるのですが。

0.1000000000000000055511151231257827021181583404541015625
になります。

このコンストラクタは
double を double のバイナリ浮動小数点値の正確な 10 進数表現である BigDecimal に変換します。

なわけです。
ということは、0.1を2進数表示→10進数にするなので、上で紹介した例のように0.1ではなくなってしまうわけです。

これでは求めているものとは違ってしまいます。

0.1を0.1として行うためには、
new BigDecimal(String.valueOf(a));
new BigDecimal(Double.toString(val));
BigDecimal.valueOf(a);

の表記でできます。

内部的に行っていることは同じです。3番目の内部が2番目だし。
いったんStringにして、それをBigDecimalのコンストラクタに投げてあげれば解決です。


そんな話でした。

よく使う割に何となく使ってしまっているBigDecimal。
たとえばStringにキャストするにも
toString, toEngineeringString(),toPlainString()
と3種類あったりします。

ちょっと調べてみると面白いよね。

たまにはこんなこともかいてみました。

またいつか引っかかる題材があったら書いてみます。

ではであー