grouping objects java 8 - java

I have something like the below :
public class MyClass {
private Long stackId
private Long questionId
}
A collection of say 100, where the stackid could be duplicate with different questionIds. Its a one to many relationship between stackId and questionId
Is there a streamy, java 8 way to convert to the below strcuture :
public class MyOtherClass {
private Long stackId
private Collection<Long> questionIds
}
Which would be a collection of 25, with each instance having a nested collection of 4 questionIds.
Input :
[{1,100},{1,101},{1,102},{1,103},{2,200},{2,201},{2,202},{1,203}]
Output
[{1, [100,101,102,103]},{2,[200,201,202,203]}]

The straight-forward way with the Stream API involves 2 Stream pipelines:
The first one creates a temporary Map<Long, List<Long>> of stackId to questionIds. This is done with the groupingBy(classifier, downstream) collectors where we classify per the stackId and values having the same stackId are mapped to their questionId (with mapping) and collected into a list with toList().
The second one converts each entry of that map into a MyOtherClass instance and collects that into a list.
Assuming you have a constructor MyOtherClass(Long stackId, Collection<Long> questionIds), a sample code would be:
Map<Long, List<Long>> map =
list.stream()
.collect(Collectors.groupingBy(
MyClass::getStackId,
Collectors.mapping(MyClass::getQuestionId, Collectors.toList())
));
List<MyOtherClass> result =
map.entrySet()
.stream()
.map(e -> new MyOtherClass(e.getKey(), e.getValue()))
.collect(Collectors.toList());
Using StreamEx library, you could do that in a single Stream pipeline. This library offers a pairing and first collectors. This enables to pair two collectors and perform a finisher operation on the two collected results:
The first one only keeps the first stackId of the grouped elements (they will all be the same, by construction)
The second one mapping each element into their questionId and collecting into a list.
The finisher operation just returns a new instance of MyOtherClass.
Sample code:
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static one.util.streamex.MoreCollectors.first;
import static one.util.streamex.MoreCollectors.pairing;
// ...
Collection<MyOtherClass> result =
StreamEx.of(list)
.groupingBy(
MyClass::getStackId,
pairing(
collectingAndThen(mapping(MyClass::getStackId, first()), Optional::get),
mapping(MyClass::getQuestionId, toList()),
MyOtherClass::new
)
).values();

List<MyClass> inputs = Arrays.asList(
new MyClass(1L, 100L),
new MyClass(1L, 101L),
new MyClass(1L, 102L),
new MyClass(1L, 103L),
new MyClass(2L, 200L),
new MyClass(2L, 201L),
new MyClass(2L, 202L),
new MyClass(2L, 203L)
);
Map<Long, List<Long>> result = inputs
.stream()
.collect(
Collectors.groupingBy(MyClass::getStackId,
Collectors.mapping(
MyClass::getQuestionId,
Collectors.toList()
)
)
);

You can use the java8 groupingBy collector. Like this:
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class RandomTest {
class MyClass {
private Long stackId;
private Long questionId;
public MyClass(Long stackId, Long questionId) {
this.stackId = stackId;
this.questionId = questionId;
}
public Long getStackId() {
return stackId;
}
public Long getQuestionId() {
return questionId;
}
}
public class MyOtherClass {
private Long stackId;
private Set<Long> questionIds;
public MyOtherClass(Long stackId, Set<Long> questionIds) {
this.stackId = stackId;
this.questionIds = questionIds;
}
public Long getStackId() {
return stackId;
}
public Set<Long> getQuestionIds() {
return questionIds;
}
}
#Test
public void test() {
List<MyClass> classes = new ArrayList<>();
List<MyOtherClass> otherClasses = new ArrayList<>();
//populate the classes list
for (int j = 1; j <= 25; j++) {
for (int i = 0; i < 4; i++) {
classes.add(new MyClass(0L + j, (100L*j) + i));
}
}
//populate the otherClasses List
classes.stream().collect(Collectors
.groupingBy(MyClass::getStackId, Collectors.mapping(MyClass::getQuestionId, Collectors.toSet())))
.entrySet().stream().forEach(
longSetEntry -> otherClasses.add(new MyOtherClass(longSetEntry.getKey(), longSetEntry.getValue())));
//print the otherClasses list
otherClasses.forEach(myOtherClass -> {
System.out.print(myOtherClass.getStackId() + ": [");
myOtherClass.getQuestionIds().forEach(questionId-> System.out.print(questionId + ","));
System.out.println("]");
});
}
}

