java 8 - stream, map and count distinct - java

My first attempt with java 8 streams...
I have an object Bid, which represents a bid of a user for an item in an auction. i have a list of bids, and i want to make a map that counts in how many (distinct) auctions the user made a bid.
this is my take on it:
bids.stream()
.collect(
Collectors.groupingBy(
bid -> Bid::getBidderUserId,
mapping(Bid::getAuctionId, Collectors.toSet())
)
).entrySet().stream().collect(Collectors.toMap(
e-> e.getKey(),e -> e.getValue().size())
);
It works, but i feel like i'm cheating, cause i stream the entry sets of the map, instead of doing a manipulation on the initial stream... must be a more correct way of doing this, but i couldn't figure it out...
Thanks

You can perform groupingBy twice:
Map<Integer, Map<Integer, Long>> map = bids.stream().collect(
groupingBy(Bid::getBidderUserId,
groupingBy(Bid::getAuctionId, counting())));
This way you have how many bids each user has in each auction. So the size of internal map is the number of auctions the user participated. If you don't need the additional information, you can do this:
Map<Integer, Integer> map = bids.stream().collect(
groupingBy(
Bid::getBidderUserId,
collectingAndThen(
groupingBy(Bid::getAuctionId, counting()),
Map::size)));
This is exactly what you need: mapping of users to number of auctions user participated.
Update: there's also similar solution which is closer to your example:
Map<Integer, Integer> map = bids.stream().collect(
groupingBy(
Bid::getBidderUserId,
collectingAndThen(
mapping(Bid::getAuctionId, toSet()),
Set::size)));

Tagir Valeev's answer is the right one (+1). Here is an additional one that does exactly the same using your own downstream Collector for the groupBy:
Map<Integer, Long> map = bids.stream().collect(
Collectors.groupingBy(Bid::getBidderUserId,
new Collector<Bid, Set<Integer>, Long>() {
#Override
public Supplier<Set<Integer>> supplier() {
return HashSet::new;
}
#Override
public BiConsumer<Set<Integer>, Bid> accumulator() {
return (s, b) -> s.add(b.getAuctionId());
}
#Override
public BinaryOperator<Set<Integer>> combiner() {
return (s1, s2) -> {
s1.addAll(s2);
return s1;
};
}
#Override
public Function<Set<Integer>, Long> finisher() {
return (s) -> Long.valueOf(s.size());
}
#Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH));
}
}));

Related

Java streams average

