Java流:将列表分组成Map of Maps

39

我该如何使用Java Streams进行以下操作?

假设我有以下类:

class Foo {
    Bar b;
}

class Bar {
    String id;
    String date;
}

我有一个List<Foo>,想把它转换成Map <Foo.b.id, Map<Foo.b.date, Foo>。也就是说:首先按Foo.b.id分组,然后按Foo.b.date分组

我尝试了以下两步骤的解决方案,但第二步甚至无法编译:

Map<String, List<Foo>> groupById =
        myList
                .stream()
                .collect(
                        Collectors.groupingBy(
                                foo -> foo.getBar().getId()
                        )
                );

Map<String, Map<String, Foo>> output = groupById.entrySet()
        .stream()
        .map(
                entry -> entry.getKey(),
                entry -> entry.getValue()
                        .stream()
                        .collect(
                                Collectors.groupingBy(
                                        bar -> bar.getDate()
                                )
                        )
        );

提前致谢。


1
你确定列表中的每个元素都是唯一的吗?也就是说,一个id和一个日期会恰好对应一个Foo对象吗? - RealSkeptic
1
你想要一个 Map <Foo.b.id, Map<Foo.b.date, Foo.b> 还是一个 Map <Foo.b.id, Map<Foo.b.date, List<Foo.b>> - assylias
@Eran,你说得对,已经编辑好了 :) - mrod
3个回答

66

假设只有不同的Foo,您可以一次性对数据进行分组:

Map<String, Map<String, Foo>> map = list.stream()
        .collect(Collectors.groupingBy(f -> f.b.id, 
                 Collectors.toMap(f -> f.b.date, Function.identity())));

通过使用静态导入,可以节省一些字符:

Map<String, Map<String, Foo>> map = list.stream()
        .collect(groupingBy(f -> f.b.id, toMap(f -> f.b.date, identity())));

5
假设 (b.id, b.date) 对是不同的。如果是这样,在第二步中,您无需分组,只需收集到 Map 中,其中键为 foo.b.date,值为 foo 本身:
Map<String, Map<String, Foo>> map = 
       myList.stream()
             .collect(Collectors.groupingBy(f -> f.b.id))    // map {Foo.b.id -> List<Foo>}
             .entrySet().stream()
             .collect(Collectors.toMap(e -> e.getKey(),                 // id
                                       e -> e.getValue().stream()       // stream of foos
                                             .collect(Collectors.toMap(f -> f.b.date, 
                                                                       f -> f))));

甚至更简单的是:
Map<String, Map<String, Foo>> map = 
       myList.stream()
             .collect(Collectors.groupingBy(f -> f.b.id, 
                                            Collectors.toMap(f -> f.b.date, 
                                                             f -> f)));

谢谢亚历克斯对代码的评论。我没有意识到在collect(Collectros.groupingBy())之后会创建一个映射表。 - Pierre C

1

另一种方法是在你的键 Bar 上支持等式合约:

class Bar {
    String id;
    String date;

    public boolean equals(Object o){
       if (o == null) return false;
       if (!o.getClass().equals(getClass())) return false;
       Bar other = (Bar)o;
       return Objects.equals(o.id, id) && Objects.equals(o.date, date);
    }

    public int hashCode(){
       return id.hashCode*31 + date.hashCode;
    }    
}

现在你可以直接使用一个 Map<Bar, Foo>

OP肯定希望具有相同“id”但不同“date”的“Bar”也被存储,但在您的方法中它们将被丢弃。您代码中的另一个缺陷是使用空值不安全的方式进行比较“id” - 对于此问题,“Objects.equals()”更好。我无法理解将“o.date”与“this.id”进行比较的目的是什么。 - Alex Salauyou
@SashaSalauyou 第一句话不正确,使用这种方法可以拥有相同的ID和不同的日期。至于空值,Map不允许空键,所以我假设它们没有任何空值。第三点是一个打字错误,可能也会影响第一点! - weston
是的,您的编辑基于iddate进行了相等性比较,而不仅仅是id。现在要搜索Foo,您需要创建一个虚拟的Bar对象--这肯定是一种方法。+1 - Alex Salauyou
@SashaSalauyou 是的,那是正确的。但除非我想执行部分查找,即查找所有与“id”匹配的内容,否则我宁愿使用一个映射而不是一个嵌套映射。 - weston

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