Related

How to write java streams for the nested loops

How do I use the java streams for the below scenario with nested loops...
package test;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
#Setter
#Getter
class Car {
String carName;
int wheelNo;
WheelDetails wheelDetails;
Car(String carName, int wheelNo) {
this.carName = carName;
this.wheelNo = wheelNo;
}
}
#Getter
#Setter
class WheelDetails {
int wheelNo;
String wheelColour;
int Size;
WheelDetails(int wheelNo, String wheelColour, int Size) {
this.wheelNo = wheelNo;
this.wheelColour = wheelColour;
this.Size = Size;
}
}
public class testMain {
public static void main(String[] args) {
List<Car> cars = new ArrayList<Car>();
cars.add(new Car("CarOne", 1));
cars.add(new Car("CarTwo", 2));
List<WheelDetails> wheelDetailsList = new ArrayList<WheelDetails>();
wheelDetailsList.add(new WheelDetails(1, "Black", 16));
wheelDetailsList.add(new WheelDetails(2, "Grey", 17));
for (Car car : cars) {
for (WheelDetails wheelDetails : wheelDetailsList) {
if (car.getWheelNo() == wheelDetails.getWheelNo()) {
car.setWheelDetails(wheelDetails);
}
}
}
}
}
not able to find the options to do the above in the streams it will be great helpful
have gone trough the other areas but could not find it... any link or solution will be helpful......
Try this:
cars.forEach(car -> wheelDetailsList.stream()
.filter(wheelDetails -> wheelDetails.wheelNo == car.wheelNo).forEach(car::setWheelDetails));
The following solution modifies elements in the list of cars:
cars.forEach(car -> car.setWheelDetails(
wheelDetailsList.stream()
.filter(wd -> car.getWheelNo() == wd.getWheelNo())
.findFirst()
.orElse(null)
));
Another faster solution is based on a temporary map <Integer, WheelDetails>:
Map<Integer, WheelDetails> wheelMap = wheelDetailsList
.stream()
.collect(Collectors.toMap(
WheelDetails::getWheelNo, // key
wd -> wd, // value
(wd1, wd2) -> wd1, // (optional) merge function to resolve conflicts
LinkedHashMap::new // keep insertion order
));
cars.forEach(car -> car.setWheelDetails(wheelMap.get(car.getWheelNo())));

java 8 stream groupingBy into collection of custom object

I have the following class structure
public class Store {
private Long storeId;
private Long masterStoreId;
private String operatorIdentifier;
}
public class StoreInfo {
private String operatorIdentifier;
private Set<Long> slaveStoreIds;
public StoreInfo(String operatorIdentifier, Set<Long> slaveStoreIds) {
super();
this.operatorIdentifier = operatorIdentifier;
this.slaveStoreIds = slaveStoreIds;
}
}
I want to collect the "List<Store" into a "Map<Long, StoreInfo>". Is it possible to do so in a single operation/iteration?
List<Store> stores;
Map<Long, Set<Long>> slaveStoresAgainstMasterStore = stores.stream().collect(Collectors
.groupingBy(Store::getMasterStoreId, Collectors.mapping(Store::getStoreId, Collectors.toSet())));
Map<Long, StoreInfo> storeInfoAgainstMasterStore = stores.stream()
.collect(
Collectors
.toMap(Store::getMasterStoreId,
val -> new StoreInfo(val.getOperatorIdentifier(),
slaveStoresAgainstMasterStore.get(val.getMasterStoreId())),
(a1, a2) -> a1));
As masterStoreId and operatorIdentifier are same same in group(comfirmed in comment) you can groupingBy both creating pair of them using AbstractMap.SimpleEntry. Then using Collectors.toMap create map.
Map<Long, StoreInfo> storeInfoMap =
stores.stream()
.collect(Collectors.groupingBy(
e -> new AbstractMap.SimpleEntry<>(e.getMasterStoreId(),
e.getOperatorIdentifier()),
Collectors.mapping(Store::getStoreId, Collectors.toSet())))
.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey().getKey(),
e -> new StoreInfo(e.getKey().getValue(), e.getValue())));
To complete the implementation, you were attempting. You need to ensure merging capability within StoreInfo such as :
public StoreInfo(String operatorIdentifier, Long slaveStoreId) {
this.operatorIdentifier = operatorIdentifier;
this.slaveStoreIds = new HashSet<>();
this.slaveStoreIds.add(slaveStoreId);
}
public static StoreInfo mergeStoreInfo(StoreInfo storeInfo1, StoreInfo storeInfo2) {
Set<Long> slaveIds = storeInfo1.getSlaveStoreIds();
slaveIds.addAll(storeInfo2.getSlaveStoreIds());
return new StoreInfo(storeInfo1.getOperatorIdentifier(), slaveIds);
}
this would simplify the implementation of collector and you an invoke these correspondingly:
Map<Long, StoreInfo> storeInfoAgainstMasterStore = stores.stream()
.collect(Collectors.toMap(Store::getMasterStoreId,
store -> new StoreInfo(store.getOperatorIdentifier(), store.getStoreId()),
StoreInfo::mergeStoreInfo));

