Adicionando BigDecimals usando Streams

178

Eu tenho uma coleção de BigDecimals (neste exemplo, a LinkedList) que gostaria de adicionar. É possível usar fluxos para isso?

Notei que a Streamclasse tem vários métodos

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

Cada um dos quais tem um sum()método conveniente . Mas, como sabemos, floate doublearitmética é quase sempre uma má idéia.

Então, existe uma maneira conveniente de resumir BigDecimals?

Este é o código que tenho até agora.

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

Como você pode ver, estou resumindo os BigDecimals usando BigDecimal::doubleValue(), mas isso (como esperado) não é preciso.

Edição pós-resposta para posteridade:

Ambas as respostas foram extremamente úteis. Eu queria acrescentar um pouco: meu cenário da vida real não envolve uma coleção de BigDecimals brutos , eles são embrulhados em uma fatura. Mas pude modificar a resposta de Aman Agnihotri para explicar isso usando a map()função for 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
fonte

Respostas:

353

Resposta original

Sim, isso é possível:

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

O que ele faz é:

  1. Obtenha a List<BigDecimal>.
  2. Transforme-o em um Stream<BigDecimal>
  3. Chame o método de redução.

    3.1 Fornecemos um valor de identidade para adição, a saber BigDecimal.ZERO.

    3.2 Nós especificamos o BinaryOperator<BigDecimal>, que adiciona dois BigDecimal, através de uma referência de método BigDecimal::add.

Resposta atualizada, após a edição

Vejo que você adicionou novos dados, portanto a nova resposta se tornará:

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

É basicamente o mesmo, exceto que eu adicionei uma totalMappervariável, que tem uma função a partir Invoicede BigDecimale retorna o valor total desta factura.

Então eu obtenho a Stream<Invoice>, mapeio para Stream<BigDecimal>ae reduzo para a BigDecimal.

Agora, a partir de um ponto de design do OOP, aconselho você a realmente usar o total()método, que você já definiu, e fica ainda mais fácil:

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

Aqui, usamos diretamente a referência do mapmétodo no método

skiwi
fonte
12
+1 para Invoice::totalvs invoice -> invoice.total().
ryvantage
12
+1 para referências de método e para adicionar quebras de linha entre operações de fluxo, as quais IMHO melhoram consideravelmente a legibilidade.
Stuart Marcas
como é que ele funciona se eu queria acrescentar digamos Invoice :: total eo Invoice :: imposto em uma nova matriz
Richard Lau
A biblioteca padrão Java já possui funções para somar números inteiros / duplos Collectors.summingInt(), mas as perde por BigDecimals. Em vez de escrever reduce(blah blah blah)que é difícil de ler, seria melhor escrever para o coletor ausente BigDecimale ter .collect(summingBigDecimal())no final do seu pipeline.
Csharpfolk
2
Esta abordagem pode levar a NullponterException
gstackoverflow
11

Esta postagem já tem uma resposta marcada, mas a resposta não filtra valores nulos. A resposta correta deve impedir valores nulos usando a função Object :: nonNull como predicado.

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

Isso impede que valores nulos tentem ser somados à medida que reduzimos.

Siraj
fonte
7

Você pode resumir os valores de um BigDecimalfluxo usando um coletor reutilizável chamado :summingUp

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

O Collectorpode ser implementado assim:

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

Use esta abordagem para somar a lista de BigDecimal:

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

Essa abordagem mapeia cada BigDecimal apenas como BigDecimal e os reduz somando-os, que são retornados usando o get()método

Aqui está outra maneira simples de fazer a mesma soma:

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

Atualizar

Se eu escrevesse a expressão classe e lambda na pergunta editada, eu a escreveria da seguinte maneira:

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
fonte
Não é .map(n -> n)inútil lá? Também get()não é necessário.
Rohit Jain
@RohitJain: Atualizado. Obrigado. Eu usei get()como ele retorna o valor do Optionalque é retornado pela reducechamada. Se alguém quiser trabalhar com Optionalou apenas imprimir a soma, então sim, get()não é necessário. Mas imprimir o opcional opcional imprime diretamente a Optional[<Value>]sintaxe que duvido que o usuário precisaria. Portanto, get()é necessário obter um valor a partir do Optional.
Aman Agnihotri
@ryvantage: Sim, sua abordagem é exatamente como eu teria feito. :)
Aman Agnihotri
Não use uma getchamada incondicional ! Se valuesfor uma lista vazia, o opcional não conterá nenhum valor e lançará um NoSuchElementExceptionquando getfor chamado. Você pode usar em seu values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)lugar.
eee
4

Se você não se importa com uma dependência de terceiros, há uma classe chamada Collectors2 em Eclipse Collections que contém métodos que retornam Collectors para somar e resumir BigDecimal e BigInteger. Esses métodos usam uma Function como parâmetro para que você possa extrair um valor BigDecimal ou BigInteger de um objeto.

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

Nota: Sou um colaborador das Coleções Eclipse.

Donald Raab
fonte