Добавление 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;
    }
}
ryvantage
источник

Ответы:

354

Оригинальный ответ

Да, это возможно

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

Что это делает:

  1. Получить List<BigDecimal>.
  2. Преврати это в Stream<BigDecimal>
  3. Вызовите метод Reduce.

    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и возвращает полную стоимость этого счета - фактуры.

Затем я получаю Stream<Invoice>, сопоставить его с Stream<BigDecimal>и затем уменьшить его доBigDecimal .

Теперь, с точки зрения ООП, я бы посоветовал вам также использовать total()метод, который вы уже определили, тогда это даже станет проще:

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

Здесь мы напрямую используем ссылку на mapметод в методе.

skiwi
источник
12
+1 для Invoice::totalпротив invoice -> invoice.total().
ryvantage
12
+1 для ссылок на методы и для добавления разрывов строк между операциями потока, оба из которых IMHO значительно улучшают читаемость.
Стюарт Маркс
как бы это работало, если бы я хотел добавить, скажем, Invoice :: total и Invoice :: tax в новый массив
Ричард Лау
Стандартная библиотека Java уже имеет функции для суммирования целых / двойных чисел Collectors.summingInt(), но пропускает их по BigDecimals. Вместо того, чтобы писать 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);

Это предотвращает попытку суммирования нулевых значений при уменьшении.

Siraj
источник
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()не нужно.
Рохит Джайн
@RohitJain: обновлено. Спасибо. Я использовал, get()поскольку он возвращает значение, Optionalкоторое возвращается при reduceвызове. Если кто-то хочет работать с Optionalили просто распечатать сумму, то да, get()не нужно. Но печать Optional напрямую печатает Optional[<Value>]синтаксис, который, я сомневаюсь, понадобится пользователю. Так get()что нужно, чтобы получить значение от Optional.
Аман Агнихотри
@ryvantage: Да, ваш подход именно так, как я бы это сделал. :)
Аман Агнихотри
Не используйте безусловный getзвонок! Если valuesэто пустой список, необязательный не будет содержать значения и будет вызывать, NoSuchElementExceptionкогда getвызывается. Вы можете использовать values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)вместо этого.
еее
4

Если вы не возражаете против сторонней зависимости, существует класс с именем Collectors2 в Eclipse Collections который содержит методы, возвращающие Collectors для суммирования и суммирования 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 Collections.

Дональд Рааб
источник