Java groupingBy: sum multiple fields

南笙酒味 提交于 2019-12-07 12:32:18

问题


This question is an extension to the post: Java 8 groupingby with returning multiple field.

For the same problem, how do you return a list of Customer? For example, it should return:

Customer("A",4500,6500)
Customer("B",3000,3500)
Customer("C",4000,4500)

回答1:


@Pankaj Singhal's post is the right idea if you want a Map<String, Customer> as the result set +1. However, I would extract the merging logic into its own function e.g. in the Customer class you would have a function as such:

public static Customer merge(Customer first, Customer second) {
        first.setTotal(first.getTotal() + second.getTotal());
        first.setBalance(first.getBalance() + second.getBalance());
        return first;
}

Then the stream query would become:

Map<String, Customer> retObj = 
           listCust.stream()
                   .collect(Collectors.toMap(Customer::getName, Function.identity(), Customer::merge)); 
  • listCust.stream() creates a stream object i.e. Stream<Customer>.
  • collect performs a mutable reduction operation on the elements of this stream using the provided Collector.
  • The result of toMap is the provided collector, the toMap method extracts the keys Customer::getName and values Function.identity() and if the mapped keys contain duplicates, the merge function Customer::merge is used to resolve collisions.

There are three benefits I see with extracting the merging logic into its own function:

  • The code is more compact.
  • The code is more readable.
  • The complexity of the merging is isolated away from the stream.

if however, your intention is to retrieve a Collection<Customer>:

Collection<Customer> result = listCust.stream()
                .collect(Collectors.toMap(Customer::getName,
                        Function.identity(),
                        Customer::merge))
                .values();

or List<Customer> as the result set then all you have to do is call values() and pass the result of that to the ArrayList constructor:

List<Customer> result = new ArrayList<>(listCust.stream()
                .collect(Collectors.toMap(Customer::getName,
                        Function.identity(),
                        Customer::merge))
                .values());

Update:

if you don't want to mutate the objects in the source then simply modify the merge function as follows:

public static Customer merge(Customer first, Customer second) {
        Customer customer = new Customer(first.getName(), first.getTotal(), first.getBalance());
        customer.setTotal(customer.getTotal() + second.getTotal());
        customer.setBalance(customer.getBalance() + second.getBalance());
        return customer;
}

and everthing else stays as is.




回答2:


Use the following code:

Map<String, Customer> retObj =
    listCust.stream()
        .collect(Collectors.groupingBy(Customer::getName,
                    Collector.of(
                        Customer::new,
                        (c1, c2) -> {
                            c1.setName(c2.getName());
                            c1.setTotal(c1.getTotal() + c2.getTotal());
                            c1.setBalance(c1.getBalance() + c2.getBalance());
                        },
                        (c3, c4) -> {
                            c3.setTotal(c3.getTotal() + c4.getTotal());
                            c3.setBalance(c3.getBalance() + c4.getBalance());
                            return c3;
                        })));

System.out.println(retObj);
System.out.println(retObj.values());       //If you want only the list of all Customers

Output:

{
 A=Customer [name=A, total=4500.0, balance=6500.0], 
 B=Customer [name=B, total=3000.0, balance=3500.0], 
 C=Customer [name=C, total=4000.0, balance=4500.0]
}



回答3:


The other answers here are great, but they mutate the Customer instances in the input, which may be unexpected.

To avoid this, use a custom Collector.

First, create a method that returns a Collector that takes a Stream<Customer> and merges them into a single Customer:

public static Collector<Customer, Customer, Customer> customerCollector() {
    return Collector.of(Customer::new, TestBench::merge,
            (l, r) -> {
                merge(l, r);
                return l;
            });
}

public static void merge(final Customer first, final Customer second) {
    first.setName(second.getName());
    first.setTotal(first.getTotal() + second.getTotal());
    first.setBalance(first.getBalance() + second.getBalance());
}

This assumes that Customer has a noargs constructor.

Then you can do:

Collection<Customer> result = listCust.stream()
        .collect(groupingBy(Customer::getName, customerCollector()))
        .values();

Note I use groupingBy rather than toMap - the groupingBy collector is specifially designed to group elements.




回答4:


This can be done like so,

List<Customer> result = customers.stream().collect(Collectors.collectingAndThen(
        Collectors.toMap(Customer::getName, c -> Arrays.asList(c.getTotal(), c.getBalance()),
                (l1, l2) -> IntStream.range(0, l1.size()).mapToDouble(i -> l1.get(i) + l2.get(i)).boxed()
                        .collect(Collectors.toList())),
        m -> m.entrySet().stream().map(e -> new Customer(e.getKey(), e.getValue().get(0), e.getValue().get(1)))
                .collect(Collectors.toList())));

First create the Map and then use a Finisher function to compute a List of Customer instances using the Map.Entry.



来源:https://stackoverflow.com/questions/51452737/java-groupingby-sum-multiple-fields

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!