如何使用Java Stream将嵌套的for循环转换为HashMap

3

我可以帮助你翻译,以下是需要翻译的内容:

我正在尝试使用Java流将下面的嵌套for循环转换为哈希映射,但我在收集器步骤中遇到了困难。你能帮忙吗?

现有代码:

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;
}

到目前为止我尝试过:

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, ???)
              }

这可能指向正确的方向 https://dev59.com/N2855IYBdhLWcg3wy3sc#20887747 - Mark
你现有的代码无法工作。outputListOutputList的拼写不一致,而且它的类型是Map,与方法的返回类型HashMap不匹配。如果这是“现有代码”,为什么不直接从IDE中复制可工作的代码,而要在浏览器中进行原型设计呢? - Holger
@Holger,感谢您指出错误。实际上我无法粘贴生产代码,所以我用Employee和Department作为示例进行了原型设计。已经修复了问题中的复制粘贴错误。 - Sivasakthi Jayaraman
3个回答

3
看起来您的主要问题是在做完flatMap后保留流的当前emp引用。为了保留这个引用,您需要flatMap到一些可以保存Employee和Department的类,比如通用Tuple(也称为Pair)。
Java没有直观的Tuple类内置到其API中,因此您的选择有:
  1. 使用提供Tuple类的第三方库(例如javatuples)。
  2. 自己动手:构建自己的通用Tuple类(参见相关SO question)。
  3. 快速:添加一个专门为此lambda设计的私有内部类。
编辑: 评论(感谢@Holger!)启示我们,似乎每个员工有很多部门。我的原始代码存在风险,因为会有重复的键而抛出异常,而OP的原始代码只是覆盖了映射条目。考虑使用groupingBy收集器,并更改此方法的返回类型。
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())))
}

以下是使用选项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;
  }
}

@Holger 好发现。我已经更新了返回类型。另一种选择是将结果映射设置为变量,然后从该映射创建新的HashTable。虽然,没有真正的好理由强制此方法返回HashMap而不是Map。 - souldzin
为什么不使用 Map.Entry 而是使用 EmployeeDepartmentPair - rkosegi
我刚刚注意到,即使是原始代码的类型映射也存在不一致性。此外,还存在逻辑问题。如果我们假设一个员工可能有多个部门(否则,为什么是列表),那么在生成的映射中将会发生冲突。两个员工可能具有相同的姓名,这使得情况更加糟糕。您的代码会在冲突情况下抛出异常,但原始代码静默覆盖的行为也不是解决方案... - Holger
@rkosegi 你可以这样做。Map.Entry被一些人视为非官方的Pair,但这只是一个风格问题。我个人认为,如果不是map entry,就不应该使用它。 - souldzin
@Holger 很好的想法,我会更新答案并包括 Collectors.groupingBy,因为每个员工似乎都有很多部门。 - souldzin

2
这段代码不适合用流的方式呈现,因为你需要使用第一个过滤器中的部门来计算要放入映射中的值。所以你需要对部门进行两次过滤:第二次是为了找到第一次匹配成功的部门并获取它的Id值。
个人认为,这种格式最好保留原样,因为它更清晰地表明了它的确切功能,并且更容易调试。相比之下,以下是将相同的代码转换为流的示例:
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); 
}

实际上,你的问题中缺失了 Collectors.toMap() 方法,它需要三个参数:

  1. 键;
  2. 值(需要从键计算出来);
  3. 合并函数,告诉收集器在出现重复项时该怎么做;在这种情况下,只保留第一个值。也有一个只有两个参数的 toMap() 方法,但是如果插入重复值,则会抛出 IllegalStateException 异常。

0

我知道有点晚了,但是在你已经很好的答案中,这里有一个贡献和解释:

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 只是 Map.Entry 接口的一个实现:

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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接