Group by Map Value Item [duplicate] - java-8

I need to convert Map<K, List<V>> to Map<V, List<K>>.
I've been struggling with this issue for some time.
It's obvious how to do conversion Map<K, V> to Map<V, List<K>>:
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, toList())
)
But I can't find solve an initial issue. Is there some easy-to-ready-java-8 way to do it?

I think you were close, you would need to flatMap those entries to a Stream and collect from there. I've used the already present SimpleEntry, but you can use a Pair of some kind too.
initialMap.entrySet()
.stream()
.flatMap(entry -> entry.getValue().stream().map(v -> new SimpleEntry<>(entry.getKey(), v)))
.collect(Collectors.groupingBy(
Entry::getValue,
Collectors.mapping(Entry::getKey, Collectors.toList())
));
Well, if you don't want to create the extra overhead of those SimpleEntry instances, you could do it a bit different:
Map<Integer, List<String>> result = new HashMap<>();
initialMap.forEach((key, values) -> {
values.forEach(value -> result.computeIfAbsent(value, x -> new ArrayList<>()).add(key));
});

Related

java 8 not seeing desired output with using map with filter

I see different result with using map with filter than using foreach with filter:
public class test1
{
public static void main (String[] args) throws java.lang.Exception
{
// your code goes here
Map<String, String> map = new HashMap<>();
map.put("a", "a");
map.put("c", "a");
Set<String> vs = new HashSet<>();
vs.add("b");
vs.add("c");
List<String> list = new ArrayList<>();
vs.stream()
.filter(a -> map.containsKey(a))
.map(c -> list.add(c));
System.out.println("here "+ list.size());
vs.stream()
.filter(a -> map.containsKey(a))
.forEach(c -> list.add(c));
System.out.println("here "+ list.size());
}
}
here is the output:
here 0
here 1
can somebody explain?
Terminal operations produces a non-stream, result such as primitive value, a collection or no value at all. Terminal operations are typically preceded by intermediate operations which return another Stream which allows operations to be connected in a form of a query. e.g. forEach()
Intermediate operations return another Stream which allows you to call multiple operations in a form of a query. Intermediate operations do not get executed until a terminal operation is invoked as there is a possibility they could be processed together when a terminal operation is executed. e.g. map()
In the following code, you didn't invoke a terminal operation in the last such as forEach() or collect(). That's why c -> list.add(c) isn't executed along with .filter(a -> map.containsKey(a)).
vs.stream()
.filter(a -> map.containsKey(a))
.map(c -> list.add(c));
Examine the result after using the following code snippet instead of above one,
vs.stream()
.filter(a -> map.containsKey(a)) // intermediate
.map(t -> list.add(t)) // intermediate
.collect(Collectors.toList()); // terminal
In our first stream you need terminal operation.
Without this, map is not executed.
Second stream have terminal operation (foreach) and whole stream is executed.
Add terminal operation to first stream(like count()) and you will see
Here 1
Here 2

Fastest way to convert key value pairs to grouped by key objects map using java 8 stream

