Додавання BigDecimals за допомогою потоків


178

У мене є колекція BigDecimals (у цьому прикладі, а LinkedList), яку я хотів би скласти разом. Чи можна використовувати для цього потоки?

Я помітив, що в Streamкласі є кілька методів

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

Кожен з яких має зручний sum()метод. Але, як ми знаємо, floatі doubleарифметика майже завжди погана ідея.

Отже, чи є зручний спосіб підбити підсумки BigDecimals?

Це код, який у мене є досі.

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

Як бачите, я підбиваю підсумки BigDecimals BigDecimal::doubleValue(), але це (як очікувалося) не точно.

Редагування після відповіді для нащадків:

Обидві відповіді були надзвичайно корисними. Мені хотілося додати трохи: мій сценарій реального життя не включає колекцію сировини BigDecimal, вони загорнуті у рахунок-фактуру. Але мені вдалося змінити відповідь Амана Агніхотрі, щоб врахувати це, використовуючи map()функцію для потоку:

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

Відповіді:


354

Оригінальна відповідь

Так, це можливо:

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

Що це робить:

  1. Отримайте а List<BigDecimal>.
  2. Перетворіть його на a Stream<BigDecimal>
  3. Викличте метод зменшення.

    3.1. Ми надаємо значення ідентичності для додавання, а саме BigDecimal.ZERO.

    3.2. Ми вказуємо BinaryOperator<BigDecimal>, що додає два BigDecimal, через посилання на метод BigDecimal::add.

Оновлена ​​відповідь після редагування

Я бачу, що ви додали нові дані, тому новою відповіддю стане:

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

Це в основному ті ж, за винятком того, що я додав totalMapperзмінну, яка має функцію від Invoiceдо BigDecimalі повертає повну вартість цього рахунку - фактури.

Тоді я отримую a Stream<Invoice>, відображаю його на a, Stream<BigDecimal>а потім зменшую його до a BigDecimal.

Тепер, з точки зору проекту OOP, я б порадив вам також реально використовувати total()метод, який ви вже визначили, тоді це стає навіть простіше:

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

Тут ми безпосередньо використовуємо посилання на mapметод у методі.


12
+1 для Invoice::totalпроти invoice -> invoice.total().
ryvantage

12
+1 для посилань на методи та для додавання розривів рядків між потоковими операціями, обидва з яких IMHO значно покращують читабельність.
Стюарт відзначає

як би це працювало, якби я хотів додати, скажімо, рахунок-фактура :: загальна сума та рахунок-фактура :: податок у новий масив
Річард Лау

Стандартна бібліотека Java вже має функції для підсумовування цілих чисел / пар, як Collectors.summingInt(), але їх не вистачає дляBigDecimal s. Замість того, щоб писати, reduce(blah blah blah)що важко читати, краще було б написати пропущений колектор BigDecimalі мати .collect(summingBigDecimal())в кінці вашого конвеєра.
csharpfolk

2
Такий підхід може призвести до NullponterException
gstackoverflow

11

У цій публікації вже є перевірена відповідь, але відповідь не фільтрує нульові значення. Правильна відповідь повинна запобігати нульовим значенням, використовуючи функцію Object :: nonNull як предикат.

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

Це запобігає спробі підрахувати нульові значення під час зменшення.


7

Ви можете підсумувати значення BigDecimalпотоку за допомогою колектора для багаторазового використання з назвою :summingUp

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

CollectorМоже бути реалізована наступним чином:

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

5

Скористайтеся таким підходом, щоб підсумувати список BigDecimal:

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

Цей підхід відображає кожен BigDecimal лише як BigDecimal і зменшує їх шляхом підсумовування, яке потім повертається за допомогою get()методу.

Ось ще один простий спосіб зробити те саме підсумок:

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

Оновлення

Якби я писав вираз класу та лямбда в редагованому запитанні, я написав би це так:

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

Хіба .map(n -> n)тут марно? Також get()не потрібен.
Rohit Jain

@RohitJain: оновлено. Дякую. Я використовував, get()як він повертає значення, Optionalяке повертається reduceдзвінком. Якщо ви хочете працювати з Optionalабо просто роздрукувати суму, то так, get()це не потрібно. Але друкуючи Необов’язково безпосередньо друкується Optional[<Value>]на основі синтаксису, який, я сумніваюся, користувачеві знадобиться. Таким чином get(), потрібно, щоб отримати значення з Optional.
Аман Агніхотрі

@ryvantage: Так, ваш підхід саме такий, як я би це зробив. :)
Аман Агніхотрі

Не використовуйте безумовний getдзвінок! Якщо valuesце порожній список, необов'язковий не буде містити значення і буде кидати a, NoSuchElementExceptionколи getвикликається. Ви можете використовувати values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)замість цього.
eee

4

Якщо ви не заперечуєте залежність третьої сторони, є клас з ім'ям Collectors2 в Eclipse , Collections , який містить методи повернення Колектори для підсумовування і узагальнення BigDecimal і BigInteger. Ці методи приймають функцію як параметр, тому ви можете отримати значення об'єкта BigDecimal або BigInteger.

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

Примітка. Я є членом колекції Eclipse.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.