これらの落とし穴をマスターせずに、BigDecimal を気軽に使用する勇気はありますか?

背景

私は金融関連のプロジェクトに従事してきたので、BigDecimal についてはよく知っていますが、無知、理解、または不適切な使用により多くの学生が損失を被るのを見てきました。

したがって、財務関連のプロジェクトに従事している場合、またはプロジェクトに金額の計算が含まれる場合は、時間をかけてこの記事を読み、BigDecimal についてすべて学ぶ必要があります。

BigDecimal の概要

Java の java.math パッケージで提供される API クラス BigDecimal は、有効桁数が 16 桁を超える数値に対して正確な演算を実行するために使用されます。倍精度浮動小数点変数 double は 16 ビットの有効数を扱うことができますが、実際のアプリケーションでは、より大きな数またはより小さな数を演算して処理する必要がある場合があります。

一般に、正確な計算精度を必要としない数値の場合、Float および Double を直接使用して処理できますが、Double.valueOf(String) および Float.valueOf(String) は精度が失われます。したがって、正確な計算結果が必要な場合は、BigDecimal クラスを使用して操作する必要があります。

BigDecimal オブジェクトは、+、-、*、/ などの従来の算術演算子に対応するメソッドを提供し、これを通じて対応する演算を実行できます。BigDecimal は不変であり、四則演算を行うたびに新しいオブジェクトが生成されるため、加算、減算、乗算、除算を行う場合は、演算後の値を忘れずに保存してください。

BigDecimal の 4 つの落とし穴

BigDecimal を使用する場合、使用シーンには 4 つの落とし穴があることを理解しておく必要があります。これらのケースをマスターすれば、他の人が欠陥のあるコードを書いたときに、それを一目で認識できるようになります。

最初: 浮動小数点型の落とし穴

BigDecimal の落とし穴を理解する前に、よくある問題について説明します。Float、Double、その他の浮動小数点型を計算に使用すると、正確な値ではなく近似値が得られる可能性があります。

たとえば、次のコード:

	@Test
	public void test1(){
		BigDecimal a = new BigDecimal(0.01);
		BigDecimal b = BigDecimal.valueOf(0.01);
		System.out.println("a = " + a);
		System.out.println("b = " + b);
	}

結果は何ですか? 0.1? いいえ、上記のコードの実行結果は 0.100000024 です。この結果は、0.1 のバイナリ表現が無限ループするために発生します。

コンピュータのリソースには限りがあるため、2進数で0.1を正確に表現する方法はなく、限られた精度の中で0.1に近い2進数を最大化する「近似値」でしか表現できず、精度が落ちてしまいます。場合。

上記の現象については誰もが知っているので、詳細には触れません。同時に、科学表記法を使用する場合は浮動小数点型も考慮できますが、金額計算を伴う場合は BigDecimal を使用して計算する必要があるという結論になります。

では、BigDecimal は上記の浮動小数点の問題を確実に回避できるのでしょうか? 次の例を考えてみましょう。

	@Test
	public void test1(){
		BigDecimal a = new BigDecimal(0.01);
		BigDecimal b = BigDecimal.valueOf(0.01);
		System.out.println("a = " + a);
		System.out.println("b = " + b);
	}

上記の単体テストのコードで、a と b の結果は何でしょうか?

a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01

上記の例は、BigDecimal を使用した場合でも結果に精度の問題が依然として発生することを示しています。これには、BigDecimal オブジェクトを作成するときに、初期値がある場合、それを新しい BigDecimal の形式にするか BigDecimal#valueOf メソッドを使用するかが関係します。

上記の現象が発生する理由は、新しい BigDecimal を使用する際に渡される 0.1 がすでに浮動小数点型であるためであり、上記の値はあくまで近似値であるため、新しい BigDecimal を使用する際にはこの近似値が完全に保持されます。

BigDecimal#valueOf は異なり、ソースコードは次のように実装されています。

    public static BigDecimal valueOf(double val) {
        // Reminder: a zero double returns '0.0', so we cannot fastpath
        // to use the constant ZERO.  This might be important enough to
        // justify a factory approach, a cache, or a few private
        // constants, later.
        return new BigDecimal(Double.toString(val));
    }

valueOf 内では、Double#toString メソッドを使用して浮動小数点値を文字列に変換するため、精度の低下の問題は発生しません。

この時点で、基本的な結論を導き出します:まず、BigDecimal コンストラクターを使用する場合は、浮動小数点型の代わりに文字列を渡すようにします。次に、最初の条件が満たされない場合は、BigDecimal#valueOf メソッドを使用して構築できます。初期化値。

ここを拡張すると、BigDecimal の一般的な構築方法は次のとおりです。