I need to create two methods using streams. A method that returns an average score of each task.
public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {}
and a method that returns a task with the highest average score.
public String easiestTask(Stream<CourseResult> results) {}
I can only modify those 2 methods.
Here is CourseResult class
public class CourseResult {
private final Person person;
private final Map<String, Integer> taskResults;
public CourseResult(final Person person, final Map<String, Integer> taskResults) {
this.person = person;
this.taskResults = taskResults;
}
public Person getPerson() {
return person;
}
public Map<String, Integer> getTaskResults() {
return taskResults;
}
}
And methods that create CourseResult objects.
private final String[] programTasks = {"Lab 1. Figures", "Lab 2. War and Peace", "Lab 3. File Tree"};
private final String[] practicalHistoryTasks = {"Shieldwalling", "Phalanxing", "Wedging", "Tercioing"};
private Stream<CourseResult> programmingResults(final Random random) {
int n = random.nextInt(names.length);
int l = random.nextInt(lastNames.length);
return IntStream.iterate(0, i -> i + 1)
.limit(3)
.mapToObj(i -> new Person(
names[(n + i) % names.length],
lastNames[(l + i) % lastNames.length],
18 + random.nextInt(20)))
.map(p -> new CourseResult(p, Arrays.stream(programTasks).collect(toMap(
task -> task,
task -> random.nextInt(51) + 50))));
}
private Stream<CourseResult> historyResults(final Random random) {
int n = random.nextInt(names.length);
int l = random.nextInt(lastNames.length);
AtomicInteger t = new AtomicInteger(practicalHistoryTasks.length);
return IntStream.iterate(0, i -> i + 1)
.limit(3)
.mapToObj(i -> new Person(
names[(n + i) % names.length],
lastNames[(l + i) % lastNames.length],
18 + random.nextInt(20)))
.map(p -> new CourseResult(p,
IntStream.iterate(t.getAndIncrement(), i -> t.getAndIncrement())
.map(i -> i % practicalHistoryTasks.length)
.mapToObj(i -> practicalHistoryTasks[i])
.limit(3)
.collect(toMap(
task -> task,
task -> random.nextInt(51) + 50))));
}
Based on these methods I can calculate an average of each task by dividing sum of scores of this task by 3, because there are only 3 Persons tho I can make it so it divides by a number equal to number of CourseResult objects in a stream if these methods get their .limit(3) changed.
I don't know how to access keys of taskResults Map. I think I need them to then return a map of unique keys. A value for each unique key should be an average of values from taskResults map assigend to those keys.
For your first question: map each CourseResult to taskResults, flatmap to get all entries of each taskResults map form all CourseResults, group by map keys (task names) and collect averaging the values for same keys:
public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {
return results.map(CourseResult::getTaskResults)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.averagingInt(Map.Entry::getValue)));
}
You can use the same approach for your second question to calculate the average for each task and finaly stream over the entries of the resulting map to find the task with the highest average.
public String easiestTask(Stream<CourseResult> results) {
return results.map(CourseResult::getTaskResults)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.averagingInt(Map.Entry::getValue)))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("No easy task found");
}
To avoid code duplication you can call the first method within the second:
public String easiestTask(Stream<CourseResult> results) {
return averageScoresPerTask(results).entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("No easy task found");
}
EDIT
To customize the calculation of the average regardless how many items your maps contain, don't use the inbuilt operations like Collectors.averagingInt or Collectors.averagingDouble. Instead wrap your collector in collectingAndThen and sum the scores using Collectors.summingInt and finally after collecting divide using a divisor according if the task name starts with Lab or not:
public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {
return results.map(CourseResult::getTaskResults)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(Map.Entry::getKey, Collectors.summingInt(Map.Entry::getValue)),
map -> map.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getKey().startsWith("Lab") ? e.getValue() / 3. : e.getValue() / 4.))
));
}
To create a map containing an average score for each task, you need to flatten the map taskResults of every CourseResult result object in the stream and group the data by key (i.e. by task name).
For that you can use collector groupingBy(), as its downstream collector that would be responsible for calculation the average from the score-values mapped to the same task you can use averagingDouble().
That's how it might look like:
public Map<String, Double> averageScoresPerTask(Stream<CourseResult> results) {
return results
.map(CourseResult::getTaskResults) // Stream<Map<String, Integer>> - stream of maps
.flatMap(map -> map.entrySet().stream()) // Stream<Map.Entry<String, Integer>> - stream of entries
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.averagingDouble(Map.Entry::getValue)
));
}
To find the easiest task, you can use this map instead of passing the stream as an argument because the logic of this method requires applying the same operations. It would make sense in the real life scenario when you're retrieving the data that is stored somewhere (it would be better to avoid double-processing it) and more over in your case you can't generate a stream from the source twice and pass into these two methods because in your case stream data is random. Passing the same stream into both method is not an option because you can execute a stream pipeline only once, when it hits the terminal operation - it's done, you can't use it anymore, hence you can't pass the same stream with random data in these two methods.
public String easiestTask(Map<String, Double> averageByTask) {
return averageByTask.entrySet().stream()
.max(Map.Entry.comparingByValue()) // produces result of type Optianal<Map.Entry<String, Double>>
.map(Map.Entry::getKey) // transforming into Optianal<String>
.orElse("no data"); // or orElseThrow() if data is always expected to be present depending on your needs
}

Getting data from Map inside a Map with streams

