Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to merge a list of similar objects but sum up some properties with Java 8

Suppose I have the list below I would like to return a result that has only one Person with the name "Sam" - "Fred" but with 25 amount

public class Java8Test{


    private static class Person {
            private String name;
            private String lastName;
            private int amount;

            public Person(String name, String lastName, int amount) {
                this.name = name;
                this.lastName = lastName;
                this.amount = amount;
            }
        }


        public static void main(String[] args) {
            List<Person> people = new ArrayList<>();
            people.add(new Person("Sam","Fred",10));
            people.add(new Person("Sam","Fred",15));
            people.add(new Person("Jack","Eddie",10));
            // WHAT TO DO HERE ?


        }
    }

NOTE:

The example above is only for clarification, what I am looking for is a general map/reduce like functionality with Java 8.

like image 683
Adelin Avatar asked Dec 14 '17 16:12

Adelin


4 Answers

You could iterate your people list and use a map to merge people with the same name - lastName pair:

Map<String, Person> map = new HashMap<>();
people.forEach(p -> map.merge(
    p.getName() + " - " + p.getLastName(),                  // name - lastName
    new Person(p.getName(), p.getLastName, p.getAmount()),  // copy the person
    (o, n) -> o.setAmount(o.getAmount() + n.getAmount()))); // o=old, n=new

Now map.values() is a reduced Collection<Person> as per your requirements.

If you have the possibility to add a copy-constructor and a couple of methods to the Person class:

public Person(Person another) {
    this.name = another.name;
    this.lastName = another.lastName;
    this.amount = another.amount;
}

public String getFullName() {
    return this.name + " - " + this.lastName;
}

public Person merge(Person another) {
    this.amount += another.amount;
}

Then, you could simplify the first version of the code, as follows:

Map<String, Person> map = new HashMap<>();
people.forEach(p -> map.merge(p.getFullName(), new Person(p), Person::merge));

This utilizes the Map.merge method, which is very useful for this cases.

like image 145
fps Avatar answered Nov 15 '22 09:11

fps


You can use groupingBy, reducing and others stuff to : - group Person by same name and lastName - sum the value of their amount - create a person with these attributs

people = people.stream().collect(
                         Collectors.groupingBy(o -> Arrays.asList(o.name, o.lastName), 
                               Collectors.summingInt(p->p.amount))
            .entrySet()
            .stream()
            .map(Person::apply).collect(Collectors.toList());

people.forEach(System.out::println);

//Prints : 
Sam Fred 25
Jack Eddie 10

And these two are same (my IDE suggest it to me, I assume that i don't really know how it works, if someones knows : explain it to us in comment)

.map(Person::apply) 
.map(e -> new Person(e.getKey().get(0), e.getKey().get(1), e.getValue())
like image 21
azro Avatar answered Nov 15 '22 08:11

azro


I can think of this sort of hacky way to do it:

TreeSet<Person> res = people.stream()
       .collect(Collector.of(
                    () -> new TreeSet<>(Comparator.comparing(Person::getName)
                                        .thenComparing(Person::getLastName)),
                    (set, elem) -> {
                        if (!set.contains(elem)) {
                            set.add(elem);
                        } else {
                            Person p = set.ceiling(elem);
                            p.setAmount(elem.getAmount() + p.getAmount());
                        }
                    },
                    (left, right) -> {
                        throw new IllegalArgumentException("not for parallel");
                    }));

That is without changing the definition of the Person at all. It's a Set that's returned (according to firstname and lastname), but that is what you want here anyway.

like image 26
Eugene Avatar answered Nov 15 '22 09:11

Eugene


You can use the stream groupingBy to group by multiple columns:

Function<Person, List<Object>> key = p -> Arrays.asList(p.name, p.lastName);
final Map<List<Object>, Integer> collect = people.stream()
    .collect(Collectors.groupingBy(key, Collectors.summingInt(p -> p.amount)));
System.out.println(collect);

results in

{[Sam, Fred]=25, [Jack, Eddie]=10}

From there, you can create new Person instances from the map values.

like image 31
Alex Savitsky Avatar answered Nov 15 '22 09:11

Alex Savitsky