I want to do a Map<Person, Double>, where Double is average of Integer values that is stored in another Map <String, Integer> which is one of the fields of stream's elements.
public Map<Person,Double> totalScores(Stream<CourseResult> programmingResults) {
return
programmingResults.collect(Collectors.groupingBy(
CourseResult::getPerson,
// And there is a problem, I want to get values from `Map <String, Integer>`
// and do the `averagingInt`, but only get
//`Bad return type in lambda expression:
// Collection<Integer> cannot be converted to int`
Collectors.averagingInt(
s -> s.getTaskResults().values()
)
));
}
How can I get these values in the right way?
There's some of the classes I'm using:
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;
}
}
Note that values() is a Collection<Integer>. You can't average by that. You can use a flat-mapping Collector, to flatten each group of persons to just integers, rather than CourseResult. After that, you can do an average by the identity function.
return programmingResults.collect(
Collectors.groupingBy(
CourseResult::getPerson,
Collectors.flatMapping(s->s.getTaskResults().values().stream(),
Collectors.averagingInt(x -> x)
)
)
);
Edit: If there is only one CourseResult per Person, then you don't need groupingBy. Just use toMap and calculate the average using another stream.
return programmingResults.collect(
Collectors.toMap(
CourseResult::getPerson,
result -> result.getTaskResults().values()
.stream().mapToInt(x -> x).average().orElse(0))
);
If it is guaranteed that the input stream contains CourseResult instances which have unique persons (and grouping + flatMapping of the task results may not be needed), it may be sufficient to use toMap collector:
public Map<Person,Double> totalScores(Stream<CourseResult> results) {
return
results.collect(Collectors.toMap(
CourseResult::getPerson,
cr -> cr.getTaskResults().values() // Collection<Integer>
.stream() // Stream<Integer>
.collect(Collectors.averagingInt(Integer::intValue))
)
));
}
Related
I have a fun puzzler. Say I have a list of String values:
["A", "B", "C"]
Then I have to query another system for a Map<User, Long> of users with an attribute that corresponds to those values in the list with a count:
{name="Annie", key="A"} -> 23
{name="Paul", key="C"} -> 16
I need to return a new List<UserCount> with a count of each key. So I expect:
{key="A", count=23},
{key="B", count=0},
{key="C", count=16}
But I'm having a hard time computing when one of my User objects has no corresponding count in the map.
I know that map.computeIfAbsent() does what I need, but how can I apply it based on what's on the contents of the original list?
I think I need to stream the over the original list, then apply compute? So I have:
valuesList.stream()
.map(it -> valuesMap.computeIfAbsent(it.getKey(), k-> OL))
...
But here's where I get stuck. Can anyone provide any insight as to how I accomplish what I need?
You can create an auxiliary Map<String, Long> which will associate each string key with the count and then generate a list of UserCount based on it.
Example:
public record User(String name, String key) {}
public record UserCount(String key, long count) {}
public static void main(String[] args) {
List<String> keys = List.of("A", "B", "C");
Map<User, Long> countByUser =
Map.of(new User("Annie", "A"), 23L,
new User("Paul", "C"), 16L));
Map<String, Long> countByKey = countByUser.entrySet().stream()
.collect(Collectors.groupingBy(entry -> entry.getKey().key(),
Collectors.summingLong(Map.Entry::getValue)));
List<UserCount> userCounts = keys.stream()
.map(key -> new UserCount(key, countByKey.getOrDefault(key, 0L)))
.collect(Collectors.toList());
System.out.println(userCounts);
}
Output
[UserCount[key=A, count=23], UserCount[key=B, count=0], UserCount[key=C, count=16]]
Regarding the idea of utilizing computeIfAbsent() with stream - this approach is wrong and discouraged by the documentation of the Stream API.
Sure, you can use computeIfAbsent() to solve this problem, but not in conjunction with streams. It's not a good idea to create a stream that operates via side effects (at least without compelling reason).
And I guess you even don't need Java 8 computeIfAbsent(), plain and simple putIfAbsent() will be sufficient.
The following code will produce the same result:
Map<String, Long> countByKey = new HashMap<>();
countByUser.forEach((k, v) -> countByKey.merge(k.key(), v, Long::sum));
keys.forEach(k -> countByKey.putIfAbsent(k, 0L));
List<UserCount> userCounts = keys.stream()
.map(key -> new UserCount(key, countByKey.getOrDefault(key, 0L)))
.collect(Collectors.toList());
And instead of applying forEach() on a map and list, you can create two enhanced for loops if this options looks convoluted.
Another educational and parallel friendly version would be to gather the logic in one place and build your own custom accumulator and combiner for the Collector
public static void main(String[] args) {
Map<User, Long> countByUser =
Map.of(new User("Alice", "A"), 23L,
new User("Bob", "C"), 16L);
List<String> keys = List.of("A", "B", "C");
UserCountAggregator userCountAggregator =
countByUser.entrySet()
.parallelStream()
.collect(UserCountAggregator::new,
UserCountAggregator::accumulator,
UserCountAggregator::combiner);
List<UserCount> userCounts = userCountAggregator.getUserCounts(keys);
System.out.println(userCounts);
}
Output
[UserCount(key=A, count=23), UserCount(key=B, count=0), UserCount(key=C, count=16)]
User and UserCount classes with Lombok's #Value
#Value
class User {
private String name;
private String key;
}
#Value
class UserCount {
private String key;
private long count;
}
And the UserCountAggregator which contains your custom accumulator and combiner
class UserCountAggregator {
private Map<String, Long> keyCounts = new HashMap<>();
public void accumulator(Map.Entry<User, Long> userLongEntry) {
keyCounts.put(userLongEntry.getKey().getKey(),
keyCounts.getOrDefault(userLongEntry.getKey().getKey(), 0L)
+ userLongEntry.getValue());
}
public void combiner(UserCountAggregator other) {
other.keyCounts
.forEach((key, value) -> keyCounts.merge(key, value, Long::sum));
}
public List<UserCount> getUserCounts(List<String> keys) {
return keys.stream()
.map(key -> new UserCount(key, keyCounts.getOrDefault(key, 0L)))
.collect(Collectors.toList());
}
}
final Map<User,Long> valuesMap = ...
// First, map keys to counts (assuming keys are unique for each user)
final Map<String,Long> keyToCountMap = valuesMap.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey().key, e -> e.getValue()));
final List<UserCount> list = valuesList.stream()
.map(key -> new UserCount (key, keyToCountMap.getOrDefault(key, 0L)))
.collect(Collectors.toList());
I have a map like this. Map<long,List<Student>> studentMap
Key is a number 1,2,3,4...
Student object is :
public class Student {
private long addressNo;
private String code;
private BigDecimal tax;
private String name;
private String city;
// getter and setters`
}
What i want to do is to convert it Map<long,List<StudentInfo>> studentInfoMap object and group id, addressNo and code fields.I want key are same for both maps.
I can group the map by using these codes but summingDouble is not working for BigDecimal.Also I cannot convert my studentMap to studentInfoMap.:(
studentInfoMap.values().stream()
.collect(
Collectors.groupingBy(StudentInfo::getCode,
Collectors.groupingBy(StudentInfo::getAddressNo,
Collectors.summingDouble(StudentInfo::getTax))));
My studentInfo object is :
public class StudentInfo {
private long addressNo;
private String code;
private BigDecimal tax;
// getter and setters`
}
For a one-to-one conversion from Student to StudentInfo:
class StudentInfo {
public static StudentInfo of(Student student) {
StudentInfo si = new StudentInfo();
si.setAddressNo(student.getAddressNo());
si.setCode(student.getCode());
si.setTax(student.getTax());
return si;
}
}
To convert from one Map to the other:
Map<Long,List<Student>> studentMap = ...
Map<Long,List<StudentInfo>> studentInfoMap = studentMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, //same key
entry -> entry.getValue().stream()
.map(StudentInfo::of) //conversion of Student to StudentInfo
.collect(Collectors.toList()) //or simply `.toList()` as of Java 16
));
Now your grouping....
From the JavaDoc for java.util.stream.Stream<T> public abstract <R, A> R collect(java.util.stream.Collector<? super T, A, R> collector):
The following will classify Person objects by city:
Map<String, List<Person>> peopleByCity
= personStream.collect(Collectors.groupingBy(Person::getCity));
The following will classify Person objects by state and city, cascading two Collectors together:
Map<String, Map<String, List<Person>>> peopleByStateAndCity
= personStream.collect(Collectors.groupingBy(Person::getState,
Collectors.groupingBy(Person::getCity)));
Note how the last example produces a Map with another Map as its values.
Now, summingDouble over StudentInfo::getTax produces a BigDecimal, not a Map. Replacing with groupingBy will work to classify Students that have the same amount for getTax:
Map<String, Map<Long, Map<BigDecimal, List<StudentInfo>>>> summary =
studentInfoMap.values().stream()
.flatMap(List::stream) //YOU ALSO NEED THIS
.collect(
Collectors.groupingBy(StudentInfo::getCode,
Collectors.groupingBy(StudentInfo::getAddressNo,
Collectors.groupingBy(StudentInfo::getTax)))
);
Edit: Retaining the 1,2,3,4 original keys
To retain the original keys you can iterate or stream the original entrySet, which contains both key and value:
Map<Long,Map<String, Map<Long, Map<BigDecimal, List<StudentInfo>>>>> summaryWithKeys =
studentInfoMap.entrySet().stream() //NOTE streaming the entrySet not just values
.collect(
Collectors.toMap(Map.Entry::getKey, //Original Key with toMap
entry -> entry.getValue().stream() //group the value-List members
.collect(Collectors.groupingBy(StudentInfo::getCode,
Collectors.groupingBy(StudentInfo::getAddressNo,
Collectors.groupingBy(StudentInfo::getTax))))
));
Just as an exercise, if you want a flat map (Map<MyKey,List>) you need a composite key MyKey
As per my comment, if you are looking to have a single flat Map, you could design a composite key, which would need to implement both equals() and hashCode() to contract. For example, this is what Lombok would generate for StudentInfo (yes, its easier to depend on lombok and use #EqualsAndHashCode):
public boolean equals(final Object o) {
if(o == this) return true;
if(!(o instanceof StudentInfo)) return false;
final StudentInfo other = (StudentInfo) o;
if(!other.canEqual((Object) this)) return false;
if(this.getAddressNo() != other.getAddressNo()) return false;
final Object this$code = this.getCode();
final Object other$code = other.getCode();
if(this$code == null ? other$code != null : !this$code.equals(other$code)) return false;
final Object this$tax = this.getTax();
final Object other$tax = other.getTax();
if(this$tax == null ? other$tax != null : !this$tax.equals(other$tax)) return false;
return true;
}
protected boolean canEqual(final Object other) {return other instanceof StudentInfo;}
public int hashCode() {
final int PRIME = 59;
int result = 1;
final long $addressNo = this.getAddressNo();
result = result * PRIME + (int) ($addressNo >>> 32 ^ $addressNo);
final Object $code = this.getCode();
result = result * PRIME + ($code == null ? 43 : $code.hashCode());
final Object $tax = this.getTax();
result = result * PRIME + ($tax == null ? 43 : $tax.hashCode());
return result;
}
You might then use StudentInfo as the composite key as follows:
Map<Long, List<Student>> studentMap = ...
Map<StudentInfo,List<Student>>> summaryMap = studentMap.values().stream()
.collect(Collectors.groupingBy(StudentInfo::of))
));
This means that you now have a nested map referenced by the composite key. Students that have exactly the same addressNo, code and tax will be part of the List referenced by each such key.
Edit: Retaining original keys
Similarly, if you wanted to retain the original keys, you could either add them into the composite key, or similar as above:
Map<Long, List<Student>> studentMap = ...
Map<Long, Map<StudentInfo,List<Student>>>> summaryMap = studentMap.entrySet().stream()
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.groupingBy(StudentInfo::of)))
));
Map<Integer, Object> map = new HashMap<>();
map.put(1, studentInfoMap.values().stream().map(
student -> student.getAddressNo()
).collect(Collectors.toList()));
map.put(2, studentInfoMap.values().stream().map(
student -> student.getCode()
).collect(Collectors.toList()));
// and so ...
To convert the map from Student to StudentInfo, whilst keeping the same keys, you could do something like this:
Set<Long> keys = studentMap.keySet();
List<Long> keylist = new ArrayList<>(keys);
Map<Long, List<StudentInfo>> studentInfoMap = new HashMap<>();
for(int i = 0; i < keys.size(); i++){
long key = keylist.get(i);
List<Student> list = studentMap.get(key);
List<StudentInfo> result = new ArrayList<>();
// Create the new StudentInfo object with your values
list.forEach(s -> result.add(new StudentInfo(s.name())));
// Put them into the new Map with the same keys
studentInfoMap.put(key, result);
}
I would like to convert a stream of Objects to a Map. The key is the object itself and the value is Function.identity(). My goal is to create an incremental index for every Person.
public class Person {
private String firstName;
private String lastName;
}
/* Expected Result
Key:[Person1], value:1
Key:[Person2], value:2
Key:[Person3], value:3
*/
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
return persons.filter(p -> "John".equalIgnoreCase(p.getFirstName)
.collect(Collectors.toMap(Person, Function.identity()));
}
My problem is that after applying the .filter(), I can't put my object as a key (or even value) in .toMap() method.
You could make it in two steps:
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
List<Person> filterd = persons.filter(p -> "John".equalIgnoreCase(p.getFirstName))
.collect(Collectors.toList());
return IntStream.range(0, filterd.size())
.boxed()
.collect(Collectors.toMap(filterd::get, i -> i + 1));
}
You could use AtomicInteger to generate an increasing value as value to your Map:
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
final AtomicInteger atomicInteger = new AtomicInteger();
return persons
.filter(p -> "John".equalsIgnoreCase(p.getFirstName()))
.collect(Collectors.toMap(Function.identity(), notUsed -> atomicInteger.incrementAndGet()));
}
Beware of using this with parallel streams since it relies on the order of the processing. It will, however, render unique values since AtomicInteger is thread safe.
You can try something like this:
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
AtomicInteger counter = new AtomicInteger(0);
return persons.filter(p -> "John".equalIgnoreCase(p.getFirstName())
.collectors.toMap(p->p, counter.getAndIncrement());
}
My current approach exploiting Streams API in conjunction with forEach loop:
public Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product,Integer>> shopping) {
Map<String, Client> result = new HashMap<>();
Map<Client, Map<String, BigDecimal>> temp =
shopping.entrySet()
.stream()
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.flatMapping(e -> e.getValue().entrySet().stream(),
Collectors.groupingBy(e -> e.getKey().getCategory(),
Collectors.mapping(ee -> ee.getKey().getPrice().multiply(BigDecimal.valueOf(ee.getValue())),
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))))));
/*curious, how could I refactor that piece of code, so the method uses only one stream chain? */
temp.forEach((client, value)
-> value.forEach((category, value1)
-> {
if (!result.containsKey(category) ||
temp.get(result.get(category)).get(category).compareTo(value1) < 0)
result.put(category, client);
}));
return result;
}
As the method's name sugggests, I want to find a map Map <String, Client>, containing Client with most purchases (as value) in specified category (as key) in each product's category
shopping is basically a map: Map<Client, Map<Product,Integer>>,
The outer Key represents the Client
The inner Key represents the Product. Product class members are name, category, price (BigDecimal)
the inner maps value (Integer) reprents the number of specified product which belong to a specific client
Not sure, if that's even possible? Collectors.collectingAndThen maybe could be useful?
You can use StreamEx library, and do smth like this
public static Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping) {
return EntryStream.of(shopping)
.flatMapKeyValue(((client, productQuantityMap) ->
EntryStream.of(productQuantityMap)
.mapToValue((p, q) -> p.getPrice().multiply(BigDecimal.valueOf(q)))
.mapKeys(Product::getCategory)
.map(e -> new ClientCategorySpend(client, e.getKey(), e.getValue())))
)
.groupingBy(
ClientCategorySpend::getCategory,
Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(ClientCategorySpend::getSpend)),
t -> t.get().getClient())
);
}
You were pretty much doomed the moment you grouped by client. The
top level Collectors.groupingBy must use the category as the grouping by key.
To do that you would flatMap before collecting, so you get a flat stream of
client + category + spend elements.
Here's one way to do it. I'll first define a POJO for the elements of the flattened stream:
static class ClientCategorySpend
{
private final Client client;
private final String category;
private final BigDecimal spend;
public ClientCategorySpend(Client client, String category, BigDecimal spend)
{
this.client = client;
this.category = category;
this.spend = spend;
}
public String getCategory()
{
return category;
}
public Client getClient()
{
return client;
}
public BigDecimal getSpend()
{
return spend;
}
}
And now the function:
public static Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping)
{
// <1>
Collector<? super ClientCategorySpend, ?, BigDecimal> sumOfSpendByClient = Collectors.mapping(ClientCategorySpend::getSpend,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add));
// <2>
Collector<? super ClientCategorySpend, ?, Map<Client, BigDecimal>> clientSpendByCategory = Collectors.groupingBy(
ClientCategorySpend::getClient,
sumOfSpendByClient
);
// <3>
Collector<? super ClientCategorySpend, ?, Client> maxSpendingClientByCategory = Collectors.collectingAndThen(
clientSpendByCategory,
map -> map.entrySet().stream()
.max(Comparator.comparing(Map.Entry::getValue))
.map(Map.Entry::getKey).get()
);
return shopping.entrySet().stream()
// <4>
.flatMap(
entry -> entry.getValue().entrySet().stream().map(
entry2 -> new ClientCategorySpend(entry.getKey(),
entry2.getKey().category,
entry2.getKey().price.multiply(BigDecimal.valueOf(entry2.getValue())))
)
).collect(Collectors.groupingBy(ClientCategorySpend::getCategory, maxSpendingClientByCategory));
}
Once I have a stream of ClientCategorySpend (4), I group it by category. I use
the clientSpendByCategory collector (2) to create a map between the client and the total spend in the category. This in turn depends on sumToSpendByClient (1) which is basically a reducer that sums up the spends. You then get to use collectingAndThen as you suggested,
reducing each Map<Client, BigDecimal> to a single client using max.
This should do ;)
public Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping) {
return shopping
.entrySet()
.stream()
.map(entry -> Pair.of(
entry.getKey(),
entry.getValue()
.entrySet()
.stream()
.map(e -> Pair.of(
e.getKey().getCategory(),
e.getKey().getPrice().multiply(
BigDecimal.valueOf(e.getValue()))))
.collect(Collectors.toMap(
Pair::getKey,
Pair::getValue,
BigDecimal::add))))
// Here we have: Stream<Pair<Client, Map<String, BigDecimal>>>
// e.g.: per each Client we have a map { category -> purchase value }
.flatMap(item -> item.getValue()
.entrySet()
.stream()
.map(e -> Pair.of(
e.getKey(), Pair.of(item.getKey(), e.getValue()))))
// Here: Stream<Pair<String, Pair<Client, BigDecimal>>>
// e.g.: entries stream { category, { client, purchase value } }
// where there are category duplicates, so we must select those
// with highest purchase value for each category.
.collect(Collectors.toMap(
Pair::getKey,
Pair::getValue,
(o1, o2) -> o2.getValue().compareTo(o1.getValue()) > 0 ?
o2 : o1))
// Now we have: Map<String, Pair<Client, BigDecimal>>,
// e.g.: { category -> { client, purchase value } }
// so just get rid of unnecessary purchase value...
.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().getKey()));
}
Pair is org.apache.commons.lang3.tuple.Pair. If you do not want to use Appache Commons library you may use java.util.AbstractMap.SimpleEntry instead.
I have a Person class which has gender and age fields. I want to get the sum of age for each gender by below code.
public void test() {
List<Person> roster = new ArrayList();
Map<Integer, Integer> totalAgeByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0,
Person::getAge,
Double::sum)));
}
static class Person {
private int gender;
private double age;
public int getGender() {
return gender;
}
public double getAge() {
return age;
}
}
}
I am having a compile error with above code as below:
Bad return type in method reference: cannot convert double to U.
It complains the last part of the steam statement Double::sum. If I change the type of age to integer and use Integer::sum in the above code, it works fine. I wander what wrong with the Double::sum in this case.
In addition, is it possible to group by the sex field and return Map<Integer, List<Person>> from one steam statement?
You've got the following 2 slight problems here: (1) your identify is being interpreted as an Integer; and (2) the values of your map are Double(s).
Map<Integer, Double> totalAgeByGender = // not Map<Integer, Integer>
roster.stream()
.collect(Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0d, // not 0
Person::getAge,
Double::sum)));
As mentioned below, you could make this more concise and less error prone using Collectors.summingDouble:
Map<Integer, Double> totalAgeByGender = roster.stream()
.collect(Collectors.groupingBy(
Person::getGender,
Collectors.summingDouble(Person::getAge)));
You can group by sex (gender) easily enough:
Map<Integer, List<Person>> sexToPeople = roster.stream()
.collect(Collectors.groupingBy(
Person::getGender,
Collectors.toList()));