sort value of a map by comparator with java streams api - java

I have a Map and want to sort the values by a Comparator putting the result into a LinkedHashMap.
Map<String, User> sorted = test.getUsers()
.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(SORT_BY_NAME))
.collect(LinkedHashMap::new, (map, entry) -> map.put(entry.getKey(), entry.getValue()),
LinkedHashMap::putAll);
test.setUsers(sorted);
All works, however, I wonder if this can be simplified.
Actually I create a new Map and put that new Map into the setUsers(). Can I change the stream directly without creating a new LinkedHashMap?
With Collections.sort(Comparator), the list is directly sorted. However, Collections.sort does not work with Maps.

Why not use Collectors.toMap()?
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue,
(a, b) -> { throw new AssertionError("impossible"); },
LinkedHashMap::new));

You cannot perform in-place sorting on a collection that does not support array-style indexing, so for maps it is out of the question (except you use something like selection sort on a LinkedHashMap, but that would be a bad idea). Creating a new map is unavoidable. You can still simplify it though, by following shmosel's answer.

Related

How to fix Duplicate Key IllegalStateException while using Collectors.toMap()

I have a stream that processes some strings and collects them in a map.
But getting the following exception:
java.lang.IllegalStateException:
Duplicate key test#yahoo.com
(attempted merging values [test#yahoo.com] and [test#yahoo.com])
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:133)
I'm using the following code:
Map<String, List<String>> map = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmails(email)
));
The flavor of toMap() you're using in your code (which expects only keyMapper and valueMapper) disallow duplicates merely because it's not capable to handle them. And exception message explicitly tells you that.
Judging by the resulting type Map<String, List<String>> and by the exception message which shows strings enclosed in square brackets, it is possible to make the conclusion that processEmails(email) produces a List<String> (although it's not obvious from your description and IMO worth specifying).
There are multiple ways to solve this problem, you can either:
Use this another version of toMap(keyMapper,valueMapper,mergeFunction) which requires the third argument, mergeFunction - a function responsible for resolving duplicates.
Map<String, List<String>> map = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmails(email),
(list1, list2) -> list1 // or { list1.addAll(list2); return list1} depending on the your logic of resolving duplicates you need
));
Make use of the collector groupingBy(classifier,downstream) to preserve all the emails retrieved by processEmails() that are associated with the same key by storing them into a List. As a downstream collector we could utilize a combination of collectors flatMapping() and toList().
Map<String, List<String>> map = emails.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.flatMapping(email -> processEmails(email).stream(),
Collectors.toList())
));
Note that the later option would make sense only if processEmails() somehow generates different results for the same key, otherwise you would end up with a list of repeated values which doesn't seem to be useful.
But what you definitely shouldn't do in this case is to use distinct(). It'll unnecessarily increase the memory consumption because it eliminates the duplicates by maintaining a LinkedHashSet under the hood. It would be wasteful because you're already using Map which is capable to deal with duplicated keys.
You have duplicate emails. The toMap version you're using explicitly doesn't allow duplicate keys. Use the toMap that takes a merge function. How to merge those processEmails results depends on your business logic.
Alternatively, use distinct() before collecting, because otherwise you'll probably end up sending some people multiple emails.
try using
Collectors.toMap(Function keyFuntion, Function valueFunction, BinaryOperator mergeFunction)
You obviously have to write your own merge logic, a simple mergeFunction could be
(x1, x2) -> x1

Java 8 stream - filter on nested map and rebuild

I have a nested map in the form
TreeMap<LocalDate, Map<Long,TreeMap<BigDecimal,String>>>
and I have to process this map and end up with a map of the same structure where the nested TreeMap
TreeMap<BigDecimal, String>>
has exactly two elements.
I can find the elements I want with
values.entrySet().stream().flatMap(date -> date.getValue().entrySet().stream()
.map(type -> type.getValue().entrySet()))
.filter(valueMap -> valueMap.size() == 2 )
but I can't work out how to express the .collect() to re-assemble the structure. Any pointers will be welcome.
You're losing information when calling flatMap and map. You need to preserve keys in order to be able to rebuild your structure.
With a slight change, you can just filter the inner maps and collect them using an inner stream, without affecting the structure of the outer stream:
Map<LocalDate, Map<Long, TreeMap<BigDecimal, String>>> result = values.entrySet().stream()
.collect(
Collectors.toMap(
Entry::getKey,
entry -> entry.getValue()
.entrySet()
.stream()
.filter(subEntry -> subEntry.getValue().size() == 2)
.collect(Collectors.toMap(Entry::getKey,
Entry::getValue)))
);

