14

Consider the below WorkExperience class:

public class WorkExperience {
    private int year;
    private List<Skills> skill;

    public WorkExperience(int year, List<Skills> skill) {
        this.year = year;
        this.skill = skill;
    }   
    //getter setter         
}

public class Skills {
    private String skills;

    public Skills(String skills) {
        this.skills = skills;
    }

    @Override
    public String toString() {
        return "Skills [skills=" + skills + "]";
    }
}     

let's say I want to group by my skills by year, this is how we can do groupBy by year:

public static void main(String[] args) {

    List<Skills> skillSet1 = new  ArrayList<>();
    skillSet1.add(new Skills("Skill-1"));
    skillSet1.add(new Skills("Skill-2"));
    skillSet1.add(new Skills("Skill-3"));

    List<Skills> skillSet2 = new  ArrayList<>();
    skillSet2.add(new Skills("Skill-1"));
    skillSet2.add(new Skills("Skill-4"));
    skillSet2.add(new Skills("Skill-2"));


    List<Skills> skillSet3 = new  ArrayList<>();
    skillSet3.add(new Skills("Skill-1"));
    skillSet3.add(new Skills("Skill-9"));
    skillSet3.add(new Skills("Skill-2"));

    List<WorkExperience> workExperienceList = new ArrayList<>();
    workExperienceList.add(new WorkExperience(2017,skillSet1));
    workExperienceList.add(new WorkExperience(2017,skillSet2));
    workExperienceList.add(new WorkExperience(2018,skillSet3));

    Map<Integer, Set<List<Skills>>> collect = workExperienceList.stream().collect(
        Collectors.groupingBy(
            WorkExperience::getYear,
            Collectors.mapping(WorkExperience::getSkill, Collectors.toSet())
        )
    );
}

groupBy is returning: Map<Integer, Set<List<Skills>>>
but what I need is: Map<Integer, Set<Skills>>

How to convert List stream into single Container?

Omar Einea
  • 2,478
  • 7
  • 23
  • 35
Niraj Sonawane
  • 10,225
  • 10
  • 75
  • 104

4 Answers4

14

An alternative to flatMapping using Java 8 features only would be

Map<Integer, Set<Skills>> map = workExperienceList.stream()
    .collect(Collectors.toMap(
        WorkExperience::getYear,
        we -> new HashSet<>(we.getSkill()),
        (s1, s2)-> { s1.addAll(s2); return s1; }));

you may optimize this a bit

Map<Integer, Set<Skills>> map = workExperienceList.stream()
    .collect(Collectors.toMap(
        WorkExperience::getYear,
        we -> new HashSet<>(we.getSkill()),
        (s1, s2) -> {
            if(s1.size() > s2.size()) { s1.addAll(s2); return s1; }
            else { s2.addAll(s1); return s2; }
        }));
Holger
  • 285,553
  • 42
  • 434
  • 765
  • 2
    Or even the `flatMapping` method implementation as suggested [in this answer of yours](https://stackoverflow.com/a/39131049/1746118). – Naman Jan 18 '18 at 02:32
12

We can use Collectors.flatMapping collector added in Java-9. By using flatMapping, we can flatten the intermediate Lists into a single container. flatMapping can be used in the cases where elements of the original stream are convertible to a stream.

workExperienceList.stream().collect(Collectors.groupingBy(
                              WorkExperience::getYear, 
                              Collectors.flatMapping(workexp -> workexp.getSkill().stream(), 
                                             Collectors.toSet())));

API Note:

The flatMapping() collectors are most useful when used in a multi-level reduction, such as downstream of a groupingBy or partitioningBy.

Naman
  • 27,789
  • 26
  • 218
  • 353
Niraj Sonawane
  • 10,225
  • 10
  • 75
  • 104
1

Another way to accomplish what you desire is to implement your own collector by using the static factory method Collector.of():

Map<Integer, Set<Skills>> collect = workExperienceList.stream()
    .collect(Collector.of(
        HashMap::new,
        ( map, e ) -> map.computeIfAbsent(e.getYear(), k -> new HashSet<>()).addAll(e.getSkill()),
        ( left, right ) -> {
            right.forEach(( year, set ) -> left.computeIfAbsent(year, k -> new HashSet<>()).addAll(set));
            return left;
        })
    );

Which is rather messy and bloated when compared to the other answers.

Lino
  • 19,604
  • 6
  • 47
  • 65
0

Try to be more object oriented. So I suppose to create a new small object

public static class AsGroup {
    private final Integer year;
    private final Collection<Skill> skillSet;

    public AsGroup(Integer year, Collection<Skill> skillSet) {

        this.year = year;
        this.skillSet = skillSet;
    }

    public AsGroup addSkills(AsGroup asGroupSkills) {
        this.skillSet.addAll(asGroupSkills.skillSet);
        return this;
    }
}

And then you can solve your problem with:

Map<Integer, Optional<com.company.Main.AsGroup>> groupedByYear = workExperienceList.stream()
                .map(workExperience ->
                        new AsGroup(workExperience.getYear(), new HashSet<>(workExperience.getSkill()))
                ).collect(groupingBy((asGroup) -> asGroup.year,
                          reducing((group1, group2) -> (group1.addSkills(group2))))
                );

You can use it as below:

groupedByYear.forEach(((year, groupedSkills) -> System.out.println(year + " " + groupedSkills.get().skillSet)));

It prints following:

2017 [Skill [skill=Skill-1], Skill [skill=Skill-1], Skill [skill=Skill-4], Skill [skill=Skill-2], Skill [skill=Skill-2], Skill [skill=Skill-3]]
2018 [Skill [skill=Skill-1], Skill [skill=Skill-2], Skill [skill=Skill-9]]
Pang
  • 9,564
  • 146
  • 81
  • 122
nick318
  • 575
  • 4
  • 18