3

I am trying to convert the below nested for loop into hashmap using java stream but i got struck in the collector step. Could you please help?

Existing code:

private static HashMap<String, Long> getOutput(List<Employee> eList) {
    HashMap<String, Long> outputList = new HashMap<>();
    for (Employee employee : eList) {
           List<Department> departmentList = employee.getDepartmentList();
              for (Department department : departmentList) {
                 if (department.getType().equals(DepartmentType.SCIENCE)) {
                     outputList.put(employee.getName(),department.getDepartmentId()));
                  }
              }
    }
    return outputList;
}

So far i tried:

private static HashMap<String, Long> getOutput(List<Employee> eList) {
                       return  eList.stream()
                                    .flatMap(emp -> emp.getDepartmentList().stream()
                                    .filter(dept -> dept.getType().equals(DepartmentType.SCIENCE))
                                    .collect(HashMap::new, ???)
              }
Sivasakthi Jayaraman
  • 4,724
  • 3
  • 17
  • 27
  • This might point you int the right direction http://stackoverflow.com/a/20887747/4252352 – Mark Jan 25 '17 at 02:16
  • Your existing code is not working. The spelling of `outputList` and `OutputList` is inconsistent, further, it has type `Map` which does not match the method’s return type `HashMap`. If this was “existing code”, what stopped you from simply copying the working code from your IDE rather than prototyping it within your browser? – Holger Jan 25 '17 at 09:59
  • @Holger, thank you for pointing out the mistake. Actually i can't paste the production code so i prototyped Employee and Department as an example. Fixed the copy paste error in the question. – Sivasakthi Jayaraman Jan 25 '17 at 17:16

3 Answers3

3

It seems like your main issue is preserving the stream's current emp reference after you've done the flatMap. To keep this reference, you will need to flatMap to some sort of class that can hold both the Employee and Department - such as a generic Tuple (aka Pair).

Java doesn't have an intuitive Tuple class built into it's API, so your options are:

  1. Use a 3rd party library that provides a Tuple class (e.g. javatuples)
  2. DIY: Build your own generic Tuple class (see related SO question)
  3. Quick: Add a private inner class which is specifically intended for this lambda

Edit:

The comments (thanks @Holger!) have enlightened that it appears that there are many departments per employee. My original code risks throwing an exception since there would be duplicate keys, while the OP's original code simply overwrites the map entries. Consider using the groupingBycollector and changing the return type of this method.

private static Map<String, List<Long>> getOutput(List<Employee> eList) {
  return eList.stream()
    // get a stream of employee / department pairs
    .flatMap(emp -> emp.getDepartmentList().stream().map(dep -> new EmployeeDepartmentPair(emp, dep))
    // filter the departments to SCIENCE
    .filter(x -> x.department.getType().equals(DepartmentType.SCIENCE))
    // group departmentIds by employee name
    .collect(Collectors.groupingBy(x -> x.employee.getName(), Collectors.mapping(x -> x.department.getDepartmentId(), Collectors.toList())))
}

Original (see above edit):

Here's some updated code using option 3:

private static Map<String, Long> getOutput(List<Employee> eList) {
  return  eList.stream()
    .flatMap(emp -> emp.getDepartmentList().stream().map(dep -> new EmployeeDepartmentPair(emp, dep))
    .filter(x -> x.department.getType().equals(DepartmentType.SCIENCE))
    .collect(Collectors.toMap(x -> x.employee.getName(), x -> x.department.getDepartmentId()));
}

private static class EmployeeDepartmentPair {
  public final Employee employee;
  public final Department department;

  public EmployeeDepartmentPair(Employee emp, Department d) {
    this.employee = emp;
    this.department = d;
  }
}
Community
  • 1
  • 1
souldzin
  • 1,428
  • 11
  • 23
  • @Holger good catch. I've updated the return type. The other alternative is to set the resulting map to a variable then create a new HashTable from that map. Although, there's not really a good reason to force this method to return HashMap instead of Map. – souldzin Jan 25 '17 at 09:59
  • Why not use `Map.Entry` instead of `EmployeeDepartmentPair` ? – rkosegi Jan 25 '17 at 10:01
  • I’ve just noted that even the OP’s original code is inconsistent regarding the map type. Besides that, there is a logical problem. If we assume that there might be multiple departments for an employee (otherwise, why is it a list), there will be collisions in the resulting map. The fact that two employees might have the same name makes matters even worse. Your code will throw an exception in the collision case, but the original’s behavior of silently overwriting is not a solution either… – Holger Jan 25 '17 at 10:03
  • @rkosegi You could. `Map.Entry` is seen as the unofficial `Pair` by some, but it's a matter of style. I personally feel that if it's not a map entry, I shouldn't use it. – souldzin Jan 27 '17 at 04:01
  • @Holger Good thinking, I will update the answer to include `Collectors.groupingBy` since it looks like there are many departments per employee. – souldzin Jan 27 '17 at 04:04
2

This is never going to pretty as a stream, since you need the departments from your first filter to calculate the value to put into the map. So you need to filter the departments twice: the second time to find the one that gave you a positive match the first time and get its Id value.

IMHO, this code is best formatted in its current form, since it makes it clearer to grasp what it is exactly that it does, as well as simpler to debug. Here is, by comparison, the same code turned into a stream:

return eList.stream()
    .flatMap(emp -> emp.getDepartmentList().stream()
    .filter(dep -> dep.getType().equals(DepartmentType.SCIENCE))).collect(
    Collectors.toMap(Employee::getName, emp -> emp.getDepartmentList().stream()
    .filter(dep ->dep.getType.equals(DepartmentType.SCIENCE)
    .findFirst().get().getDepartmentId())), (s, a) -> a); 
}

Essentially, what was missing in your question was the Collectors.toMap()method, which takes as parameters:

  1. The key;
  2. The value (which needs to be computed from the key);
  3. A merge function, which tells the collector what to do in case of duplicate items; in this case, only keep the first value. There is also a toMap() method with only two arguments, but it throws an IllegalStateException if duplicate values are inserted.
MikaelF
  • 3,518
  • 4
  • 20
  • 33
0

I know it's a little late but here is a contribution + explanation into your already beautiful mix of answer:

private static HashMap<String, Long> getOutput(List<Employee> eList) {
        return eList
                .stream() // for each Employee
                .flatMap(emp -> emp.getDepartmentList() // get all his departments
                                    .stream()                   
                                    //filter departments by your predicate
                                    .filter(dept -> dept.getType().equals(DepartmentType.SCIENCE))
                                    // build an Entry with the employee and each department  
                                    .map(dept -> new SimpleEntry(emp.getName(),dept.getDepartmentId())))
                // Each SimpleEntry<Name,DeptId> is then added to the your final Map
                .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue, (val1, val2) ->{ return val1;},HashMap::new));
}

SimpleEntry is just an Implementation of the Map.Entry Interface:

public class SimpleEntry implements Entry<String, Long> {

    private String name;
    private Long deptId;

    public SimpleEntry(String name, Long deptId) {
        this.name = name;
        this.deptId = deptId;
    }
    @Override
    public String getKey() {
        return this.name;
    }
    @Override
    public Long getValue() {
        return this.deptId;
    }
    @Override
    public Long setValue(Long value) {
        return this.deptId = value;
    }
}
arthur
  • 3,245
  • 4
  • 25
  • 34