I have a problem with receiving data from Map nested in another Map.
private Map<Customer, Map<Item,Integer>> orders;
I'm generating this map from JSON, its add Customer if he is not on the list with Items and their number.
If Customer is already in the map then key Item in the second map is updated and if a key was there already then Integer which is the number of items is updated.
Classes Customer and Items are not connected I mean Class Customer don't have field Items and class Items don't have a field Customer.
public class Customer {
private String name;
private String surname;
private Integer age;
private BigDecimal money;
}
public class Item {
private String name;
private String category;
private BigDecimal price;
}
Using streams I want to get for example Customer who paid the most for items but I have problem with getting this data from the map, it was not so hard with List but now I can't figure it out.
Ok I did figure out something like this and it seems to be working but I'm sure it can be simplified.
Customer key = customersMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue()
.entrySet()
.stream()
.map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
.collect(Collectors.toList())))
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, t -> t.getValue().stream().reduce(BigDecimal.ZERO, BigDecimal::add)))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.orElseThrow()
.getKey();
Your answer Naman was very helpful so maybe you can give me advice about this.
This is how I'm receiving it from JSON.
JsonConverterCustomer jsonConverterCustomer = new JsonConverterCustomer(FILENAME3);
List<Order> orders = jsonConverterCustomer.fromJson().orElseThrow();
Map<Customer, Map<Item, Integer>> customersMap = new HashMap<>();
for (Order order : orders) {
if (!customersMap.containsKey(order.getCustomer())) {
addNewCustomer(customersMap, order);
} else {
for (Product product : order.getItems()) {
if (!customersMap.get(order.getCustomer()).containsKey(items)) {
addNewCustomerItem(item, customersMap.get(order.getCustomer()));
} else {
updateCustomerItem(customersMap, order, item);
}
}
}
}
private static void updateCustomerProduct(Map<Customer, Map<Item, Integer>> customersMap, Order order, Item item) {
customersMap.get(order.getCustomer())
.replace(item,
customersMap.get(order.getCustomer()).get(item),
customersMap.get(order.getCustomer()).get(item) + 1);
}
private static void addNewCustomerItem(Item item, Map<Item, Integer> itemIntegerMap) {
itemIntegerMap.put(item, 1);
}
private static void addNewCustomer(Map<Customer, Map<Item, Integer>> customersMap, Order order) {
Map<Item, Integer> temp = new HashMap<>();
addNewCustomerItem(order.getItems().get(0), temp);
customersMap.put(order.getCustomer(), temp);
}
Order class is a class which one help me receiving data from JSON
It is a simple class with Customer as a field and List as a field.
As you can see I'm receiving List of Orders and from it, I'm creating this Map.
Can I make it more functional? Using streams? I was trying to do but not sure how;/
There are two possible ways to make it more maintainable/readable as Jason pointed out and at the same time simplify the logic performed.
One, you can get rid of one of the stages in the pipeline and merge map and reduce into a single pipeline.
Another would be to abstract out per customer computation of the total amount paid by them.
So the abstraction would look like the following and work on the inner maps for your input:
private BigDecimal totalPurchaseByCustomer(Map<Item, Integer> customerOrders) {
return customerOrders.entrySet()
.stream()
.map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
Now to easily fit this in while you iterate for each customer entry, you can do that in a single collect itself:
private Customer maxPayingCustomer(Map<Customer, Map<Item, Integer>> customersMap) {
Map<Customer, BigDecimal> customerPayments = customersMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> totalPurchaseByCustomer(e.getValue())));
return customerPayments.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElseThrow();
}

Java 8 Join Map with Collectors.toMap