BigDecimal(int)       创建一个具有参数所指定整数值的对象。
BigDecimal(double)    创建一个具有参数所指定双精度值的对象。
BigDecimal(long)      创建一个具有参数所指定长整数值的对象。
BigDecimal(String)    创建一个具有参数所指定以字符串表示的数值的对象。

パラメータの型がdoubleである構築メソッドでは上記の問題が発生するため、使用する場合には特に注意が必要です。

2 番目: 浮動小数点精度の落とし穴

2 つの BigDecimal 値が等しいかどうかを比較する場合、どのように比較しますか? equals メソッドと CompareTo メソッドを使用する必要がありますか?

まず例を見てみましょう。

    @Test
    public void test2(){
        BigDecimal a = new BigDecimal("0.01");
        BigDecimal b = new BigDecimal("0.010");
        System.out.println(a.equals(b));
        System.out.println(a.compareTo(b));
    }

一見同じように見えますが、実は根本的に異なります。

equals メソッドは、比較用に BigDecimal によって実装された equals メソッドに基づいています。直感的な印象としては、2 つのオブジェクトが同じかどうかを比較することです。では、コードはどのように実装されているのでしょうか?

    @Override
    public boolean equals(Object x) {
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
        if (scale != xDec.scale)
            return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) {
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
        } else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);

        return this.inflated().equals(xDec.inflated());
    }

コードを注意深く読むと、equals メソッドは値が等しいかどうかを比較するだけでなく、精度が同じかどうかも比較していることがわかります。上記の例では、2 つの精度が異なるため、equals メソッドの結果は当然 false になります。CompareTo メソッドは Comparable インターフェイスを実装します。実際に比較されるのは値のサイズです。返される値は -1 (より小さい)、0 (等しい)、および 1 (より大きい) です。

基本的な結論:通常、2 つの BigDecimal 値のサイズを比較する場合は、このメソッドによって実装されている CompareTo メソッドを使用します。比較の精度が厳密に制限されている場合は、equals メソッドの使用を検討できます。

さらに、このシナリオは、BigDecimal("0")、BigDecimal("0.0")、BigDecimal("0.00") の比較など、0 の値を比較する場合によく発生します。この場合、比較には CompareTo メソッドを使用する必要があります。

3 番目: 設定精度の落とし穴

プロジェクトでは、多くの学生が BigDecimal で計算する際に計算結果の精度と丸めモードを設定していないのを見て非常に不安になりましたが、ほとんどの場合は問題ありません。ただし、次のシナリオでは必ずしもそうであるとは限りません。

    @Test
    public void test3(){
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("3.0");
        a.divide(b);
    }


上記のコードを実行すると結果はどうなるでしょうか? 算術例外!

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

    at java.math.BigDecimal.divide(BigDecimal.java:1690)
    ...

この例外の発生については、公式ドキュメントでも説明されています。

商に終端のない 10 進展開があり、演算が正確な結果を返すように指定されている場合、ArithmeticException がスローされます。それ以外の場合は、他の演算の場合と同様に、除算の正確な結果が返されます。

要約すると、除算演算中に商が無限小数 (0.333...) であり、演算の結果が正確な数値であることが期待される場合、ArithmeticException がスローされます。

この時点では、divide メソッドを使用する場合の結果の精度を指定するだけです。

    @Test
    public void test3(){
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("3.0");
        BigDecimal c = a.divide(b, 2,RoundingMode.HALF_UP);
        System.out.println(c);
    }

上記のコードを実行すると、入力結果は 0.33 になります。

基本的な結論: BigDecimal で (すべての) 演算を実行する場合は、常に精度と丸めモードを明示的に指定してください。

