Java
BigDecimal
java8
Java9

【Java】BigDecimalをちゃんと使う~2018~

Java9でBigDecimal#ROUND_~系がやっと@Deprecatedになりましたね。
にもかかわらず、未だにBigDecimalを解説するブログやエントリで、BigDecimal#setScaleBigDecimal#divideなどを説明するときにBigDecimal#ROUND_~系を利用して説明されるのはなんなんですか?それ全部、Java9で@Deprecatedですよ?
と、最近モヤモヤするので記事書くことにしました。

まずは基本的なことから。

生成・Factoryメソッド

基本

基本的に以下の方法で生成する。

BigDecimal value = BigDecimal.valueOf(1234.567);// 数値系からの生成
BigDecimal value = new BigDecimal("1234.567");// 文字列から生成

0,1,10は定義済なのでこれらを使う。

BigDecimal.ZERO
BigDecimal.ONE
BigDecimal.TEN

文字列からの生成に関する注意

文字列からの生成はscaleも文字列で指定した通りになる。

System.out.println(new BigDecimal("1234.567").scale());  // → 3
System.out.println(new BigDecimal("1234.56700").scale());// → 5

数値からの生成のNGパターン

数値(double)からの生成はBigDecimal#valueOfを使ってください。newで生成するととんでもないことになります。

BigDecimal value = new BigDecimal(1234.567);// NG!!数値系からの生成
System.out.println(value);// → 1234.5670000000000072759576141834259033203125

ResultSet#getBigDecimal

ResultSetから取得する場合はResultSet#getBigDecimalがあるのでこれを使う。

BigDecimal value = rs.getBigDecimal("COL");

以下は無駄なのでNG

BigDecimal value = BigDecimal.valueOf(rs.getDouble("COL"));
BigDecimal value = new BigDecimal(rs.getString("COL"));

四則演算

加算

BigDecimal a = BigDecimal.valueOf(123);
BigDecimal b = BigDecimal.valueOf(456);

// value = a + b
BigDecimal value = a.add(b);

System.out.println(value);// 579

減算

BigDecimal a = BigDecimal.valueOf(123);
BigDecimal b = BigDecimal.valueOf(456);

// value = a - b
BigDecimal value = a.subtract(b);

System.out.println(value);// -333

乗算

BigDecimal a = BigDecimal.valueOf(123);
BigDecimal b = BigDecimal.valueOf(456);

// value = a * b
BigDecimal value = a.multiply(b);

System.out.println(value);// 56088

10倍・100倍等

BigDecimal#scaleByPowerOfTenを使うといい。

BigDecimal src = BigDecimal.valueOf(0.123);
BigDecimal value = src.scaleByPowerOfTen(2);// 100倍(10の2乗倍)
System.out.println(value);// 12.3

src.multiply(BigDecimal.valueOf(100))よりだいぶスッキリするし、100のインスタンス作らなくていい。

マイナス化(符号反転)

BigDecimal#negateを使うといい。

BigDecimal src = BigDecimal.valueOf(123);
BigDecimal value = src.negate();
System.out.println(value);// -123

以下のような書き方はやめてほしい。

BigDecimal value = src.multiply(BigDecimal.valueOf(-1));

除算

import java.math.RoundingMode;
//・・・

BigDecimal a = BigDecimal.valueOf(123);
BigDecimal b = BigDecimal.valueOf(456);

// value = a / b
BigDecimal value = a.divide(b, 3 /* ← scale */, RoundingMode.HALF_UP /* ← 四捨五入 */);

System.out.println(value);// 0.270

RoundingModeに置き換え可能なメソッドは利用NG

以下のような書き方はNG

BigDecimal value = a.divide(b, 3, BigDecimal.ROUND_HALF_UP);

RoundingModeについては下のほうでに詳細を書くので確認してください。

scaleを指定する

scaleを指定しないBigDecimal#divideメソッドもありますが、
割り切れない時ArithmeticExceptionが発生するのでscale指定版を使うのがいい。

1/10・1/100等

BigDecimal#scaleByPowerOfTenを使うのがいい。

BigDecimal src = BigDecimal.valueOf(8);
BigDecimal value = src.scaleByPowerOfTen(-2);// 1/100(10の-2乗倍)
System.out.println(value);// 0.08

四捨五入・切り捨て・切り上げ

import java.math.RoundingMode;
//・・・


BigDecimal src = BigDecimal.valueOf(123.456);

//四捨五入
BigDecimal value = src.setScale(2, RoundingMode.HALF_UP);
System.out.println(value); // → 123.46

//切り上げ
BigDecimal value = src.setScale(2, RoundingMode.UP);

//切り捨て
BigDecimal value = src.setScale(2, RoundingMode.DOWN);

RoundingModeに置き換え可能なメソッドは利用NG

以下のような書き方はNG

