For the sake of this example, let's assume I have a simple type Tuple with two attributes:
interface Tuple<T, U> {
T getFirst();
U getSecond();
}
Now I want to transform a collection of (first, second) tuples into a map which maps each first value to a set of all second values contained in tuples with that specific first value. The method groupSecondByFirst() shows a possible implementation doing what I want:
<T, U> Map<T, Set<U>> groupSecondByFirst(Set<Tuple<T, U>> tuples) {
Map<T, Set<U>> result = new HashMap<>();
for (Tuple<T, U> i : tuples) {
result.computeIfAbsent(i.getFirst(), x -> new HashSet<>()).add(i.getSecond());
}
return result;
}
If the input was [(1, "one"), (1, "eins"), (1, "uno"), (2, "two"), (3, "three")] the output would be { 1 = ["one", "eins", "uno"], 2 = ["two"], 3 = ["three"] }
I would like to know whether and how I can implement this using the streams framework. The best I got is the following expression, which returns a map which contains the full tuple as values and not just their second elements:
Map<T, Set<Tuple<T, U>>> collect = tuples.stream().collect(
Collectors.groupingBy(Tuple::getFirst, Collectors.toSet()));
I found a solution; It involves Collections.mapping(), which can wrap a collector and apply mapping function over stream to supply elements to the wrapped collector:
static <T, U> Map<T, Set<U>> groupSecondByFirst(Collection<Tuple<T, U>> tuples) {
return tuples
.stream()
.collect(
Collectors.groupingBy(
Tuple::getFirst,
Collectors.mapping(
Tuple::getSecond,
Collectors.toSet())));
}
Related
I have this code:
SortedMap<String, Double> starsPerActivity = new TreeMap<>();
for(Product p : products.values()) {
for(Rating r : ratings) {
if(r.getProductName() == p.getName()) {
starsPerActivity.put(p.getActivityName(), this.getStarsOfProduct(p.getName()));
}
}
}
return starsPerActivity;
And I want to rewrite this piece of code with streams.
I tried, but I don't know how.
The method starsPerActivity() returns a map that associates a name of the activity to the average number of stars for the products belonging to that activity, with the activity names sorted alphabetically. Activities whose products have not been rated should not appear in the result.
You can use the third Collectors.toMap overload:
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapFactory)
Which allows you to define what map implementation you'd like to use:
SortedMap<String, Double> starsPerActivity = products.values().stream()
.filter(p -> ratings.stream()
.anyMatch(r -> r.getProductName().equals(p.getName())))
.collect(Collectors.toMap(
Product::getActivityName,
p -> getStarsOfProduct(p.getName()),
Double::max, TreeMap::new
));
Also I noticed that you used r.getProductName() == p.getName() in your if statement, which is probably applied to Strings. This is discouraged, see: How do I compare strings in Java
Note that this implementation picks the rating with the highest value. You can change this behaviour by replacing Double::max with the logic you like. If you want the same behaviour you currently have then you could use this, which is close enough: (a, b) -> b which simply picks the latest put value.
SortedMap<String, Double> starsPerActivity = new TreeMap<>();
products.values().forEach(p -> ratings.stream().filter(r -> r.getProductName().equals(p.getName()))
.forEachOrdered(r -> starsPerActivity.put(p.getActivityName(), this.getStarsOfProduct(p.getName()))));
How can I flatten a Stream of Maps (of the same types) to a single Map in Java 8?
Map<String, Long> toMap(Stream<Map<String, Long>> stream) {
return stream. ???
}
My syntax may be a bit off, but flatMap should do most of the work for you :
Map<String, Long> toMap(Stream<Map<String, Long>> stream) {
return stream.flatMap (map -> map.entrySet().stream()) // this would create a flattened
// Stream of all the map entries
.collect(Collectors.toMap(e -> e.getKey(),
e -> e.getValue())); // this should collect
// them to a single map
}
I would like to propose a solution using reduce(), which is more intuitive for me. I would use it inline though.
Map<String, Long> toMap(Stream<Map<String, Long>> stream) {
return stream.reduce(new HashMap<>(), Util::reduceInto);
}
And in Util.java:
public static <R, T> Map<R, T> reduceInto(Map<R, T> into, Map<R, T> valuesToAdd) {
reduceInto.putAll(valuesToAdd);
return reduceInto;
}
In this case reduceInto() works for any type of map and uses mutability to avoid creating a new Map for each item of the Stream.
Important: although this method allows repeated keys in the stream, reduceInto() is not associative, meaning that if you have repeated keys there is no guarantee of which will be the final value.
I have a method which returns company as key and list of employeer as values
<T> Map<String, List<T>> getUserPerCompany(final Function<User, T> converter).
The method accepts the converter parameter which in the tests returns the String (name + lastname of the employee). It should returns: Map<String, List<String>>. I created this implementation:
return getUserStream().collect(toMap(Company::getName, c -> converter.apply(c.getUsers())));
Error is:
apply (domain.User) in Function cannot be applied to (java.util.List<domain.User>)
My problem is that I do not know how to pass the employee to the 'apply' list instead of the list in full.
My other attempts:
return getUserStream().collect(toMap(Company::getName, c -> converter.apply((User) c.getUsers().listIterator())));
return getUserStream().collect(toMap(Company::getName, c -> converter.apply((User) c.getUsers().subList(0, c.getUsers().size()))));
return getUserStream().collect(toMap(Company::getName, c -> converter.apply((User) c.getUsers().iterator())));
I suppose this is what you're looking for
<T> Map<String, List<T>> getUserPerCompany(final Function<User, T> converter) {
return getUserStream().collect(Collectors.toMap(
c -> c.getName(),
c -> c.getUsers()
.stream()
.map(converter)
.collect(Collectors.toList())
));
}
Usage example is
final Map<String, List<String>> users = getUserPerCompany(user -> user.getName() + " " + user.getSurname());
Basically you need to map each User, applying the input Function.
You can use Collectors.groupingBy() and write custom Collector:
<T> Map<String, List<T>> getUserPerCompany(final Function<User, T> converter) {
return getUserStream().collect(
Collectors.groupingBy(
c -> c.getName(),
Collector.of(
ArrayList::new, //init accumulator
(list, c)-> c.getUsers() //processing each element
.stream()
.map(converter)
.forEach(list::add),
(result1, result2) -> { //confluence 2 accumulators
result1.addAll(result2); //in parallel execution
return result1;
}
)
)
);
}
I am creating a Map from a List as follows:
List<String> strings = Arrays.asList("a", "bb", "ccc");
Map<String, Integer> map = strings.stream()
.collect(Collectors.toMap(Function.identity(), String::length));
I want to keep the same iteration order as was in the List. How can I create a LinkedHashMap using the Collectors.toMap() methods?
The 2-parameter version of Collectors.toMap() uses a HashMap:
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
{
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
To use the 4-parameter version, you can replace:
Collectors.toMap(Function.identity(), String::length)
with:
Collectors.toMap(
Function.identity(),
String::length,
(u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
},
LinkedHashMap::new
)
Or to make it a bit cleaner, write a new toLinkedMap() method and use that:
public class MoreCollectors
{
public static <T, K, U> Collector<T, ?, Map<K,U>> toLinkedMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
{
return Collectors.toMap(
keyMapper,
valueMapper,
(u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
},
LinkedHashMap::new
);
}
}
Make your own Supplier, Accumulator and Combiner:
List<String> myList = Arrays.asList("a", "bb", "ccc");
// or since java 9 List.of("a", "bb", "ccc");
LinkedHashMap<String, Integer> mapInOrder = myList
.stream()
.collect(
LinkedHashMap::new, // Supplier LinkedHashMap to keep the order
(map, item) -> map.put(item, item.length()), // Accumulator
Map::putAll); // Combiner
System.out.println(mapInOrder); // prints {a=1, bb=2, ccc=3}
The right solution for this problem is
Current ----> 2 parameter version
Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName));
Right ----> Use 4-parameter version of the Collectors.toMap to tell supplier to supply a new LinkedHashMap:
Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName, (u, v) -> u, LinkedHashMap::new));
This will help.
In Kotlin, toMap() is order-preserving.
fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V>
Returns a new map containing all key-value pairs from the given collection of pairs.
The returned map preserves the entry iteration order of the original collection. If any of two pairs would have the same key the last one gets added to the map.
Here's its implementation:
public fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V> {
if (this is Collection) {
return when (size) {
0 -> emptyMap()
1 -> mapOf(if (this is List) this[0] else iterator().next())
else -> toMap(LinkedHashMap<K, V>(mapCapacity(size)))
}
}
return toMap(LinkedHashMap<K, V>()).optimizeReadOnlyMap()
}
The usage is simply:
val strings = listOf("a", "bb", "ccc")
val map = strings.map { it to it.length }.toMap()
The underlying collection for map is a LinkedHashMap (which is insertion-ordered).
Simple function to map array of objects by some field:
public static <T, E> Map<E, T> toLinkedHashMap(List<T> list, Function<T, E> someFunction) {
return list.stream()
.collect(Collectors.toMap(
someFunction,
myObject -> myObject,
(key1, key2) -> key1,
LinkedHashMap::new)
);
}
Map<String, MyObject> myObjectsByIdMap1 = toLinkedHashMap(
listOfMyObjects,
MyObject::getSomeStringField()
);
Map<Integer, MyObject> myObjectsByIdMap2 = toLinkedHashMap(
listOfMyObjects,
MyObject::getSomeIntegerField()
);
Since Java 9 you can collect a list of map entries with the same order as in the original list:
List<String> strings = Arrays.asList("a", "bb", "ccc");
List<Map.Entry<String, Integer>> entries = strings.stream()
.map(e -> Map.entry(e, e.length()))
.collect(Collectors.toList());
System.out.println(entries); // [a=1, bb=2, ccc=3]
Or you can collect a list of maps with a single entry in the same way:
List<String> strings = Arrays.asList("a", "bb", "ccc");
List<Map<String, Integer>> maps = strings.stream()
.map(e -> Map.of(e, e.length()))
.collect(Collectors.toList());
System.out.println(maps); // [{a=1}, {bb=2}, {ccc=3}]
I am creating a Map from a List as follows:
List<String> strings = Arrays.asList("a", "bb", "ccc");
Map<String, Integer> map = strings.stream()
.collect(Collectors.toMap(Function.identity(), String::length));
I want to keep the same iteration order as was in the List. How can I create a LinkedHashMap using the Collectors.toMap() methods?
The 2-parameter version of Collectors.toMap() uses a HashMap:
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
{
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
To use the 4-parameter version, you can replace:
Collectors.toMap(Function.identity(), String::length)
with:
Collectors.toMap(
Function.identity(),
String::length,
(u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
},
LinkedHashMap::new
)
Or to make it a bit cleaner, write a new toLinkedMap() method and use that:
public class MoreCollectors
{
public static <T, K, U> Collector<T, ?, Map<K,U>> toLinkedMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
{
return Collectors.toMap(
keyMapper,
valueMapper,
(u, v) -> {
throw new IllegalStateException(String.format("Duplicate key %s", u));
},
LinkedHashMap::new
);
}
}
Make your own Supplier, Accumulator and Combiner:
List<String> myList = Arrays.asList("a", "bb", "ccc");
// or since java 9 List.of("a", "bb", "ccc");
LinkedHashMap<String, Integer> mapInOrder = myList
.stream()
.collect(
LinkedHashMap::new, // Supplier LinkedHashMap to keep the order
(map, item) -> map.put(item, item.length()), // Accumulator
Map::putAll); // Combiner
System.out.println(mapInOrder); // prints {a=1, bb=2, ccc=3}
The right solution for this problem is
Current ----> 2 parameter version
Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName));
Right ----> Use 4-parameter version of the Collectors.toMap to tell supplier to supply a new LinkedHashMap:
Map<Integer, String> mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName, (u, v) -> u, LinkedHashMap::new));
This will help.
In Kotlin, toMap() is order-preserving.
fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V>
Returns a new map containing all key-value pairs from the given collection of pairs.
The returned map preserves the entry iteration order of the original collection. If any of two pairs would have the same key the last one gets added to the map.
Here's its implementation:
public fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V> {
if (this is Collection) {
return when (size) {
0 -> emptyMap()
1 -> mapOf(if (this is List) this[0] else iterator().next())
else -> toMap(LinkedHashMap<K, V>(mapCapacity(size)))
}
}
return toMap(LinkedHashMap<K, V>()).optimizeReadOnlyMap()
}
The usage is simply:
val strings = listOf("a", "bb", "ccc")
val map = strings.map { it to it.length }.toMap()
The underlying collection for map is a LinkedHashMap (which is insertion-ordered).
Simple function to map array of objects by some field:
public static <T, E> Map<E, T> toLinkedHashMap(List<T> list, Function<T, E> someFunction) {
return list.stream()
.collect(Collectors.toMap(
someFunction,
myObject -> myObject,
(key1, key2) -> key1,
LinkedHashMap::new)
);
}
Map<String, MyObject> myObjectsByIdMap1 = toLinkedHashMap(
listOfMyObjects,
MyObject::getSomeStringField()
);
Map<Integer, MyObject> myObjectsByIdMap2 = toLinkedHashMap(
listOfMyObjects,
MyObject::getSomeIntegerField()
);
Since Java 9 you can collect a list of map entries with the same order as in the original list:
List<String> strings = Arrays.asList("a", "bb", "ccc");
List<Map.Entry<String, Integer>> entries = strings.stream()
.map(e -> Map.entry(e, e.length()))
.collect(Collectors.toList());
System.out.println(entries); // [a=1, bb=2, ccc=3]
Or you can collect a list of maps with a single entry in the same way:
List<String> strings = Arrays.asList("a", "bb", "ccc");
List<Map<String, Integer>> maps = strings.stream()
.map(e -> Map.of(e, e.length()))
.collect(Collectors.toList());
System.out.println(maps); // [{a=1}, {bb=2}, {ccc=3}]