Collect results of a map operation in a Map using Collectors.toMap or groupingBy

I've got a list of type List<A> and with map operation getting a collective list of type List<B> for all A elements merged in one list.
List<A> listofA = [A1, A2, A3, A4, A5, ...]
List<B> listofB = listofA.stream()
.map(a -> repo.getListofB(a))
.flatMap(Collection::stream)
.collect(Collectors.toList());
without flatmap
List<List<B>> listOflistofB = listofA.stream()
.map(a -> repo.getListofB(a))
.collect(Collectors.toList());
I want to collect the results as a map of type Map<A, List<B>> and so far trying with various Collectors.toMap or Collectors.groupingBy options but not able to get the desired result.
You can use the toMap collector with a bounded method reference to get what you need. Also notice that this solution assumes you don't have repeated A instances in your source container. If that precondition holds this solution would give you the desired result. Here's how it looks.
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(Function.identity(), repo::getListofB);
If you have duplicate A elements, then you have to use this merge function in addition to what is given above. The merge function deals with key conflicts if any.
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(Function.identity(), repo::getListofB,
(a, b) -> {
a.addAll(b);
return a;
}));
And here's a much more succinct Java9 approach which uses the flatMapping collector to handle repeated A elements.
Map<A, List<B>> aToBmap = listofA.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.flatMapping(a -> getListofB(a).stream(),
Collectors.toList())));
It would be straight forward,
listofA.stream().collect(toMap(Function.identity(), a -> getListofB(a)));
In this answer, I'm showing what happens if you have repeated A elements in your List<A> listofA list.
Actually, if there were duplicates in listofA, the following code would throw an IllegalStateException:
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(
Function.identity(),
repo::getListofB);
The exception might be thrown because Collectors.toMap doesn't know how to merge values when there is a collision in the keys (i.e. when the key mapper function returns duplicates, as it would be the case for Function.identity() if there were repeated elements in the listofA list).
This is clearly stated in the docs:
If the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.
The docs also give us the solution: in case there are repeated elements, we need to provide a way to merge values. Here's one such way:
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(
Function.identity(),
a -> new ArrayList<>(repo.getListofB(a)),
(left, right) -> {
left.addAll(right);
return left;
});
This uses the overloaded version of Collectors.toMap that accepts a merge function as its third argument. Within the merge function, Collection.addAll is being used to add the B elements of each repeated A element into a unqiue list for each A.
In the value mapper function, a new ArrayList is created, so that the original List<B> of each A is not mutated. Also, as we're creating an Arraylist, we know in advance that it can be mutated (i.e. we can add elements to it later, in case there are duplicates in listofA).
To collect a Map where keys are the A objects unchanged, and the values are the list of corresponding B objects, you can replace the toList() collector by the following collector :
toMap(Function.identity(), a -> repo.getListOfB(a))
The first argument defines how to compute the key from the original object : identity() takes the original object of the stream unchanged.
The second argument defines how the value is computed, so here it just consists of a call to your method that transforms a A to a list of B.
Since the repo method takes only one parameter, you can also improve clarity by replacing the lambda with a method reference :
toMap(Function.identity(), repo::getListOfB)

Java Sorting a Map using Steams VS TreeMap

