I need to map a list of pairs of objects into <ocurrences, list of Objs with those ocurrences>, I've tried using streams directly on the input list of pairs but I'm still kind of new to java and couldn't figure it out, so I was trying to do something like this, but it's probably not close to the best way to do it.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
Map<Integer,ArrayList<Obj>> nBorders = new HashMap<>();
List<Obj> list = new ArrayList<>();
for(Pair<Obj, Obj> pair : lf) {
list.add(pair.getKey());
list.add(pair.getValue());
}
nBorders = list.stream().collect(Collectors.groupingBy(...);
return nBorders;
}
so for example, for lf = {(o1,o2),(o3,o2),(o5,o4),(o4,o1),(o3,o4),(o7,o1),(o5,o8),(o3,o10),(o4,o5),(o3,o7),(o9,o8)} the result should be {(1,{o9,o10}),(2,{o2,o7,o8,}),(3,{o1,o5}),(4,{o3,o4})}.
I'm really confused on how to do this, if someone could help, I'd appreciate it, thanks.
This can be done this way:
create a stream from the pairs to concatenate first/second values using Stream::flatMap
count the occurrences - build an intermediate map <Obj, Integer> using Collectors.groupingBy + Collectors.summingInt (to keep integer)
create an inverse map <Integer, List> from the stream of the entries in the intermediate map using Collectors.groupingBy + Collectors.mapping
Optionally, if an order in the resulting map is critical, a LinkedHashMap may be created from the entries of the intermediate frequency map sorted by value.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
return lf.stream() // Stream<Pair>
.flatMap(p -> Stream.of(p.getKey(), p.getValue())) // Stream<Obj>
.collect(Collectors.groupingBy(
obj -> obj,
Collectors.summingInt(obj -> 1)
)) // Map<Obj, Integer>
.entrySet()
.stream() // Stream<Map.Entry<Obj, Integer>>
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.groupingBy(
Map.Entry::getValue, // frequency is key
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
)); // Map<Integer, List<Obj>>
}
Related
I have a list of pairs as follows:
// pairs = [(k1, obj1), (k2, obj1)]
List<Pair<String, Object> pairs;
Then I want to expand those objects to many objects and reduce them by key
pairs.stream().map(pair -> {
// every object is expanded into many objects, obj11, obj12, ...
List<Object> objects = expand(pair.getRight());
// the return is [(k1, obj11), (k1, obj12), (k2, obj21), (k2, obj22)]
return objects.stream().map(object -> new MutablePair<String, Object>(pair.getLeft(), object)).collect(Collectors.toList());
}).reduce(...
// how to reduce it by key and get a list of pairs
...);
My question is, how to reduce the expanded objects by key and get a list of pairs?
I mean the expected result is:
pairs = [(k1, obj11), (k1, obj12), (k2, obj21), (k2, obj22)]
Looks like you need flatMap instead of map, and collect instead of reduce:
List<Pair<String, Object>> expandedPairs =
pairs.stream()
.flatMap(pair -> expand(pair.getRight()).stream()
.map(object -> new MutablePair<String, Object>(pair.getLeft(), object)))
.collect(Collectors.toList());
I am trying to rewrite the method below using streams but I am not sure what the best approach is? If I use flatMap on the values of the entrySet(), I lose the reference to the current key.
private List<String> asList(final Map<String, List<String>> map) {
final List<String> result = new ArrayList<>();
for (final Entry<String, List<String>> entry : map.entrySet()) {
final List<String> values = entry.getValue();
values.forEach(value -> result.add(String.format("%s-%s", entry.getKey(), value)));
}
return result;
}
The best I managed to do is the following:
return map.keySet().stream()
.flatMap(key -> map.get(key).stream()
.map(value -> new AbstractMap.SimpleEntry<>(key, value)))
.map(e -> String.format("%s-%s", e.getKey(), e.getValue()))
.collect(Collectors.toList());
Is there a simpler way without resorting to creating new Entry objects?
A stream is a sequence of values (possibly unordered / parallel). map() is what you use when you want to map a single value in the sequence to some single other value. Say, map "alturkovic" to "ALTURKOVIC". flatMap() is what you use when you want to map a single value in the sequence to 0, 1, or many other values. Hence why a flatMap lambda needs to turn a value into a stream of values. flatMap can thus be used to take, say, a list of lists of string, and turn that into a stream of just strings.
Here, you want to map a single entry from your map (a single key/value pair) into a single element (a string describing it). 1 value to 1 value. That means flatMap is not appropriate. You're looking for just map.
Furthermore, you need both key and value to perform your mapping op, so, keySet() is also not appropriate. You're looking for entrySet(), which gives you a set of all k/v pairs, juts what we need.
That gets us to:
map.entrySet().stream()
.map(e -> String.format("%s-%s", e.getKey(), e.getValue()))
.collect(Collectors.toList());
Your original code makes no effort to treat a single value from a map (which is a List<String>) as separate values; you just call .toString() on the entire ordeal, and be done with it. This means the produced string looks like, say, [Hello, World] given a map value of List.of("Hello", "World"). If you don't want this, you still don't want flatmap, because streams are also homogenous - the values in a stream are all of the same kind, and thus a stream of 'key1 value1 value2 key2 valueA valueB' is not what you'd want:
map.entrySet().stream()
.map(e -> String.format("%s-%s", e.getKey(), myPrint(e.getValue())))
.collect(Collectors.toList());
public static String myPrint(List<String> in) {
// write your own algorithm here
}
Stream API just isn't the right tool to replace that myPrint method.
A third alternative is that you want to smear out the map; you want each string in a mapvalue's List<String> to first be matched with the key (so that's re-stating that key rather a lot), and then do something to that. NOW flatMap IS appropriate - you want a stream of k/v pairs first, and then do something to that, and each element is now of the same kind. You want to turn the map:
key1 = [value1, value2]
key2 = [value3, value4]
first into a stream:
key1:value1
key1:value2
key2:value3
key2:value4
and take it from there. This explodes a single k/v entry in your map into more than one, thus, flatmapping needed:
return map.entrySet().stream()
.flatMap(e -> e.getValue().stream()
.map(v -> String.format("%s-%s", e.getKey(), v))
.collect(Collectors.toList());
Going inside-out, it maps a single entry within a list that belongs to a single k/v pair into the string Key-SingleItemFromItsList.
Adding my two cents to excellent answer by #rzwitserloot. Already flatmap and map is explained in his answer.
List<String> resultLists = myMap.entrySet().stream()
.flatMap(mapEntry -> printEntries(mapEntry.getKey(),mapEntry.getValue())).collect(Collectors.toList());
System.out.println(resultLists);
Splitting this to a separate method gives good readability IMO,
private static Stream<String> printEntries(String key, List<String> values) {
return values.stream().map(val -> String.format("%s-%s",key,val));
}
I have a large list of items that I need to convert into a map of items of same type:
List<Item> items = //10^6 items of different types
Map<Type, List<Item>> itemsByType = new ConcurrentHashMap<>();
for (Item item : items) {
itemsByType.computeIfAbsent(
item.getType(),
i -> new ArrayList<>()
).add(item);
}
Each type is then ordered by long type identifier; and, each list of type-items is ordered by long item identifier. And, finally, the ordered list is processed.
This works fine, but I'm wondering if there's a more efficient way to do all of this...?
You can use java-8 groupingBy
Map<Type, List<Item>> itemsByType = items.stream()
.sorted(Comparator) //for any sorting you can use sorted with comparator
.collect(Collectors.groupingBy(Item::getType));
If you want ConcurrentHashMap you can use groupingByConcurrent
ConcurrentMap<Type, List<Item>> itemsByType = items.stream()
.collect(Collectors.groupingByConcurrent(Item::getType));
You can use the overloaded groupingBy with TreeMap so the map is already sorted based on key
TreeMap<Type, List<Item>> map = list
.stream()
.collect(Collectors.groupingBy(
Item::Type,
() -> new TreeMap<>(Comparator.comparingLong(Type::getId)),
Collectors.toList()));
You can also collect the map with sorted keys and sorted values in one chain
Map<Type, List<Item>> str = list1.stream()
.collect(
Collectors.groupingBy(
Item::Type,
() -> new TreeMap<>(Comparator.comparingLong(Type::getId)),
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingLong(Item::getId))
.collect(Collectors.toList()))));
You could use a MultiMap, e.g., guava's. Here is their code example:
ListMultimap<String, String> multimap = ArrayListMultimap.create();
for (President pres : US_PRESIDENTS_IN_ORDER) {
multimap.put(pres.firstName(), pres.lastName());
}
for (String firstName : multimap.keySet()) {
List<String> lastNames = multimap.get(firstName);
out.println(firstName + ": " + lastNames);
}
... produces output such as:
Zachary: [Taylor]
John: [Adams, Adams, Tyler, Kennedy] // Remember, Quincy!
George: [Washington, Bush, Bush]
Grover: [Cleveland, Cleveland] // Two, non-consecutive terms, rep'ing NJ!
...
A TreeMultimap has sorted keys and values, which is what you want, if I understood your title correctly.
A Multimap is particularly useful in case you need to check if a certain value is present for a certain key, because that is supported without getting the collection for the key and then searching that:
multimap.containsEntry("John", "Adams");
I have a nested collection as such
Map<Integer, Map<Integer, List<Integer>>> nodes = new TreeMap<>()
I need to convert the inner map into a List<List<Integer>>. The order of the inner list has to be preserved. Essentially for each entry in the outer map, iterate through the inner map, add the List as is to the List of Lists.
I can do it the old fashioned way.
List<List<Integer>> result = new ArrayList<>();
for(Map.Entry<Integer, TreeMap<Integer, List<Integer>>> entry : nodes.entrySet()) {
Map<Integer, List<Integer>> outer = entry.getValue();
ArrayList<Integer> tmp = new ArrayList<>();
for (Map.Entry<Integer, List<Integer>> inner : outer.entrySet()) {
tmp.addAll(inner.getValue());
}
result.add(tmp);
}
How do to this with lambdas? This doesn't work
nodes.entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()).map(e2 -> result.add(e2.getValue()))
How do to this with lambdas? This doesn't work
Here you never invoke a termination operation, so the stream is never consumed. :
nodes.entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()).map(e2 -> result.add(e2.getValue()))
Add any terminal operation such as count() and you could see the stream operated.
Don't forget that Streams are lazy and so the computation is effectively performed only when the terminal operation is invoked.
So you guess that your way is not the right way to do things with Stream.
You don't need to use the List as a variable that you will populate in the stream. Streams are designed to collect as they produce a result and the collect to a List is finally the terminal operation that missed in your initial code.
Besides as a side note you should just stream the values of each Map level instead of the entries since you never use the keys.
Here the code with for each step the actual return type :
List<List<Integer>> result =
nodes.values() // Collection<Map<Integer, List<Integer>>>
.stream() // Stream<Map<Integer, List<Integer>>>
.flatMap(m -> m.values() // Collection<List<Integer>>>
.stream()) // Stream<List<Integer>>>
// flatMap() prevents Stream<Stream<...>>.
// Indeed we get just Stream<List...>>
.collect(Collectors.toList());
This one should do the trick:
Map<Integer, Map<Integer, List<Integer>>> nodes = new TreeMap<>();
List<List<Integer>> list = nodes.values()
.stream()
.flatMap( map -> map.values().stream() )
.collect( Collectors.toList() );
Explanation:
First you get stream of maps from map by using:
nodes.values().stream()
then you flatten those maps with:
.flatMap( map -> map.values().stream() )
And finally collect them with:
.collect( Collectors.toList() )
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.