按多个属性对对象内的列表进行分组:Java 8

Group a list inside an object by multiple attributes : Java 8

如何按两个对象属性对对象内的列表进行分组?

我正在使用 Drools 7.9.0 和 Java 8。我有一个 Result class 用作每个匹配的 Drools 规则的 return。

import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
public class Result implements Serializable {

    private Integer id;
    private String name;
    private List<Occurrences> occurrences = new ArrayList<>();

    public void addOccurrence(Occurrences occurrence) {
        this.occurrences.add(occurrence);
    }
}

Drools 执行后,我得到一个 List<Result>。转换为 JSON 看起来像这样。

当前结果:

[
     {
        name: "Whatever"
        id: 0001,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Whatever"
        id: 0001,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Whatever"
        id: 0002,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Other thing"
        id: 0002,
        occurrences: [{
           (...)
        }]
    }
]

我需要对我的 Result 列表进行分组,这样 ocurrencesidname 分组,就像这样。

预期结果:

[
    {
        name: "Whatever"
        id: 0001,
        occurrences: [{
           (...)
        }, {
           (...)
        }]
    },
    {
        name: "Whatever"
        id: 0002,
        occurrences: [{
           (...)
        }]
    },
    {
        name: "Other thing"
        id: 0002,
        occurrences: [{
           (...)
        }]
    }
]

实现这个的最佳方法是什么?我有两个选择:

  1. 更改 Drools 规则,以便我的 List<Result> 已经按照我需要的方式构建。也许用 addResult 方法创建一个 class 来检查 idname 的列表,并将匹配项添加到正确的条目中。但这并不理想,因为它会增加规则内部的复杂性。
  2. Post-处理我的 List<Result>,因此它将 occurrencesidname 分组。但我不知道如何以优化和简单的方式做到这一点。

执行第二个选项的最佳方法是什么?

你可以这样做,

Map<String, List<Result>> resultsWithSameIdAndName = results.stream()
    .collect(Collectors.groupingBy(r -> r.getId() + ":" + r.getName()));

List<Result> mergedResults = resultsWithSameIdAndName.entrySet().stream()
    .map(e -> new Result(Integer.valueOf(e.getKey().split(":")[0]), 
        e.getKey().split(":")[1],
        e.getValue().stream().flatMap(r -> r.getOccurrences().stream())
            .collect(Collectors.toList())))
        .collect(Collectors.toList());

第二个选项的简单方法可以是:

Map<String, List<Occurences>> resultsMapped = new HashMap<>();
List<Result> collect = results.forEach(r -> resultsMapped.merge(String.format("%s%d", r.name, r.id), 
            r.occurrences, (currentOccurences, newOccurences) -> {
               currentOccurences.addAll(newOccurences);
               return currentOccurences;
            });

在地图中存储你的出现,键是名称和 id 连接在一个字符串中。此键未优化。

您也可以为此使用一些流。这将按 ID 和名称分组,然后为每个组创建一个新的结果对象,其中包含该组的所有实例。

Collection<Result> processed = results.stream().collect(Collectors.groupingBy(r -> r.getId() + r.getName(), 
            Collectors.collectingAndThen(Collectors.toList(),
            l -> new Result(l.get(0).getId(), l.get(0).getName(), 
                    l.stream().flatMap(c -> c.getOccurrences().stream()).collect(Collectors.toList()))
    ))).values();

这需要将 AllArgsConstructor 添加到结果 class,以及一些更多的 getter,但您可以根据需要进行调整。 用于分组的密钥在此示例中不安全。

我会在 Result 中添加以下方法:

Result addOccurrences (List<Occurrences> occurrences) {
  this.occurrences.addAll (occurrences);
  return this;
}

然后你可以使用流:

List<Result> collect = results.stream ()
  .collect (Collectors.groupingBy (Result::getId, Collectors.groupingBy (Result::getName, Collectors.toList ())))
  .values ()
  .stream ()
  .map (Map::values)
  .flatMap (Collection::stream)
  .map (Collection::stream)
  .map (resultStream -> resultStream.reduce ((r0, r1) -> r0.addOccurrences (r1.getOccurrences ())))
  .filter (Optional::isPresent)
  .map (Optional::get)
  .collect (Collectors.toList ());