Model:
public class AgencyMapping {
private Integer agencyId;
private String scoreKey;
}
public class AgencyInfo {
private Integer agencyId;
private Set<String> scoreKeys;
}
My code:
List<AgencyMapping> agencyMappings;
Map<Integer, AgencyInfo> agencyInfoByAgencyId = agencyMappings.stream()
.collect(groupingBy(AgencyMapping::getAgencyId,
collectingAndThen(toSet(), e -> e.stream().map(AgencyMapping::getScoreKey).collect(toSet()))))
.entrySet().stream().map(e -> new AgencyInfo(e.getKey(), e.getValue()))
.collect(Collectors.toMap(AgencyInfo::getAgencyId, identity()));
Is there a way to get the same result and use more simpler code and faster?
You can simplify the call to collectingAndThen(toSet(), e -> e.stream().map(AgencyMapping::getScoreKey).collect(toSet())))) with a call to mapping(AgencyMapping::getScoreKey, toSet()).
Map<Integer, AgencyInfo> resultSet = agencyMappings.stream()
.collect(groupingBy(AgencyMapping::getAgencyId,
mapping(AgencyMapping::getScoreKey, toSet())))
.entrySet()
.stream()
.map(e -> new AgencyInfo(e.getKey(), e.getValue()))
.collect(toMap(AgencyInfo::getAgencyId, identity()));
A different way to see it using a toMap collector:
Map<Integer, AgencyInfo> resultSet = agencyMappings.stream()
.collect(toMap(AgencyMapping::getAgencyId, // key extractor
e -> new HashSet<>(singleton(e.getScoreKey())), // value extractor
(left, right) -> { // a merge function, used to resolve collisions between values associated with the same key
left.addAll(right);
return left;
}))
.entrySet()
.stream()
.map(e -> new AgencyInfo(e.getKey(), e.getValue()))
.collect(toMap(AgencyInfo::getAgencyId, identity()));
The latter example is arguably more complicated than the former. Nevertheless, your approach is pretty much the way to go apart from using mapping as opposed to collectingAndThen as mentioned above.
Apart from that, I don't see anything else you can simplify with the code shown.
As for faster code, if you're suggesting that your current approach is slow in performance then you may want to read the answers here that speak about when you should consider going parallel.
You are collecting to an intermediate map, then streaming the entries of this map to create AgencyInfo instances, which are finally collected to another map.
Instead of all this, you could use Collectors.toMap to collect directly to a map, mapping each AgencyMapping object to the desired AgencyInfo and merging the scoreKeys as needed:
Map<Integer, AgencyInfo> agencyInfoByAgencyId = agencyMappings.stream()
.collect(Collectors.toMap(
AgencyMapping::getAgencyId,
mapping -> new AgencyInfo(
mapping.getAgencyId(),
new HashSet<>(Set.of(mapping.getScoreKey()))),
(left, right) -> {
left.getScoreKeys().addAll(right.getScoreKeys());
return left;
}));
This works by grouping the AgencyMapping elements of the stream by AgencyMapping::getAgencyId, but storing AgencyInfo objects in the map instead. We get these AgencyInfo instances from manually mapping each original AgencyMapping object. Finally, we're merging AgencyInfo instances that are already in the map by means of a merge function that folds left scoreKeys from one AgencyInfo to another.
I'm using Java 9's Set.of to create a singleton set. If you don't have Java 9, you can replace it with Collections.singleton.

Java 8 Stream Collect Sets

I have two maps, namely
Map<String, Set<String>> courseTeacherMap = {course1: [teacher1, teacher2], ...}
Map<String, Set<String>> teacherStudentMap = {teacher1: [student1, student2], ...}
And I defined a class courseStudentPair that has a very simple structure
public class courseStudentPair{
String studentName; // Taken from teacherStudentMap
String courseName; // Taken from courseTeacherMap
}
And my goal is to get a Set<courseStudentPair> out of the two maps. As long as a teacher A is teaching a course C, every student that is in the value set of key A in teacherStudentMap is considered to be a student taking C.
For example, given
Map<String, Set<String>> courseTeacherMap = {c1: [t1], c2:[t2], c3:[t1, t2]}
Map<String, Set<String>> teacherStudentMap = {t1: [s1], t2:[s1, s2]}
The result should be *(student, course) denotes a courseStudentPair object in the example below*
Set<courseStudentPair> result = [(c1, s1), (c2, s1), (c2, s2), (c3, s1), (c3, s2)]
It's quite straightforward to do it with for loops, but I am learning the stream function in Java 8 and this seems quite complicated to me. You can assume the courseStudentPair class has constructor or builder defined.
In the same spirit, you can generate each combination of (course, teacher) and then lookup for the students associated with this teacher. This may generate duplicates (for instance (c3, s1)), so be sure your CourseStudentPair class implements equals() and hashCode() based on those two fields.
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;
...
Set<CourseStudentPair> result =
courseTeacherMap.entrySet()
.stream()
.flatMap(e -> e.getValue()
.stream()
.flatMap(t -> teacherStudentMap.getOrDefault(t, emptySet()).stream().map(s -> new CourseStudentPair(e.getKey(), s))))
.collect(toSet());
/*
Output:
CourseStudentPair{studentName='c1', courseName='s1'}
CourseStudentPair{studentName='c2', courseName='s2'}
CourseStudentPair{studentName='c2', courseName='s1'}
CourseStudentPair{studentName='c3', courseName='s2'}
CourseStudentPair{studentName='c3', courseName='s1'}
*/
List<Pair<String, String>> result = courseTeacherMap.entrySet()
.stream()
.flatMap(entry -> Optional.ofNullable(entry.getValue())
.orElse(new HashSet<>())
.stream()
.flatMap(teacher -> Optional.ofNullable(teacherStudentMap.get(teacher))
.orElse(new HashSet<>())
.stream()
.map(student -> Pair.of(entry.getKey(), student))))
.distinct()
.collect(Collectors.toList());
I edited to make it null-safe, in case a teacher has no students for example or your Map might have a key mapped to null for example.

