I made up this function, it seems to make the work, but I wondered if there was an even cleaner solution.
public static <K, V> Map<V, List<K>> reverseMap(Map<K, List<V>> map) {
return map.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().map(value -> new AbstractMap.SimpleEntry<>(value, entry.getKey())))
.collect(Collectors.groupingBy(
AbstractMap.SimpleEntry::getKey,
Collectors.mapping(AbstractMap.SimpleEntry::getValue, Collectors.toList())));
}
Bonus question: I have a java 8 constraint for this one, but how could later versions improve it? I assume I'd no longer have to use AbstractMap.SimpleEntry since Java 9 introduced the Map.entry(k, v) function.
Since you did not necessitate streams in your question, I'm going to advocate for a non-stream solution:
//example input
Map<Integer, List<Integer>> map = new HashMap<>();
map.put(1, Arrays.asList(11, 12, 13, 4));
map.put(2, Arrays.asList(21, 22, 23, 4));
map.put(3, Arrays.asList(31, 32, 33, 4));
//reversing
Map<Integer, List<Integer>> reversed = new HashMap<>();
map.forEach((key, list) -> {
list.forEach(value -> {
reversed.computeIfAbsent(value, k -> new ArrayList<>()).add(key);
});
});
//end result:
//{32=[3], 33=[3], 4=[1, 2, 3], 21=[2], 22=[2], 23=[2], 11=[1], 12=[1], 13=[1], 31=[3]}
In your stream-based solution, you will be creating new Entry objects per key-(list-value) pair in the map, which will then have to make additional entries when recombined into a map. By using a direct approach, you avoid this excess object creation and directly create the entries you need.
Note that not everything has to be a Stream, the "old" way of doing things can still be correct, if not better (in readability and performance terms) than a Stream implementation.
Related
I'm having a hard time converting a Map containing some integers as keys and a list of random strings as values.
E.g. :
1 = ["a", "b", "c"]
2 = ["a", "b", "z"]
3 = ["z"]
I want to transform it into a Map of distinct strings as keys and lists the integers as values.
E.g. :
a = [1, 2]
b = [1, 2]
c = [1]
z = [2,3]
Here's what I have so far:
Map<Integer, List<String>> integerListMap; // initial list is already populated
List<String> distinctStrings = new ArrayList<>();
SortedMap<String, List<Integer>> stringListSortedMap = new TreeMap<>();
for(Integer i: integers) {
integerListMap.put(i, strings);
distinctStrings.addAll(strings);
}
distinctStrings = distinctStrings.stream().distinct().collect(Collectors.toList());
for(String s : distinctStrings) {
distinctStrings.put(s, ???);
}
Iterate over your source map's value and put each value into the target map.
final Map<String, List<Integer>> target = new HashMap<>();
for (final Map.Entry<Integer, List<String>> entry = source.entrySet()) {
for (final String s : entry.getValue()) {
target.computeIfAbsent(s, k -> new ArrayList<>()).add(entry.getKey());
}
}
Or with the Stream API by abusing Map.Entry to build the inverse:
final Map<String, List<Integer>> target = source.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(s -> Map.entry(s, e.getKey()))
.collect(Collectors.groupingBy(e::getKey, Collectors.mapping(e::getValue, Collectors.toList())));
Although this might not be as clear as introducing a new custom type to hold the inverted mapping.
Another alternative would be using a bidirectial map. Many libraries come implementations of these, such as Guava.
There's no need to apply distinct() since you're storing the data into the Map and keys are guaranteed to be unique.
You can flatten the entries of the source map, so that only one string (let's call it name) and a single integer (let's call it number) would correspond to a stream element, and then group the data by string.
To implement this problem using streams, we can utilize flatMap() operation to perform one-to-many transformation. And it's a good practice to define a custom type for that purpose as a Java 16 record, or a class (you can also use a Map.Entry, but note that approach of using a custom type is more advantages because it allows writing self-documenting code).
In order to collect the data into a TreeMap you can make use of the three-args version of groupingBy() which allows to specify mapFactory.
record NameNumber(String name, Integer number) {}
Map<Integer, List<String>> dataByProvider = Map.of(
1, List.of("a", "b", "c"),
2, List.of("a", "b", "z"),
3, List.of("z")
);
NavigableMap<String, List<Integer>> numbersByName = dataByProvider.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(name -> new NameNumber(name, entry.getKey()))
)
.collect(Collectors.groupingBy(
NameNumber::name,
TreeMap::new,
Collectors.mapping(NameNumber::number, Collectors.toList())
));
numbersByName.forEach((name, numbers) -> System.out.println(name + " -> " + numbers));
Output:
a -> [2, 1]
b -> [2, 1]
c -> [1]
z -> [3, 2]
Sidenote: while using TreeMap it's more beneficial to use NavigableMap as an abstract type because it allows to access methods like higherKey(), lowerKey(), firstEntry(), lastEntry(), etc. which are declared in the SortedMap interface.
I have a TreeMap<Integer, Integer> instance and I want to reassign the key value mappings in that way that the lowest key is assigned to the lowest value and the highest key to the highest key.
Here is how I do it without streams:
TreeMap<Integer, Integer> map = new TreeMap<>();
map.put(1, 6);
map.put(2, 9);
map.put(4, 2);
map.put(3, 1);
map.put(8, 10);
map.put(5, 10);
ArrayList<Integer> valueList = new ArrayList<Integer>(map.values());
Collections.sort(valueList);
int i = 0;
for (Map.Entry entry : map.entrySet()) {
entry.setValue(valueList.get(i++));
}
System.out.println(map);
output:
{1=1, 2=2, 3=6, 4=9, 5=10, 8=10}
Any hints how to perform such a task utilizing java-8 Stream API are welcome.
Thx
I have found out a solution that is fairly easy to read and use:
Iterator<Integer> keyIterator = map.keySet().iterator();
TreeMap<Integer, Integer> newMap = map.values().stream()
.sorted()
.map(value -> new SimpleEntry<>(keyIterator.next(), value))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> l, TreeMap::new));
.. or shorter thanks to #HadiJ:
map.values().stream()
.sorted()
.collect(Collectors.toMap(k -> keyIterator.next(), Function.identity(), (l, r) -> l, TreeMap::new));
... but it has a significant drawback:
I cannot guarantee this will work in parallel since it depends on the result of keyIterator.next() which is also not checked. Read more at the section Stateless Behaviors. I'd rather not use java-stream in this way.
If I were you, I'd use the advantage of the beauty of iterators:
Iterator<Integer> values = valueList.iterator();
Iterator<Integer> keys = map.keySet().iterator();
TreeMap<Integer, Integer> newMap = new TreeMap<>(); // create a new Map
while (values.hasNext() && keys.hasNext()) { // iterate simultaneously
newMap.put(keys.next(), values.next()); // put the key-value
}
Your approach is not bad. You can shorten it to
PriorityQueue<Integer> q = new PriorityQueue<>(map.values());
map.entrySet().forEach(e -> e.setValue(q.remove()));
I don't think that this task is a good candidate for the Stream API.
I was watching a JavaOne video By Venkat Subramanian about lambdas.
He had an example like this:
Map<String, Integer> scores = new HashMap<>();
scores.put("Jack", 12);
scores.put("Jill", 15);
scores.put("Tom", 11);
scores.put("Darla", 15);
scores.put("Nick", 15);
scores.put("Nancy", 11);
System.out.println(groupByScores(scores));
with this method:
public static Map<Integer, List<String>> groupByScores(Map<String, Integer> scores) {
return scores.keySet().stream().collect(Collectors.groupingBy(scores::get));
}
The one thing that's a little bothersome about this is that it's essentially iterating over the keySet, calling map.get(key) for each key. But that's an antipattern in plain non-lambda code.
How can I get the same result, producing a "peopleByAge" map, but by iterating over the entrySet instead of the keySet? The hard part is obviously everything past stream().
return scores.entrySet().stream()
.collect(Collectors.groupingBy(Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey,
Collectors.toList())));
I would like to put the frequencies of the numbers in a TreeMap with the frequencies as the keys and the numbers that have that frequency in an ArrayList.
I have two problems:
1) I'm getting a "non-static methods cannot be referenced from a static context" error in the first parameter (AFAIK the stream references an object - what's going on?)
2) There are 4 parameters for Collectors.toMap() - it seems like parameter 4 requires initialization with a new TreeMap>, parameter 2 could be an ArrayList add() function and parameter 3 could be null (maybe). How is this done?
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> array = Arrays.asList(1, 2, 4, 5, 6, 4, 8, 4, 2, 3);
Map<Integer, Long> m = array.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(m);
TreeMap<Long, List<Integer>> tm = m.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, ...));
At the moment I can't use see how to get from
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html to where I need to be.
I think Collectors.groupingBy makes more sense than Collectors.toMap to achieve what you are looking for:
Map<Long, List<Integer>> tm =
m.entrySet()
.stream()
.collect(Collectors.groupingBy(Map.Entry::getValue, // group the entries by the
// value (the frequency)
TreeMap::new, // generate a TreeMap
Collectors.mapping (Map.Entry::getKey,
Collectors.toList()))); // the
// value of the output TreeMap
// should be a List of the
// original keys
You can replace Collectors.toList() with Collectors.toCollection(ArrayList::new) to make sure that the values of the output Map are ArrayLists (though the current implementation of toList() already results in java.util.ArrayList instances).
For your sample input, this produces the following TreeMap:
{1=[1, 3, 5, 6, 8], 2=[2], 3=[4]}
I wouldn't use streams to create the inverted map. Instead, I would just do:
Map<Long, List<Integer>> tm = new TreeMap<>();
m.forEach((num, freq) -> tm.computeIfAbsent(freq, k -> new ArrayList<>()).add(num));
System.out.println(tm); // {1=[1, 3, 5, 6, 8], 2=[2], 3=[4]}
As the code to create the inverted map is short, you could use Collectors.collectingAndThen to create both the frequencies and inverted map in one step:
TreeMap<Long, List<Integer>> invertedFrequenciesMap = array.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(Function.identity(), Collectors.counting()),
map -> {
TreeMap<Long, List<Integer>> tm = new TreeMap<>();
map.forEach((num, freq) ->
tm.computeIfAbsent(freq, k -> new ArrayList<>()).add(num));
return tm;
}));
You are almost correct in your reasoning... Just that the 3-rd argument is where you merge your values for the same key - so you can't omit it.
TreeMap<Long, List<Integer>> tm = m.entrySet().stream()
.collect(Collectors.toMap(
Entry::getValue,
x -> {
List<Integer> list = new ArrayList<>();
list.add(x.getKey());
return list;
},
(left, right) -> {
left.addAll(right);
return left;
},
TreeMap::new));
Consider a simple HashMap<Integer,Integer>. How can I get all the values stored against keys which are multiple of, say 5? I have worked on Java and Collections for some time now, but suddenly I am clueless.
Any help would be highly appreciated.
Regards,
Salil
List<Integer> values = new ArrayList<>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getKey() % 5 == 0) {
values.add(entry.getValue());
}
}
FWIW, a comparable Java 8 approach might look like
map.entrySet().stream()
.filter(entry -> entry.getKey() % 5 == 0)
.map(Entry<Integer, Integer>::getValue)
.collect(toList());
Guava has a few helper methods to help you achieve this. In particular, Maps.filterKeys(Map, Predicate)
// populate a Map
Map<Integer, Integer> map = new HashMap<>();
map.put(3, 0);
map.put(15, 42);
map.put(75, 1234);
// filter it
Map<Integer, Integer> filtered = Maps.filterKeys(map, new Predicate<Integer>() {
#Override
public boolean apply(Integer input) {
return input % 5 == 0; // filter logic
}
});
// get the values
System.out.println(filtered.values());
prints
[1234, 42]
As Louis notes, "this isn't actually more efficient -- or even shorter -- than the "traditional" for-each approach."