如何高效地将 List 中的对象组合起来?

4

假设我有以下列表:

List<StringInteger> test = new ArrayList<>(); //StringInteger is just a pojo with String and int
test.add(new StringInteger("a", 1));
test.add(new StringInteger("b", 1));
test.add(new StringInteger("a", 3));
test.add(new StringInteger("c", 1));
test.add(new StringInteger("a", 1));
test.add(new StringInteger("c", -1));

System.out.println(test); // [{ a : 1 }, { b : 1 }, { a : 3 }, { c : 1 }, { a : 1 }, { c : -1 }]

我需要编写一个方法,它可以通过字符串键将项目合并并添加整数。因此,结果列表将为 [{ a : 5 }, { b : 1 }, { c : 0 }]
我可以使用 HashMap 完成这个方法,但如果我按照这种方式 - 我必须创建 Map,然后使用增强 for 循环与 if(containsKey(...)),最后再将其转换回 List,这看起来有点冗余。
是否有更优雅的解决方案?我认为 Stream API 中的 flatMap 应该可以做到,但我无法弄清楚如何实现。
以下是我的笨拙解决方案。它可行,但我相信可以更简单地解决问题。
Map<String, Integer> map = new HashMap<>();
for (StringInteger stringInteger : test) {
    if (map.containsKey(stringInteger.getKey())) {
        int previousValue = map.get(stringInteger.getKey());
        map.put(stringInteger.getKey(), previousValue + stringInteger.getValue());
    } else {
        map.put(stringInteger.getKey(), stringInteger.getValue());
    }
}

List<StringInteger> result = map.entrySet()
    .stream()
    .map(stringIntegerEntry -> new StringInteger(stringIntegerEntry.getKey(), stringIntegerEntry.getValue()))
    .collect(Collectors.toList());

System.out.println(result); // [{ a : 5 }, { b : 1 }, { c : 0 }]

3
为这个问题使用“Map”并不是过度设计;这是你需要做的事情。 - Louis Wasserman
4个回答

4
最简单的实现方式可能是:
List<StringInteger> combined = test.stream()
  .collect(
     Collectors.groupingBy(
        StringInteger::getKey,
        Collectors.summingInt(StringInteger::getValue)))
  .entrySet()
  .stream()
  .map(entry -> new StringInteger(entry.getKey(), entry.getValue()))
  .toList();

4

这是一个完整的示例,基于两个好答案(Ivanchenko)(Wasserman)中所见代码。

在Java 16+中,我们使用记录来定义您的StringInt类。

名称StringInt被使用而不是StringInteger,以强调我们具有原始类型int作为成员字段类型,而不是Integer类作为类型。

我认为对于你的目标,Map < String, Integer >足够了。

record StringInt( String string , int integer ) { }

List < StringInt > inputs =
        List.of(
                new StringInt( "a" , 1 ) ,
                new StringInt( "b" , 1 ) ,
                new StringInt( "a" , 3 ) ,
                new StringInt( "c" , 1 ) ,
                new StringInt( "a" , 1 ) ,
                new StringInt( "c" , - 1 )
        );

Map < String, Integer > results =
        inputs
                .stream()
                .collect(
                        Collectors.groupingBy(
                                StringInt :: string ,                          // Key
                                Collectors.summingInt( StringInt :: integer )  // Value
                        ) );

结果 = {a=5, b=1, c=0}

或者,如果您坚持将 StringInt 对象实例化为结果:

record StringInt( String string , int integer ) { }

List < StringInt > inputs =
        List.of(
                new StringInt( "a" , 1 ) ,
                new StringInt( "b" , 1 ) ,
                new StringInt( "a" , 3 ) ,
                new StringInt( "c" , 1 ) ,
                new StringInt( "a" , 1 ) ,
                new StringInt( "c" , - 1 )
        );

List < StringInt > results =
        inputs
                .stream()
                .collect(
                        Collectors.groupingBy(
                                StringInt :: string ,                          // Key
                                Collectors.summingInt( StringInt :: integer )  // Value
                        ) )
                .entrySet()                                                    // Returns a Set < Entry < String , Integer > >
                .stream()
                .map( 
                    entry -> new StringInt( entry.getKey() , entry.getValue() ) 
                )
                .toList();

当类型推断没有问题时,最好避免在lambda参数中使用显式类型。如果你觉得流元素的类型可能不明显,那么更好的方法是将生成map的部分提取到单独的方法中而不是提供显式类型,这样并不会减少复杂性(如果您不同意此编辑,请回滚)。 - Alexander Ivanchenko
@AlexanderIvanchenko 我确实费了些心思来插入lambda参数的显式类型,因为对于学习流和lambda的学生来说,类型并不是立即明显的。但在这种情况下,我会听从你的判断。我也能看出上下文中的getKeygetValue方法可能已经足够了。 - Basil Bourque

3

正如 @LouisWasserman 在评论中所说,HashMap是这项任务的正确工具。

要将整个代码转换为流,可以使用内置的 collector groupingBy() 与下游收集器组合使用 summingInt

result = test.stream()
    .collect(Collectors.groupingBy(   // creates an intermediate map Map<String, Integer>
        StringInteger::getKey,                         // mapping a key
        Collectors.summingInt(StringInteger::getValue) // generating a value
    ))
    .entrySet().stream()
    .map(entry -> new StringInteger(entry.getKey(), entry.getValue()))
    .toList();

2

这是我的笨拙解决方案。它可以工作,但我相信它可以比这更简单。

只是为了向您展示,您可以更整洁地在映射中进行加法计算,而无需使用流:

for (StringInteger stringInteger : test) {
    map.merge(stringInteger.getKey(), stringInteger.getValue(), Integer::sum);
}

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