How create a new map from the values in an existing map - java

Having the next original map:
G1=[7,8,45,6,9]
G2=[3,9,34,2,1,65]
G3=[6,5,9,1,67,5]
Where G1, G2 and G3 are groups of people's ages, How can I create a new map like this:
45=[7,8,45,6,9]
65=[3,9,34,2,1,65]
67=[6,5,9,1,67,5]
Where the new keys are the max people's age in each group.
I have tried this:
Map<Integer, List<Integer>> newMap = originalMap.entrySet().stream()
.collect(Collectors.toMap(Collections.max(x -> x.getValue()), x -> x.getValue()));
But the compiler say me: "The target type of this expression must be a functional interface" in this fragment of code:
Collections.max(x -> x.getValue())
Any help with this will be appreciated.

toMap consumes function for it's keyMapper and valueMapper. You're doing this correctly for the valueMapper in your code but not for the keyMapper thus you need to include the keyMapper function as follows:
originalMap.entrySet()
.stream()
.collect(toMap(e -> Collections.max(e.getValue()), Map.Entry::getValue));
note the e -> Collections.max(e.getValue()).
Further, since you're not working with the map keys, you can avoid having to call entrySet() and instead work on the map values:
originalMap.values()
.stream()
.collect(Collectors.toMap(Collections::max, Function.identity()));

Related

How to combine Map values from parent Map with java 8 stream

I have a map inside a map which looks like this:
Map<String, Map<Integer, BigDecimal>> mapInMap; //--> with values like:
/*
"a": (1: BigDecimal.ONE),
"b": (2: BigDecimal.TEN),
"c": (1: BigDecimal.ZERO)
*/
And I would like to combine the inner maps by expecting the following result:
Map<Integer, BigDecimal> innerMapCombined; //--> with values:
/*
1: BigDecimal.ZERO,
2: BigDecimal.TEN
*/
This is my solution with predefining the combined map and using forEach:
Map<Integer, BigDecimal> combined = new HashMap<>();
mapInMap.forEach((str, innerMap) -> {
innerMap.forEach(combined::putIfAbsent);
});
But this will ignore (1: BigDecimal.ZERO).
Could you provide a 1-line solution with java 8 stream?
The issue with your problem is that as soon as you initialize your maps, and add the duplicate keys on the inner maps, you will rewrite those keys, since those maps do not accept duplicated keys. Therefore, you need to first change this:
Map<String, Map<Integer, BigDecimal>> mapInMap;
to a Map that allows duplicated keys, for instance Multimap from Google Guava:
Map<String, Multimap<Integer, BigDecimal>> mapInMap = new HashMap<>();
where the inner maps are created like this:
Multimap<Integer, BigDecimal> x1 = ArrayListMultimap.create();
x1.put(1, BigDecimal.ONE);
mapInMap.put("a", x1);
Only now you can try to solve your problem using Java 8 Streams API. For instance:
Map<Integer, BigDecimal> map = multiMap.values()
.stream()
.flatMap(map -> map.entries().stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v2));
The duplicate keys conflicts are solved using mergeFunction parameter of the toMap method. We explicitly express to take the second value (v1, v2) -> v2 in case of duplicates.
Problem:
To address why your current solution doesn't work is because Map#putIfAbsent method only adds and doesn't replace a value in a map if is already present.
Solution using for-each:
Map#put is a way to go, however its limitation is that you cannot decide whether you want to keep always the first value for such key, calculate a new one or use always the last value. For such reason I recommend to use either a combination of Map#computeIfPresent and Map#putIfAbsent or better a method that does all that at once which is Map#merge(K, V, BiFunction) with a BiFunction remappingFunction:
remappingFunction - the function to recompute a value if present
Map<Integer, BigDecimal> resultMap = new HashMap<>();
for (Map<Integer, BigDecimal> map: mapInMap.values()) {
for (Map.Entry<Integer, BigDecimal> entry: map.entrySet()) {
resultMap.merge(entry.getKey(), entry.getValue(), (l, r) -> r);
}
}
Solution using Stream API:
To rewrite it in the Stream-alike solution, the approach would be identical. The only difference is the declarative syntax of Stream API, however, the idea is very same.
Just flatMap the structure and collect to a map with a Collector.toMap(Function, Function, BinaryOperator using BinaryOperator mergeFunction to merge duplicated keys.
mergeFunction - a merge function, used to resolve collisions between values associated with the same key, as supplied to Map.merge(Object, Object, BiFunction)
Map<Integer, BigDecimal> resultMap = mapInMap.values().stream()
.flatMap(entries -> entries.entrySet().stream())
.collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (l, r) -> r));
Note: #dreamcrash also deserves a credit for his good Stream API answer in terms of speed.
Result:
{1=1, 2=10} is the result when you pring out such map (note that BigDecimal is printed as a number). This output matches your expected output.
1=BigDecimal.ZERO
2=BigDecimal.TEN
Notice the similarities between Map#merge(K, V, BiFunction) and Collector.toMap(Function, Function, BinaryOperator that use a very similar approach to the same result.

