きり丸の技術日記

技術検証したり、資格等をここに残していきます。

JavaでNullを意識せずにNullableな配列項目をStream APIで処理する

超がつく小ネタ。よく忘れるのでブログ化。

環境

結論

Optional型のofNullableメソッドでNullでも問題なく処理できるようにする。

そのあと、Stream APIflatMapメソッドとCollection型のstreamメソッドを組み合わせる。

Optional.ofNullable(detail) // Optional<List<Class>>
  .stream() // Stream<List<Class>>
  .flatMap(Collection::stream) // Stream<Class>
  ... // 以降は普段通りのStream処理をする

ソースコード

https://github.com/hirotoKirimaru/cucumber-sample/blob/6c47974ce8b5d03067b280a7d5267bf4deebebaa/src/main/java/kirimaru/biz/domain/Entry.java#L24

終わりに

Stream APIは未だに苦手です。

気付かないうちに配列の配列になってしまい、どういう風に処理をすればいいのか分からなくなります。

簡単な処理は覚えてきているので、難しい処理もどんどん慣れていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

f:id:nainaistar:20201013111905p:plain

IntelliJ IDEAでGradleのテストが遅いときに試す(タスクランナーの変更)

小ネタ。処理が遅いと同僚が困っていたので、教えたら劇的に改善しました。

環境

スクランナーを変更する

Gradleの設定画面から、ビルド及び実行に使用(Build and run using)とテストの実行に使用(Run tests using)をデフォルトのGradleからIntelliJ IDEAに変更します。

これをするだけで、15分くらいかかっていた処理が1分弱で終わるようになる可能性があります。

f:id:nainaistar:20210323000701p:plain

その他のメリット

スクランナーがGradleだと、DisplayNameでテスト名を表示してくれないので、メソッド名を頑張って名付ける必要があります。

スクランナーがIntelliJ IDEAだと、DisplayNameが有効になるので、テストの条件等を表現しやすいです。

次の画像は、「test_02」のDisplayNameが有効かどうかが分かる画像。

スクランナー:Gradle f:id:nainaistar:20210323000715p:plain

スクランナー:IntelliJ IDEA f:id:nainaistar:20210323000728p:plain

デメリット

上手くいけば高速化できますが、悪いと原因不明のエラーでテストができなくなる可能性があります。

今回の検証中でも、変更してから「Command line is too long 」エラーが起きて動かなくなりました。

このような不明なエラーに悩まされてしまう可能性はゼロではないので、速度に不満が無ければGradleのタスクランナーを使用していた方が総合的には早くなるかもしれません。

また、CI上で実行するテストもGradleなので、ローカルとリモートの挙動の違いが起きると調査が非常につらいです。

あと、最終的によく見るメッセージはCIのメッセージなので、メソッド名をサボることによるメリットも大きいとは言えません。

終わりに

個人的には高速化が便利なのでタスクランナーをIntelliJ IDEAに変更しています。

しかし、IDEをバージョンアップしたことによって発生しうるタスクランナーのエラー等もあるので、諸手を上げてオススメすることはできません。

デフォルトのGradleタスクランナーでの処理が重すぎて耐えきれない、という人は一度こちらの設定を試してはいかがでしょうか。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

公式ヘルプ:Gradle でのテスト pleiades.io

Intellij IDEAのgradleプロジェクトビルド時に Command line is too long が出る場合の対処 qiita.com

f:id:nainaistar:20210323001446p:plain

JavaのBigDecimalをまとめる(リストのBigDecimalの計算も)【2021年】

自分が知りたかったBigDecimalの知識を集める記事。なお、基本型を扱う関係上、新規性は多くありません。強いて言えば、2021年でも扱えるってことくらいですかね。

環境

  • Java
    • 15

この記事を読んでわかること一覧

  • precisionとscale(有効桁数と精度)
  • 型宣言
  • 足し算
  • 引き算
  • 掛け算
  • 割り算
  • 10倍や100倍、または1/10や1/100
  • 余り
  • 四捨五入等の丸めを行う
  • 比較
  • ±変換
  • 文字列化
  • リストのBigDecimalを計算する
  • 1.00000を1に丸める

precisionとscale(有効桁数と精度)