Consider the following Java HashMap.
Map<String, String> unsortMap = new HashMap<String, String>();
unsortMap.put("Z", "z");
unsortMap.put("B", "b");
unsortMap.put("A", "a");
unsortMap.put("C", "c");
Now I wish to sort this Map by Key. One option is for me to use a TreeMap for this purpose.
Map<String, String> treeMap = new TreeMap<String, String>(unsortMap);
Another option is for me to Use Java Streams with Sorted(), as follows.
Map<String, Integer> sortedMap = new HashMap<>();
unsortMap.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(x -> sortedMap.put(x.getKey(), x.getValue()));
Out of these two, which option is preferred and why (may be in terms of performance)?
Thank you
As pointed out by others dumping the sorted stream of entries into a regular HashMap would do nothing... LinkedHashMap is the logical choice.
However, an alternative to the approaches above is to make full use of the Stream Collectors API.
Collectors has a toMap method that allows you to provide an alternative implementation for the Map. So instead of a HashMap you can ask for a LinkedHashMap like so:
unsortedMap.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1, // you will never merge though ask keys are unique.
LinkedHashMap::new
));
Between using a TreeMap vs LinkedHashMap ... The complexity of construction is likely to be the same something like O(n log n)... Obviously the TreeMap solution is a better approach if you plan to keep adding more elements to it... I guess you should had started with a TreeMap in that case. The LinkedHashMap option has the advantage that lookup is going to be O(1) on the Linked or the original unsorted map whereas as TreeMap's is something like O(log n) so if you would need to keep the unsorted map around for efficient lookup whereas in if you build the LinkedHashMap you could toss the original unsorted map (thus saving some memory).
To make things a bit more efficient with LinkedHashMap you should provide an good estimator of the required size at construction so that there is not need for dynamic resizing, so instead of LinkedHashMap::new you say () -> new LinkedHashMap<>(unsortedMap.size()).
I'm my opinion the use of a TreeMap is more neat... as keeps the code smaller so unless there is actual performance issue that could be addressed using the unsorted and sorted linked map approach I would use the Tree.
Your stream code won't even sort the map, because it is performing the operation against a HashMap, which is inherently unsorted. To make your second stream example work, you may use LinkedHashMap, which maintains insertion order:
Map<String, Integer> sortedMap = new LinkedHashMap<>();
unsortMap.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(x -> sortedMap.put(x.getKey(), x.getValue()));
But now your two examples are not even the same underlying data structure. A TreeMap is backed by a tree (red black if I recall correctly). You would use a TreeMap if you wanted to be able to iterate in a sorted way, or search quickly for a key. A LinkedHashMap is hashmap with a linked list running through it. You would use this if you needed to maintain insertion order, for example when implementing a queue.
The second way does not work, when you call HashMap#put, it does not hold the put order. You might need LinkedHashMap.
TreeMap v.s. Stream(LinkedHashMap):
code style. Using TreeMap is more cleaner since you can achieve it in one line.
space complexity. If the original map is HashMap, with both method you need to create a new Map. If If the original map is LinkedHashMap, then you only need create a new Map with the first approach. You can re-use the LinkedHashMap with the second approach.
time complexity. They should both have O(nln(n)).

Java 8 HashMap<Integer, ArrayList<Integer>>

I am new Java 8 and want to sort a Map based on Key and then sort each list within values.
I tried to look for a Java 8 way to sort Keys and also value.
HashMap> map
map.entrySet().stream().sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (e1, e2) -> e2, LinkedHashMap::new));
I am able to sort the Map and I can collect each values within map to sort but is their a way we can do in java 8 and can both be combined.
To sort by key, you could use a TreeMap. To sort each list in the values, you could iterate over the values of the map by using the Map.values().forEach() methods and then sort each list by using List.sort. Putting it all together:
Map<Integer, List<Integer>> sortedByKey = new TreeMap<>(yourMap);
sortedByKey.values().forEach(list -> list.sort(null)); // null: natural order
This sorts each list in-place, meaning that the original lists are mutated.
If instead you want to create not only a new map, but also new lists for each value, you could do it as follows:
Map<Integer, List<Integer>> sortedByKey = new TreeMap<>(yourMap);
sortedByKey.replaceAll((k, originalList) -> {
List<Integer> newList = new ArrayList<>(originalList);
newList.sort(null); // null: natural order
return newList;
});
EDIT:
As suggested in the comments, you might want to change:
sortedByKey.values().forEach(list -> list.sort(null));
By either:
sortedByKey.values().forEach(Collections::sort);
Or:
sortedByKey.values().forEach(list -> list.sort(Comparator.naturalOrder()));
Either one of the two options above is much more expressive and shows the developer's intention in a better way than using null as the comparator argument to the List.sort method.
Same considerations apply for the approach in which the lists are not modified in-place.

Categories

Resources