How to create a reverse map when original map contains collection as the value?

Let's say my original Map contains the following:
Map<String, Set<String>> original = Maps.newHashMap();
original.put("Scott", Sets.newHashSet("Apple", "Pear", "Banana");
original.put("Jack", Sets.newHashSet("Banana", "Apple", "Orange");
And I want to create a reversed Map containing the following:
"Apple": ["Scott", "Jack"]
"Pear": ["Scott"]
"Banana": ["Scott", "Jack"]
"Orange": ["Jack"]
I know it can be done in old fashion (pre-Java 8), but how do I achieve the same using Java Stream API?
Map<String, Set<String>> reversed = original.entrySet().stream().map(x -> ????).collect(??)
There's similar question posted here, but that only works for single valued Maps.
You can break the Map into key-value pairs (where each key and value is a single String) by using flatMap, and then collect them as you wish:
Map<String,Set<String>> rev =
original.entrySet ()
.stream ()
.flatMap (e -> e.getValue ()
.stream ()
.map (v -> new SimpleEntry<String,String>(v,e.getKey ())))
.collect(Collectors.groupingBy (Map.Entry::getKey,
Collectors.mapping (Map.Entry::getValue,
Collectors.toSet())));
System.out.println (rev);
Output:
{Apple=[Jack, Scott], Pear=[Scott], Orange=[Jack], Banana=[Jack, Scott]}
A more imperative but simpler solution would be using forEach :
Map<String, Set<String>> original,result; // initialised
original.forEach((key, value) -> value.forEach(v ->
result.computeIfAbsent(v, k -> new HashSet<>()).add(key)));

Convert Map<String, Object> to Map<String, Set<Object>> with filter and streams

I would like to convert my map which looks like this:
{
key="someKey1", value=Apple(id="1", color="green"),
key="someKey2", value=Apple(id="2", color="red"),
key="someKey3", value=Apple(id="3", color="green"),
key="someKey4", value=Apple(id="4", color="red"),
}
to another map which puts all apples of the same color into the same list:
{
key="red", value=list={apple1, apple3},
key="green", value=list={apple2, apple4},
}
I tried the following:
Map<String, Set<Apple>> sortedApples = appleMap.entrySet()
.stream()
.collect(Collectors.toMap(l -> l.getColour, ???));
Am I on the right track? Should I use filters for this task? Is there an easier way?
Collectors.groupingBy is more suitable than Collectors.toMap for this task (though both can be used).
Map<String, List<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour));
Or, to group them into Sets use:
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour,
Collectors.mapping(Function.identity(),
Collectors.toSet())));
or (as Aomine commented):
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour, Collectors.toSet()));
if you want to proceed with toMap you can get the result as follows:
map.values() // get the apples
.stream() // Stream<Apple>
.collect(toMap(Apple::getColour, // group by colour
v -> new HashSet<>(singleton(v)), // have values as set of apples
(l, r) -> {l.addAll(r); return l;})); // merge colliding apples by colour
stream over the map values instead of entrySet because we're not concerned with the map keys.
Apple::getColour is the keyMapper function used to extract the "thing" we wish to group by, in this case, the Apples colour.
v -> new HashSet<>(singleton(v)) is the valueMapper function used for the resulting map values
(l, r) -> {l.addAll(r); return l;} is the merge function used to combine two HashSet's when there is a key collision on the Apple's colour.
finally, the resulting map is a Map<String, Set<Apple>>
but this is better with groupingBy and toSet as downstream:
map.values().stream().collect(groupingBy(Apple::getColour, toSet()));
stream over the map values instead of entrySet because we're not concerned with the map keys.
groups the Apple's by the provided classification function i.e. Apple::getColour and then collect the values in a Set hence the toSet downstream collector.
finally, the resulting map is a Map<String, Set<Apple>>
short, readable and the idiomatic approach.
You could also do it without a stream:
Map<String, Set<Apple>> res = new HashMap<>();
map.values().forEach(a -> res.computeIfAbsent(a.getColour(), e -> new HashSet<>()).add(a));
iterate over the map values instead of entrySet because we're not concerned with the map keys.
if the specified key a.getColour() is not already associated with a value, attempts to compute its value using the given mapping function e -> new HashSet<>() and enters it into the map. we then add the Apple to the resulting set.
if the specified key a.getColour() is already associated with a value computeIfAbsent returns the existing value associated with it and then we call add(a) on the HashSet to enter the Apple into the set.
finally, the resulting map is a Map<String, Set<Apple>>
You can use Collectors.groupingBy and Collectors.toSet()
Map<String, Set<Apple>> sortedApples = appleMap.values() // Collection<Apple>
.stream() // Stream<Apple>
.collect(Collectors.groupingBy(Apple::getColour, // groupBy colour
Collectors.mapping(a -> a, Collectors.toSet()))); // collect to Set
You've asked how to do it with streams, yet here's another way:
Map<String, Set<Apple>> result = new LinkedHashMap<>();
appleMap.values().forEach(apple ->
result.computeIfAbsent(apple.getColor(), k -> new LinkedHashSet<>()).add(apple));
This uses Map.computeIfAbsent, which either returns the set mapped to that color or puts an empty LinkedHashSet into the map if there's nothing mapped to that color yet, then adds the apple to the set.
EDIT: I'm using LinkedHashMap and LinkedHashSet to preserve insertion order, but could have used HashMap and HashSet, respectively.

