javalistjava-stream

Creating a new list of normalized object copies from a Java list using the Java Stream API


From a REST service, I will get the response as List of Employees. The list may contains multiple addresses for same employee as defined below.

[Employee{empId=1, name='Emp1', address='Emp1 Address1'},
Employee{empId=1, name='Emp1', address='Emp1 Address 2'},
Employee{empId=2, name='Emp2', address='Emp2 Address 1'}]

By creating an another list i.e List<EmployeeNormalized>, the above response needs to be processed in a normalized way, as defined below.

[EmployeeNormalized{empId=1, name='Emp1',
addresses=[Emp1 Address1, Emp1 Address 2]},
EmployeeNormalized{empId=2, name='Emp2', addresses=[Emp2 Address 1]}]

Code snippet:

class Employee {
    private int empId;
    private String name;
    private String address;
    // 50 other properties

    public Employee(int empId, String name, String address) {
        this.empId = empId;
        this.name = name;
        this.address = address;
    }
   // Setters and Getters
}

class EmployeeNormalized {
    private int empId;
    private String name;
    private List<String> addresses;
    // 50 other properties

    public EmployeeNormalized(int empId, String name, List<String> address) {
        this.empId = empId;
        this.name = name;
        this.addresses = address;
    }
   // Setters and Getters
} 

List<EmployeeNormalized> must contain unique employee objects and the List<String> in the EmployeeNormalized class should accommodate all the addresses for that employee.

Note that the Employee class has around 50 properties.

How do I create this normalized form of the list?


Solution

  • Stream solution:

    public class Normalize {
    
        public static void main(String[] args) {
            List<Employee> employees = new ArrayList<>();
            employees.add(new Employee(1, "Emp1", "Address 1"));
            employees.add(new Employee(1, "Emp1", "Address 2"));
            employees.add(new Employee(2, "Emp2", "Address 3"));
            List<EmployeeNormalized> employeeNormalizedList = employees.stream()
                    .map(new Function<Employee, EmployeeNormalized>() {
    
                        private final Map<Integer, EmployeeNormalized> employeeIdMap = new HashMap<>();
    
                        @Override
                        public EmployeeNormalized apply(Employee employee) {
                            EmployeeNormalized normalized = this.employeeIdMap.computeIfAbsent(employee.getEmpId(),
                                    key -> new EmployeeNormalized(employee.getEmpId(), employee.getName(), new ArrayList<>()));
                            normalized.getAddresses().add(employee.getAddress());
                            return normalized;
                        }
                    })
                    .distinct()
                    .collect(Collectors.toList());
            employeeNormalizedList.forEach(System.out::println);
        }
    }
    

    Quite complex, need to call distinct to get rid of duplicating instances.

    I would go for simple loop:

    public class Normalize {
    
        public static void main(String[] args) {
            List<Employee> employees = new ArrayList<>();
            employees.add(new Employee(1, "Emp1", "Address 1"));
            employees.add(new Employee(1, "Emp1", "Address 2"));
            employees.add(new Employee(2, "Emp2", "Address 3"));
    
            Map<Integer, EmployeeNormalized> employeeIdMap = new HashMap<>();
            for (Employee employee : employees) {
                EmployeeNormalized normalized = employeeIdMap.computeIfAbsent(employee.getEmpId(), key -> new EmployeeNormalized(employee.getEmpId(), employee.getName(), new ArrayList<>()));
                normalized.getAddresses().add(employee.getAddress());
            }
            List<EmployeeNormalized> employeeNormalizedList = new ArrayList<>(employeeIdMap.values());
            employeeNormalizedList.forEach(System.out::println);
        }
    }
    

    Basically both solutions use employee id as unique identifer, and map instances to id. If id is met for the first time, create instance and add address, if there is already instances for this id, get the instance and add address.

    Edit: Since computeIfAbsent is undesireable due to many properties, you can add no argument constructor and transfer property values with setters. The best option would be to use mapping library, then you could do it with computeIfAbsent as well.

    public class Normalize {
    
        public static void main(String[] args) {
            List<Employee> employees = new ArrayList<>();
            employees.add(new Employee(1, "Emp1", "Address 1"));
            employees.add(new Employee(1, "Emp1", "Address 2"));
            employees.add(new Employee(2, "Emp2", "Address 3"));
    
            Map<Integer, EmployeeNormalized> employeeIdMap2 = new HashMap<>();
            for (Employee employee : employees) {
                int id = employee.getEmpId();
                EmployeeNormalized normalized = employeeIdMap2.get(id);
                if (normalized == null) {
                    normalized = new EmployeeNormalized();
                    normalized.setEmpId(id);
                    normalized.setName(employee.getName());
                    normalized.setAddresses(new ArrayList<>());
                    //set other properties
                    //or even better use mapping library to create normalized and transfer property values
                    employeeIdMap2.put(id, normalized);
                }
                normalized.getAddresses().add(employee.getAddress());
            }
            List<EmployeeNormalized> employeeNormalizedList2 = new ArrayList<>(employeeIdMap2.values());
            employeeNormalizedList2.forEach(System.out::println);
        }
    }