Aggiunta di BigDecimals utilizzando gli stream


178

Ho una raccolta di BigDecimals (in questo esempio a LinkedList) che vorrei aggiungere insieme. È possibile utilizzare i flussi per questo?

Ho notato che la Streamclasse ha diversi metodi

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

Ognuno dei quali ha un sum()metodo conveniente . Ma, come sappiamo, floate l' doublearitmetica è quasi sempre una cattiva idea.

Quindi, c'è un modo conveniente per riassumere BigDecimals?

Questo è il codice che ho finora.

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

Come puoi vedere, sto riassumendo i BigDecimals usando BigDecimal::doubleValue(), ma questo (come previsto) non è preciso.

Modifica post-risposta per i posteri:

Entrambe le risposte sono state estremamente utili. Volevo aggiungere un po ': il mio scenario di vita reale non prevede una raccolta di dati grezzi BigDecimal, sono avvolti in una fattura. Ma sono stato in grado di modificare la risposta di Aman Agnihotri per renderlo conto usando la map()funzione per lo streaming:

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

Risposte:


354

Risposta originale

Sì, questo è possibile:

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

Quello che fa è:

  1. Ottieni a List<BigDecimal>.
  2. Trasformalo in a Stream<BigDecimal>
  3. Chiama il metodo di riduzione.

    3.1. Forniamo un valore di identità da aggiungere, vale a dire BigDecimal.ZERO.

    3.2. Specifichiamo il BinaryOperator<BigDecimal>, che aggiunge due BigDecimal, tramite un riferimento al metodo BigDecimal::add.

Risposta aggiornata, dopo la modifica

Vedo che hai aggiunto nuovi dati, quindi la nuova risposta diventerà:

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

E 'in gran parte la stessa, solo che ho aggiunto una totalMappervariabile, che ha una funzione da Invoiceal BigDecimale restituisce il prezzo totale di tale fattura.

Quindi ottengo un Stream<Invoice>, lo mappa a un Stream<BigDecimal>e poi lo riduco a BigDecimal.

Ora, da un punto di vista OOP, ti consiglio di utilizzare anche il total()metodo, che hai già definito, quindi diventa ancora più semplice:

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

Qui utilizziamo direttamente il riferimento al mapmetodo nel metodo.


12
+1 per Invoice::totalvs invoice -> invoice.total().
ryvantage

12
+1 per i riferimenti ai metodi e per l'aggiunta di interruzioni di riga tra le operazioni dello stream, entrambe le quali IMHO migliora notevolmente la leggibilità.
Stuart segna il

come avrebbe funzionato se avessi voluto aggiungere diciamo fattura :: totale e fattura :: imposta in un nuovo array
Richard Lau,

La libreria standard Java ha già funzioni per sommare numeri interi / doppi come Collectors.summingInt(), ma manca per BigDecimals. Invece di scrivere reduce(blah blah blah)che è difficile da leggere, sarebbe meglio scrivere un raccoglitore mancante BigDecimale avere .collect(summingBigDecimal())alla fine della pipeline.
csharpfolk,

2
Questo approccio può portare a NullponterException
gstackoverflow,

11

Questo post ha già una risposta verificata, ma la risposta non filtra per valori null. La risposta corretta dovrebbe impedire i valori null utilizzando la funzione Object :: nonNull come predicato.

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

In questo modo si evita il tentativo di sommare i valori null mentre si riduce.


7

Puoi riassumere i valori di uno BigDecimalstream usando un Collector riutilizzabile chiamato :summingUp

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

Il Collectorpuò essere implementato in questo modo:

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

5

Usa questo approccio per sommare l'elenco di BigDecimal:

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

Questo approccio mappa ogni BigDecimal solo come BigDecimal e li riduce sommandoli, che viene quindi restituito utilizzando il get()metodo

Ecco un altro modo semplice per fare la stessa somma:

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

Aggiornare

Se dovessi scrivere l'espressione di classe e lambda nella domanda modificata, l'avrei scritto come segue:

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

Non è .map(n -> n)inutile lì? Inoltre get()non è necessario.
Rohit Jain,

@RohitJain: aggiornato. Grazie. Ho usato get()in quanto restituisce il valore del Optionalquale viene restituito dalla reducechiamata. Se uno vuole lavorare con Optionalo semplicemente stampare la somma, allora sì, get()non è necessario. Ma la stampa dell'Opzionale stampa direttamente la Optional[<Value>]sintassi di base che dubito che l'utente avrebbe bisogno. Quindi get()è necessario in un modo per ottenere il valore da Optional.
Aman Agnihotri,

@ryvantage: Sì, il tuo approccio è esattamente come l'avrei fatto. :)
Aman Agnihotri il

Non usare una getchiamata incondizionata ! Se valuesè un elenco vuoto, l'opzione non conterrà alcun valore e genererà un NoSuchElementExceptionquando getviene chiamato. Puoi usare values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)invece.
Eee,

4

Se non ti dispiace un terzo delle dipendenze parte, v'è una classe denominata Collectors2 in Eclipse Collezioni che contiene i metodi di tornare collezionisti per la somma e che riassume BigDecimal e BigInteger. Questi metodi accettano una funzione come parametro in modo da poter estrarre un valore BigDecimal o BigInteger da un oggetto.

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: sono un committer per le raccolte Eclipse.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.