Aggregate multiple fields grouping by multiple fields in Java 8
You can do this by creating a class for the grouping key and writing a collector:
I'm simply ading the values per key and count the occurances in a map. In the finisher I devide the sums through the count.
You could get rid of the countMap by sublassing Employee, adding the count and using this class for the supplier/subtotal and using some casting...
You could also make to groupBys one for the sum and another for the count and computing the avarages with the two created maps...
public class Employee {
private String name;
private String department;
private String gender;
private String designation;
private Integer salary;
private Integer bonus;
private Integer perks;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getDepartment()
{
return department;
}
public void setDepartment(String department)
{
this.department = department;
}
public String getGender()
{
return gender;
}
public void setGender(String gender)
{
this.gender = gender;
}
public String getDesignation()
{
return designation;
}
public void setDesignation(String designation)
{
this.designation = designation;
}
public Integer getSalary()
{
return salary;
}
public void setSalary(Integer salary)
{
this.salary = salary;
}
public Integer getBonus()
{
return bonus;
}
public void setBonus(Integer bonus)
{
this.bonus = bonus;
}
public Integer getPerks()
{
return perks;
}
public void setPerks(Integer perks)
{
this.perks = perks;
}
public Employee(String name, String department, String gender, String designation, Integer salary, Integer bonus,
Integer perks)
{
super();
this.name = name;
this.department = department;
this.gender = gender;
this.designation = designation;
this.salary = salary;
this.bonus = bonus;
this.perks = perks;
}
public Employee()
{
super();
}
public static void main(String[] args) {
List<Employee> values = new ArrayList<>();
values.add(new Employee("bill", "dep1", "male", "des1", 100000, 5000, 20));
values.add(new Employee("john", "dep1", "male", "des1", 80000, 4000, 10));
values.add(new Employee("lisa", "dep1", "female", "des1", 80000, 4000, 10));
values.add(new Employee("rosie", "dep1", "female", "des2", 70000, 3000, 15));
values.add(new Employee("will", "dep2", "male", "des1", 60000, 3500, 18));
values.add(new Employee("murray", "dep2", "male", "des1", 70000, 3000, 13));
Map<EmployeeGroup, Employee> resultMap = values.stream().collect(Collectors.groupingBy(e-> new EmployeeGroup(e) , new EmployeeCollector()));
System.out.println(new ArrayList(resultMap.values()));
}
@Override
public String toString()
{
return "Employee [name=" + name + ", department=" + department + ", gender=" + gender + ", designation=" + designation + ", salary=" + salary + ", bonus=" + bonus + ", perks=" + perks + "]";
}
}
Class for the aggregating key
public class EmployeeGroup
{
private String department;
private String gender;
private String designation;
public String getDepartment()
{
return department;
}
public void setDepartment(String department)
{
this.department = department;
}
public String getGender()
{
return gender;
}
public void setGender(String gender)
{
this.gender = gender;
}
public String getDesignation()
{
return designation;
}
public void setDesignation(String designation)
{
this.designation = designation;
}
public EmployeeGroup(Employee employee) {
this.department = employee.getDepartment();
this.gender = employee.getGender();
this.designation = employee.getDesignation();
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((department == null) ? 0 : department.hashCode());
result = prime * result + ((designation == null) ? 0 : designation.hashCode());
result = prime * result + ((gender == null) ? 0 : gender.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
EmployeeGroup other = (EmployeeGroup) obj;
if (department == null)
{
if (other.department != null)
return false;
} else if (!department.equals(other.department))
return false;
if (designation == null)
{
if (other.designation != null)
return false;
} else if (!designation.equals(other.designation))
return false;
if (gender == null)
{
if (other.gender != null)
return false;
} else if (!gender.equals(other.gender))
return false;
return true;
}
}
Collector
public class EmployeeCollector implements Collector<Employee, Employee, Employee> {
private Map<EmployeeGroup,Integer> countMap = new HashMap<>();
@Override
public Supplier<Employee> supplier() {
return () -> new Employee();
}
@Override
public BiConsumer<Employee, Employee> accumulator() {
return this::accumulator;
}
@Override
public BinaryOperator<Employee> combiner() {
return this::accumulator;
}
@Override
public Function<Employee, Employee> finisher() {
return e -> {
Integer count = countMap.get(new EmployeeGroup(e));
e.setBonus(e.getBonus()/count);
e.setPerks(e.getPerks()/count);
e.setSalary(e.getSalary()/count);
return e;
};
}
@Override
public Set<Characteristics> characteristics() {
return Stream.of(Characteristics.UNORDERED)
.collect(Collectors.toCollection(HashSet::new));
}
public Employee accumulator(Employee subtotal, Employee element) {
if (subtotal.getDepartment() == null) {
subtotal.setDepartment(element.getDepartment());
subtotal.setGender(element.getGender());
subtotal.setDesignation(element.getDesignation());
subtotal.setPerks(element.getPerks());
subtotal.setSalary(element.getSalary());
subtotal.setBonus(element.getBonus());
countMap.put(new EmployeeGroup(subtotal), 1);
} else {
subtotal.setPerks(subtotal.getPerks() + element.getPerks());
subtotal.setSalary(subtotal.getSalary() + element.getSalary());
subtotal.setBonus(subtotal.getBonus() + element.getBonus());
EmployeeGroup group = new EmployeeGroup(subtotal);
countMap.put(group, countMap.get(group)+1);
}
return subtotal;
}
}
Java 8: grouping by multiple fields and then sort based on value
To sort base on the value
of Info
,
- Make the stream sorted, by comparing the
value
, such that grouping will execute in order. - When calling groupingBy, Specify LinkedHashMap in
mapFactory
to preserve the insertion order .
Following program demonstrates how to implement.
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Info {
private String account;
private String opportunity;
private Integer value;
private String desc;
private Info(String account, String opportunity, Integer value, String desc) {
super();
this.account = account;
this.opportunity = opportunity;
this.value = value;
this.desc = desc;
}
public static void main(String[] args) {
List<Info> infos = new ArrayList<>();
infos.add(new Info("12", "absddd", 4, "production"));
infos.add(new Info("1234", "abss", 10, "vip"));
infos.add(new Info("1234", "abs", 4, "test"));
infos.add(new Info("1234", "abs", 5, "testing"));
infos.add(new Info("123", "abss", 8, "vip"));
infos.add(new Info("12", "absooo", 2, "test"));
Map<String, Map<String, List<Info>>> sortedResult = infos.stream().sorted(Info::compareByValueDesc)
.collect(Collectors.groupingBy(Info::getAccount, LinkedHashMap::new,
Collectors.groupingBy(r -> r.getOpportunity(), LinkedHashMap::new, Collectors.toList())));
sortedResult.forEach((key, value) -> System.out.println(key + value.toString()));
}
public static int compareByValueDesc(Info other1, Info other2) {
return -other1.value.compareTo(other2.value);
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getOpportunity() {
return opportunity;
}
public void setOpportunity(String opportunity) {
this.opportunity = opportunity;
}
public String toString() {
return this.value.toString();
}
}
Java 8 lambdas grouping by multiple fields
groupingBy(Pojo::getMajorCategory, groupingBy(Pojo::getMinorCategory))
should work, I think?
JAVA List GroupBy multiple fields and Sum
You can use Stream API.
Use Collectors.toMap
and use AbstractMap.SimpleEntry
as key of map. Then define merge function for multiple values of the same key.
List<myPojo> res = new ArrayList<>(rows.stream()
.collect(Collectors.toMap(
e -> new AbstractMap.SimpleEntry<>(e.getName(), e.getCurrency()),
Function.identity(),
(a, b) -> new myPojo(a.getName(), a.getCurrency(), a.getAmount().add(b.getAmount()))))
.values());
Demo:
List<myPojo> list = new ArrayList<>();
list.add(new myPojo("A", "USD", new BigDecimal(1.0)));
list.add(new myPojo("A", "USD", new BigDecimal(2.0)));
list.add(new myPojo("A", "USD", new BigDecimal(3.0)));
list.add(new myPojo("B", "USD", new BigDecimal(1.0)));
list.add(new myPojo("B", "USD", new BigDecimal(2.0)));
list.add(new myPojo("B", "USD", new BigDecimal(3.0)));
list.add(new myPojo("A", "US", new BigDecimal(1.0)));
list.add(new myPojo("A", "US", new BigDecimal(2.0)));
list.add(new myPojo("A", "US", new BigDecimal(3.0)));
List<myPojo> res = new ArrayList<>(list.stream()
.collect(Collectors.toMap(
e -> new AbstractMap.SimpleEntry<>(e.getName(), e.getCurrency()),
Function.identity(),
(a, b) -> new myPojo(a.getName(), a.getCurrency(), a.getAmount().add(b.getAmount()))))
.values());
System.out.println(res.toString());
Output:
[myPojo [name=B, currency=USD, amount=6],
myPojo [name=A, currency=USD, amount=6],
myPojo [name=A, currency=US, amount=6]]
Note: Try to capitalize the name of the class like MyPojo
for better convention
Java Streams API - Grouping by multiple fields
You need to pull out a class to use as a key:
class ProductKey {
private String productName;
private String productCode;
private String price;
private String productId;
private String country;
private String languageCode;
// constructor, equals, hashCode, etc.
// leave out the fields we're not grouping by
}
Then you just need to do:
products.stream().collect(
Collectors.groupingBy(
product -> new ProductKey(product.getProductName(), ...),
Collectors.mapping(Product::getComment, Collectors.toList())));
Group by in Java 8 on multiple fields with aggregations
You should create the custom key for your map. The simplest way is to use Arrays.asList
:
Function<WebRecord, List<Object>> keyExtractor = wr ->
Arrays.<Object>asList(wr.getFiveMinuteWindow(), wr.getCdn(), wr.getIsp(),
wr.getResultCode(), wr.getTxnTime());
Map<List<Object>, Integer> aggregatedData = webRecords.stream().collect(
Collectors.groupingBy(keyExtractor, Collectors.summingInt(WebRecord::getReqBytes)));
In this case the keys are lists of 5 elements in fixed order. Not quite object-oriented, but simple. Alternatively you can define your own type which represents the custom key and create proper hashCode
/equals
implementations.
Java Streams: Grouping a List by two fields
If you know that
one
ContactNumber
can only ever be associated with oneOwner
Then, you don't need inner maps. Just group directly by ContactNumber
:
Map<ContactNumber, List<Car>> groupByOwnerContactNumber = cars.stream()
.collect(Collectors.groupingBy(c -> c.getOwner().getContactNumber()));
Don't be afraid to use lambda expressions ;)
EDIT:
As per the second part of your question:
if a
ContactNumber
could be shared by multipleOwners
You could do a nested grouping by using the overloaded version of Collectors.groupingBy
that accepts a downstream collector:
Map<ContactNumber, Map<Owner, List<Car>>> groupByOwnerAndContactNumber =
cars.stream().collect(Collectors.groupingBy(
c -> c.getOwner().getContactNumber(),
Collectors.groupingBy(Car::getOwner)));
Another way would be to group by a compound key (ContactNumber, Owner)
. You can achieve this by letting the key be a List<Object>
:
Map<List<Object>, List<Car>> groupByCompundOwnerContactNumber =
cars.stream().collect(Collectors.groupingBy(
c -> Arrays.asList(c.getOwner().getContactNumber(), c.getOwner())));
This doesn't require a nested grouping operation.
Note: if you're using Java 9 you can use List.of
instead of Arrays.asList
.
Related Topics
What Is a "Surrogate Pair" in Java
Do I Need <Class> Elements in Persistence.Xml
How to Make a Jar File That Includes Dll Files
Java.Lang.Unsupportedclassversionerror: Bad Version Number in .Class File
Random Weighted Selection in Java
Java - Best Approach to Parse Huge (Extra Large) JSON File
Nosuchmethoderror in Javax.Persistence.Table.Indexes()[Ljavax/Persistence/Index
Why Does a Java Method Reference with Return Type Match the Consumer Interface
Sqlexception: No Suitable Driver Found for Jdbc:Derby://Localhost:1527
What Causes "'Void' Type Not Allowed Here" Error
The JPA Hashcode()/Equals() Dilemma
What Does the Java Assert Keyword Do, and When Should It Be Used
Why Do People Still Use Primitive Types in Java