Agregar BigDecimals usando Streams

178

Tengo una colección de BigDecimals (en este ejemplo, a LinkedList) que me gustaría agregar. ¿Es posible usar transmisiones para esto?

Noté que la Streamclase tiene varios métodos

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

Cada uno de los cuales tiene un sum()método conveniente . Pero, como sabemos, floaty la doublearitmética es casi siempre una mala idea.

Entonces, ¿hay una manera conveniente de resumir BigDecimals?

Este es el código que tengo hasta ahora.

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 puede ver, estoy resumiendo los BigDecimals usando BigDecimal::doubleValue(), pero esto (como se esperaba) no es preciso.

Edición posterior a la respuesta para la posteridad:

Ambas respuestas fueron extremadamente útiles. Quería agregar un poco: mi escenario de la vida real no involucra una colección de BigDecimals sin procesar , están envueltos en una factura. Pero pude modificar la respuesta de Aman Agnihotri para dar cuenta de esto usando la map()función de transmisión:

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
fuente

Respuestas:

354

Respuesta original

Sí, esto es posible:

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

Lo que hace es:

  1. Obtener a List<BigDecimal>.
  2. Convertirlo en un Stream<BigDecimal>
  3. Llame al método de reducción.

    3.1. Proporcionamos un valor de identidad para la suma, a saber BigDecimal.ZERO.

    3.2. Especificamos el BinaryOperator<BigDecimal>, que agrega dos BigDecimal, a través de una referencia de método BigDecimal::add.

Respuesta actualizada, después de editar

Veo que ha agregado nuevos datos, por lo tanto, la nueva respuesta será:

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 casi lo mismo, excepto que he agregado una totalMappervariable, que tiene una función desde Invoicehasta BigDecimaly devuelve el precio total de esa factura.

Luego obtengo a Stream<Invoice>, lo mapeo a a Stream<BigDecimal>y luego lo reduzco a a BigDecimal.

Ahora, desde un punto de diseño de OOP, le aconsejaría que también use el total()método, que ya ha definido, incluso se vuelve más fácil:

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

Aquí usamos directamente la referencia del mapmétodo en el método.

skiwi
fuente
12
1 para Invoice::totalvs invoice -> invoice.total().
ryvantage
12
+1 para referencias de métodos y para agregar saltos de línea entre operaciones de flujo, las cuales, en mi humilde opinión, mejoran considerablemente la legibilidad.
Stuart Marks
¿Cómo funcionaría si quisiera agregar, digamos, Factura :: total y Factura :: impuestos en una nueva matriz
Richard Lau
La biblioteca estándar de Java ya tiene funciones para sumar enteros / dobles Collectors.summingInt(), pero las pierde por BigDecimals. En lugar de escribir reduce(blah blah blah)que es difícil de leer, sería mejor escribir el recopilador perdido BigDecimaly tenerlo .collect(summingBigDecimal())al final de su canalización.
csharpfolk
2
Este enfoque puede llevar a NullponterException
gstackoverflow
11

Esta publicación ya tiene una respuesta marcada, pero la respuesta no filtra los valores nulos. La respuesta correcta debería evitar valores nulos mediante el uso de la función 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);

Esto evita que los valores nulos intenten sumarse a medida que reducimos.

Siraj
fuente
7

Puede resumir los valores de una BigDecimalsecuencia utilizando un recopilador reutilizable denominado :summingUp

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

El Collectorpuede implementarse así:

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

Use este enfoque para sumar la lista de BigDecimal:

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

Este enfoque asigna cada BigDecimal solo como BigDecimal y los reduce al sumarlos, que luego se devuelven utilizando el get()método.

Aquí hay otra forma simple de hacer la misma suma:

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

Actualizar

Si tuviera que escribir la clase y la expresión lambda en la pregunta editada, la habría escrito de la siguiente manera:

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
fuente
¿No es .map(n -> n)inútil allí? Tampoco get()es necesario.
Rohit Jain
@RohitJain: actualizado. Gracias. Utilicé get()ya que devuelve el valor del Optionalque devuelve la reducellamada. Si uno quiere trabajar con el Optionalo simplemente imprimir la suma, entonces sí, get()no es necesario. Pero imprimir el Opcional imprime directamente la Optional[<Value>]sintaxis basada que dudo que el usuario necesite. Por get()lo tanto, es necesario para obtener el valor de Optional.
Aman Agnihotri
@ryvantage: Sí, su enfoque es exactamente como lo habría hecho. :)
Aman Agnihotri
¡No uses una getllamada incondicional ! Si valueses una lista vacía, lo opcional no contendrá ningún valor y arrojará un NoSuchElementExceptioncuándo getse llama. Puedes usar values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)en su lugar.
eee
4

Si no le importa una dependencia de terceros, hay una clase llamada Collectors2 en Eclipse Collections que contiene métodos que devuelven Collectors para sumar y resumir BigDecimal y BigInteger. Estos métodos toman una función como parámetro para que pueda extraer un valor BigDecimal o BigInteger de un 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: Soy un committer para Eclipse Collections.

Donald Raab
fuente