使用Java流将平面列表转换为带有子对象的域对象

时间:2019-06-14 12:47:18

标签: java java-stream

我有传入的对象,它们具有从JDBC结果集中实例化的平面非规范化结构。传入的对象反映了结果集,其中包含大量重复数据,因此我想将数据转换为带有嵌套子集合的父对象列表,即对象图或规范化列表。

传入对象的类如下:

class IncomingFlatItem {
    String clientCode;
    String clientName;
    String emailAddress;
    boolean emailHtml;
    String reportCode;
    String reportLanguage;
}

因此,传入的数据包含每个客户端的多个对象,我想将其聚合到一个客户端对象中,该对象包含该客户端的电子邮件地址对象列表和报告对象列表。

所以Client对象看起来像这样:

class Client {
    String clientCode;
    String clientName;
    Set<EmailAddress> emailAddresses;
    Set<Report> reports;
}

奇怪的是,我找不到现有的答案。我正在查看嵌套流或链接流,但是我想找到最优雅的方法,并且我绝对希望避免for循环。

5 个答案:

答案 0 :(得分:1)

您可以做的一件事是使用构造函数参数和流利的API来发挥自己的优势。想到“嵌套”流和流API(带有动态数据)会很快变得复杂。

这只是使用流利的API简化了事情(您可以改用合适的构建器模式)

class Client {
    String clientCode;
    String clientName;
    Set<EmailAddress> emailAddresses = new HashSet<>();
    Set<Report> reports = new HashSet<>();

    public Client(String clientCode, String clientName) {
        super();
        this.clientCode = clientCode;
        this.clientName = clientName;
    }

    public Client emailAddresses(String address, boolean html) {
        this.emailAddresses = 
             Collections.singleton(new EmailAddress(address, html));
        return this;
    }

    public Client reports(String... reports) {
        this.reports = Arrays.stream(reports)
                        .map(Report::new)
                        .collect(Collectors.toSet());
        return this;
    }

    public Client merge(Client other) {
        this.emailAddresses.addAll(other.emailAddresses);
        this.reports.addAll(other.reports);

        if (null == this.clientName)
            this.clientName = other.clientName;
        if (null == this.clientCode)
            this.clientCode = other.clientCode;

        return this;
    }
}

class EmailAddress {
    public EmailAddress(String e, boolean html) {

    }
}

class Report {
    public Report(String r) {

    }
}

然后...

Collection<Client> clients = incomingFlatItemsCollection.stream()
        .map(flatItem -> new Client(flatItem.clientCode, flatItem.clientName)
                          .emailAddresses(flatItem.emailAddress, flatItem.emailHtml)
                          .reports(flatItem.reportCode, flatItem.reportLanguage))
        .collect(Collectors.groupingBy(Client::getClientCode,
                Collectors.reducing(new Client(null, null), Client::merge)))
        .values();

或者您也可以只使用将IncomingFlatItem对象转换为Client的映射函数。

答案 1 :(得分:1)

您可以使用映射功能将List<IncomingFlatItem>转换为Set<Reports/EmailAddress>的方式:

Function<List<IncomingFlatItem>, Set<EmailAddress>> inferEmailAddress =
        incomingFlatItems -> incomingFlatItems.stream()
                .map(obj -> new EmailAddress(obj.getEmailAddress(), 
                                             obj.isEmailHtml()))
                .collect(Collectors.toSet());

Function<List<IncomingFlatItem>, Set<Report>> inferReports =
        incomingFlatItems -> incomingFlatItems.stream()
                .map(obj -> new Report(obj.getReportCode(), 
                                       obj.getReportLanguage()))
                .collect(Collectors.toSet());

,并进一步使用groupingBy并将条目映射为List<Client>

List<Client> transformIntoGroupedNormalisedContent(
                  List<IncomingFlatItem> incomingFlatItemList) {
    return incomingFlatItemList.stream()
            .collect(Collectors.groupingBy(inc ->
                    Arrays.asList(inc.getClientCode(), inc.getClientName())))
            .entrySet()
            .stream()
            .map(e -> new Client(e.getKey().get(0), 
                                 e.getKey().get(1),
                                 inferEmailAddress.apply(e.getValue()), 
                                 inferReports.apply(e.getValue())))
            .collect(Collectors.toList());
}

答案 2 :(得分:1)

您可以使用此:

List<Client> clients = items.stream()
        .collect(Collectors.groupingBy(i -> Arrays.asList(i.getClientCode(), i.getClientName())))
        .entrySet().stream()
        .map(e -> new Client(e.getKey().get(0), e.getKey().get(1),
                e.getValue().stream().map(i -> new EmailAddress(i.getEmailAddress(), i.isEmailHtml())).collect(Collectors.toSet()),
                e.getValue().stream().map(i -> new Report(i.getReportCode(), i.getReportLanguage())).collect(Collectors.toSet())))
        .collect(Collectors.toList());

在开始时,您将项目按clientCodeclientName进行分组。然后,将结果映射到Client对象。

确保为.equals()hashCode()实现了EmailAddressReport方法,以确保它们在集合中是不同的。

答案 3 :(得分:1)

感谢所有提到Collectors.groupingBy()的回答者。这是设置可以使用reduce()的流的关键。我曾经错误地认为我应该能够单独使用reduce来解决问题,而无需使用groupingBy

也感谢创建一个流畅的API的建议。我添加了IncomingFlatItem.getEmailAddress()IncomingFlatItem.getReport()来从IncomingFlatItem流畅地获取域对象-以及一种将整个平面项目转换为带有其电子邮件和报告且已经嵌套的适当域对象的方法:< / p>

public Client getClient() {
    Client client = new Client();
    client.setClientCode(clientCode);
    client.setClientName(clientName);
    client.setEmailAddresses(new ArrayList());
    client.getEmailAddresses().add(this.getEmailAddress());
    client.setReports(new ArrayList<>());
    client.getReports().add(this.getReport());
    return client;
}

我还按照@SamuelPhilip的建议,在.equals().hashCode()Client上创建了基于业务ID的EmailAddressReport方法

最后,对于域对象,我在.addReport(Report r)类上创建了.addEmail(EmailAddress e)Client,如果尚不存在,则会将子对象添加到Client中。我放弃了Set的{​​{1}}集合类型,因为域模型标准是List,而List意味着要向Sets进行大量转换。

因此,流代码和lambda看起来很简洁。

共有3个步骤:

  1. Lists映射到IncomingFlatItems
  2. 由客户将Clients分组到地图中(严重依赖Clients
  3. 将每个组减少为一个Client.equals()

这是功能算法:

Client

由于切线太长,我花了很长时间才真正了解List<Client> unflatten(List<IncomingFlatItem> flatItems) { return flatItems.parallelStream() .map(IncomingFlatItem::getClient) .collect(Collectors.groupingByConcurrent(client -> client)) .entrySet().parallelStream() .map(kvp -> kvp.getValue() .stream() .reduce(new Client(), (client1, client2) -> { client1.getReports() .forEach(client2::addReport); client1.getEmailAddresses() .forEach(client2::addEmail); return client2; })) .collect(Collectors.toList()); } -我找到了一种解决方案,该解决方案在使用reduce时通过了我的测试,但是在.stream()时完全失败了它在这里的用法。我也必须使用.parallelStream(),否则它将被CopyOnWriteArrayList

随机掉落

答案 4 :(得分:0)

如果您不想遍历条目集(不想处理Map.Entry),或者更喜欢没有groupingBy的其他解决方案,则也可以将toMap与合并功能以汇总您的值。这种方法之所以有效,是因为Client可以容纳初始的单个项目以及所有EmailAddress的累积集合(注意:为简洁起见,我使用了一个实用函数com.google.common.collectSets.union,但是您可以使用例如HashSet)。

以下代码演示了如何执行此操作(以与EmailAddress相同的方式添加Reports,并添加所需的其他字段)。我保留了合并功能的内联功能,没有添加AllArgsConstructor,但是可以自由重构。

static Client mapFlatItemToClient(final IncomingFlatItem item) {
    final Client client = new Client();
    client.clientCode = item.clientCode;
    client.emailAddresses = Collections.singleton(mapFlatItemToEmail(item));
    return client;
}

static EmailAddress mapFlatItemToEmail(final IncomingFlatItem item) {
    final EmailAddress address = new EmailAddress();
    address.emailAddress = item.emailAddress;
    return address;
}

public static void example() {
    final List<IncomingFlatItem> items = new ArrayList<>();

    // Aggregated Client Info by Client Code
    final Map<String, Client> intermediateResult = items.stream()
            .collect(
                    Collectors.<IncomingFlatItem, String, Client> toMap(
                            flat -> flat.clientCode,
                            flat -> mapFlatItemToClient(flat),
                            (lhs, rhs) -> {
                                final Client client = new Client();
                                client.clientCode = lhs.clientCode;
                                client.emailAddresses = Sets.union(lhs.emailAddresses, rhs.emailAddresses);
                                return client;
                            }));

    final Collection<Client> aggregatedValues = intermediateResult.values();
}