【问题标题】:How to iterate nested lists with lambda streams?如何使用 lambda 流迭代嵌套列表?
【发布时间】:2015-05-26 17:53:40
【问题描述】:

我正在尝试使用 `stream 将以下代码重构为 lambda 表达式,尤其是嵌套的 foreach 循环:

public static Result match (Response rsp) {
    Exception lastex = null;

    for (FirstNode firstNode : rsp.getFirstNodes()) {
        for (SndNode sndNode : firstNode.getSndNodes()) {
            try {
                if (sndNode.isValid())
                return parse(sndNode); //return the first match, retry if fails with ParseException
            } catch (ParseException e) {
                lastex = e;
            }
        }
    }

    //throw the exception if all elements failed
    if (lastex != null) {
        throw lastex;
    }

    return null;
}

我开始:

rsp.getFirstNodes().forEach().?? // how to iterate the nested 2ndNodes?

【问题讨论】:

  • 您将直接返回第一个元素 - parse(sndNode);。它将根据什么标准进入下一个元素?它在做什么样的比赛?
  • 我返回第一个匹配条件且没有解析错误的元素。
  • 我将使用flatMap() 展平列表,然后应用map() 操作进行解析(如果不成功则返回null),filter() 去除空值,最后findFirst() 返回第一个解析值。而且我会放弃抛出最后一个异常(为什么是最后一个?为什么不是第一个或倒数第二个?),因为它只会引起混乱。
  • 感谢您的建议。关于异常:我想返回第一个成功解析的 2ndnode。我不在乎解析失败的节点。只有当没有元素被解析时,我才想抛出异常以指示存在解析错误(而不仅仅是与过滤器不匹配的元素)。
  • ...并完成@biziclop findFirst().orElseThrow(WhatEverRuntimeException::new);的答案

标签: java lambda java-8 java-stream


【解决方案1】:

看平面图:

flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
返回由替换每个元素的结果组成的流 此流的内容与生成的映射流的内容 将提供的映射函数应用于每个元素。

假设isValid() 不抛出的代码示例

Optional<SndNode> sndNode = rsp.getFirstNodes()
  .stream()
  .flatMap(firstNode -> firstNode.getSndNodes().stream())  //This is the key line for merging the nested streams
  .filter(sndNode -> sndNode.isValid())
  .findFirst();

if (sndNode.isPresent()) {
    try {
        parse(sndNode.get());
    } catch (ParseException e) {
        lastex = e;
    }
}

【讨论】:

  • 更多关于解决方案的细节会很方便。
  • 添加了代码示例编辑。我相信有人可以弄清楚如何在流中保持 try catch 和 return 逻辑以使解决方案成为一行。
  • 您也可以在#findFirstOptional&lt;T&gt; 结果上使用#map#orElse(异常可以用框指定或简单地重新抛出)。
【解决方案2】:

我担心使用流和 lambda 表达式可能会影响您的性能。您当前的解决方案返回第一个有效且可解析的节点,但无法中断流上的操作,例如 for-each (source)。

此外,因为您可以有两个不同的输出(返回结果或抛出异常),所以单行表达式无法做到这一点。

这是我想出的。它可能会给你一些想法:

public static Result match(Response rsp) throws Exception {
    Map<Boolean, List<Object>> collect = rsp.getFirstNodes().stream()
            .flatMap(firstNode -> firstNode.getSndNodes().stream()) // create stream of SndNodes
            .filter(SndNode::isValid) // filter so we only have valid nodes
            .map(node -> {
                // try to parse each node and return either the result or the exception
                try {
                    return parse(node);
                } catch (ParseException e) {
                    return e;
                }
            }) // at this point we have stream of objects which may be either Result or ParseException
            .collect(Collectors.partitioningBy(o -> o instanceof Result)); // split the stream into two lists - one containing Results, the other containing ParseExceptions

    if (!collect.get(true).isEmpty()) {
        return (Result) collect.get(true).get(0);
    }
    if (!collect.get(false).isEmpty()) {
        throw (Exception) collect.get(false).get(0); // throws first exception instead of last!
    }
    return null;
}

如开头所述,可能存在性能问题,因为这将尝试解析每个有效节点


编辑:

为避免解析所有节点,您可以使用reduce,但它有点复杂和丑陋(并且需要额外的类)。这也会显示所有 ParseExceptions 而不仅仅是最后一个。

private static class IntermediateResult {

    private final SndNode node;
    private final Result result;
    private final List<ParseException> exceptions;

    private IntermediateResult(SndNode node, Result result, List<ParseException> exceptions) {
        this.node = node;
        this.result = result;
        this.exceptions = exceptions;
    }

    private Result getResult() throws ParseException {
        if (result != null) {
            return result;
        }
        if (exceptions.isEmpty()) {
            return null;
        }
        // this will show all ParseExceptions instead of just last one
        ParseException exception = new ParseException(String.format("None of %s valid nodes could be parsed", exceptions.size()));
        exceptions.stream().forEach(exception::addSuppressed);
        throw exception;
    }

}

public static Result match(Response rsp) throws Exception {
    return Stream.concat(
                    Arrays.stream(new SndNode[] {null}), // adding null at the beginning of the stream to get an empty "aggregatedResult" at the beginning of the stream
                    rsp.getFirstNodes().stream()
                            .flatMap(firstNode -> firstNode.getSndNodes().stream())
                            .filter(SndNode::isValid)
            )
            .map(node -> new IntermediateResult(node, null, Collections.<ParseException>emptyList()))
            .reduce((aggregatedResult, next) -> {
                if (aggregatedResult.result != null) {
                    return aggregatedResult;
                }

                try {
                    return new IntermediateResult(null, parse(next.node), null);
                } catch (ParseException e) {
                    List<ParseException> exceptions = new ArrayList<>(aggregatedResult.exceptions);
                    exceptions.add(e);
                    return new IntermediateResult(null, null, Collections.unmodifiableList(exceptions));
                }
            })
            .get() // aggregatedResult after going through the whole stream, there will always be at least one because we added one at the beginning
            .getResult(); // return Result, null (if no valid nodes) or throw ParseException
}

EDIT2:

一般来说,在使用findFirst()等终端操作符时也可以使用惰性求值。因此,通过对要求的微小更改(即返回 null 而不是抛出异常),应该可以执行以下操作。但是,flatMapfindFirst 不使用惰性求值 (source),因此此代码尝试解析所有节点。

private static class ParsedNode {
    private final Result result;

    private ParsedNode(Result result) {
        this.result = result;
    }
}

public static Result match(Response rsp) throws Exception {
    return rsp.getFirstNodes().stream()
            .flatMap(firstNode -> firstNode.getSndNodes().stream())
            .filter(SndNode::isValid)
            .map(node -> {
                try {
                    // will parse all nodes because of flatMap
                    return new ParsedNode(parse(node));
                } catch (ParseException e ) {
                    return new ParsedNode(null);
                }
            })
            .filter(parsedNode -> parsedNode.result != null)
            .findFirst().orElse(new ParsedNode(null)).result;
}

【讨论】:

  • 太好了,谢谢!是否可以使用 findFirst() 对其进行优化,从而不必解析每个有效节点?
  • @membersound 我已经编辑了我的答案并添加了另一个解决方案 - 有点难看,但不解析所有节点。另外,正如有人建议的那样,有点不清楚为什么它会抛出最后一个ParseException,所以我也修复了它 - 现在如果没有Result,你将看到所有ParseExceptions。
  • 我认为这很好地说明了为什么使用流并不总是最好的解决方案。 :)
【解决方案3】:

尝试使用map 转换原始来源。

   rsp.getFirstNodes().stream().map(FirstNode::getSndNodes)
               .filter(sndNode-> sndNode.isValid())
               .forEach(sndNode->{
   // No do the sndNode parsing operation Here.
   })

【讨论】:

  • stream() 是否保持原始FirstNodes 列表的顺序?我必须按照它包含在Response 中的顺序对其进行迭代,这就是我从element.forEach 开始的原因...?
  • 没有流不能向您保证处理了哪些订单元素
【解决方案4】:

你可以像下面这样迭代嵌套循环

allAssessmentsForJob.getBody().stream().forEach(assessment -> {
        jobAssessments.stream().forEach(jobAssessment -> {
            if (assessment.getId() == jobAssessment.getAssessmentId()) {
                jobAssessment.setAssessment(assessment);
            }
        });
    });

【讨论】:

    【解决方案5】:

    有点晚了,但这里有一个可读的方法:

       Result = rsp.getFirstNodes()
            .stream()
            .flatMap(firstNode -> firstNode.getSndNodes.stream())
            .filter(secondNode::isValid))
            .findFirst()
            .map(node -> this.parseNode(node)).orElse(null);
    

    解释:你把所有的firstNodesstream() 都搞定了。从你带来的每个 firstNode 中取出 n SndNodes。您检查每个SndNodes 以找到第一个 有效的。如果没有有效的 SndNode,那么我们将得到一个空值。如果有,它会被解析成Result

    parseMethod() 与原来的没有变化:

    public Result parseNode(SndNode node){
            try {
            ...
            ... // attempt to parsed node 
        } catch (ParseException e) {
            throw new ParseException;
        }   
    } 
    

    【讨论】:

      【解决方案6】:

      您可以使用 StreamSupport 提供了一个 stream 方法的事实,该方法采用 SpliteratorIterable 具有 spliterator 方法。

      然后您只需要一种机制将您的结构扁平化为 Iterable - 类似这样。

      class IterableIterable<T> implements Iterable<T> {
      
          private final Iterable<? extends Iterable<T>> i;
      
          public IterableIterable(Iterable<? extends Iterable<T>> i) {
              this.i = i;
          }
      
          @Override
          public Iterator<T> iterator() {
              return new IIT();
          }
      
          private class IIT implements Iterator<T> {
      
              // Pull an iterator.
              final Iterator<? extends Iterable<T>> iit = i.iterator();
              // The current Iterator<T>
              Iterator<T> it = null;
              // The current T.
              T next = null;
      
              @Override
              public boolean hasNext() {
                  boolean finished = false;
                  while (next == null && !finished) {
                      if (it == null || !it.hasNext()) {
                          if (iit.hasNext()) {
                              it = iit.next().iterator();
                          } else {
                              finished = true;
                          }
                      }
                      if (it != null && it.hasNext()) {
                          next = it.next();
                      }
                  }
                  return next != null;
              }
      
              @Override
              public T next() {
                  T n = next;
                  next = null;
                  return n;
              }
          }
      
      }
      
      public void test() {
          List<List<String>> list = new ArrayList<>();
          List<String> first = new ArrayList<>();
          first.add("First One");
          first.add("First Two");
          List<String> second = new ArrayList<>();
          second.add("Second One");
          second.add("Second Two");
          list.add(first);
          list.add(second);
          // Check it works.
          IterableIterable<String> l = new IterableIterable<>(list);
          for (String s : l) {
              System.out.println(s);
          }
          // Stream it like this.
          Stream<String> stream = StreamSupport.stream(l.spliterator(), false);
      }
      

      您现在可以直接从您的Iterable 流式传输。

      初步研究表明,这应该使用flatMap 来完成,但无论如何。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-03-21
        • 1970-01-01
        • 2011-10-14
        • 1970-01-01
        • 2016-06-29
        • 2020-02-02
        • 1970-01-01
        相关资源
        最近更新 更多