BigDecimalを扱うのであれば抑えておくべき内容。

  • precision
    • 有効桁数。最初に0以外の数字が出てからの桁数。
  • scale
    • 精度。小数点以下の桁数。

scaleはsetScaleメソッドが存在するが、precisionに関してはコンストラクタか、掛け算や割り算のパラメータMathContextで設定しなければならない。

(例)

BigDecimal example = BigDecimal.valueOf(100.00001);
example.scale(); // 5 
example.precision(): // 8

BigDecimal example2 = BigDecimal.valueOf(0.00001);
example2.scale(); // 5(実際には0.000010が生成されて6になる)
example.precision(); // 1(実際には0.000010が生成されて2になる)

型宣言

BigDecimalの型宣言をする方法。

valueOfのstaticファクトリーメソッドを使いましょう。new 時にDouble型を渡すと、意図しない数値になります。

System.out.println(new BigDecimal(123.45));
// 123.4500000000000028421709430404007434844970703125
System.out.println(new BigDecimal("123.45"));
// 123.45
System.out.println(BigDecimal.valueOf(123.45));
// 123.45

また、0, 1, 10に関してはstatic変数が用意されているので、こちらを使用しましょう。

BigDecimal.ONE;
BigDecimal.ZERO;
BigDecimal.TEN;

足し算

addメソッドを使用します。また、小数点以下の桁数は多い方に合わせます。

(例)1.0 + 1.00 = 2.00

BigDecimal.ONE.add(BigDecimal.ONE);
// BigDecimal.valueOf(2);

引き算

subtractメソッドを使用します。また、小数点以下の桁数は多い方に合わせます。

(例)1.0 - 1.00 = 0.00

BigDecimal.ONE.subtract(BigDecimal.ONE);
// BigDecimal.valueOf(0);

掛け算

multiplyメソッドを使用します。

単純に掛け算するタイプのメソッドと、有効桁数を設定して丸め設定をするタイプのメソッドがあります。計算する桁数が膨大なのであまり細かく計算する必要が無い場合は、後者を使用しましょう。

// 12345.6789 * 10 で有効桁数を2桁、それ以下を切り捨てする計算
assertThat(
    BigDecimal.valueOf(12345.6789)
        .multiply(BigDecimal.TEN, new MathContext(2, RoundingMode.DOWN))
        .toPlainString()
).isEqualTo("120000");

// 12345.6789 * 10 でscaleが変わっていないため、計算結果が123456.7890となる。
assertThat(
    BigDecimal.valueOf(12345.6789)
        .multiply(BigDecimal.TEN)
        .toPlainString()
).isEqualTo("123456.7890");

割り算

divideメソッドを使用します。

単純に割り算するタイプのメソッドと、有効桁数を設定して丸め設定をするタイプのメソッド、精度を設定して丸め設定をするタイプのメソッドがあります。

割り切れないとExceptionが発生するので、必ずRoundingModeは使用しましょう。

// 単純に1 / 3 の割り算する. 割り切れないとArithmeticExceptionが発生する.
assertThatThrownBy(
    () ->   BigDecimal.ONE.divide(BigDecimal.valueOf(3))
).isInstanceOfSatisfying(
    ArithmeticException.class,
    (e) -> e.getMessage().equals("Non-terminating decimal expansion; no exact representable decimal result.")
);

// 1 / 3 の割り算をした後に、第二引数のscaleで丸める(切り上げする)
assertThat(
    BigDecimal.ONE
        .divide(BigDecimal.valueOf(3), 3, RoundingMode.UP)
        .toPlainString()
).isEqualTo("0.334");

// 1 / 3 の割り算をした後に、MathContextの有効数字で丸める(切り上げする)
assertThat(
    BigDecimal.ONE
        .divide(BigDecimal.valueOf(3), new MathContext(3, RoundingMode.UP))
        .toPlainString()
).isEqualTo("0.334");

また、エラーにならないように適宜丸め処理を行う関係上、精度は意識しておきましょう。

精度を増やさないままに丸めてしまうと、意図しない結果になりかねません。

// 1 / 3の精度を増やさなかったので、0.3を切り上げて1になってしまう
assertThat(
    BigDecimal.ONE
        .divide(BigDecimal.valueOf(3), RoundingMode.UP)
        .toPlainString()
).isEqualTo("1");