Using streams to collect to Map

I have the following TreeMap:
TreeMap<Long,String> gasType = new TreeMap<>(); // Long, "Integer-Double"
gasType.put(1L, "7-1.50");
gasType.put(2L, "7-1.50");
gasType.put(3L, "7-3.00");
gasType.put(4L, "8-5.00");
gasType.put(5L, "8-7.00");
Map<Integer,TreeSet<Long>> capacities = new TreeMap<>);
The key is of the form 1L (a Long), and value of the form "7-1.50" (a String concatenation of an int and a double separated by a -).
I need to create a new TreeMap where the keys are obtained by taking the int part of the values of the original Map (for example, for the value "7-1.50", the new key will be 7). The value of the new Map would be a TreeSet containing all the keys of the original Map matching the new key.
So, for the input above, the value for the 7 key will be the Set {1L,2L,3L}.
I can do this without Streams, but I would like to do it with Streams. Any help is appreciated. Thank you.
Here's one way to do it:
Map<Integer,TreeSet<Long>> capacities =
gasType.entrySet()
.stream ()
.collect(Collectors.groupingBy (e -> Integer.parseInt(e.getValue().substring(0,e.getValue ().indexOf("-"))),
TreeMap::new,
Collectors.mapping (Map.Entry::getKey,
Collectors.toCollection(TreeSet::new))));
I modified the original code to support integers of multiple digits, since it appears you want that.
This produces the Map:
{7=[1, 2, 3], 8=[4, 5]}
If you don't care about the ordering of the resulting Map and Sets, you can let the JDK decide on the implementations, which would somewhat simplify the code:
Map<Integer,Set<Long>> capacities =
gasType.entrySet()
.stream ()
.collect(Collectors.groupingBy (e -> Integer.parseInt(e.getValue().substring(0,e.getValue ().indexOf("-"))),
Collectors.mapping (Map.Entry::getKey,
Collectors.toSet())));
You may try this out,
final Map<Integer, Set<Long>> map = gasType.entrySet().stream()
.collect(Collectors.groupingBy(entry -> Integer.parseInt(entry.getValue().substring(0, 1)),
Collectors.mapping(Map.Entry::getKey, Collectors.toSet())));
UPDATE
If you want to split the value based on "-" since there may be more that one digit, you can change it like so:
final Map<Integer, Set<Long>> map = gasType.entrySet().stream()
.collect(Collectors.groupingBy(entry -> Integer.parseInt(entry.getValue().split("-")[0]),
Collectors.mapping(Map.Entry::getKey, Collectors.toSet())));
Other solution would be like this
list = gasType.entrySet()
.stream()
.map(m -> new AbstractMap.SimpleImmutableEntry<Integer, Long>(Integer.valueOf(m.getValue().split("-")[0]), m.getKey()))
.collect(Collectors.toList());
and second step:
list.stream()
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue,Collectors.toCollection(TreeSet::new))));
or in one step:
gasType.entrySet()
.stream()
.map(m -> new AbstractMap.SimpleImmutableEntry<>(Integer.valueOf(m.getValue().split("-")[0]), m.getKey()))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toCollection(TreeSet::new))))

