grouping objects java 8

2019-02-17 17:01发布

问题:

I have something like the below :

public class MyClass {
private Long stackId
private Long questionId
}

A collection of say 100, where the stackid could be duplicate with different questionIds. Its a one to many relationship between stackId and questionId

Is there a streamy, java 8 way to convert to the below strcuture :

public class MyOtherClass {
private Long stackId
private Collection<Long> questionIds
}

Which would be a collection of 25, with each instance having a nested collection of 4 questionIds.

Input :

[{1,100},{1,101},{1,102},{1,103},{2,200},{2,201},{2,202},{1,203}]

Output

[{1, [100,101,102,103]},{2,[200,201,202,203]}]

回答1:

The straight-forward way with the Stream API involves 2 Stream pipelines:

  • The first one creates a temporary Map<Long, List<Long>> of stackId to questionIds. This is done with the groupingBy(classifier, downstream) collectors where we classify per the stackId and values having the same stackId are mapped to their questionId (with mapping) and collected into a list with toList().
  • The second one converts each entry of that map into a MyOtherClass instance and collects that into a list.

Assuming you have a constructor MyOtherClass(Long stackId, Collection<Long> questionIds), a sample code would be:

Map<Long, List<Long>> map = 
    list.stream()
        .collect(Collectors.groupingBy(
            MyClass::getStackId,
            Collectors.mapping(MyClass::getQuestionId, Collectors.toList())
        ));

List<MyOtherClass> result = 
    map.entrySet()
       .stream()
       .map(e -> new MyOtherClass(e.getKey(), e.getValue()))
       .collect(Collectors.toList());

Using StreamEx library, you could do that in a single Stream pipeline. This library offers a pairing and first collectors. This enables to pair two collectors and perform a finisher operation on the two collected results:

  • The first one only keeps the first stackId of the grouped elements (they will all be the same, by construction)
  • The second one mapping each element into their questionId and collecting into a list.
  • The finisher operation just returns a new instance of MyOtherClass.

Sample code:

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static one.util.streamex.MoreCollectors.first;
import static one.util.streamex.MoreCollectors.pairing;

// ...

Collection<MyOtherClass> result = 
    StreamEx.of(list)
            .groupingBy(
                MyClass::getStackId,
                pairing(
                    collectingAndThen(mapping(MyClass::getStackId, first()), Optional::get),
                    mapping(MyClass::getQuestionId, toList()),
                    MyOtherClass::new
                )
            ).values();


回答2:

List<MyClass> inputs = Arrays.asList(
    new MyClass(1L, 100L),
    new MyClass(1L, 101L),
    new MyClass(1L, 102L),
    new MyClass(1L, 103L),
    new MyClass(2L, 200L),
    new MyClass(2L, 201L),
    new MyClass(2L, 202L),
    new MyClass(2L, 203L)
);

Map<Long, List<Long>> result = inputs
    .stream()
    .collect(
      Collectors.groupingBy(MyClass::getStackId,
        Collectors.mapping(
          MyClass::getQuestionId, 
          Collectors.toList()
        )
      )
    );


回答3:

You can use the java8 groupingBy collector. Like this:

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class RandomTest {

    class MyClass {
        private Long stackId;
        private Long questionId;

        public MyClass(Long stackId, Long questionId) {
            this.stackId = stackId;
            this.questionId = questionId;
        }

        public Long getStackId() {
            return stackId;
        }

        public Long getQuestionId() {
            return questionId;
        }
    }

    public class MyOtherClass {
        private Long stackId;
        private Set<Long> questionIds;

        public MyOtherClass(Long stackId, Set<Long> questionIds) {
            this.stackId = stackId;
            this.questionIds = questionIds;
        }

        public Long getStackId() {
            return stackId;
        }

        public Set<Long> getQuestionIds() {
            return questionIds;
        }
    }

    @Test
    public void test() {
        List<MyClass> classes = new ArrayList<>();
        List<MyOtherClass> otherClasses = new ArrayList<>();

        //populate the classes list
        for (int j = 1; j <= 25; j++) {
            for (int i = 0; i < 4; i++) {
                classes.add(new MyClass(0L + j, (100L*j) + i));
            }
        }

        //populate the otherClasses List
        classes.stream().collect(Collectors
                .groupingBy(MyClass::getStackId, Collectors.mapping(MyClass::getQuestionId, Collectors.toSet())))
                .entrySet().stream().forEach(
                longSetEntry -> otherClasses.add(new MyOtherClass(longSetEntry.getKey(), longSetEntry.getValue())));

        //print the otherClasses list
        otherClasses.forEach(myOtherClass -> {
            System.out.print(myOtherClass.getStackId() + ": [");
            myOtherClass.getQuestionIds().forEach(questionId-> System.out.print(questionId + ","));
            System.out.println("]");
        });
    }
}