Java Stream Collectors.Groupingby() Multiple Fields

Java Stream Grouping by multiple fields individually in declarative way in single loop

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)).

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 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())));

Collectors nested grouping-by with multiple fields

If you can use Java 9 or higher, you can use Collectors.flatMapping() to achieve that:

Map<String, Map<String, Long>> eventList = list.stream()
.collect(Collectors.groupingBy(MyObject::getSite, Collectors.flatMapping(
o -> Stream.of(o.getSource(), o.getSeverity()),
Collectors.groupingBy(Function.identity(), Collectors.counting())
)));

The result will be this:

{
USA={maint=2, HARMLESS=2},
GERMANY={CPU_Checker=1, MINOR=1}
}

If you are not able to use Java 9 you can implement the flatMapping() function yourself. You can take a look at Java 9 Collectors.flatMapping rewritten in Java 8, which should help you with that.

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 stream groupingBy and sum multiple fields

A workaround could be to deal with grouping with key as List and casting while mapping back to object type.

List<Foo> result = fooList.stream()
.collect(Collectors.groupingBy(foo ->
Arrays.asList(foo.getName(), foo.getCode(), foo.getAccount()),
Collectors.summingInt(Foo::getTime)))
.entrySet().stream()
.map(entry -> new Foo((String) entry.getKey().get(0),
(Integer) entry.getKey().get(1),
entry.getValue(),
(Integer) entry.getKey().get(2)))
.collect(Collectors.toList());

Cleaner way would be to expose APIs for merge function and performing a toMap.


Edit: The simplification with toMap would look like the following

List<Foo> result = new ArrayList<>(fooList.stream()
.collect(Collectors.toMap(foo -> Arrays.asList(foo.getName(), foo.getCode()),
Function.identity(), Foo::aggregateTime))
.values());

where the aggregateTime is a static method within Foo such as this :

static Foo aggregateTime(Foo initial, Foo incoming) {
return new Foo(incoming.getName(), incoming.getCode(),
incoming.getAccount(), initial.getTime() + incoming.getTime());
}

Group by multiple fields and filter by common value of a field

I. Solution:

A more cleaner and readable solution would be to have a set of empPFcode values ([221]), then filter the employee list only by this set.

First you can use Collectors.groupingBy() to group by empPFcode, then you can use Collectors.mapping(Employee::getCollegeName, Collectors.toSet()) to get a set of collegeName values.

Map<String, Set<String>> pairMap = list.stream().collect(Collectors.groupingBy(Employee::getEmpPFcode,
Collectors.mapping(Employee::getCollegeName, Collectors.toSet())));

will result in: {220=[AB, AC], 221=[DP]}

Then you can remove the entries which includes more than one collegeName:

pairMap.values().removeIf(v -> v.size() > 1); 

will result in: {221=[DP]}

The last step is filtering the employee list by the key set. You can use java.util.Set.contains() method inside the filter:

List<Employee> distinctElements = list.stream().filter(emp -> pairMap.keySet().contains(emp.getEmpPFcode()))
.collect(Collectors.toList());

II. Solution:

If you use Collectors.groupingBy() nested you'll get a Map<String,Map<String,List<Employee>>>:

{
220 = {AB=[...], AC=[...]},
221 = {DP=[...]}
}

Then you can filter by the map size (Map<String,List<Employee>>) to eliminate the entries which has more than one map in their values (AB=[...], AC=[...]).

You still have a Map<String,Map<String,List<Employee>>> and you only need List<Employee>. To extract the employee list from the nested map, you can use flatMap().

Try this:

List<Employee> distinctElements = list.stream()
.collect(Collectors.groupingBy(Employee::getEmpPFcode, Collectors.groupingBy(Employee::getCollegeName)))
.entrySet().stream().filter(e -> e.getValue().size() == 1).flatMap(m -> m.getValue().values().stream())
.flatMap(List::stream).collect(Collectors.toList());


Related Topics



Leave a reply



Submit