Menambahkan BigDecimal menggunakan Streaming

178

Saya memiliki koleksi BigDecimals (dalam contoh ini, a LinkedList) yang ingin saya tambahkan bersama. Apakah mungkin menggunakan stream untuk ini?

Saya perhatikan Streamkelas memiliki beberapa metode

Stream::mapToInt
Stream::mapToDouble
Stream::mapToLong

Masing-masing memiliki sum()metode yang mudah . Tapi, seperti yang kita tahu, floatdan doubleberhitung hampir selalu merupakan ide yang buruk.

Jadi, apakah ada cara mudah untuk menyimpulkan BigDecimals?

Ini adalah kode yang saya miliki sejauh ini.

public static void main(String[] args) {
    LinkedList<BigDecimal> values = new LinkedList<>();
    values.add(BigDecimal.valueOf(.1));
    values.add(BigDecimal.valueOf(1.1));
    values.add(BigDecimal.valueOf(2.1));
    values.add(BigDecimal.valueOf(.1));

    // Classical Java approach
    BigDecimal sum = BigDecimal.ZERO;
    for(BigDecimal value : values) {
        System.out.println(value);
        sum = sum.add(value);
    }
    System.out.println("Sum = " + sum);

    // Java 8 approach
    values.forEach((value) -> System.out.println(value));
    System.out.println("Sum = " + values.stream().mapToDouble(BigDecimal::doubleValue).sum());
    System.out.println(values.stream().mapToDouble(BigDecimal::doubleValue).summaryStatistics().toString());
}

Seperti yang Anda lihat, saya menyimpulkan BigDecimal menggunakan BigDecimal::doubleValue(), tapi ini (seperti yang diharapkan) tidak tepat.

Sunting pasca jawaban untuk keturunan:

Kedua jawaban itu sangat membantu. Saya ingin menambahkan sedikit: skenario kehidupan nyata saya tidak melibatkan koleksi mentah BigDecimal, mereka dibungkus dalam faktur. Tapi, saya bisa mengubah jawaban Aman Agnihotri untuk akun ini dengan menggunakan map()fungsi stream:

public static void main(String[] args) {

    LinkedList<Invoice> invoices = new LinkedList<>();
    invoices.add(new Invoice("C1", "I-001", BigDecimal.valueOf(.1), BigDecimal.valueOf(10)));
    invoices.add(new Invoice("C2", "I-002", BigDecimal.valueOf(.7), BigDecimal.valueOf(13)));
    invoices.add(new Invoice("C3", "I-003", BigDecimal.valueOf(2.3), BigDecimal.valueOf(8)));
    invoices.add(new Invoice("C4", "I-004", BigDecimal.valueOf(1.2), BigDecimal.valueOf(7)));

    // Classical Java approach
    BigDecimal sum = BigDecimal.ZERO;
    for(Invoice invoice : invoices) {
        BigDecimal total = invoice.unit_price.multiply(invoice.quantity);
        System.out.println(total);
        sum = sum.add(total);
    }
    System.out.println("Sum = " + sum);

    // Java 8 approach
    invoices.forEach((invoice) -> System.out.println(invoice.total()));
    System.out.println("Sum = " + invoices.stream().map((x) -> x.total()).reduce((x, y) -> x.add(y)).get());
}

static class Invoice {
    String company;
    String invoice_number;
    BigDecimal unit_price;
    BigDecimal quantity;

    public Invoice() {
        unit_price = BigDecimal.ZERO;
        quantity = BigDecimal.ZERO;
    }

    public Invoice(String company, String invoice_number, BigDecimal unit_price, BigDecimal quantity) {
        this.company = company;
        this.invoice_number = invoice_number;
        this.unit_price = unit_price;
        this.quantity = quantity;
    }

    public BigDecimal total() {
        return unit_price.multiply(quantity);
    }

    public void setUnit_price(BigDecimal unit_price) {
        this.unit_price = unit_price;
    }

    public void setQuantity(BigDecimal quantity) {
        this.quantity = quantity;
    }

    public void setInvoice_number(String invoice_number) {
        this.invoice_number = invoice_number;
    }

    public void setCompany(String company) {
        this.company = company;
    }

    public BigDecimal getUnit_price() {
        return unit_price;
    }

    public BigDecimal getQuantity() {
        return quantity;
    }

    public String getInvoice_number() {
        return invoice_number;
    }

    public String getCompany() {
        return company;
    }
}
ryvantage
sumber

Jawaban:

354

Jawaban asli

Ya, ini mungkin:

List<BigDecimal> bdList = new ArrayList<>();
//populate list
BigDecimal result = bdList.stream()
        .reduce(BigDecimal.ZERO, BigDecimal::add);

