Group by Multiple Field Names in Java 8

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,

  1. Make the stream sorted, by comparing the value, such that grouping will execute in order.
  2. 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 one Owner

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 multiple Owners

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



Leave a reply



Submit