拡張すると、丸めモードは RoundingMode 列挙クラスで定義され、合計 8 つのタイプがあります。

  • RoundingMode.UP: ゼロから四捨五入する丸めモード。非ゼロ部分を破棄する前に、常に数値をインクリメントします (非ゼロ破棄部分の前の数値に常に 1 を加算します)。この丸めモードでは、計算値のサイズが決して小さくならないことに注意してください。
  • RoundingMode.DOWN: ゼロに近い丸めモード。部分を破棄する前に数値を増分しないでください (破棄される部分の前の数値に 1 を加えないでください。つまり、切り捨てられます)。この丸めモードでは、計算値のサイズが増加することはありません。
  • RoundingMode.CEILING: 正の無限大に近い丸めモード。BigDecimal が正の場合、丸め動作は ROUNDUP と同じになり、負の場合、丸め動作は ROUNDDOWN と同じになります。この丸めモードでは計算値が決して減らないことに注意してください。
  • RoundingMode.FLOOR: 負の無限大に近い丸めモード。BigDecimal が正の場合、丸め動作は ROUNDDOWN と同じになり、負の場合、丸め動作は ROUNDUP と同じになります。この丸めモードでは計算値が増加することはありません。
  • RoundingMode.HALF_UP: 「最も近い」数値に向かって四捨五入する丸めモード、または 2 つの隣接する数値間の距離が等しい場合は切り上げます。破棄部分が 0.5 以上の場合、丸め動作は ROUND_UP と同じになります。それ以外の場合、丸め動作は ROUND_DOWN と同じになります。なお、これは小学校で習った丸めモード(四捨五入)です。
  • RoundingMode.HALF_DOWN: 「最も近い」数値に向かって四捨五入する、または 2 つの隣接する数値までの距離が等しい場合は切り上げる丸めモード。廃棄部分が 0.5 を超える場合、丸め動作は ROUND_UP と同じになります。それ以外の場合、丸め動作は ROUND_DOWN (10 分の 1 に四捨五入) と同じになります。
  • RoundingMode.HALF_EVEN: 「最も近い」数値に向かって丸めます。隣接する 2 つの数値までの距離が等しい場合は、隣接する偶数に向かって丸めます。切り捨てられる部分の左側の数値が奇数の場合、丸め動作は ROUNDHALFUP と同じになり、偶数の場合、丸め動作は ROUNDHALF_DOWN と同じになります。この丸めモードにより、一連の計算が繰り返されるときの累積誤差が最小限に抑えられることに注意してください。この丸めモードは「バンカーズ丸め」とも呼ばれ、主に米国で使用されています。5 ポイントに四捨五入するケースは 2 つあります。前の桁が奇数の場合はその桁が配置され、そうでない場合は破棄されます。次の例は、小数点以下 1 桁を保持したこの丸め方法の結果です。1.15 ==> 1.2 、1.25 ==> 1.2
  • RoundingMode.UNNECESSARY: 要求された操作の結果が正確であるため、丸めが必要ないことをアサートします。正確な結果を取得する演算にこの丸めモードが指定されている場合、ArithmeticException がスローされます。

通常、使用する丸めは RoundingMode.HALF_UP です。

4 番目: 3 つの文字列出力の落とし穴

BigDecimalを使用した後、String型に変換する必要がありますが、どうすればよいでしょうか。文字列に直接?

まず次のコードを見てみましょう。

@Test
public void test4(){
    BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
    System.out.println(a.toString());
}

実行結果は上記の対応する値ですか? そうではない:

3.563453525545672E+16

つまり、当初は文字列を出力したかったのですが、結果は科学的表記法の値でした。

ここでは、BigDecimal で文字列を変換する 3 つの方法を理解する必要があります。

  • toPlainString(): 科学表記法は使用しません。
  • toString(): 必要に応じて科学表記法を使用します。
  • toEngineeringString(): 必要に応じてエンジニアリング表記を使用します。科学的表記法と似ていますが、指数の累乗がすべて 3 の倍数である点が異なります。これは、多くの単位変換が 10^3 であるため、工学アプリケーションに便利です。

3 つの方法で表示される結果の例は次のとおりです。

計算方法

 基本的な結論:データ結果の表示形式に応じて、さまざまな文字列出力メソッドが使用されますが、最も一般的に使用されるメソッドは toPlainString() です

さらに、NumberFormat クラスの format() メソッドは、パラメータとして BigDecimal オブジェクトを使用でき、BigDecimal を使用して、通貨値、パーセント値、有効数字 16 桁を超える一般的な値を書式設定できます。

使用例は以下のとおりです。

NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用
NumberFormat percent = NumberFormat.getPercentInstance();  //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小数点最多3位

BigDecimal loanAmount = new BigDecimal("15000.48"); //金额
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘

System.out.println("金额:\t" + currency.format(loanAmount));
System.out.println("利率:\t" + percent.format(interestRate));
System.out.println("利息:\t" + currency.format(interest));

出力は次のとおりです。

金额: ¥15,000.48 
利率: 0.8% 
利息: ¥120.00

まとめ

この記事では、BigDecimal の使用シナリオの落とし穴と、これらの落とし穴に基づいて導き出された「ベスト プラクティス」を紹介します。一部のシナリオでは BigDecimal を使用することをお勧めします。BigDecimal を使用すると精度が向上しますが、特に大規模で複雑な操作を処理する場合は、double や float と比較してパフォーマンスがある程度低下します。したがって、一般的な精度の計算に BigDecimal を使用する必要はありません。使用する必要がある場合は、上記の落とし穴を回避する必要があります。
—————————————
著作権に関する声明: この記事は CSDN ブロガー「Programming New Vision」によるオリジナルの記事であり、CC 4.0 BY-SA 著作権契約に従っています。オリジナルのソース リンクとこれを添付してください。声明。
元のリンク: https://blog.csdn.net/wo541075754/article/details/125863927

これらの落とし穴を理解せずに BigDecimal を使用する勇気はありますか? _springboot bigdecmal reality 4e_Program New Vision ブログ - CSDN ブログ

おすすめ

転載: blog.csdn.net/Alex_81D/article/details/132294888