Apa yang dilakukannya adalah:

  1. Dapatkan a List<BigDecimal>.
  2. Ubah menjadi Stream<BigDecimal>
  3. Panggil metode pengurangan.

    3.1. Kami memberikan nilai identitas untuk penambahan, yaitu BigDecimal.ZERO.

    3.2. Kami menentukan BinaryOperator<BigDecimal>, yang menambahkan dua BigDecimal, melalui referensi metode BigDecimal::add.

Jawaban yang diperbarui, setelah diedit

Saya melihat bahwa Anda telah menambahkan data baru, oleh karena itu jawaban baru akan menjadi:

List<Invoice> invoiceList = new ArrayList<>();
//populate
Function<Invoice, BigDecimal> totalMapper = invoice -> invoice.getUnit_price().multiply(invoice.getQuantity());
BigDecimal result = invoiceList.stream()
        .map(totalMapper)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

Sebagian besar sama, kecuali bahwa saya telah menambahkan totalMappervariabel, yang memiliki fungsi dari Invoiceke BigDecimaldan mengembalikan harga total faktur itu.

Lalu saya mendapatkan Stream<Invoice>, memetakannya menjadi Stream<BigDecimal>dan kemudian menguranginya menjadi BigDecimal.

Sekarang, dari titik desain OOP saya akan menyarankan Anda untuk juga benar-benar menggunakan total()metode, yang telah Anda tetapkan, maka itu bahkan menjadi lebih mudah:

List<Invoice> invoiceList = new ArrayList<>();
//populate
BigDecimal result = invoiceList.stream()
        .map(Invoice::total)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

Di sini kita langsung menggunakan referensi metode dalam mapmetode.

skiwi
sumber
12
1 untuk Invoice::totalvs invoice -> invoice.total().
ryvantage
12
+1 untuk referensi metode dan untuk menambahkan jeda baris antara operasi streaming, yang keduanya IMHO meningkatkan keterbacaan secara signifikan.
Stuart Marks
bagaimana cara kerjanya jika saya ingin menambahkan katakanlah Faktur :: total dan Faktur :: pajak ke array baru
Richard Lau
Pustaka standar Java sudah memiliki fungsi untuk menjumlahkan bilangan bulat / ganda seperti Collectors.summingInt(), tetapi melewatkannya selama BigDecimals. Daripada menulis reduce(blah blah blah)yang sulit dibaca, akan lebih baik untuk menulis kolektor yang hilang untuk BigDecimaldan miliki .collect(summingBigDecimal())di akhir saluran Anda.
csharpfolk
2
Pendekatan ini dapat menyebabkan NullponterException
gstackoverflow
11

Posting ini sudah memiliki jawaban yang dicentang, tetapi jawabannya tidak memfilter untuk nilai nol. Jawaban yang benar harus mencegah nilai nol dengan menggunakan fungsi Object :: nonNull sebagai predikat.

BigDecimal result = invoiceList.stream()
    .map(Invoice::total)
    .filter(Objects::nonNull)
    .filter(i -> (i.getUnit_price() != null) && (i.getQuantity != null))
    .reduce(BigDecimal.ZERO, BigDecimal::add);

Ini mencegah nilai nol dari upaya untuk dijumlahkan seperti yang kita kurangi.

Siraj
sumber
7

Anda dapat meringkas nilai-nilai BigDecimalaliran menggunakan Kolektor yang dapat digunakan kembali bernama :summingUp

BigDecimal sum = bigDecimalStream.collect(summingUp());

The Collectordapat diimplementasikan seperti ini:

public static Collector<BigDecimal, ?, BigDecimal> summingUp() {
    return Collectors.reducing(BigDecimal.ZERO, BigDecimal::add);
}
Igor Akkerman
sumber
5

Gunakan pendekatan ini untuk menjumlahkan daftar BigDecimal:

List<BigDecimal> values = ... // List of BigDecimal objects
BigDecimal sum = values.stream().reduce((x, y) -> x.add(y)).get();

Pendekatan ini memetakan masing-masing BigDecimal sebagai BigDecimal saja dan menguranginya dengan menjumlahkannya, yang kemudian dikembalikan menggunakan get()metode.

Berikut cara mudah lain untuk melakukan penjumlahan yang sama:

List<BigDecimal> values = ... // List of BigDecimal objects
BigDecimal sum = values.stream().reduce(BigDecimal::add).get();

Memperbarui

Jika saya menulis ekspresi kelas dan lambda dalam pertanyaan yang diedit, saya akan menulisnya sebagai berikut:

import java.math.BigDecimal;
import java.util.LinkedList;

public class Demo
{
  public static void main(String[] args)
  {
    LinkedList<Invoice> invoices = new LinkedList<>();
    invoices.add(new Invoice("C1", "I-001", BigDecimal.valueOf(.1), BigDecimal.valueOf(10)));
    invoices.add(new Invoice("C2", "I-002", BigDecimal.valueOf(.7), BigDecimal.valueOf(13)));
    invoices.add(new Invoice("C3", "I-003", BigDecimal.valueOf(2.3), BigDecimal.valueOf(8)));
    invoices.add(new Invoice("C4", "I-004", BigDecimal.valueOf(1.2), BigDecimal.valueOf(7)));

    // Java 8 approach, using Method Reference for mapping purposes.
    invoices.stream().map(Invoice::total).forEach(System.out::println);
    System.out.println("Sum = " + invoices.stream().map(Invoice::total).reduce((x, y) -> x.add(y)).get());
  }

  // This is just my style of writing classes. Yours can differ.
  static class Invoice
  {
    private String company;
    private String number;
    private BigDecimal unitPrice;
    private BigDecimal quantity;

    public Invoice()
    {
      unitPrice = quantity = BigDecimal.ZERO;
    }

    public Invoice(String company, String number, BigDecimal unitPrice, BigDecimal quantity)
    {
      setCompany(company);
      setNumber(number);
      setUnitPrice(unitPrice);
      setQuantity(quantity);
    }

    public BigDecimal total()
    {
      return unitPrice.multiply(quantity);
    }

    public String getCompany()
    {
      return company;
    }

    public void setCompany(String company)
    {
      this.company = company;
    }

    public String getNumber()
    {
      return number;
    }

    public void setNumber(String number)
    {
      this.number = number;
    }

    public BigDecimal getUnitPrice()
    {
      return unitPrice;
    }

    public void setUnitPrice(BigDecimal unitPrice)
    {
      this.unitPrice = unitPrice;
    }

    public BigDecimal getQuantity()
    {
      return quantity;
    }

    public void setQuantity(BigDecimal quantity)
    {
      this.quantity = quantity;
    }
  }
}
Aman Agnihotri
sumber
Bukankah tidak .map(n -> n)berguna di sana? Juga get()tidak diperlukan.
Rohit Jain
@RohitJain: Diperbarui. Terima kasih. Saya menggunakan get()karena mengembalikan nilai Optionalyang dikembalikan oleh reducepanggilan. Jika seseorang ingin bekerja dengan Optionalatau hanya mencetak jumlahnya, maka ya, get()tidak diperlukan. Tapi mencetak Opsional langsung mencetak Optional[<Value>]sintaksis yang saya ragu pengguna akan perlu. Jadi get()diperlukan cara untuk mendapatkan nilai dari Optional.
Aman Agnihotri
@ryvantage: Ya, pendekatan Anda persis seperti yang akan saya lakukan. :)
Aman Agnihotri
Jangan gunakan getpanggilan tanpa syarat ! Jika valuesdaftar kosong, opsional tidak akan berisi nilai dan akan membuang NoSuchElementExceptionketika getdipanggil. Anda bisa menggunakannya values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO).
eee
4

Jika Anda tidak keberatan dengan ketergantungan pihak ketiga, ada kelas bernama Collectors2 di Eclipse Collections yang berisi metode pengembalian Kolektor untuk menjumlahkan dan merangkum BigDecimal dan BigInteger. Metode ini menggunakan Function sebagai parameter sehingga Anda bisa mengekstraksi nilai BigDecimal atau BigInteger dari objek.

List<BigDecimal> list = mList(
        BigDecimal.valueOf(0.1),
        BigDecimal.valueOf(1.1),
        BigDecimal.valueOf(2.1),
        BigDecimal.valueOf(0.1));

BigDecimal sum =
        list.stream().collect(Collectors2.summingBigDecimal(e -> e));
Assert.assertEquals(BigDecimal.valueOf(3.4), sum);

BigDecimalSummaryStatistics statistics =
        list.stream().collect(Collectors2.summarizingBigDecimal(e -> e));
Assert.assertEquals(BigDecimal.valueOf(3.4), statistics.getSum());
Assert.assertEquals(BigDecimal.valueOf(0.1), statistics.getMin());
Assert.assertEquals(BigDecimal.valueOf(2.1), statistics.getMax());
Assert.assertEquals(BigDecimal.valueOf(0.85), statistics.getAverage());

Catatan: Saya pengendara untuk Eclipse Collections.

Donald Raab
sumber