Java 8: change type of entries return Map

I got the follwoing question:
I have a Map<?,?> and I parse it from an PList file, simplified like:
Map<String, String> m = (Map<String, String>) getMap();
The getMap() method just reads the file (.plist).
I want to parse all the values to String, but unfortunately the Map contains Integers, causing an error later in the process.
So I wanted to write a method using the filter to convert everything to String:
My approach is:
m.entrySet().stream()
.map(e -> e.getValue())
.filter(e -> e instanceof Integer)
.map(e -> String.valueOf(e))
.collect(Collectors.toMap(e -> e.keys(), e -> e.getValue()));
The problem is, that the collect at the end is not working, how can I fix this?
The result should be a map again.
Thank you a lot!
You're misunderstanding how Collectors.toMap works - it takes two functions, one that given an entry produces a new key, and one that given an entry produce a new value. Each entry in the map then has both of these functions applied to it and the resulting key/value for that single element are used to construct the new entry in the new map.
Also, by mapping each entry to just the value, you lose the association between keys and values, which means you can't reconstruct the map correctly.
A corrected version would be:
Map<String, String> n;
n = m.entrySet()
.stream()
.filter(e -> e.getValue() instanceof Integer)
.collect(Collectors.toMap(e -> e.getKey(),
e -> String.valueOf(e.getValue())));
You have a few errors in your code.
First, as you are mapping each entry to its value, you are losing the keys.
Then, when you filter, you are keeping only Integer values in the stream, which will yield an incomplete map.
Finally, in Collectors.toMap you are using e.keys() and e.getValue, which are incorrect, because e.keys() is not a method of neither Map.Entry nor String, and because you'd need to use e.getValue() instead of e.getValue.
The code should be as follows:
m.entrySet().stream().collect(Collectors.toMap(
e -> e.getKey(),
e -> e.getValue() instanceof Integer ?
String.valueOf(e.getValue()) :
e.getValue()));
The replaceAll method is probably more suitable for this. It can be used like:
m.replaceAll((k, v) -> String.valueOf(v));
Map<String, String> m2 = m.entrySet().stream()
.collect(Collectors.toMap(e -> Objects.toString(e.getKey()),
e -> Objects.toString(e.getValue())));
This will turn both key and value into strings by toString. A null (value) will be turned into "null".

Categories

Resources