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()))));
Related
What kind of map "hm" is?
Map<String,Person> hm;
try (BufferedReader br = new BufferedReader(new FileReader("person.txt")) {
hm = br.lines().map(s -> s.split(","))
.collect(Collectors.toMap(a -> a[0] , a -> new Person(a[0],a[1],Integer.valueOf(a[2]),Integer.valueOf(a[3]))));
Does it depend on declaration?
Map<String,Person> hm = new HashMap<>();
Map<String,Person> hm = new TreeMap<>();
No, initializing the variable referenced by hm is pointless, since the stream pipeline creates a new Map instance, which you then assign to hm.
The actual returned Map implementation is an implementation detail. Currently it returns a HashMap by default, but you can request a specific Map implementation by using a different variant of toMap().
You can see one implementation here:
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);
}
You can see that it passes a method reference to a HashMap constructor, which means a HashMap instance will be created. If you call the 4 argument toMap variant, you can control the type of Map implementation to be returned.
Similarly, toList() returns an ArrayList and toSet a HashSet (at least in Java 8), but that can change in future versions, since it's not part of the contract.
I have a generic function which accepts Collection<? extends T> ts.
I'm also passing:
Function<? extends T, ? extends K> classifier which maps each item T to a key K (possible to have duplicates)
Function<? extends T, Integer> evaluator which gives an integer value for the item.
The function itself has a built-in calculation ("int to int") for every produced Integer (could be something like squaring for our example)
Finally, I'd like to sum all of the values for each key.
So the end result is: Map<K, Integer>.
For example,
Let's say we have the list ["a","a", "bb"] and we use Function.identity to classify, String::length to evaluate and squaring as the built-in function. Then the returned map will be: {"a": 2, "b": 4}
How can I do that? (I guess that preferably using Collectors.groupingBy)
Here's one way to do it:
public static <T,K> Map<K,Integer> mapper (
Collection<T> ts,
Function<T, K> classifier,
Function<T, Integer> evaluator,
Function<Integer,Integer> calculator)
{
return
ts.stream()
.collect(Collectors.groupingBy(classifier,
Collectors.summingInt(t->evaluator.andThen(calculator).apply(t))));
}
The output for:
System.out.println (mapper(Arrays.asList("a","a","bb"),Function.identity(),String::length,i->i*i));
is
{bb=4, a=2}
Or another approach:
private static <K, T> Map<K, Integer> map(Collection<? extends T> ts,
Function<? super T, ? extends K> classifier,
Function<? super T, Integer> evaluator,
Function<Integer, Integer> andThen) {
return ts.stream()
.collect(Collectors.groupingBy(
classifier,
Collectors.mapping(evaluator.andThen(andThen),
Collectors.reducing(0, Integer::sum))
));
}
And use it with:
public static void main(String[] args) {
System.out.println(map(
Arrays.asList("a", "a", "bb"),
Function.identity(),
String::length,
x -> x * x));
}
When a duplicate key entry is found during Collectors.toMap(), the merge function (o1, o2) is called.
Question: how can I get the key that caused the duplication?
String keyvalp = "test=one\ntest2=two\ntest2=three";
Pattern.compile("\n")
.splitAsStream(keyval)
.map(entry -> entry.split("="))
.collect(Collectors.toMap(
split -> split[0],
split -> split[1],
(o1, o2) -> {
//TODO how to access the key that caused the duplicate? o1 and o2 are the values only
//split[0]; //which is the key, cannot be accessed here
},
HashMap::new));
Inside the merge function I want to decide based on the key which if I cancel the mapping, or continue and take on of those values.
You need to use a custom collector or use a different approach.
Map<String, String> map = new Hashmap<>();
Pattern.compile("\n")
.splitAsStream(keyval)
.map(entry -> entry.split("="))
.forEach(arr -> map.merge(arr[0], arr[1], (o1, o2) -> /* use arr[0]));
Writing a custom collector is rather more complicated. You need a TriConsumer (key and two values) is similar which is not in the JDK which is why I am pretty sure there is no built in function which uses. ;)
The merge function has no chance to get the key, which is the same issue, the builtin function has, when you omit the merge function.
The solution is to use a different toMap implementation, which does not rely on Map.merge:
public static <T, K, V> Collector<T, ?, Map<K,V>>
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper) {
return Collector.of(HashMap::new,
(m, t) -> {
K k = keyMapper.apply(t);
V v = Objects.requireNonNull(valueMapper.apply(t));
if(m.putIfAbsent(k, v) != null) throw duplicateKey(k, m.get(k), v);
},
(m1, m2) -> {
m2.forEach((k,v) -> {
if(m1.putIfAbsent(k, v)!=null) throw duplicateKey(k, m1.get(k), v);
});
return m1;
});
}
private static IllegalStateException duplicateKey(Object k, Object v1, Object v2) {
return new IllegalStateException("Duplicate key "+k+" (values "+v1+" and "+v2+')');
}
(This is basically what Java 9’s implementation of toMap without a merge function will do)
So all you need to do in your code, is to redirect the toMap call and omit the merge function:
String keyvalp = "test=one\ntest2=two\ntest2=three";
Map<String, String> map = Pattern.compile("\n")
.splitAsStream(keyvalp)
.map(entry -> entry.split("="))
.collect(toMap(split -> split[0], split -> split[1]));
(or ContainingClass.toMap if its neither in the same class nor static imports)<\sup>
The collector supports parallel processing like the original toMap collector, though it’s not very likely to get a benefit from parallel processing here, even with more elements to process.
If, if I get you correctly, you only want to pick either, the older or newer value, in the merge function based on the actual key, you could do it with a key Predicate like this
public static <T, K, V> Collector<T, ?, Map<K,V>>
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper,
Predicate<? super K> useOlder) {
return Collector.of(HashMap::new,
(m, t) -> {
K k = keyMapper.apply(t);
m.merge(k, valueMapper.apply(t), (a,b) -> useOlder.test(k)? a: b);
},
(m1, m2) -> {
m2.forEach((k,v) -> m1.merge(k, v, (a,b) -> useOlder.test(k)? a: b));
return m1;
});
}
Map<String, String> map = Pattern.compile("\n")
.splitAsStream(keyvalp)
.map(entry -> entry.split("="))
.collect(toMap(split -> split[0], split -> split[1], key -> condition));
There are several ways to customize this collector…
There is, of course, simple and trivial trick - saving the key in the 'key mapper' function and getting the key in the 'merge' function. So, the code may look like the following (assuming the key is Integer):
final AtomicInteger key = new AtomicInteger();
...collect( Collectors.toMap(
item -> { key.set(item.getKey()); return item.getKey(); }, // key mapper
item -> ..., // value mapper
(v1, v2) -> { log(key.get(), v1, v2); return v1; } // merge function
);
Note: this is not good for parallel processing.
I have this class.
class Assignment {
private Integer index;
private List<Quantity> quantities;
}
Then, I have a list of objects from that class.
List<Assigment> assignments = new ArrayList<>();
Is there a way to create a Map that contains the index from Assignment and the List<Quantity> as values?
This is what I have tried so far.
assignments.stream().collect(groupingBy(Assignment::getIndex));
But this gives me a Map<Integer, List<Assignment>> and I want a Map<Integer, List<Quantity>>.
I have tried using forEach method - and it workes - but I'm sure there must be a way to do it in one liner - or at least using only collect and groupingBy methods
It looks like there is no flat-mapping collector that you can use as a down-stream for groupingBy in Java8, but it has been proposed and accepted for Java9: https://bugs.openjdk.java.net/browse/JDK-8071600
public static <T, U, A, R>
Collector<T, ?, R> flatMapping(Function<? super T, ? extends Stream<? extends U>> mapper,
Collector<? super U, A, R> downstream) {
BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
return Collector.of(downstream.supplier(),
(r, t) -> mapper.apply(t).sequential().forEach(u -> downstreamAccumulator.accept(r, u)),
downstream.combiner(),
downstream.finisher(),
downstream.characteristics().stream().toArray(Collector.Characteristics[]::new));
}
If you use that one, and also add a quantities method to Assignment that returns a Stream<Quantity>, you can use this code:
Map<Integer, List<Quantity>> result = assignments.stream()
.collect(groupingBy(Assignment::getIndex,
flatMapping(Assignment::quantities, toList())));
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())));
}