如何使用 lambda 流迭代嵌套列表?

Posted

技术标签:

【中文标题】如何使用 lambda 流迭代嵌套列表?【英文标题】:How to iterate nested lists with lambda streams? 【发布时间】: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);的答案 【参考方案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 来完成,但无论如何。

【讨论】:

以上是关于如何使用 lambda 流迭代嵌套列表?的主要内容,如果未能解决你的问题,请参考以下文章

通过 cql 流分析在 json msg 中迭代嵌套列表

如何将嵌套列表映射到 Python 中的可迭代字典

如何有效地对抽象列表进行嵌套迭代?

需要处理嵌套列表时如何将递归转换为迭代?

列表理解Python中的嵌套控制流

如何使用 Java 8/lambda 执行嵌套的“if”语句?