Ajout de BigDecimals à l'aide de Streams

178

J'ai une collection de BigDecimals (dans cet exemple, a LinkedList) que je voudrais ajouter ensemble. Est-il possible d'utiliser des flux pour cela?

J'ai remarqué que la Streamclasse a plusieurs méthodes

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

Chacun d'eux a une sum()méthode pratique . Mais, comme nous le savons, floatet l' doublearithmétique est presque toujours une mauvaise idée.

Alors, y a-t-il un moyen pratique de résumer BigDecimals?

C'est le code que j'ai jusqu'à présent.

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

Comme vous pouvez le voir, je résume les BigDecimals en utilisant BigDecimal::doubleValue(), mais ce n'est (comme prévu) pas précis.

Modification post-réponse pour la postérité:

Les deux réponses ont été extrêmement utiles. Je voulais ajouter un peu: mon scénario réel n'implique pas une collection de bruts BigDecimal, ils sont emballés dans une facture. Mais, j'ai pu modifier la réponse d'Aman Agnihotri pour en tenir compte en utilisant la map()fonction pour 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
la source

Réponses:

354

Réponse originale

Oui, c'est possible:

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

Ce qu'il fait c'est:

  1. Obtenez un fichier List<BigDecimal>.
  2. Transformez-le en Stream<BigDecimal>
  3. Appelez la méthode de réduction.

    3.1. Nous fournissons une valeur d'identité pour l'addition, à savoir BigDecimal.ZERO.

    3.2. Nous spécifions le BinaryOperator<BigDecimal>, qui ajoute deux BigDecimal, via une référence de méthode BigDecimal::add.

Réponse mise à jour, après modification

Je vois que vous avez ajouté de nouvelles données, donc la nouvelle réponse deviendra:

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

C'est essentiellement la même chose, sauf que j'ai ajouté une totalMappervariable, qui a une fonction de Invoiceà BigDecimalet renvoie le prix total de cette facture.

Ensuite, j'obtiens a Stream<Invoice>, mappe-le à a Stream<BigDecimal>puis le réduit à a BigDecimal.

Maintenant, à partir d'un point de conception POO, je vous conseillerais d'utiliser également la total()méthode, que vous avez déjà définie, alors cela devient même plus facile:

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

Ici, nous utilisons directement la référence de méthode dans la mapméthode.

skiwi
la source
12
+1 pour Invoice::totalvs invoice -> invoice.total().
ryvantage
12
+1 pour les références de méthode et pour l'ajout de sauts de ligne entre les opérations de flux, tous deux améliorant considérablement la lisibilité.
Stuart marque
comment cela fonctionnerait-il si je voulais ajouter, disons Invoice :: total et Invoice :: tax dans un nouveau tableau
Richard Lau
La bibliothèque standard Java a déjà des fonctions pour additionner les entiers / doubles comme Collectors.summingInt(), mais les manque pour BigDecimals. Au lieu d'écrire ce reduce(blah blah blah)qui est difficile à lire, il serait préférable d'écrire le collecteur manquant BigDecimalet de l'avoir .collect(summingBigDecimal())à la fin de votre pipeline.
csharpfolk
2
Cette approche peut conduire à NullponterException
gstackoverflow
11

Cette publication a déjà une réponse vérifiée, mais la réponse ne filtre pas les valeurs nulles. La bonne réponse doit empêcher les valeurs nulles en utilisant la fonction Object :: nonNull comme prédicat.

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

Cela empêche les valeurs nulles de tenter d'être additionnées lorsque nous réduisons.

Siraj
la source
7

Vous pouvez résumer les valeurs d'un BigDecimalflux à l'aide d'un collecteur réutilisable nommé :summingUp

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

Le Collectorpeut être implémenté comme ceci:

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

Utilisez cette approche pour additionner la liste de BigDecimal:

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

Cette approche mappe chaque BigDecimal en tant que BigDecimal uniquement et les réduit en les additionnant, qui est ensuite renvoyée à l'aide de la get()méthode.

Voici une autre façon simple de faire la même somme:

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

Mettre à jour

Si je devais écrire la classe et l'expression lambda dans la question éditée, je l'aurais écrite comme suit:

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
la source
N'est-ce pas .map(n -> n)inutile là-bas? N'est get()pas non plus nécessaire.
Rohit Jain
@RohitJain: Mis à jour. Merci. J'ai utilisé get()car il renvoie la valeur du Optionalqui est retourné par l' reduceappel. Si l'on veut travailler avec le Optionalou simplement imprimer la somme, alors oui, ce get()n'est pas nécessaire. Mais l'impression de l'Optionnel imprime directement la Optional[<Value>]syntaxe basée sur laquelle je doute que l'utilisateur aurait besoin. Il get()est donc nécessaire de manière à obtenir la valeur du Optional.
Aman Agnihotri
@ryvantage: Oui, votre approche est exactement comme je l'aurais fait. :)
Aman Agnihotri
N'utilisez pas un getappel inconditionnel ! Si valuesest une liste vide, l'option facultative ne contiendra aucune valeur et lancera unNoSuchElementException quand getest appelé. Vous pouvez utiliser à la values.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO)place.
eee
4

Si une dépendance tierce ne vous dérange pas, il existe une classe nommée Collectors2 dans Eclipse Collections qui contient des méthodes renvoyant des Collectors pour additionner et résumer BigDecimal et BigInteger. Ces méthodes prennent une fonction comme paramètre afin que vous puissiez extraire une valeur BigDecimal ou BigInteger d'un objet.

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

Remarque: je suis un committer pour les collections Eclipse.

Donald Raab
la source