読者です 読者をやめる 読者になる 読者になる

きどたかのブログ

いつか誰かがこのブログからトラブルを解決しますように。

Javaプログラマーのためのjava.math.BigDecimalまとめ

以前書いたメモ的なエントリーを読み返してみて、

簡単なエントリーに書きなおそうと思いたった。

 

java.math.BigDecimalの構造

 BigDecimal = unscaled value \times 10^{-scale} = BigInteger \times 10^{-scale}

 

「精度(precision)」と「スケール(scale)」と「一般的な桁数」の違い

精度とスケールの違いを正しく把握しているかを試すのに、

この質問をしてみるといいでしょう。

質問者「1000の精度(precision)はいくつですか?」

正解者「わかりません。3か2か1です。」

精度(precision)はBigIntegerの桁数(unscaled valueの桁数)と同じです。

see also precision()

文字列から精度を判断することは出来ないことがあり、

スケール(scale)が値によって精度(precision)が変わります。

スケール(scale)は小数点以下の桁数ですが、それは小数点があった場合の話。

  • スケールが0以上の場合、「スケール=小数点以下の桁数」
  • スケールが負の場合、「スケール=一般的な桁数- 精度(precision)」
  • スケールが0以上の場合、「一般的な桁数=精度(precision)」
  • 「精度(precision) は1以上の値」

スケール(scale)は単位だともいえます。

1千というのはスケール=-3をベースにした1という数字といえます。 

 

「精度(precision)による丸め操作」と「スケール(scale)による丸め操作」

加減乗算のメソッドでおこなわれるのは「精度(precision)による丸め操作」です。

除算では、「スケール(scale)による丸め操作」のほうが多いです。

「スケール(scale)による丸め操作」をおこなうのは

おおよそこれらのメソッドです(ほかにもあったかな?)。

小数点以下第N位までを扱う場合や、

千円単位で表示したいときなどに使うのは「スケール(scale)による丸め操作」です。

 

四則演算(加算)

メソッド 結果の精度  結果のスケール
add(java.math.BigDecimal) 演算結果で増減 max(this.scale(), augend.scale())
add(java.math.BigDecimal, java.math.MathContext) 指定値 演算結果で増減

 

四則演算(減算)

メソッド 結果の精度  結果のスケール
subtract(java.math.BigDecimal) 演算結果で増減 max(this.scale(), subtrahend.scale())
subtract(java.math.BigDecimal, java.math.MathContext) 指定値 演算結果で増減

 

四則演算(乗算)

メソッド 結果の精度  結果のスケール
multiply(java.math.BigDecimal) 演算結果で増減 this.scale()+ multiplicand.scale()
multiply(java.math.BigDecimal, java.math.MathContext) 指定値 演算結果で増減

 

四則演算(除算)

メソッド 結果の精度 結果のスケール
divide(java.math.BigDecimal) 演算結果で増減 this.scale() - divisor.scale()
divide(java.math.BigDecimal, int) 演算結果で増減 this.scale()
divide(java.math.BigDecimal, java.math.RoundingMode) 演算結果で増減 this.scale()
divide(java.math.BigDecimal, int, int) 演算結果で増減 指定値
divide(java.math.BigDecimal, int, java.math.RoundingMode) 演算結果で増減 指定値
divide(java.math.BigDecimal, java.math.MathContext) 指定値 演算結果で増減
divideToIntegralValue(java.math.BigDecimal) 演算結果で増減 this.scale() - divisor.scale()
divideToIntegralValue(java.math.BigDecimal, java.math.MathContext) 演算結果で増減 this.scale() - divisor.scale()

割り切れないことがあるため、一般的に必ず丸め方法を指定します。 

古いプログラムでは丸め方法をintで指定していましたが、

現在はjava.math.RoundingModeを使うのが一般的です。

 

剰余演算+α

内部でdivideToIntegralValueをもちいているため、

結果の「商」に関しては精度やスケールはdivideToIntegralValueと同じです。

それにおうじて「余り」の部分は変化しますが、

「余り」のスケールが変化するのは考えにくいですね。

 

累乗演算(べき乗演算)

引数をみたら分かるとおり、intによる累乗しかできません。
ある種のプログラムでは本当に困ったことです。
金融系のプログラムはとくに金利の計算やら利回りやらで、
このメソッドでそれを実現することは至難の業です。
この問題に対応するには、C/C++のライブラリなどを使うほうが現実的です。
IBMのプラットフォームのPOWER7やzSeriesなどでは、
ハードとして10進浮動小数点演算のユニットが組み込まれており、
それを呼び出せるC/C++の関数が用意されています。
たしかHP-UXでも10進浮動小数点演算はできるはずです。
Decimal Floating PointだとかDFPだとかで検索してみてください。
 

