Addieren von BigDecimals mithilfe von Streams

176

Ich habe eine Sammlung von BigDecimals (in diesem Beispiel a LinkedList), die ich zusammenfügen möchte. Ist es möglich, dafür Streams zu verwenden?

Mir ist aufgefallen, dass die StreamKlasse mehrere Methoden hat

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

Jedes davon hat eine bequeme sum()Methode. Aber, wie wir wissen, floatund doubleArithmetik ist fast immer eine schlechte Idee.

Gibt es eine bequeme Möglichkeit, BigDecimals zusammenzufassen?

Dies ist der Code, den ich bisher habe.

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());
}

Wie Sie sehen können, fasse ich die BigDecimals mit zusammen BigDecimal::doubleValue(), aber dies ist (wie erwartet) nicht genau.

Post-Answer-Bearbeitung für die Nachwelt:

Beide Antworten waren äußerst hilfreich. Ich wollte ein wenig hinzufügen: Mein reales Szenario beinhaltet keine Sammlung von Rohdaten BigDecimal, sie sind in einer Rechnung verpackt. Aber ich konnte Aman Agnihotris Antwort ändern, um dies zu berücksichtigen, indem ich die map()Funktion für Stream verwendete:

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
quelle

Antworten:

348

Ursprüngliche Antwort

Ja, das ist möglich:

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

Was es tut ist:

  1. Erhalten Sie a List<BigDecimal>.
  2. Verwandle es in ein Stream<BigDecimal>
  3. Rufen Sie die Reduktionsmethode auf.

    3.1. Wir liefern nämlich einen Identitätswert zur Addition BigDecimal.ZERO.

    3.2. Wir spezifizieren die BinaryOperator<BigDecimal>, die zwei addiert BigDecimal, über eine Methodenreferenz BigDecimal::add.

Aktualisierte Antwort nach der Bearbeitung

Ich sehe, dass Sie neue Daten hinzugefügt haben, daher lautet die neue Antwort:

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);

Es ist größtenteils dasselbe, außer dass ich eine totalMapperVariable hinzugefügt habe , die eine Funktion von Invoicebis hat BigDecimalund den Gesamtpreis dieser Rechnung zurückgibt.

Dann erhalte ich a Stream<Invoice>, ordne es a zu Stream<BigDecimal>und reduziere es dann auf a BigDecimal.

Aus Sicht des OOP-Entwurfs würde ich Ihnen raten total(), die bereits definierte Methode auch tatsächlich zu verwenden , dann wird es noch einfacher:

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

Hier verwenden wir direkt die Methodenreferenz in der mapMethode.

Skiwi
quelle
12
+1 für Invoice::totalvs invoice -> invoice.total().
Ryvantage
12
+1 für Methodenreferenzen und zum Hinzufügen von Zeilenumbrüchen zwischen Stream-Operationen, die meiner Meinung nach die Lesbarkeit erheblich verbessern.
Stuart Marks
Wie würde es funktionieren, wenn ich beispielsweise Invoice :: total und Invoice :: tax in ein neues Array aufnehmen möchte
Richard Lau
Die Java-Standardbibliothek verfügt bereits über Funktionen zum Summieren von Ganzzahlen / Doppelwerten Collectors.summingInt(), die jedoch für BigDecimals fehlen . Anstatt reduce(blah blah blah)schwer lesbares Schreiben zu schreiben , ist es besser, einen fehlenden Kollektor für BigDecimalund .collect(summingBigDecimal())am Ende Ihrer Pipeline zu schreiben .
Csharpfolk
2
Dieser Ansatz kann zu NullponterException
gstackoverflow
11

Dieser Beitrag hat bereits eine überprüfte Antwort, aber die Antwort filtert nicht nach Nullwerten. Die richtige Antwort sollte Nullwerte verhindern, indem die Object :: nonNull-Funktion als Prädikat verwendet wird.

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

Dies verhindert, dass Nullwerte beim Reduzieren summiert werden.

Siraj
quelle
7

Sie können die Werte eines BigDecimalStreams mit einem wiederverwendbaren Collector mit folgenden Namen zusammenfassen summingUp:

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

Das Collectorkann folgendermaßen implementiert werden:

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

Verwenden Sie diesen Ansatz, um die Liste von BigDecimal zusammenzufassen:

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

Dieser Ansatz ordnet jedes BigDecimal nur einem BigDecimal zu und reduziert es durch Summieren, das dann mithilfe der get()Methode zurückgegeben wird.

Hier ist eine weitere einfache Möglichkeit, dieselbe Summierung durchzuführen:

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

Aktualisieren

Wenn ich den Klassen- und Lambda-Ausdruck in die bearbeitete Frage schreiben würde, hätte ich ihn wie folgt geschrieben:

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
quelle
Ist da nicht .map(n -> n)nutzlos? Wird auch get()nicht benötigt.
Rohit Jain
@RohitJain: Aktualisiert. Vielen Dank. Ich habe verwendet, get()da es den Wert von zurückgibt, der Optionalvom reduceAufruf zurückgegeben wird. Wenn man mit dem arbeiten Optionaloder einfach die Summe ausdrucken will, dann ist das ja get()nicht nötig. Beim Drucken der Optionalen Option wird jedoch direkt Optional[<Value>]die Syntax gedruckt, die der Benutzer meines Erachtens nicht benötigen würde. Wird get()also in gewisser Weise benötigt, um den Wert aus dem zu erhalten Optional.
Aman Agnihotri
@ryvantage: Ja, dein Ansatz ist genau so, wie ich es gemacht hätte. :)
Aman Agnihotri
Verwenden Sie keinen bedingungslosen getAnruf! Wenn valueses sich um eine leere Liste handelt, enthält das optionale Element keinen Wert und löst ein NoSuchElementExceptionWann aus, getdas aufgerufen wird. Sie können values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)stattdessen verwenden.
eee
4

Wenn Ihnen eine Abhängigkeit von Drittanbietern nichts ausmacht, gibt es in Eclipse Collections eine Klasse namens Collectors2, die Methoden enthält, die Collectors zum Summieren und Zusammenfassen von BigDecimal und BigInteger zurückgeben. Diese Methoden verwenden eine Funktion als Parameter, sodass Sie einen BigDecimal- oder BigInteger-Wert aus einem Objekt extrahieren können.

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());

Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.

Donald Raab
quelle