For Java 8 streams, there are several inbuilt collectors available like java.util.stream.Collectors.toList(), java.util.stream.Collectors.summarizingInt() etc. This article will explain how to create your own custom collector. This can also be used to collect/return multiple values from streams.
Example problem:
- Consider we have employee class having name, role & department.
- From the list of such employees, we have to get –
- Count of Employees with role “Lead”
- Count of employees with department “Sales”
- This should be achieved in single stream iteration using custom collector.
Example implementation:
Lets create simple Employee object. For simplicity we will omit getter/setter methods.
1 2 3 4 5 6 7 8 9 10 11 |
class Employee { String name; String role; String department; public Employee(String name, String role, String department) { this.name = name; this.role = role; this.department = department; } } |
As a final result, we will be expecting single object having both the counts. Output class will be like this.
1 2 3 4 |
class EmployeeStats { int leadEmployeeCount; int salesDepartmentEmployeeCount; } |
Now lets create a collector which can convert list of employees into EmployeeStats with counts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class EmployeeStatsCollector implements Collector<Employee, EmployeeStats, EmployeeStats> { @Override public Supplier<EmployeeStats> supplier() { return EmployeeStats::new; } @Override public BiConsumer<EmployeeStats, Employee> accumulator() { return (s, e) -> { if (e.role.contains("Lead")) s.leadEmployeeCount++; if (e.department.contains("Sales")) s.salesDepartmentEmployeeCount++; }; } @Override public BinaryOperator<EmployeeStats> combiner() { return (_this, other) -> { _this.leadEmployeeCount = _this.leadEmployeeCount + other.leadEmployeeCount; _this.salesDepartmentEmployeeCount = _this.salesDepartmentEmployeeCount + other.salesDepartmentEmployeeCount; return _this; }; } @Override public Function<EmployeeStats, EmployeeStats> finisher() { return (e -> e); } @Override public Set<Characteristics> characteristics() { return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); } } |
Explanation of EmployeeStatsCollector
java.util.stream.Collector expects below mandatory methods implemented. Lets look at their meaning from our example perspective.
- supplier –
- java.util.function.Supplier that can provide new instance of EmployeeStats.
- We simply pass a supplier which provides new instance of EmployeeStatsusing “new” operator.
- accumulator –
- java.util.function.BiConsumer that will take EmployeeStats & Employee objects & updates statistics into EmployeeStats as per values in Employee object.
- We create BiConsumer which takes 2 method arguments EmployeeStats & Employee.
- Based on role & department of employee we increase leadEmployeeCount & salesDepartmentEmployeeCount in EmployeeStat.
- combiner –
- This is somewhat similar to accumulator. Accumulator updates EmployeeStats instance from Employee object. Combiner updated EmployeeStats instance from another EmployeeStats instance.
- We create BinaryOperator which takes 2 EmployeeStats.
- We add values from other EmployeeStats into _this EmployeeStats & return _this which will have added values of both EmployeeStats instances.
- finisher –
- This is for final transformation.
- For our example we will just return existing EmployeeStats instance as it is without any changes.
- characteristics – For our example we will use Collector.Characteristics.IDENTITY_FINISH
How Java Stream uses collector:
Now Java stream API might use above implementations in two ways.
- First create new instance of EmployeeStats using supplier
- Then call accumulator for each Employee object one by one & pass earlier EmployeeStats object in each call.
- At the end, call finisher & get final EmployeeStats result.
Or
- First create new instance of EmployeeStats using supplier
- Then call accumulator for some Employee object one by one & pass earlier EmployeeStats object in each call.
- Create another new instance of EmployeeStats using supplier
- Then call accumulator for remaining Employee object one by one & pass earlier another EmployeeStats object in each call.
- At the end, call combiner to combine both EmployeeStats objects & get final EmployeeStats result.
Testing EmployeeStatsCollector:
Now lets test our accumulator.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class EmployeeStatsExample { public static void main(String[] args) { List<Employee> employees = Arrays.asList(new Employee("Tom", "Lead", "Marketing"), new Employee("John", "Manager", "Marketing"), new Employee("Hary", "Lead", "Sales"), new Employee("Jimmy", "Manager", "Sales"), new Employee("Ravi", "Lead", "Developer")); EmployeeStats employeeStats = employees.stream().collect(new EmployeeStatsCollector()); System.out.println("leadEmployeeCount = " + employeeStats.leadEmployeeCount); System.out.println("salesDepartmentEmployeeCount = " + employeeStats.salesDepartmentEmployeeCount); } } |
Output:
1 2 |
leadEmployeeCount = 3 salesDepartmentEmployeeCount = 2 |