如何使用 lambda 表达式调试 stream() . map (...) ?

在我们的项目中,我们正在迁移到 java 8,并且我们正在测试它的新特性。

在我的项目中,我使用 Guava 谓词和函数来过滤和转换一些使用 Collections2.transformCollections2.filter的集合。

在这次迁移中,我需要将例如番石榴代码更改为 java8。所以,我正在做的改变是这样的:

List<Integer> naturals = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10,11,12,13);


Function <Integer, Integer> duplicate = new Function<Integer, Integer>(){
@Override
public Integer apply(Integer n)
{
return n * 2;
}
};


Collection result = Collections2.transform(naturals, duplicate);

去..。

List<Integer> result2 = naturals.stream()
.map(n -> n * 2)
.collect(Collectors.toList());

使用番石榴我非常舒服地调试代码,因为我可以调试每个转换过程,但我关心的是如何调试,例如 .map(n -> n*2)

通过使用调试器,我可以看到以下代码:

@Hidden
@DontInline
/** Interpretively invoke this form on the given arguments. */
Object interpretWithArguments(Object... argumentValues) throws Throwable {
if (TRACE_INTERPRETER)
return interpretWithArgumentsTracing(argumentValues);
checkInvocationCounter();
assert(arityCheck(argumentValues));
Object[] values = Arrays.copyOf(argumentValues, names.length);
for (int i = argumentValues.length; i < values.length; i++) {
values[i] = interpretName(names[i], values);
}
return (result < 0) ? null : values[result];
}

但是调试代码并不像 Guava 那样简单,实际上我找不到 n * 2转换。

有没有一种方法可以看到这种转换,或者有没有一种方法可以轻松调试这些代码?

编辑: 我已经添加了来自不同评论的答案,并发布了答案

感谢 Holger回答了我的问题,使用 lambda 块的方法使我能够看到转换过程并调试 lambda 体内发生的事情:

.map(
n -> {
Integer nr = n * 2;
return nr;
}
)

多亏了 Stuart Marks,方法引用的方法也允许我调试转换过程:

static int timesTwo(int n) {
Integer result = n * 2;
return result;
}
...
List<Integer> result2 = naturals.stream()
.map(Java8Test::timesTwo)
.collect(Collectors.toList());
...

感谢 Marlon Bernardes的回答,我注意到我的 Eclipse 没有显示它应该显示的内容,而且使用 eek ()有助于显示结果。

97469 次浏览

在使用 Eclipse 或 IntelliJIDEA 时,调试 lambda 表达式通常没有问题。只需设置一个断点,并确保不要检查整个 lambda 表达式(只检查 lambda 主体)。

Debugging Lambdas

另一种方法是使用 peek检查流的元素:

List<Integer> naturals = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13);
naturals.stream()
.map(n -> n * 2)
.peek(System.out::println)
.collect(Collectors.toList());

更新:

我认为您会感到困惑,因为 map是一个 intermediate operation-换句话说: 它是一个惰性操作,只有在执行了 terminal operation之后才会执行。所以当你调用 stream.map(n -> n * 2)的时候,这个 lambda 主体并没有被执行。您需要设置一个断点,并在调用终端操作(本例中为 collect)之后检查它。

检查 流程运作以获得进一步的解释。

更新2:

引用 Holger 的评论:

这里的棘手之处在于,对 map 和 lambda 的调用 表达式位于一行中,因此行断点将在两行中停止 完全不相干的行为。

map(之后插入换行符 将允许您仅为 lambda 表达式设置断点。 调试器不显示 一个 return语句。将 lambda 更改为 n -> { int result=n * 2; return result; } 将允许您检查结果。再次,插入行 在一行一行地走的时候适当地断开。

调试 lambdas 也可以很好地使用 NetBeans,我使用的是 NetBeans 8和 JDK 8u5。

如果在有 lambda 的行上设置了断点,那么在设置管道时实际上会命中一次,然后在每个流元素上命中一次。使用您的示例,第一次命中断点时将是设置流管道的 map()调用:

first breakpoint

您可以看到 main的调用堆栈和局部变量以及参数值,正如您所期望的那样。如果继续单步执行,将再次命中“相同”断点,只不过这次是在对 lambda 的调用中:

enter image description here

请注意,这一次调用堆栈位于流机制的深处,并且局部变量是 lambda 本身的局部变量,而不是封闭的 main方法。(我已经更改了 naturals列表中的值,以明确这一点。)

正如 Marlon Bernardes指出的(+ 1) ,您可以使用 peek在管道中检查值。不过,如果您使用的是并行流,则要小心。可以跨不同的线程以不可预知的顺序打印这些值。如果将值存储在来自 peek的调试数据结构中,那么该数据结构当然必须是线程安全的。

最后,如果您正在对 lambdas 进行大量的调试(特别是多行语句 lambdas) ,那么最好将 lambda 提取到一个命名方法中,然后使用方法引用来引用它。比如说,

static int timesTwo(int n) {
return n * 2;
}


public static void main(String[] args) {
List<Integer> naturals = Arrays.asList(3247,92837,123);
List<Integer> result =
naturals.stream()
.map(DebugLambda::timesTwo)
.collect(toList());
}

这可能使您在调试时更容易看到发生了什么。此外,这种提取方法使得单元测试更加容易。如果您的 lambda 非常复杂,以至于您需要单步执行它,那么您可能无论如何都需要对它进行大量的单元测试。

Intellij IDEA 15似乎使它更加容易,它允许在 lambda 所在的线段停止,参见第一个特性: http://blog.jetbrains.com/idea/2015/06/intellij-idea-15-eap-is-open/

IntelliJ 有这样一个很好的插件,作为一个 Java 流调试器插件。你应该检查一下: https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

它通过添加跟踪当前流链(Trace Current Stream Chain)按钮来扩展 IDEA 调试器工具窗口,当调试器停留在 Stream API 调用链中时,该按钮将变为活动的。

它具有良好的界面,可用于处理单独的流操作,并使您有机会遵循您应该调试的一些值。

Java Stream Debugger

您可以通过点击这里从 Debug 窗口手动启动它:

enter image description here

为了提供更多更新的细节(2019年10月) ,IntelliJ 添加了一个非常好的集成来调试这种非常有用的代码。

当我们停在一个包含 lambda 的行时,如果我们按 F7(step into) ,那么 IntelliJ 将突出显示要调试的代码片段。我们可以切换哪些块与 Tab调试,一旦我们决定,然后我们单击 F7再次。

这里有一些截图来说明:

F7(步进)键,将显示高光(或选择模式) enter image description here

多次使用 Tab选择要调试的代码段 enter image description here

F7(进入)键进入 enter image description here

使用 IDE 进行调试总是很有帮助的,但是通过流中的每个元素进行调试的理想方法是在终端方法操作之前使用 Peak () ,因为 Java 蒸汽是延迟计算的,所以除非调用终端方法,否则不会计算相应的流。

List<Integer> numFromZeroToTen = Arrays.asList(1,2,3,4,5,6,7,8,9,10);


numFromZeroToTen.stream()
.map(n -> n * 2)
.peek(n -> System.out.println(n))
.collect(Collectors.toList());