// 1 / 3 の割り算をする前に精度を増やしておき、増やした精度で丸める(切り上げする)
assertThat(
    BigDecimal.ONE
        .setScale(3, RoundingMode.UP)
        .divide(BigDecimal.valueOf(3), RoundingMode.UP)
        .toPlainString()
).isEqualTo("0.334");

// (1 / 3) * 100で割り算で切り上げてしまったので、小数点以下が変になる
assertThat(
    BigDecimal.ONE
        .divide(BigDecimal.valueOf(3), 3, RoundingMode.UP)
        .multiply(BigDecimal.valueOf(100))
        .toPlainString()
).isEqualTo("33.400");

// (1 / 3) * 100で割り算の精度を十分用意しておき、後で丸めると目的通りになる
assertThat(
    BigDecimal.ONE
        .divide(BigDecimal.valueOf(3), 6, RoundingMode.UP)
        .multiply(BigDecimal.valueOf(100))
        .setScale(3, RoundingMode.UP)
        .toPlainString()
).isEqualTo("33.334");

10倍や100倍、または1/10や1/100

scaleByPowerOfTenメソッドを使用すると、10倍や1/10倍が簡単にできます。

// 10倍
BigDecimal.ONE.scaleByPowerOfTen(1);
// 100倍
BigDecimal.ONE.scaleByPowerOfTen(2);
// 1/10倍
BigDecimal.ONE.scaleByPowerOfTen(-1);
// 1/100倍
BigDecimal.ONE.scaleByPowerOfTen(-2);

movePointRightメソッドで10倍やmovePointLeftメソッドで1/10倍も簡単にできます。

// 10倍
BigDecimal.TEN.movePointRight(1);
// 1/10倍
BigDecimal.ONE.movePointLeft(1);

注意点としては、multiplyメソッドでは有効桁数や精度が変わりますが、10倍や1/10倍するメソッドでは精度が変わりません。10倍した場合は問題ないでしょうが、1/10倍した時の結果は注意しましょう。

assertThat(
    BigDecimal.ONE.movePointRight(1).toPlainString()
).isEqualTo("10");

assertThat(
    BigDecimal.TEN.movePointLeft(1).toPlainString()
).isEqualTo("1.0"); // 1.0が期待値?1ではない?

assertThat(
    BigDecimal.ONE.movePointLeft(3).toPlainString()
).isEqualTo("0.001");

assertThat(
    BigDecimal.ONE.multiply(BigDecimal.TEN).toPlainString()
).isEqualTo("10");

assertThat(
    BigDecimal.TEN.divide(BigDecimal.TEN).toPlainString()
).isEqualTo("1");

余り

remainderメソッドで余りを求めることができます。

assertThat(
    BigDecimal.valueOf(10.11)
        .remainder(BigDecimal.valueOf(3))
).isEqualTo(BigDecimal.valueOf(1.11));

四捨五入等の丸めを行う

setScaleで意図的に丸めることができます。掛け算や割り算で精度を指定しない場合は、実行対象のBigDecimalの精度が使われます。

ですので、最初と最後にsetScaleをすると、意図を表現しやすいです。処理順序に左右される意図的な丸めを発生させない時に使用します。

// (1/3) * 100を最後に3桁の切り上げで丸める。
assertThat(
    BigDecimal.ONE // 精度1桁
        .setScale(6, RoundingMode.UP) // 精度6桁
        .divide(BigDecimal.valueOf(3), RoundingMode.UP) // 精度6桁
        .multiply(BigDecimal.valueOf(100)) // 精度6桁
        .setScale(3, RoundingMode.UP) // 精度3桁
        .toPlainString()
).isEqualTo("33.334");

比較

compareToを使用します。比較結果によって、数値が返却されるので、その値を元に処理してください。

  • A > B
    • 1
  • A < B
    • -1
  • A = B
    • 0
// 10 > 1の時は1
assertThat(
    BigDecimal.TEN.compareTo(BigDecimal.ONE)
).isEqualTo(1);

// 1 < 10の時は-1
assertThat(
    BigDecimal.ONE.compareTo(BigDecimal.TEN)
).isEqualTo(-1);

// 一致している時は0
// 0.0 と 0.00の比較
assertThat(
    BigDecimal.ONE.setScale(1)
        .compareTo(BigDecimal.ONE.setScale(2))
).isEqualTo(0);

