如何使用 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 逻辑以使解决方案成为一行。 您也可以在#findFirst
的Optional<T>
结果上使用#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
,但它有点复杂和丑陋(并且需要额外的类)。这也会显示所有 ParseException
s 而不仅仅是最后一个。
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 而不是抛出异常),应该可以执行以下操作。但是,flatMap
和 findFirst
不使用惰性求值 (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
,你将看到所有ParseException
s。
我认为这很好地说明了为什么使用流并不总是最好的解决方案。 :)【参考方案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);
解释:你把所有的firstNodes
和stream()
都搞定了。从你带来的每个 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
方法的事实,该方法采用 Spliterator
和 Iterable
具有 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 流迭代嵌套列表?的主要内容,如果未能解决你的问题,请参考以下文章