Group and sort by multiple attribute using stream: Java 8

I have List of MainEntity
public class MainEntity {
private String keyword;
private double cost;
private String company;
}
and I have CompanyEntity
public class CompanyEntity {
private double cost;
private String company;
}
I am trying to transform my list into Map<String,List<CompanyEntity>> where key will be keyword and List<CompanyEntity> will have average of all the costs and sorted too. I am trying to do it in stream and Java 8.
For a particular keyword as input I am doing this.
List<MainEntity> entityList = keyWordMap.get(entity.getKeyword());
entityList.add(entity);
keyWordMap.put(entity.getKeyword(), entityList);
Map<String, Double> average = (keyWordMap.get(keyword)).stream()
.collect(groupingBy(MainEntity::getCompany,
Collectors.averagingDouble(MainEntity::getCtr)));
result.put(keyword, new ArrayList<>());
for (Map.Entry<String, Double> entity : average.entrySet()) {
result.get(keyword).add(new CompanyEntity(entity.getKey(), entity.getValue()));
}
But I trying to create a map for all keywords. Is is possible or iterating whole list again makes sense?
Currently keyowordMap is of type Map<String,MainEntity> which I did by iterating list of MainEntity, but I want Map<String,List<MainEntity>>.
First, make a keyWordMap
Map<String, List<MainEntity>> keyWordMap =
mainEntityList
.stream()
.collect(Collectors.groupingBy(MainEntity::getKeyword));
Then iterate the map, for each keyword, you can directly get the list of CompanyEntity sort by average value and using map() to transform the data and collect as List, then put in result
Map<String,List<CompanyEntity>> result = ....
for (Map.Entry<String, List<MainEntity> entry : keyWordMap.entrySet()) {
List<CompanyEntity> list = entry.getValue().stream()
.collect(groupingBy(MainEntity::getCompany,
Collectors.averagingDouble(MainEntity::getCtr)))
.entrySet()
.stream()
.sorted(Comparator.comparing(e -> e.getValue()))
.map(e -> new CompanyEntity(e.getKey(), e.getValue()))
.collect(Collectors.toList());
result.put(entry.getKey(), list);
}
Or you want to do this in one-shot
Map<String,List<CompanyEntity>> mapData =
mainEntityList
.stream()
.collect(Collectors.groupingBy(MainEntity::getKeyWord,
Collectors.groupingBy(MainEntity::getCtr,
Collectors.averagingDouble(MainEntity::getCtr))))
.entrySet()
.stream()
.collect(Collectors.toMap(m -> m.getKey(),
m -> m.entrySet()
.stream()
.sorted(Comparator.comparing(e -> e.getValue()))
.map(e -> new CompanyEntity(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
The other answer completely changed its answer after initially misunderstanding the question and in good StackOverflow spirits it attracted the first upvote so is now accepted and highest upvoted. But this has a few more steps in the code showing what's happening:
This should get you the result:
import java.io.IOException;
import java.util.AbstractMap.SimpleEntry;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Value;
public class CompanyEntityStackOverflowQuestion {
public static void main(String[] args) throws IOException {
//setup test data
MainEntity one = new MainEntity("key1", 10D, "company1");
MainEntity two = new MainEntity("key2", 5D, "company2");
MainEntity three = new MainEntity("key1", 7D, "company3");
MainEntity four = new MainEntity("key2", 3D, "company4");
List<MainEntity> mainEntityList = List.of(one, two, three, four);
//group list by keyword
Map<String, List<MainEntity>> mainEntityByKeyword = mainEntityList.stream()
.collect(Collectors.groupingBy(MainEntity::getKeyword));
//map to companyEntity object
Stream<SimpleEntry<String, List<CompanyEntity>>> mapped = mainEntityByKeyword.entrySet().stream()
.map(entry -> new SimpleEntry<>(entry.getKey(), entry.getValue().stream().map(
getCompanyListFunction()).collect(Collectors.toList())));
//sort and calculate average
Stream<SimpleEntry<String, CompanyEntityListWithStats>> mappedToListWithStats = mapped
.map(entry -> new SimpleEntry<>(entry.getKey(),
new CompanyEntityListWithStats(entry.getValue().stream().mapToDouble(company -> company.cost).average().orElse(0D), //or use Collectors.averagingDouble(company -> company.cost))
sortList(entry.getValue()))));
//collect back to map
Map<String, CompanyEntityListWithStats> collect = mappedToListWithStats
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
//show result
System.out.println(collect);
}
//sort by cost
private static List<CompanyEntity> sortList(List<CompanyEntity> list) {
list.sort(Comparator.comparing(company -> company.cost));
return list;
}
//map MainEntity to CompanyEntity
private static Function<MainEntity, CompanyEntity> getCompanyListFunction() {
return mainEntity -> new CompanyEntity(mainEntity.cost, mainEntity.company);
}
#Value
public static class MainEntity {
public String keyword;
public double cost;
public String company;
}
#Value
public static class CompanyEntity {
public double cost;
public String company;
}
#Value
public static class CompanyEntityListWithStats {
public double average;
public List<CompanyEntity> companyList;
}
}
Output: {key1=CompanyEntityStackOverflowQuestion.CompanyEntityListWithStats(average=8.5, companyList=[CompanyEntityStackOverflowQuestion.CompanyEntity(cost=7.0, company=company3), CompanyEntityStackOverflowQuestion.CompanyEntity(cost=10.0, company=company1)]), key2=CompanyEntityStackOverflowQuestion.CompanyEntityListWithStats(average=4.0, companyList=[CompanyEntityStackOverflowQuestion.CompanyEntity(cost=3.0, company=company4), CompanyEntityStackOverflowQuestion.CompanyEntity(cost=5.0, company=company2)])}
You may be able to skip some steps, this is just quickly typed out. You can of course inline stuff to make it look a lot shorter/cleaner, but this format shows what's happening.

Is it possible to group elements without closing the stream?

Is it possible to group elements in a Stream, but then continue streaming instead of having to create a new stream from the EntrySet of the returned map?
For example, I can do this:
public static void main(String[] args) {
// map of access date to list of users
// Person is a POJO with first name, last name, etc.
Map<Date, List<Person>> dateMap = new HashMap<>();
// ...
// output, sorted by access date, then person last name
dateMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> {
Date date = e.getKey();
// group persons by last name and sort
// this part seems clunky
e.getValue().stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e2 -> {
// pool agent id is the key
String lastName = e2.getKey();
Set<Person> personSet = e2.getValue();
float avgAge = calculateAverageAge(personSet);
int numPersons = personSet.size();
// write out row with date, lastName, avgAge, numPersons
});
});
}
Which works just fine, but seems a little clunky, especially the streaming into a map, and then immediately streaming on the entry set of that map.
Is there a way to group objects in a stream, but continue streaming?
You can shorten your code by using Map.forEach, downstream collectors, TreeMap, and IntSummaryStatistics.
By grouping into a TreeMap (instead of leaving it up to the groupingBy collector), you get the names sorted automatically. Instead of immediately getting the grouped map, you add a summarizingInt collector that turns the list of persons with the same name into IntSummaryStatistics of their ages.
public static void main(String[] args) {
Map<Date, List<Person>> dateMap = new HashMap<>();
dateMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> {
Date date = e.getKey();
e.getValue().stream()
.collect(Collectors.groupingBy(Person::getLastName,
TreeMap::new,
Collectors.summarizingInt(Person::getAge)))
.forEach((name, stats) -> System.out.println(date +" "+
lastName +" "+
stats.getAverage() +" "+
stats.getCount()));
});
}
If you have control over the type of the initial map, you could use TreeMap there as well, and shorten it further:
public static void main(String[] args) {
Map<Date, List<Person>> dateMap = new TreeMap<>();
dateMap.forEach((date, persons -> { ...
There are several different ways to interpret the question, but if we restate the question as, "Is it possible to group elements within a Stream without using a terminal operation and apply stream operations to the resulting groups within the same stream pipeline," then the answer is "Yes." In this restatement of the question, terminal operation is defined in the way that the Java 8 streams API defines it.
Here is an example that demonstrates this.
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
class StreamGrouper {
public static class GroupableObj<K extends Comparable<? super K>, T>
implements Comparable<GroupableObj<K, T>> {
private K key;
private T obj;
private Set<T> setOfObj;
public GroupableObj(K key, T obj) {
if (key == null) {
throw new NullPointerException("Key may not be null");
}
this.key = key;
this.obj = obj;
}
#Override
public int compareTo(GroupableObj<K, T> otherGroupable) {
return key.compareTo(otherGroupable.key);
}
#Override
public boolean equals(Object otherObj) {
if (otherObj == null) {
return false;
}
if (otherObj instanceof GroupableObj) {
GroupableObj<?, ?> otherGroupable =
(GroupableObj<?, ?>)otherObj;
return setOfObj == otherGroupable.setOfObj &&
key.equals(otherGroupable.key);
}
return false;
}
public Set<T> getGroup() {
return setOfObj;
}
public K getKey() {
return key;
}
public T getObject() {
return obj;
}
#Override
public int hashCode() {
return key.hashCode();
}
public void setGroup(Set<T> setOfObj) {
this.setOfObj = setOfObj;
}
}
public static class PeekGrouper<K extends Comparable<? super K>, T>
implements Consumer<GroupableObj<K, T>> {
private Map<K, Set<T>> groupMap;
public PeekGrouper() {
groupMap = new HashMap<>();
}
#Override
public void accept(GroupableObj<K, T> groupable) {
K key = groupable.getKey();
Set<T> group = groupMap.computeIfAbsent(key,
(k) -> new HashSet<T>());
groupable.setGroup(group);
group.add(groupable.getObject());
}
}
public static void main(String[] args) {
Function<Double, Long> myKeyExtractor =
(dblObj) -> Long.valueOf(
(long)(Math.floor(dblObj.doubleValue()*10.0)));
PeekGrouper<Long, Double> myGrouper = new PeekGrouper<>();
Random simpleRand = new Random(20190527L);
simpleRand.doubles(100).boxed().map((dblObj) ->
new GroupableObj<Long, Double>(
myKeyExtractor.apply(dblObj), dblObj)).peek(myGrouper).
distinct().sorted().
map(GroupableObj<Long, Double>::getGroup).
forEachOrdered((grp) -> System.out.println(grp));
}
}
In order to make a program that can be compiled and executed on its own, this example moves away from using the Person objects that are referenced in the question, but the grouping concept is the same, and the code from the question could turn into something like the following.
PeekGrouper<String, Person> myGrouper = new PeekGrouper<>();
e.getValue().stream().map((p) -> new GroupableObj<String, Person>(
p.getLastName(), p)).peek(myGrouper).distinct().sorted().
forEachOrdered(e2 -> {
String lastName = e2.getKey();
Set<Person> personSet = e2.getGroup();
float avgAge = calculateAverageAge(personSet);
int numPersons = personSet.size();
// write out row with date, lastName, avgAge, numPersons
});
Please note that in order for this example to work, it is required that the stream call both the distinct function (which reduces the stream to only a single instance of each group) and the sorted function (which ensures that the entire stream has been processed and the groups have been fully "collected" before processing continues). Also note that as implemented here GroupableObj is not safe to use with parallel streams. If the terminal operation of the stream does not require that the groups be fully "collected" when it processes the objects -- for example, if the terminal operation were something like Collectors.toList() -- then a call to sorted would not be required. The critical point is that any portion of the stream that sees the groups prior to a call to sorted and prior to the end of a terminal operation (including processing during a terminal operation) may see a group that is incomplete.
For the specific example in the question, it may be somewhat less time-efficient to sort the objects before grouping them if many of them are in the same group, but if you are willing to sort the objects before grouping them, you can achieve the same functionality without performing any streaming after doing the grouping. The following is a rewrite of the first example from this answer that demonstrates this.
import java.util.Comparator;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collector;
class PreSortOrderedGrouper {
public static void main(String[] args) {
Function<Double, Long> myKeyExtractor =
(dblObj) -> Long.valueOf(
(long)(Math.floor(dblObj.doubleValue()*10.0)));
Random simpleRand = new Random(20190527L);
Consumer<Set<Double>> groupProcessor =
(grp) -> System.out.println(grp);
simpleRand.doubles(100).boxed().sorted(
Comparator.comparing(myKeyExtractor)).
collect(Collector.of(HashSet<Double>::new,
(set, dblObj) -> {
if (set.isEmpty() || myKeyExtractor.apply(set.iterator().
next()) == myKeyExtractor.apply(dblObj)) {
set.add(dblObj);
} else {
groupProcessor.accept(set);
set.clear();
set.add(dblObj);
}
},
(setOne, setTwo) -> {
throw new UnsupportedOperationException();
},
(finalSet) -> {
groupProcessor.accept(finalSet);
return Integer.valueOf(0);
}));
}
}
I can't be sure that either of these examples will feel less "clunky" to you, but if the example in your question is a pattern you use frequently, you could probably adapt one or both of these examples in ways that will suit your purposes and, aside from a few utility classes, result in no more code than you are currently using.

Java 8 Lambda Collectors.summingLong multiple columns?

I have POJO definition as follows:
class EmployeeDetails{
private String deptName;
private Double salary;
private Double bonus;
...
}
Currently, i have lambda expression for Group By 'deptName' as :
$set.stream().collect(Collectors.groupingBy(EmployeeDetails::getDeptName,
Collectors.summingLong(EmployeeDetails::getSalary));
Question Is it possible to Sum more than one column? I need to compute sum on both fields salary and bonus in one expression instead of multiple times?
SQL representation would be:
SELECT deptName,SUM(salary),SUM(bonus)
FROM TABLE_EMP
GROUP BY deptName;
You need to create an additional class that will hold your 2 summarised numbers (salary and bonus). And a custom collector.
Let's say you have
private static final class Summary {
private double salarySum;
private double bonusSum;
public Summary() {
this.salarySum = 0;
this.bonusSum = 0;
}
#Override
public String toString() {
return "Summary{" +
"salarySum=" + salarySum +
", bonusSum=" + bonusSum +
'}';
}
}
for holding sums. Then you need a collector like this:
private static class EmployeeDetailsSummaryCollector implements Collector<EmployeeDetails, Summary, Summary> {
#Override
public Supplier<Summary> supplier() {
return Summary::new;
}
#Override
public BiConsumer<Summary, EmployeeDetails> accumulator() {
return (summary, employeeDetails) -> {
summary.salarySum += employeeDetails.salary;
summary.bonusSum += employeeDetails.bonus;
};
}
#Override
public BinaryOperator<Summary> combiner() {
return (summary, summary1) -> {
summary.salarySum += summary1.salarySum;
summary.bonusSum += summary1.bonusSum;
return summary;
};
}
#Override
public Function<Summary, Summary> finisher() {
return Function.identity();
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
}
}
With these classes you can collect your results like
final List<EmployeeDetails> employees = asList(
new EmployeeDetails(/* deptName */"A", /* salary */ 100d, /* bonus */ 20d),
new EmployeeDetails("A", 150d, 10d),
new EmployeeDetails("B", 80d, 5d),
new EmployeeDetails("C", 100d, 20d)
);
final Collector<EmployeeDetails, Summary, Summary> collector = new EmployeeDetailsSummaryCollector();
final Map<String, Summary> map = employees.stream()
.collect(Collectors.groupingBy(o -> o.deptName, collector));
System.out.println("map = " + map);
Which prints this:
map = {A=[salary=250.0, bonus=30.0], B=[salary=80.0, bonus=5.0], C=[salary=100.0, bonus=20.0]}
I know you've got your answer, but here is my take(I was writing while the other was posted). There is already a Pair in java in the form of AbstractMap.SimpleEntry.
System.out.println(Stream.of(new EmployeeDetails("first", 50d, 7d), new EmployeeDetails("first", 50d, 7d),
new EmployeeDetails("second", 51d, 8d), new EmployeeDetails("second", 51d, 8d))
.collect(Collectors.toMap(EmployeeDetails::getDeptName,
ed -> new AbstractMap.SimpleEntry<>(ed.getSalary(), ed.getBonus()),
(left, right) -> {
double key = left.getKey() + right.getKey();
double value = left.getValue() + right.getValue();
return new AbstractMap.SimpleEntry<>(key, value);
}, HashMap::new)));
Grouping by is a terminal operation that yields a map. The map produced by the groupingBy in the code below is a Map<String, List<EmployeeDetails>>. I create a new stream using the Map entrySet method. I then create a new Map using Collectors.toMap. This approach uses method chaining to avoid creating another class and create more concise code.
details.stream()
.collect(Collectors.groupingBy(EmployeeDetails::getDeptName))
.entrySet()
.stream()
.collect(Collectors.toMap(x->x.getKey(), x->x.getValue()
.stream()
.mapToDouble(y -> y.getSalary() + y.getBonus())
.sum()));

Categories