Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stuck with lambda expression and Map

I have the Person class:

import java.util.*;
public class Person {
    private String name;
    Map<String,Integer> Skills=new HashMap<>(); // skill name(String) and level(int)

    public String getName(){
        return this.name;
    }
    public Map<String,Integer> getSkills(){
        return this.Skills;
    }
}

And the App class:

import java.util.*;
import java.util.Map.Entry;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;
public class App {
    private List<Person> people=new ArrayList<>(); // the people in the company

    public Map<String,Set<String>> PeoplePerSkill(){
        return this.people.stream().collect(groupingBy(p-> p.getSkills().keySet() //<-get  
                                                                           //^problem here
                                  ,mapping(Person::getName,toSet())));
    }
}

In the App class the PeoplePerSkill method need to return the Set of people names per skill. It means a skill could be owned by many people.

I stuck with the groupingBy(p->p..........., ) I just can't get the String of skill's name, I tried so many ways but things get way stranger :(.

By the way, currently my code returns Map<Object, Set<String>>

like image 541
Feng Avatar asked Jul 04 '15 16:07

Feng


3 Answers

You can do it via flat-mapping, though it probably doesn't look very beautiful:

public Map<String,Set<String>> PeoplePerSkill(){
    return this.people.stream()
        .<Entry<String, String>>flatMap(p -> 
            p.getSkills().keySet()
                .stream()
                .map(s -> new AbstractMap.SimpleEntry<>(s, p.getName())))
        .collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toSet())));
}

Here flatMap creates a stream of pairs (skill, person name), which are collected in the manner quite similar to yours. I'm using the AbstractMap.SimpleEntry class to represent the pair, you may use something else.

Using my StreamEx library this task can be solved prettier:

return StreamEx.of(this.people)
        .mapToEntry(p -> p.getSkills().keySet(), Person::getName)
        .flatMapKeys(Set::stream)
        .grouping(toSet());

Internally it's almost the same, just syntactic sugar.

Update: seems that my original solution was wrong: it returned map person_name -> [skills], but if I understand the OP correctly, he wants map skill -> [person_names]. The answer edited.

like image 171
Tagir Valeev Avatar answered Oct 29 '22 06:10

Tagir Valeev


I am not sure if streams would make your life easier here. IMO this code is much easier to read and cleaner.

public Map<String, Set<String>> peoplePerSkill() {

    Map<String, Set<String>> map = new HashMap<>();

    for (Person person : people) {
        for (String skill : person.getSkills().keySet()) {
            map.putIfAbsent(skill, new HashSet<>());
            map.get(skill).add(person.getName());
        }
    }

    return map;
}

You can also "simplify"

map.putIfAbsent(skill, new HashSet<>());
map.get(skill).add(person.getName());

with

map.computeIfAbsent(skill, k -> new HashSet<>()).add(person.getName());
like image 20
Pshemo Avatar answered Oct 29 '22 07:10

Pshemo


If you can use external libraries in your code, you might want to consider using a Multimap instead of a Map<String, Set<String>>. Unfortunately, a solution using a Multimap is going to require more boilerplate since it isn't officially supported by the JDK, but it should lead to a "cleaner" solution:

  public static void main(String[] args) {
    Person larry = new Person("larry");
    larry.getSkills().put("programming", 0);
    larry.getSkills().put("cooking", 0);

    Person nishka = new Person("nishka");
    nishka.getSkills().put("programming", 0);
    nishka.getSkills().put("cooking", 0);

    Person mitul = new Person("mitul");
    mitul.getSkills().put("running", 0);
    mitul.getSkills().put("cooking", 0);

    Person rebecca = new Person("rebecca");
    rebecca.getSkills().put("running", 0);
    rebecca.getSkills().put("programming", 0);

    List<Person> people = Arrays.asList(larry, nishka, mitul, rebecca);

    Multimap<String, String> peopleBySkills = people.stream().collect(
        collectingAndThen(toMap(Person::getName, p -> p.getSkills().keySet()),
            CollectingMultimap.<String, String, Set<String>> toMultimap()
                .andThen(invert())));
    System.out.println(peopleBySkills);
  }

  private static <K, V, I extends Iterable<V>> Function<Map<K, I>, Multimap<K, V>> toMultimap() {
    return m -> {
      Multimap<K, V> map = ArrayListMultimap.create();
      m.entrySet().forEach(e -> map.putAll(e.getKey(), e.getValue()));
      return map;
    };
  }

  private static <K, V> Function<Multimap<K, V>, Multimap<V, K>> invert() {
    return m -> {
      return Multimaps.invertFrom(m, ArrayListMultimap.create());
    };
  }

{running=[mitul, rebecca], cooking=[nishka, larry, mitul], programming=[nishka, larry, rebecca]}

Notice how I had to supply the generic parameters to toMultimap(). Java 8 has much better generic inference, but it does not infer chained method calls.

You will either need to explicitly supply the generic parameters or declare a local variable Function<Map<String, Set<String>>, Multimap<String, String>> toMultimap in order for the compiler to correctly infer the type parameters.

like image 33
Jeffrey Avatar answered Oct 29 '22 05:10

Jeffrey