equalsは精度が異なると一致しないので、精度に確信が無い限りは使わないでください。

// 0.0と0.00の比較はfalseになる
assertThat(
    BigDecimal.ONE.setScale(1)
        .equals(BigDecimal.ONE.setScale(2))
).isEqualTo(false);

0除算を回避するために、事前に0ではないことを確認することもあります。その場合、BigDecimal.ZEROとのcompareToメソッドでも問題ないです。もっと処理速度を意識する時は、compareToメソッド内で呼んでいるsignumメソッドを直接使った方が早いようです。

※ 私の手元環境で100万回実行した時の速度そんなに変わらなかったので、使いたい方を使ってください。

signumメソッドは、値がプラスかマイナスか0かで判断します。

// 0 と比較
assertThat(
    BigDecimal.ZERO
        .compareTo(BigDecimal.ZERO.setScale(2))
).isEqualTo(0);

// 絶対値がプラスでもマイナスでもない
assertThat(
    BigDecimal.ZERO.signum()
).isEqualTo(0);

±変換

negateメソッドを使用します。単純に-1を掛け算する必要が無いので、メソッドが見やすいです。

// 1 * -1 = -1
assertThat(
    BigDecimal.ONE.negate()
).isEqualTo(BigDecimal.valueOf(-1));
  
// 1 * -1 * -1 = 1
assertThat(
    BigDecimal.ONE.negate().negate()
).isEqualTo(BigDecimal.valueOf(1));

文字列化

toPlainStringメソッドを使用します。toStringメソッドだと、有効桁数や精度に合わせて丸めた表記になってしまうので、意図しないものが表示される可能性があります。意図している場合は、toStringを使用しましょう。

assertThat(
    BigDecimal.valueOf(0.0000001).toString()
).isEqualTo("1.0E-7");

assertThat(
    BigDecimal.valueOf(0.0000001).toPlainString()
).isEqualTo("0.00000010");

リストのBigDecimalを計算する

BigDecimalはプリミティブ型ではないので、IntStreamLongStream等の便利な型はありません。

StreamAPIのreduceメソッドを使用すると、目的通りの計算をすることができます。

次の例は1を10個含むリストから計算する方法です。

List<BigDecimal> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    list.add(BigDecimal.ONE);
}

// 0 + 1 * 10の計算
assertThat(
    list.stream().reduce(
        BigDecimal.ZERO, BigDecimal::add
    )
).isEqualTo(BigDecimal.TEN);

// 10 - 1 * 10の計算
assertThat(
    list.stream().reduce(
        BigDecimal.TEN, BigDecimal::subtract
    )
).isEqualTo(BigDecimal.ZERO);

RoundingModeの一覧

非常に具体的な例がJavaDocにも記載されているので、こちらを参考にしてください。具体的な値と丸め方が表形式になっているので、こちらに関しては公式を見るのが確実です。

docs.oracle.com

1.00000を1として扱う

stripTrailingZerosを実行すると、1.000等々の0で埋めた実際の小数点を確認できます。当然、小数点3桁目に0以外が含まれていた場合は、3桁の小数点であるという精度を表示してくれます。

    assertThat(
        new BigDecimal("1.00000000000").scale()
    ).isEqualTo(11);
    assertThat(
        new BigDecimal("1.00000000000").stripTrailingZeros().scale()
    ).isEqualTo(0);

    assertThat(
        new BigDecimal("1.00000000001").scale()
    ).isEqualTo(11);
    assertThat(
        new BigDecimal("1.00000000001").stripTrailingZeros().scale()
    ).isEqualTo(11);

ソースコード

BigDecimalの検証用ソース github.com

終わりに

なんだかんだ、今まではお金を直接扱うことが無かったのでBigDecimalを使う機会がありませんでした。

普段使わないので色々調べていくと、自分が理解していなかったことも多かったです。有効桁数と精度に関しては本当に分かってませんでした。

公式のJavaDoc見ればいいのでそんなに調べる機会は無いと思いますが、腰を据えて調べたくなった時に今後も記事にしていきたいです。

参考資料

Java16(en) docs.oracle.com

Java8(ja) docs.oracle.com

Java8のRoundingMode docs.oracle.com

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

BigDecimal, precision and scale stackoverflow.com