I'm trying to collect in a Map the results from the process a list of objects and that it returns a map. I think that I should do it with a Collectors.toMap but I haven't found the way.
This is the code:
public class Car {
List<VersionCar> versions;
public List<VersionCar> getVersions() {
return versions;
}
}
public class VersionCar {
private String wheelsKey;
private String engineKey;
public String getWheelsKey() {
return wheelsKey;
}
public String getEngineKey() {
return engineKey;
}
}
process method:
private static Map<String,Set<String>> processObjects(VersionCar version) {
Map<String,Set<String>> mapItems = new HashMap<>();
mapItems.put("engine", new HashSet<>(Arrays.asList(version.getEngineKey())));
mapItems.put("wheels", new HashSet<>(Arrays.asList(version.getWheelsKey())));
return mapItems;
}
My final code is:
Map<String,Set<String>> mapAllItems =
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.collect(Collectors.toMap()); // here I don't know like collect the map.
My idea is to process the list of versions and in the end get a Map with two items: wheels and engine but with a set<> with all different items for all versions. Do you have any ideas as can I do that with Collectors.toMap or another option?
The operator you want to use in this case is probably "reduce"
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.reduce((map1, map2) -> {
map2.forEach((key, subset) -> map1.get(key).addAll(subset));
return map1;
})
.orElse(new HashMap<>());
The lambda used in "reduce" is a BinaryOperator, that merges 2 maps and return the merged map.
The "orElse" is just here to return something in the case your initial collection (versions) is empty.
From a type point of view it gets rid of the "Optional"
You can use Collectors.toMap(keyMapper, valueMapper, mergeFunction). Last argument is used to resolve collisions between values associated with the same key.
For example:
Map<String, Set<String>> mapAllItems =
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(firstSet, secondSet) -> {
Set<String> result = new HashSet<>();
result.addAll(firstSet);
result.addAll(secondSet);
return result;
}
));
To get the mapAllItems, we don't need and should not define processObjects method:
Map<String, Set<String>> mapAllItems = new HashMap<>();
mapAllItems.put("engine", car.getVersions().stream().map(v -> v.getEngineKey()).collect(Collectors.toSet()));
mapAllItems.put("wheels", car.getVersions().stream().map(v -> v.getWheelsKey()).collect(Collectors.toSet()));
Or by AbstractMap.SimpleEntry which is lighter than the Map created byprocessObjects`:
mapAllItems = car.getVersions().stream()
.flatMap(v -> Stream.of(new SimpleEntry<>("engine", v.getEngineKey()), new SimpleEntry<>("wheels", v.getWheelsKey())))
.collect(Collectors.groupingBy(e -> e.getKey(), Collectors.mapping(e -> e.getValue(), Collectors.toSet())));

Java 8 Merge maps in iterator

I have an iteraror where in every iteration I´m creating a new map
Map<String, List<String>>
Now I would like to merge in every iteration the last emitted map with the new one.
If I send a list of items to getMap
{"a","a","b"}
I expect to receive a map of
["a",{"foo:a", "foo:a"}, "b",{"foo:b"}]
I try to use reduce function, but because putall only works if I use multimap and not map, is not a good option.
Here my code
public Map<String, List<String>> getMap(List<String> items){
return items().stream()
.map(item -> getNewMap(item) --> Return a Map<String, List<String>>
.reduce(new HashMap<>(), (o, p) -> {
o.putAll(p);
return o;
});
}
public Map<String, List<String>> getNewMap(String item){
Map<String, List<String>> map = new HashMap<>();
map.put(item, Arrays.asList("foo:" + item));
return map;
}
I´m looking for a no verbose way to do it.
What you want is to flat map each intermediate map to its entries and make a single map out of that.
In the following code, each item is mapped to its corresponding map. Then, each map is flat mapped to its entries and the Stream is collected into a map.
public static void main(String[] args) {
System.out.println(getMap(Arrays.asList("a", "a", "b")));
// prints "{a=[foo:a, foo:a], b=[foo:b]}"
}
public static Map<String, List<String>> getMap(List<String> items) {
return items.stream()
.map(item -> getNewMap(item))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(l1, l2) -> { List<String> l = new ArrayList<>(l1); l.addAll(l2); return l; }
));
}
public static Map<String, List<String>> getNewMap(String item) {
Map<String, List<String>> map = new HashMap<>();
map.put(item, Arrays.asList("foo:" + item));
return map;
}
In the case of multiple keys, this appends each list together.
Whenever you want to get a Map<…, List<…>> from a stream, you should first check, how the groupingBy collector fits in. In its simplest form, it receives a grouping function which determines the keys of the resulting map and will collect all elements of a group into a list. Since you want the prefix "foo:" prepended, you’ll have to customize this group collector by inserting a mapping operation before collecting the items into a list:
public static Map<String, List<String>> getMap(List<String> items) {
return items.stream().collect(Collectors.groupingBy(
Function.identity(),
Collectors.mapping("foo:"::concat, Collectors.toList())));
}
The classification function itself is as trivial as the identity function, as you want all equal elements building one group.

Converting a Map to another Map using Stream API

I have a Map<Long, List<Member>>() and I want to produce a Map<Member, Long> that is calculated by iterating that List<Member> in Map.Entry<Long, List<Member>> and summing the keys of each map entry for each member in that member list. It's easy without non-functional way but I couldn't find a way without writing a custom collector using Java 8 Stream API. I think I need something like Stream.collect(Collectors.toFlatMap) however there is no such method in Collectors.
The best way that I could found is like this:
longListOfMemberMap = new HashMap<Long, List<Member>>()
longListOfMemberMap.put(10, asList(member1, member2));
Map<Member, Long> collect = longListOfMemberMap.entrySet().stream()
.collect(new Collector<Map.Entry<Long, List<Member>>, Map<Member, Long>, Map<Member, Long>>() {
#Override
public Supplier<Map<Member, Long>> supplier() {
return HashMap::new;
}
#Override
public BiConsumer<Map<Member, Long>, Map.Entry<Long, List<Member>>> accumulator() {
return (memberLongMap, tokenRangeListEntry) -> tokenRangeListEntry.getValue().forEach(member -> {
memberLongMap.compute(member, new BiFunction<Member, Long, Long>() {
#Override
public Long apply(Member member, Long aLong) {
return (aLong == null ? 0 : aLong) + tokenRangeListEntry.getKey();
}
});
});
}
#Override
public BinaryOperator<Map<Member, Long>> combiner() {
return (memberLongMap, memberLongMap2) -> {
memberLongMap.forEach((member, value) -> memberLongMap2.compute(member, new BiFunction<Member, Long, Long>() {
#Override
public Long apply(Member member, Long aLong) {
return aLong + value;
}
}));
return memberLongMap2;
};
}
#Override
public Function<Map<Member, Long>, Map<Member, Long>> finisher() {
return memberLongMap -> memberLongMap;
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
});
// collect is equal to
// 1. member1 -> 10
// 2. member2 -> 10
The code in the example takes a Map> as parameter and produces a Map:
parameter Map<Long, List<Member>>:
// 1. 10 -> list(member1, member2)
collected value Map<Member, Long>:
// 1. member1 -> 10
// 2. member2 -> 10
However as you see it's much more ugly than the non-functional way. I tried Collectors.toMap and reduce method of Stream but I couldn't find a way to do with a few lines of code.
Which way would be the simplest and functional for this problem?
longListOfMemberMap.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().map(
member ->
new AbstractMap.SimpleImmutableEntry<>(member, entry.getKey())))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.summingLong(Entry::getValue)));
...though an even simpler but more imperative alternative might look like
Map<Member, Long> result = new HashMap<>();
longListOfMemberMap.forEach((val, members) ->
members.forEach(member -> result.merge(member, val, Long::sum)));
I will just point out that the code you have posted can be written down much more concisely when relying on Collector.of and turning your anonymous classes into lambdas:
Map<Member, Long> result = longListOfMemberMap.entrySet().stream()
.collect(Collector.of(
HashMap::new,
(acc, item) -> item.getValue().forEach(member -> acc.compute(member,
(x, val) -> Optional.ofNullable(val).orElse(0L) + item.getKey())),
(m1, m2) -> {
m1.forEach((member, val1) -> m2.compute(member, (x, val2) -> val1 + val2));
return m2;
}
));
This still cumbersome, but at least not overwhelmingly so.

Categories

Resources