In your case I would create the maps first and then loop through the elements in this list as shown:
Map<String, List<MyObject>> rows = new HashMap<>();
list.forEach(element -> rows.computeIfAbsent(element.personalId, s -> new ArrayList<>()).add(element));
You can use computeIfAbsent in order to create a new list/map as a value of the map before you can put your data in.
The same is with the second data type you created:
Map<String, Map<Integer, MyObject>> persons = new HashMap<>();
list.forEach(element -> persons.computeIfAbsent(element.personalId, s -> new HashMap<>()).put(element.rowNumber, element));
Here is a way to solve this with streams. But note that the objects must have a unique personId/rowNumber:
Map<String, List<MyObject>> rows = list.stream().collect(
Collectors.toMap(element -> element.personalId,
element -> new ArrayList<MyObject>(Arrays.asList(element))));
As well as for the other map:
Map<String, Map<Integer, MyObject>> persons = list.stream().collect(
Collectors.toMap(e -> e.personalId,
e -> new HashMap<>(Map.of(e.rowNumber, e))));
Answer from Japhei on Stack OverflowIn your case I would create the maps first and then loop through the elements in this list as shown:
Map<String, List<MyObject>> rows = new HashMap<>();
list.forEach(element -> rows.computeIfAbsent(element.personalId, s -> new ArrayList<>()).add(element));
You can use computeIfAbsent in order to create a new list/map as a value of the map before you can put your data in.
The same is with the second data type you created:
Map<String, Map<Integer, MyObject>> persons = new HashMap<>();
list.forEach(element -> persons.computeIfAbsent(element.personalId, s -> new HashMap<>()).put(element.rowNumber, element));
Here is a way to solve this with streams. But note that the objects must have a unique personId/rowNumber:
Map<String, List<MyObject>> rows = list.stream().collect(
Collectors.toMap(element -> element.personalId,
element -> new ArrayList<MyObject>(Arrays.asList(element))));
As well as for the other map:
Map<String, Map<Integer, MyObject>> persons = list.stream().collect(
Collectors.toMap(e -> e.personalId,
e -> new HashMap<>(Map.of(e.rowNumber, e))));
Map<String, Map<Integer, List<MyObject>>>object using streams without usingCollectors.groupingBy()
By looking at the map type, I can assume that a combination of personalId and rowNumber is not unique, i.e. there could be multiple occurrences of each combination (otherwise you don't need to group objects into lists). And there could be different rowNumber associated with each personalId. Only if these conclusions correct, this nested collection might have a very vague justification for existence.
Otherwise, you probably can substitute it multiple collections for different use-cases, for Map<String, MyObject> - object by id (if every id is unique):
Map<String, MyObject> objById = myList.stream()
.collect(Collectors.toMap(
MyObject::getPersonalId,
Function.identity()
));
I'll proceed assuming that you really need such a nested collection.
Now, let's address the issue with groupingBy(). This collector uses internally Objects.requireNonNull() to make sure that a key produced by the classifier function is non-null.
If you tried to use and failed because of the hostility to null-keys, that implies that either personalId, or rowNumber, or both can be null.
Now let's make a small detour and pose the question of what does it imply if a property that considered to be significant (you're going to use personalId and rowNumber to access the data, hence they are definitely important) and null means first of all?
null signifies the absence of data and nothing else. If in your application null values have an additional special meaning, that's a design flaw. If properties that are significant for managing your data for some reason appear to be null you need to fix that.
You might claim that you're quite comfortable with null values. If so let pause for a moment and imagine a situation: person enters a restaurant, orders a soup and asks the waiter to bring them a fork instead of spoon (because of they have a negative experience with a spoon, and they are enough comfortable with fork).
null isn't a data, it's an indicator of the absence of data, storing null is an antipattern. If you're storing null it obtains a special meaning because you're forced to treat it separately.
To replace personalId and rowNumber that are equal to null with default values we need only one line of code.
public static void replaceNullWithDefault(List<MyObject> list,
String defaultId,
Integer defaultNum) {
list.replaceAll(obj -> obj.getPersonalId() != null && obj.getRowNumber() != null ? obj :
new MyObject(Objects.requireNonNullElse(obj.getPersonalId(), defaultId),
Objects.requireNonNullElse(obj.getRowNumber(), defaultNum),
obj.getDescription()));
}
After that can use the proper tool instead eating soup with a fork, I mean we can process the list data with groupingBy():
public static void main(String[] args) {
List<MyObject> myList = new ArrayList<>(
List.of(
new MyObject("id1", 1, "desc1"),
new MyObject("id1", 1, "desc2"),
new MyObject("id1", 2, "desc3"),
new MyObject("id1", 2, "desc4"),
new MyObject("id2", 1, "desc5"),
new MyObject("id2", 1, "desc6"),
new MyObject("id2", 1, "desc7"),
new MyObject(null, null, "desc8")
));
replaceNullWithDefault(myList, "id0", 0); // replacing null values
Map<String, Map<Integer, List<MyObject>>> byIdAndRow = myList // generating a map
.stream()
.collect(Collectors.groupingBy(
MyObject::getPersonalId,
Collectors.groupingBy(MyObject::getRowNumber)
));
byIdAndRow.forEach((k, v) -> { // printing the map
System.out.println(k);
v.forEach((k1, v1) -> System.out.println(k1 + " -> " + v1));
});
}
Output:
id0
0 -> [MyObject{'id0', 0, 'desc8'}]
id2
1 -> [MyObject{'id2', 1, 'desc5'}, MyObject{'id2', 1, 'desc6'}, MyObject{'id2', 1, 'desc7'}]
id1
1 -> [MyObject{'id1', 1, 'desc1'}, MyObject{'id1', 1, 'desc2'}]
2 -> [MyObject{'id1', 2, 'desc3'}, MyObject{'id1', 2, 'desc4'}]
A link to Online Demo
Now, please pay attention to the usage of groupingBy() did you notice its conciseness. That's the right tool which allows generating even such a clumsy nested map.
And now we're going to eat the soup with a fork! All null properties would be used as is:
public static void main(String[] args) {
List<MyObject> myList = new ArrayList<>(
List.of(
new MyObject("id1", 1, "desc1"),
new MyObject("id1", 1, "desc2"),
new MyObject("id1", 2, "desc3"),
new MyObject("id1", 2, "desc4"),
new MyObject("id2", 1, "desc5"),
new MyObject("id2", 1, "desc6"),
new MyObject("id2", 1, "desc7"),
new MyObject(null, null, "desc8")
));
Map<String, Map<Integer, List<MyObject>>> byIdAndRow = myList // generating a map
.stream()
.collect(
HashMap::new,
(Map<String, Map<Integer, List<MyObject>>> mapMap, MyObject next) ->
mapMap.computeIfAbsent(next.getPersonalId(), k -> new HashMap<>())
.computeIfAbsent(next.getRowNumber(), k -> new ArrayList<>())
.add(next),
(left, right) -> right.forEach((k, v) -> left.merge(k, v,
(oldV, newV) -> {
newV.forEach((k1, v1) -> oldV.merge(k1, v1,
(listOld, listNew) -> {
listOld.addAll(listNew);
return listOld;
}));
return oldV;
}))
);
byIdAndRow.forEach((k, v) -> { // printing the map
System.out.println(k);
v.forEach((k1, v1) -> System.out.println(k1 + " -> " + v1));
});
}
Output:
null
null -> [MyObject{'null', null, 'desc8'}]
id2
1 -> [MyObject{'id2', 1, 'desc5'}, MyObject{'id2', 1, 'desc6'}, MyObject{'id2', 1, 'desc7'}]
id1
1 -> [MyObject{'id1', 1, 'desc1'}, MyObject{'id1', 1, 'desc2'}]
2 -> [MyObject{'id1', 2, 'desc3'}, MyObject{'id1', 2, 'desc4'}]
A link to Online Demo
Need help with .stream().map and Collectors.toMap()
java - Collect a Stream of objects into a Map - Stack Overflow
collections - Java 8 streams - collect and stream map of lists - Stack Overflow
Why does the List interface have forEach, but not map?
So I have a Map <Long, Ingredient> normalMap and my method needs to first invert the keys&values and lastly return the map as Map <String, Long>
To invert the map I wrote the following code:
invertedMap = normalMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
So far, so good.
Now I need to make my inverted Map return the Name of my Object as a String. Note: My Ingredient Object has a @ Override method which overrides the toString() method and basically return the getter method { this.getName(); }
So what my idea was to write the following code:
return invertedMap.entrySet().stream().collect(Collectors.toMap(Ingredient::toString, Function.identity()));
Unfortunately, this does not work. I'm googling and trying to find a solution and whenever I find something useful it creates new errors. Pls help.
Update: I managed to make it work using following code:
Map<String, Long> ingToString = new HashMap<>();
for (Map.Entry<Ingredient, Long> entry : invertedMap.entrySet())
{
Ingredient ing = entry.getKey();
ingToString.put(entry.getKey().toString(), entry.getValue());
}
return ingToString;But my question still is, what is the equivalent to this code but using .stream() ?
Finally came to an understanding that Collectors.GroupingBy should not be used for converting stream into a map. Because this would group similar results in a list and give it back as a Map<String,List<T>>. What I wanted was a Map<String,T>.
After skimming through the Javadocs, I found Collectors.toMap which is the exact solution I was looking for.
The below is the code that I ended up writing.
users = Files.lines(Paths.get(file.getAbsolutePath()))
.map(line -> line.split(","))
.map(fields -> new MockUser(fields[0], fields[1], fields[2]))
.collect(Collectors.toMap(
MockUser::getUsername,
user -> user));
You can do this
Map<String, MockUser> users = Files.lines(Paths.get(f.getAbsolutePath()))
.map(line -> line.split(","))
.map(fields -> new MockUser(fields[0], fields[1], fields[2]))
.collect(Collectors.groupingBy(MockUser::getUserName))
It looks like all you need is flatMap the values of the original Map and then continue processing as you currently do:
Collection< PublicationSession > wantedPublications =
effectiveDatePublicationMap.values() // Collection<List<PublicationSession>>
.stream() // Stream<List<PublicationSession>>
.flatMap(list->list.stream()) // Stream<PublicationSession>
.filter(pub -> PublicationStatus.valueOf(pub.getPublishStatus()) == PublicationStatus.COMPLETE)
.sorted(Comparator.comparing(PublicationSession::getCreateTime))
.collect(toMap(p -> p.getPublicationSession().getEffDateTime(), UnaryOperator.identity(), PICK_LATEST))
.values();
To obtain the same result of your iterative code
List< PublicationSession> publicationsToFilter = new ArrayList<>();
for (List< PublicationSession> sessions : effectiveDatePublicationMap.values()) {
publicationsToFilter.addAll(sessions);
}
with streams you first have to use flatMap to transoform your stream of List<PublicationSession> in a stream of PublicationSession, and then collect all of them in a list with collect(Collectors.toList())
List<PublicationSession> publicationsToFilter = effectiveDatePublicationMap.values()
.flatMap(Collection::stream)
.collect(Collectors.toList());
Before collecting the result, you can filter or sort as you wish.