Filter on map of map

I have below map of map and want to filter it based on a value. The result should be assigned back to same map. Please let know what is the best approach for this.
Map<String, Map<String, Employee>> employeeMap;
<
dep1, <"empid11", employee11> <"empid12",employee12>
dep2, <"empid21", employee21> <"empid22",employee22>
>
Filter: employee.getState="MI"
I tried like below but i was not able to access the employee object
currentMap = currentMap.entrySet().stream()
**.filter(p->p.getValue().getState().equals("MI"))**
.collect(Collectors.toMap(p -> p.getKey(),p->p.getValue()));
If you want to modify the map in place (and that it allows so), you can use forEach to iterate over the entries of the map, and then use removeIf for each values of the inner maps to remove the employees that satisfy the predicate:
employeeMap.forEach((k, v) -> v.values().removeIf(e -> e.getState().equals("MI")));
Otherwise, what you can do is to use the toMap collector, where the function to map the values takes care of removing the concerned employees by iterating over the entry set of the inner maps:
Map<String, Map<String, Employee>> employeeMap =
employeeMap.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey,
e -> e.getValue().entrySet().stream().filter(emp -> !emp.getValue().getState().equals("MI")).collect(toMap(Map.Entry::getKey, Map.Entry::getValue))));

Java - Lambda filter criteria, to ignore adding to map

I have a map of the format (reference to Finding average using Lambda (Double stored as String))
Map<String, Double> averages=mapOfIndicators.values().stream()
.flatMap(Collection::stream)
.filter(objectDTO -> !objectDTO.getNewIndex().isEmpty())
.collect(Collectors.groupingBy(ObjectDTO::getCountryName,
Collectors.mapping(ObjectDTO::getNewIndex,
Collectors.averagingDouble(Double::parseDouble))));
I would like to ignore the ignore the entire country mapping even if one of them is newIndex value for that country is empty?
Since Collectors.groupingBy does not allow to skip groups, you either have to analyze the filtering condition in advance so you can filter before performing groupBy or filter the map afterwards (I ignore the third option, implement your own groupingBy collector.
Analyze in advance:
Map<String, Boolean> hasEmpty=mapOfIndicators.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.groupingBy(ObjectDTO::getCountryName,
Collectors.mapping(o->o.getNewIndex().isEmpty(),
Collectors.reducing(false, Boolean::logicalOr))));
Map<String, Double> averages=mapOfIndicators.values().stream()
.flatMap(Collection::stream)
.filter(objectDTO -> !hasEmpty.get(objectDTO.getCountryName()))
.collect(Collectors.groupingBy(ObjectDTO::getCountryName,
Collectors.mapping(ObjectDTO::getNewIndex,
Collectors.averagingDouble(Double::parseDouble))));
Filter the result:
Map<String, Double> averages=mapOfIndicators.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(ObjectDTO::getCountryName,
Collectors.mapping(ObjectDTO::getNewIndex, Collectors.averagingDouble(
s->s.isEmpty()? Double.NaN: Double.parseDouble(s)))),
m->{ m.values().removeIf(d->Double.isNaN(d)); return m; }));

Resources