整数部と小数部の分け方

古くからの設計書では整数部と小数部という表現は多くあります。

そして数字を文字列で受け取っているプログラムなんかもざらにあります。

そういう場合、小数点でsplitするのが多いです。

しかし、これはプログラムのあり方として疑問を感じるところです。

似たような事例では、ある種の定数を文字列であつかって、

Enumを使わないというのがあります。日付や時間に関しても!

いずれの場合も変換ポイントというのはあるべきだと感じます。

さてBigDecimalから整数部と小数部をわける場合は、

divideAndRemainder(java.math.BigDecimal)あたりを使うのが妥当です。

しかし、商にスケールが残ってたりするので、

後々のことを考えて、setScale(int)で捨てることも考えてください。

 

整数部の桁数の調べ方

  • スケールが0の場合、「整数部の桁数=精度」
  • スケールが0より大きい場合、「整数部の桁数=精度ースケール」
  • スケールが0より小さい場合、「整数部の桁数=精度+スケール」

小数部の桁数の調べ方

  • スケールが0以下の場合、「小数部の桁数=0」
  • スケールが0より大きい場合、「小数部の桁数=スケール」

値のオーバーフローとスケールのオーバーフロー 

unscaled valueはBigIntegerで、その桁数(精度)もintで、

またスケールもintで持っていることから、

オーバーフローする可能性はゼロではありません。

BigIntegerであらわせないほど大きい値は扱えません。

一般的には除算と累乗演算以外ではほぼおきませんが、

手放しでプログラムを書いてもいけません。

 

BigDecimal同士の比較 (equals()とcompareTo(java.math.BigDecimal)の使い分け)

精度とスケールにセンシティブなプログラムに限りequals()を使用します。

一般的なプログラムはcompareTo(java.math.BigDecimal)を使用します。

 compareTo(java.math.BigDecimal)であれば1と1.0は同じと判断します。

 

符号(+-)の判定方法

signum()を使います。sign numberですね。

BigDecimal.ZEROcompareTo(java.math.BigDecimal)してもいいですが、

基本的にsignum()のほうがいいです。

また、0以上であるかを調べるのにもsignum()は使えますが、

そこは素直にcompareTo(java.math.BigDecimal)でもよいと思います。

スピードの話をするならsignum()のほうが速いです。

 

符号(+-)の反転方法

negate()を使います。

実際には設計書どおりに-1をかけるプログラムも多いのでしょうが、

符号部分の処理ですからnegate()のほうが速いです。

 

意味のないメソッドplus()

negate()とコンビで作られただけで、まったく意味のないメソッド。

これを呼んでるプログラムは考えられませんね。 

 

絶対値の求め方

abs()を使います。

これを間違える人はいないでしょう。

  

BigDecimalの文字列化の使い分け

過去の仕様変更もあり、現在では一般的にtoPlainString()を多く用います。

これは文字列中に指数部を表す"E"や指数部の符号(+-)を登場させないためです。

数字を文字列操作でなんとかする類の褒められないプログラムは実際あるので

そういうのに対応するためにはtoPlainString()を使うのが安全です。

ある種の技術的なプログラムはtoEngineeringString()を使います。

どのメソッドを使ったとしても、その文字列からまったく同じBigDecimalコンストラクタで復元することは保証されません。数値は同じでも、精度やスケールの情報がずれることがあります。

 

スケールによる丸め操作

端数処理の際に除算時の丸めと同じくらい使われるメソッドです。

基本的にこのメソッドを使う場合もRoundingModeを指定します。

 

精度による丸め操作

小数点以下の話をする場合は、ほぼsetScale()の話になりますが、

値の精度を意識するプログラムは、

除算のときにもMathContextを使った除算をしますし、

単独の丸め操作をするときもMathContextを使って丸めます。

 

小数点の移動

小数点の移動をするので、単純に10倍や、10分の1をするような操作です。

しかし、若干ふれておきたいことがあるので後述します。

 

10のN乗をかけるスケール操作

これも10倍とか100倍とかをおこなうのに適したメソッドです。

内部のBigIntegerは使いまわしたまま、

スケールを変更して新しいBigDecimalが返ってきます。

そのため通常の乗算よりもスピードは速いです。

 
本質的には小数点の移動なのですが、
前述のmovePointLeft(int)movePointRight(int)とは若干ちがいます。
 
小数点の移動とscaleByPowerOfTen(int)の違い
scaleByPowerOfTen(int)は完璧なスケールのみの操作ですが、
小数点が存在しない、スケールが負の数字の場合、
小数点の移動操作はスケールを0に調整します。
そのため、小数点がなくなった場合に、
内部のBigIntegerの値まで変更することになります。
それによって精度が増えるということがあります。
 