BigDecimal value = src.setScale(2, BigDecimal.ROUND_HALF_UP);

RoundingModeについては下のほうでに詳細を書くので確認してください。

文字列化

BigDecimal#toPlainString

BigDecimal#toStringは指数表記になる可能性があるので、BigDecimal#toPlainStringを使うほうが良いと思う。

BigDecimal value = new BigDecimal("0.0000001");
System.out.println(value.toString());     // → 1E-7
System.out.println(value.toPlainString());// → 0.0000001

jacksonにもtoPlainStringを使うオプションがあったりする。
JsonGenerator.Feature.html#WRITE_BIGDECIMAL_AS_PLAIN

小数の末尾の0を削除

BigDecimal#toStringBigDecimal#toPlainStringも、文字列にはscaleが考慮され、小数の末尾に0が続いても残される。
これを消したい場合は、BigDecimal#stripTrailingZerosを使う。
正規表現で頑張る必要はない。

BigDecimal value = new BigDecimal("1.000000");

System.out.println(value);                     // → 1.000000
System.out.println(value.stripTrailingZeros());// → 1

※ただし、BigDecimal#stripTrailingZerosはjava7まではバグがあるので注意
JDK-6480539 : BigDecimal.stripTrailingZeros() has no effect on zero itself ("0.0")

intlongdouble

BigDecimal value = BigDecimal.valueOf(12345);
int i = value.intValue();
long l = value.longValue();
double d = value.doubleValue();

floatBigIntegerもあります。

許容のチェック版

整数への変換には、許容範囲外で失われる情報があるような場合に例外を投げる版がある。
逆にいうと通常版は情報が失われても無視される。

int i = value.intValueExact();
long l = value.longValueExact();

-ValueExact系メソッドにはbyteshortBigInteger版もあります。

一致比較

通常の比較

BigDecimal#equalsscaleも一致しないとfalseを返すので、
scaleとか興味ないシステムを書いているなら、compareToの結果が0と一致するかで判定したほうが良い。

BigDecimal value1 = new BigDecimal("123.0"); // scale=1
BigDecimal value2 = new BigDecimal("123.00");// scale=2

System.out.println(value1.equals(value2));        // false

System.out.println(value1.compareTo(value2) == 0);// true

HashMapのキーに利用

BigDecimal#equalsBigDecimal#hashCodescaleも考慮されるので、
scaleとか興味ないシステムを書いていると、痛い目見るかもしれない。

BigDecimal value1 = new BigDecimal("123.0"); // scale=1
BigDecimal value2 = new BigDecimal("123.00");// scale=2

Map<BigDecimal, String> map = new HashMap<>();
map.put(value1, "data");

System.out.println(map.get(value2));// null

こういう場合はBigDecimal#stripTrailingZerosscaleを安定させるといいかもしれない。

BigDecimal value1 = new BigDecimal("123.0"); // scale=1
BigDecimal value2 = new BigDecimal("123.00");// scale=2

Map<BigDecimal, String> map = new HashMap<>();
map.put(value1.stripTrailingZeros(), "data");

System.out.println(map.get(value2.stripTrailingZeros()));// data

※ただし、BigDecimal#stripTrailingZerosはjava7まではバグがあるので注意
JDK-6480539 : BigDecimal.stripTrailingZeros() has no effect on zero itself ("0.0")

RoundingMode

RoundingModeenumで定義されていて、「四捨五入」以外にも「切り上げ」「切り捨て」「正の無限大への丸め」「負の無限大への丸め」などいろいろあるのでjavadocを一度確認してみるといいと思います。

以下はRoundingModeのjavadocの転記。

CEILING
正の無限大に近づくように丸めるモードです。
DOWN
0に近づくように丸めるモードです。
FLOOR
負の無限大に近づくように丸めるモードです。
HALF_DOWN
「もっとも近い数字」に丸める丸めモードです(両隣りの数字が等距離の場合は切り捨てます)。
HALF_EVEN
「もっとも近い数字」に丸める丸めモードです(ただし、両隣りの数字が等距離の場合は偶数側に丸めます)。
HALF_UP
「もっとも近い数字」に丸める丸めモードです(ただし、両隣りの数字が等距離の場合は切り上げます)。
UNNECESSARY
要求される演算の結果が正確であり、丸めが必要でないことを表す丸めモードです。
UP
0から離れるように丸めるモードです。

RoundingModeに置き換え可能なメソッドは利用NG

BigDecimal#ROUND_~系とこれらの利用を前提にしたメソッドは利用しないこと。(Java9で@Deprecated
それぞれ、RoundingModeを利用する対になるメソッドがあるはずなのでこれらを利用しましょう。

9で@Deprecatedになったメソッドは1.4時代の遺産です。
ただ、普通に「BigDecimal 割り算」とかでググると、BigDecimal.ROUND_HALF_UPとか使っているページばかり出てきて絶望しています。