Dodawanie BigDecimals przy użyciu strumieni

178

Mam kolekcję BigDecimals (w tym przykładzie a LinkedList), którą chciałbym dodać. Czy można do tego wykorzystać strumienie?

Zauważyłem, że Streamklasa ma kilka metod

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

Z których każdy ma wygodną sum()metodę. Ale, jak wiemy, floati doublearytmetyka jest prawie zawsze zły pomysł.

Czy jest więc wygodny sposób podsumowania BigDecimals?

To jest kod, który mam do tej pory.

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

Jak widać, podsumowuję wartości BigDecimals przy użyciu BigDecimal::doubleValue(), ale jest to (zgodnie z oczekiwaniami) nieprecyzyjne.

Edycja po odpowiedzi dla potomnych:

Obie odpowiedzi były niezwykle pomocne. Chciałem trochę dodać: mój scenariusz z życia nie zakłada zbierania surowych BigDecimals, są one zawinięte w fakturę. Ale udało mi się zmodyfikować odpowiedź Amana Agnihotri, aby to wyjaśnić, używając map()funkcji 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
źródło

Odpowiedzi:

354

Oryginalna odpowiedź

Tak, jest to możliwe:

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

Co to jest:

  1. Uzyskaj List<BigDecimal>.
  2. Zmień to w Stream<BigDecimal>
  3. Wywołaj metodę redukcji.

    3.1. Dostarczamy wartość tożsamości do dodania, a mianowicie BigDecimal.ZERO.

    3.2. Określamy BinaryOperator<BigDecimal>, co dodaje dwa BigDecimal, poprzez odwołanie do metody BigDecimal::add.

Zaktualizowana odpowiedź po edycji

Widzę, że dodałeś nowe dane, dlatego nowa odpowiedź będzie:

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

Jest to w większości to samo, z wyjątkiem tego, że dodałem totalMapperzmienną, która ma funkcję od Invoicedo BigDecimali zwraca całkowitą cenę tej faktury.

Następnie otrzymuję a Stream<Invoice>, mapuję do a, Stream<BigDecimal>a następnie redukuję do a BigDecimal.

Teraz, z punktu projektowania OOP, radziłbym również faktycznie użyć total()metody, którą już zdefiniowałeś, a wtedy stanie się to jeszcze łatwiejsze:

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

Tutaj bezpośrednio używamy odwołania do mapmetody w metodzie.

skiwi
źródło
12
+1 na Invoice::totalżywo invoice -> invoice.total().
ryvantage
12
+1 dla odwołań do metod i dla dodawania podziałów wierszy między operacjami strumieniowymi, z których oba IMHO znacznie poprawiają czytelność.
Stuart Marks
jak by to działało, gdybym chciał dodać, powiedzmy Invoice :: total i Invoice :: tax do nowej tablicy
Richard Lau
Standardowa biblioteka Java ma już funkcje do sumowania liczb całkowitych / podwójnych, takich jak Collectors.summingInt(), ale pomija je przez BigDecimals. Zamiast pisać reduce(blah blah blah)to, co jest trudne do odczytania, lepiej byłoby napisać brakujący kolektor dla BigDecimali mieć .collect(summingBigDecimal())na końcu potoku.
csharpfolk
2
Takie podejście może prowadzić do wyjątku NullponterException
gstackoverflow
11

Ten post ma już zaznaczoną odpowiedź, ale odpowiedź nie filtruje wartości null. Prawidłowa odpowiedź powinna zapobiec wartościom null, używając funkcji Object :: nonNull jako predykatu.

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

Zapobiega to próbom sumowania wartości null podczas zmniejszania.

Siraj
źródło
7

Możesz podsumować wartości BigDecimalstrumienia za pomocą kolektora wielokrotnego użytku o nazwie :summingUp

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

CollectorMogą być realizowane w ten sposób:

public static Collector<BigDecimal, ?, BigDecimal> summingUp() {
    return Collectors.reducing(BigDecimal.ZERO, BigDecimal::add);
}
Igor Akkerman
źródło
5

Użyj tego podejścia, aby zsumować listę BigDecimal:

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

To podejście mapuje każdy BigDecimal tylko jako BigDecimal i redukuje je, sumując je, a następnie zwracane przy użyciu get()metody.

Oto inny prosty sposób na wykonanie tego samego podsumowania:

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

Aktualizacja

Gdybym miał w edytowanym pytaniu zapisać klasę i wyrażenie lambda, napisałbym to następująco:

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
źródło
Czy .map(n -> n)tam nie jest bezużyteczne? Nie get()jest też potrzebne.
Rohit Jain,
@RohitJain: Zaktualizowano. Dzięki. Użyłem, get()ponieważ zwraca wartość, Optionalktóra jest zwracana przez reducewywołanie. Jeśli ktoś chce pracować z Optionallub po prostu wydrukować sumę, to tak, get()nie jest potrzebny. Ale drukowanie Opcjonalne bezpośrednio drukuje Optional[<Value>]składnię opartą na podstawie której wątpię, by użytkownik potrzebował. get()Jest więc potrzebny w celu uzyskania wartości z Optional.
Aman Agnihotri,
@ryvantage: Tak, Twoje podejście jest dokładnie takie, jak ja bym to zrobił. :)
Aman Agnihotri,
Nie używaj bezwarunkowego getpołączenia! Jeśli valuesjest pustą listą, opcja opcjonalna nie będzie zawierała żadnej wartości i spowoduje wywołanie NoSuchElementExceptionwhen get. Możesz użyć values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)zamiast tego.
eee
4

Jeśli nie przeszkadza zależność osoby trzecie, istnieje klasa o nazwie Collectors2 w Eclipse Kolekcje który zawiera metody powrocie Kolektory dla zsumowanie i podsumowując BigDecimal i BigInteger. Te metody przyjmują Function jako parametr, dzięki czemu można wyodrębnić wartość BigDecimal lub BigInteger z obiektu.

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

Uwaga: jestem promotorem Eclipse Collections.

Donald Raab
źródło