メソッド 結果の値 結果のスケール
movePointLeft(int) this \times 10^{-n} max(this.scale()+n, 0)
movePointRight(int) this \times 10^{n} max(this.scale()-n, 0)
scaleByPowerOfTen(int) this \times 10^{n} this.scale() - n
 

末尾の0を取り除いてスケールに置き換える操作

一般的なプログラムではまず使わないメソッドです。

0を取り除いてスケールに置き換えても、

数学的な意味では同じ数字ですから、

そういう学術的な用途で使うのでしょう。 

 

コンストラクタの選び方

コーディング規約的な話をするなら、新たにBigDecimalコンストラクタで生成する場合は、全て文字列から作るのが間違いが少ないです。

コンストラクタの種類

doubleを使うのは避ける。

本物の浮動小数点数BigDecimalは別物ですから、

可能な限りBigDecimalのみを使うプログラムを心掛けてください。

MathContextで生成と同時に丸めるのもオススメしません。

MathContextは精度(precision)に応じた丸め操作をしますが、

設計とコードの乖離をおさえる意味などの観点から

生成した後にround(java.math.MathContext)を使うほうがいいでしょう。

何か調査が必要になったときでもgrepしやすいです。

char配列を使うものもオススメではありません。

既にchar配列が存在するならば、Stringに直して生成するよりも高速であることは確かですが、数字をchar[]配列で操作している時点でバグの温床になっています。

intとlongを使用するコンストラクタは、valueOf(long)を使う方が良いかを考える余地があります。しかしBigDecimalのキャッシュは限定的で、Javaベンダによっても差があります。

 

全角数字とコンストラクタ

全角数字も文字列としてコンストラクタで処理できます。

ただし、指数(e、E)と符号(+-)の全角記号は受け付けません。

see also BigDecimal(java.lang.String)

処理できるからといって全角数字をよしと考えてるわけではありません。 

 

valueOf(long)を使う場合の判断基準

Oracleの実装の場合、次の点を意識した方が良いです

極力ZERO,ONE,TENはBigDecimalの定数を使う

スケール0の0~10はvalueOf(long)を使う

そのほかはコンストラクタをふつうに使う

よくある30日や31日の除算で分母となるBigDecimalはキャッシュされてません。

そのため素直にコンストラクタを使うほうがいいです。

もしくはプログラム独自でキャッシュすることを検討してください。

通常、定数はstaticに宣言しているでしょうから、その際に考えてください。

いろいろなクラスが30や31を表すBigDecimalをstaticフィールドに宣言するのは非効率ですので、共通化 vs モジュラリティは別途検討の必要があります

 

丸めモードの種類

java.math.RoundingMode

あまり付加情報がないので軽く書きます。

CEILINGなら、正の無限大に近づくように丸める。

FLOORなら、負の無限大に近づくように丸める。

UPなら0から遠のくの切り上げ。 

DOWNなら0に近づく切り下げ。

いわゆる四捨五入はHALF_UPで、これは符号部も気にせず四捨五入です。

人間味のあふれるJavaDocでなごみますよね。

これは我々の大半が小学校で習った丸めモードのことです。

 

MathContextで事前定義されている定数で使用されるのはHALF_EVENです。

HALF_UP、HALF_DOWN、HALF_EVENは、

丸めが必要な際に、どちらの方向に丸めるかについて、

どちらの数字とも等距離である状況のルールを決めているものです。

噛み砕くと「0.5の時どうする?」って話です。

 

HALF_EVENのよいところは、結果として誤差が一番少なくなる点です。

すべての計算でHALF_UPを指定している場合、

実際の計算結果よりも大きくなるバイアスがかかります。

HALF_EVENはそのバイアスをできるだけ緩和するためのものです。

しかし元本と金利から10年後の価値を出すなどの

比較的簡単な計算においてはそういう誤差はほぼでないと言っていい。

日本の長期金利で10年後を計算してみても、

丸めモードによる違いは見られませんでした。

 

HALF_EVENの解釈は面倒ですが、偶数によせるのは覚えておきましょう。

7.5を、8にするか7にするかの場合、

左の数字が7と奇数なので、偶数側の8になる。

8.5の場合も偶数側の8になる。

 

数値への各種変換

いずれのメソッドもBigDecimalの値を正確にあらわせる保証はありません。
その場合に例外にするのがExactがついてるメソッドです。
 
スケールなしの値を取り出す
このメソッドは前述の数値への変換とは異なり例外になることはないです。
単純に内部で保持しているBigIntegerを返すためです。
(まあBigIntegerになってない状態もありますけどね)
 

ulp()ってナニ?

ulpはunit in the last placeの略です。

精度の最終桁(末尾の桁)が位置するところの単位です。

ざっくりいうと、スケールで単位にしてるところの話です。

 

だいたい書いたので以上とします。