I have a CourseResult class
public class CourseResult {
private final Person person;
private final Map<String, Integer> taskResults; // Here names and results of tasks are saved
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;
}
}
Here is my method signature
public Map<String, Double> averageScoresPerTask(Stream<CourseResult> programmingResults) {
I need to return an average result of every task.
I'd flatMap that stream to a stream of the taskResults' entries, and then use Collectors.averagingDouble to get the average per task:
public Map<String, Double> averageScoresPerTask(Stream<CourseResult> programmingResults) {
return programmingResults
.flatMap(c -> c.getTaskResults().entrySet().stream())
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.averagingDouble(Map.Entry::getValue)));
}
I googled for it but I mostly found cases for grouping by aggregated fields or on to alter response of stream but not the scenario below:
I have a class User with fields category and marketingChannel.
I have to write a method in the declarative style that accepts a list of users and counts users based on
category and also based on marketingChannel individually (i.e not groupingBy(... ,groupingBy(..)) ).
I am unable to do it in a single loop. This is what I have to achieve.
I coded few methods as follows:
import java.util.*;
import java.util.stream.*;
public class Main
{
public static void main(String[] args) {
List<User> users = User.createDemoList();
imperative(users);
declerativeMultipleLoop(users);
declerativeMultipleColumn(users);
}
public static void imperative(List<User> users){
Map<String, Integer> categoryMap = new HashMap<>();
Map<String, Integer> channelMap = new HashMap<>();
for(User user : users){
Integer value = categoryMap.getOrDefault(user.getCategory(), 0);
categoryMap.put(user.getCategory(), value+1);
value = channelMap.getOrDefault(user.getMarketingChannel(), 0);
channelMap.put(user.getMarketingChannel(), value+1);
}
System.out.println("imperative");
System.out.println(categoryMap);
System.out.println(channelMap);
}
public static void declerativeMultipleLoop(List<User> users){
Map<String, Long> categoryMap = users.stream()
.collect(Collectors.groupingBy(
User::getCategory, Collectors.counting()));
Map<String, Long> channelMap = users.stream()
.collect(Collectors.groupingBy(
User::getMarketingChannel, Collectors.counting()));
System.out.println("declerativeMultipleLoop");
System.out.println(categoryMap);
System.out.println(channelMap);
}
public static void declerativeMultipleColumn(List<User> users){
Map<String, Map<String, Long>> map = users.stream()
.collect(Collectors.groupingBy(
User::getCategory,
Collectors.groupingBy(User::getMarketingChannel,
Collectors.counting())));
System.out.println("declerativeMultipleColumn");
System.out.println("groupingBy category and marketChannel");
System.out.println(map);
Map<String, Long> categoryMap = new HashMap<>();
Map<String, Long> channelMap = new HashMap<>();
for (Map.Entry<String, Map<String, Long>> entry: map.entrySet()) {
String category = entry.getKey();
Integer count = entry.getValue().size();
Long value = categoryMap.getOrDefault(category,0L);
categoryMap.put(category, value+count);
for (Map.Entry<String, Long> channelEntry : entry.getValue().entrySet()){
String channel = channelEntry.getKey();
Long channelCount = channelEntry.getValue();
Long channelValue = channelMap.getOrDefault(channel,0L);
channelMap.put(channel, channelValue+channelCount);
}
}
System.out.println("After Implerative Loop on above.");
System.out.println(categoryMap);
System.out.println(channelMap);
}
}
class User{
private String name;
private String category;
private String marketChannel;
public User(String name, String category, String marketChannel){
this.name = name;
this.category = category;
this.marketChannel = marketChannel;
}
public String getName(){
return this.name;
}
public String getCategory(){
return this.category;
}
public String getMarketingChannel(){
return this.marketChannel;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) &&
Objects.equals(category, user.category) &&
Objects.equals(marketChannel, user.marketChannel);
}
#Override
public int hashCode() {
return Objects.hash(name, category, marketChannel);
}
public static List<User> createDemoList(){
return Arrays.asList(
new User("a", "student","google"),
new User("b", "student","bing"),
new User("c", "business","google"),
new User("d", "business", "direct")
);
}
The method declerativeMultipleLoop is declarative but it has a separate loop for each field. Complexity : O(noOfFields * No of users)
The problem is in declerativeMultipleColumn Method as I end up writing imperative code and multiple loops.
I want to write the above method in completely declarative and as efficient as possible. i.e Complexity : O(No of users)
Sample output:
imperative
{business=2, student=2}
{direct=1, google=2, bing=1}
declerativeMultipleLoop
{business=2, student=2}
{direct=1, google=2, bing=1}
declerativeMultipleColumn
groupingBy category and marketChannel
{business={direct=1, google=1}, student={google=1, bing=1}}
After Implerative Loop on above.
{business=2, student=2}
{direct=1, google=2, bing=1}
If I understand your requirement it is to use a single stream operation that results in 2 separate maps. That is going to require a structure to hold the maps and a collector to build the structure. Something like the following:
class Counts {
public final Map<String, Integer> categoryCounts = new HashMap<>();
public final Map<String, Integer> channelCounts = new HashMap<>();
public static Collector<User,Counts,Counts> countsCollector() {
return Collector.of(Counts::new, Counts::accept, Counts::combine, CONCURRENT, UNORDERED);
}
private Counts() { }
private void accept(User user) {
categoryCounts.merge(user.getCategory(), 1, Integer::sum);
channelCounts.merge(user.getChannel(), 1, Integer::sum);
}
private Counts combine(Counts other) {
other.categoryCounts.forEach((c, v) -> categoryCounts.merge(c, v, Integer::sum));
other.channelCounts.forEach((c, v) -> channelCounts.merge(c, v, Integer::sum));
return this;
}
}
That can then be used as a collector:
Counts counts = users.stream().collect(Counts.countsCollector());
counts.categoryCounts.get("student")...
(Opinion only: the distinction between imperative and declarative is pretty arbitrary in this case. Defining stream operations feels pretty procedural to me (as opposed to the equivalent in, say, Haskell)).
You can compute two maps in a single forEach method:
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("a", "student", "google"),
new User("b", "student", "bing"),
new User("c", "business", "google"),
new User("d", "business", "direct"));
Map<String, Integer> categoryMap = new HashMap<>();
Map<String, Integer> channelMap = new HashMap<>();
// group users into maps
users.forEach(user -> {
categoryMap.compute(user.getCategory(),
(key, value) -> value == null ? 1 : value + 1);
channelMap.compute(user.getChannel(),
(key, value) -> value == null ? 1 : value + 1);
});
// output
System.out.println(categoryMap); // {business=2, student=2}
System.out.println(channelMap); // {direct=1, google=2, bing=1}
}
static class User {
private final String name, category, channel;
public User(String name, String category, String channel) {
this.name = name;
this.category = category;
this.channel = channel;
}
public String getName() { return this.name; }
public String getCategory() { return this.category; }
public String getChannel() { return this.channel; }
}
I have a Model and a Property class with the following signatures:
public class Property {
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Model {
private List<Property> properties = new ArrayList<>();
public List<Property> getProperties() {
return properties;
}
}
I want a Map<String, Set<Model>> from a List<Model> where the key would be the name from the Property class. How can I can I use java8 streams to group that list by its Properyes' name? All Propertyes are unique by name.
It is possible to solve in a single stream or should I split it somehow or go for the classical solution?
yourModels.stream()
.flatMap(model -> model.getProperties().stream()
.map(property -> new AbstractMap.SimpleEntry<>(model, property.getName())))
.collect(Collectors.groupingBy(
Entry::getValue,
Collectors.mapping(
Entry::getKey,
Collectors.toSet())));
Why not use forEach ?
Here is concise solution using forEach
Map<String, Set<Model>> resultMap = new HashMap<>();
listOfModels.forEach(currentModel ->
currentModel.getProperties().forEach(prop -> {
Set<Model> setOfModels = resultMap.getOrDefault(prop.getName(), new HashSet<>());
setOfModels.add(currentModel);
resultMap.put(prop.getName(), setOfModels);
})
);
I am trying to use Java-8 lambdas to solve the following problem:
Given a List<Transaction>, for each Category.minorCategory I require the sum of Transaction.amount per Category.minorCategory and a Map of Transaction.accountNumber with the sum of Transaction.amount per Transaction.accountNumber. I have this working, as per the code below.
I now have a requirement to group by Category.majorCategory, essentially returning a Map<String, Map<String, MinorCategorySummary>> keyed on Category.majorCategory.
I have everything working up until the stage of grouping by Category.majorCategory but struggle to see the solution; the paradigm shift of programming with lambdas is proving a steep learning curve.
TransactionBreakdown is where the action happens and where I'd like to return a Map<String, Map<String, MinorCategorySummary>>.
public class Transaction {
private final String accountNumber;
private final BigDecimal amount;
private final Category category;
}
public class Category {
private final String majorCategory;
private final String minorCategory;
}
public class MinorCategorySummary {
private final BigDecimal sumOfAmountPerMinorCategory;
private final Map<String, BigDecimal> accountNumberSumOfAmountMap;
private final Category category;
}
public class TransactionBreakdown {
Function<Entry<String, List<Transaction>>, MinorCategorySummary> transactionSummariser = new TransactionSummariser();
public Map<Object, MinorCategorySummary> getTransactionSummaries(List<Transaction> transactions) {
return transactions
.stream()
.collect(groupingBy(t -> t.getCategory().getMinorCategory()))
.entrySet()
.stream()
.collect(
toMap(Entry::getKey,
transactionSummariser));
}
}
public class TransactionSummariser implements Function<Entry<String, List<Transaction>>, MinorCategorySummary> {
#Override
public MinorCategorySummary apply(
Entry<String, List<Transaction>> entry) {
return new MinorCategorySummary(
entry.getValue()
.stream()
.map(Transaction::getAmount)
.collect(reducing(BigDecimal.ZERO, BigDecimal::add)),
entry.getValue()
.stream()
.collect(
groupingBy(Transaction::getAccountNumber,
mapping(Transaction::getAmount,
reducing(BigDecimal.ZERO, BigDecimal::add)))),
entry.getValue().get(0).getCategory());
}
}
Your class design seems odd to me. Why put category into the summary class only to then have the category as a map key? It would make more sense to have a summary class without category in it:
public class TransactionSummary {
private final BigDecimal amount;
private final Map<String, BigDecimal> acctToTotal;
TransactionSummary(Map<String, BigDecimal> acctToTotal) {
this.acctToTotal = Collections.unmodifiableMap(acctToTotal);
this.amount = acctToTotal.values().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public static Collector<Transaction, ?, TransactionSummary> collector() {
// this can be a static constant
return collectingAndThen(
toMap(Transaction::getAccountNumber,Transaction::getAmount,BigDecimal::add),
TransactionSummary::new
);
}
// getters
}
Now your two problems are solved clearly and with no redundancy:
Map<String, TransactionSummary> minorSummary = transactions.stream()
.collect(groupingBy(
t -> t.getCategory().getMinorCategory(),
TransactionSummary.collector()
));
Map<String, Map<String, TransactionSummary>> majorMinorSummary = transactions.stream()
.collect(groupingBy(
t -> t.getCategory().getMajorCategory(),
groupingBy(
t -> t.getCategory().getMinorCategory(),
TransactionSummary.collector()
)
));
If I have a class like
public class Property {
private String id;
private String key;
private String value;
public Property(String id, String key, String value) {
this.id = id;
this.key = key;
this.value = value;
}
//getters and setters
}
and I have a Set<Property> properties of a few properties that I would like to reduce into a Map of just the key and values from these Property objects.
Most of my solutions ended up being not so suave. I know there's a handy way to do these with a Collector but I'm not that familiar with Java8 yet. Any tips?
Set<Property> properties = new HashSet<>();
properties.add(new Property("0", "a", "A"));
properties.add(new Property("1", "b", "B"));
Map<String, String> result = properties.stream()
.collect(Collectors.toMap(p -> p.key, p -> p.value));
System.out.println(result);