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

143

在我们的项目中,我们正在迁移到Java 8,并测试其新功能。

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

在此迁移中,我需要将Guava代码更改为Java 8更改。因此,我所做的更改类型是:

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());
使用guava,我非常舒适地调试代码,因为我可以调试每个转换过程,但我的担忧是如何调试例如 .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没有显示它应该显示的内容,使用peek()方法帮助显示结果。


你不需要将临时变量 result 声明为 Integer。如果你正在将一个 int 映射到另一个 int,那么简单的 int 也可以胜任。 - Holger
另外,我要补充的是,在IntelliJ IDEA 14中有改进的调试器。现在我们可以调试Lamdas了。 - Mikhail
6个回答

103

我通常在使用Eclipse或IntelliJ IDEA调试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 是一个 中间操作 - 换句话说:它是一种懒操作,只有在执行了 终端操作 后才会执行。因此,当你调用 stream.map(n -> n * 2) 时,lambda 表达式并不会立即执行。你需要设置断点,并在调用终端操作(例如 collect)后进行检查。

请参阅流操作以获取更多解释。

更新2:

引用Holger的评论:

这里让人感到棘手的是,调用 map 和 lambda 表达式位于同一行,所以行断点将在两个完全不相关的动作上停止。

map( 后面插入一个换行符可以允许你仅对 lambda 表达式设置断点。而且,调试器不显示 return 语句的中间值并不是不寻常的情况。将 lambda 表达式更改为 n -> { int result=n * 2; return result; } 将允许你检查结果。同样,在逐行调试时适当插入换行符...


我正在使用 Eclipse Kepler SR2(已从 Eclipse 的市场安装了支持 Java 8 的插件)。 - Marlon Bernardes
你也在使用Eclipse吗?只需在.map行上设置断点,然后多次按F8即可。 - Marlon Bernardes
我使用和你一样的Eclipse。我在.map上设置了断点,但只能看到n的值,无法看到转换n*2。在Guava中,我可以调试apply方法并查看参数值和转换块,并调试行为。 - Federico Piazza
10
这里的棘手之处在于map调用和lambda表达式位于同一行,因此行断点将停在两个完全无关的操作上。在map(后插入一个换行符可以让您仅为lambda表达式设置断点。而且,调试器不显示return语句的中间值并不罕见。将lambda改为n -> {int result = n * 2; return result;}可以让您检查result。同样,在逐行步进时适当插入换行符... - Holger
1
@Marlon Bernardes:当然,您可以将其添加到答案中,因为这就是评论的目的:帮助改进内容。顺便说一句,我已经编辑了引用的文本并添加了代码格式... - Holger
显示剩余9条评论

41

IntelliJ有一个非常好用的插件,名为Java Stream Debugger。你可以前往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


2
很遗憾,这并不适用于可选项,它们可以变得同样复杂。 - Daniel Alder

25

使用NetBeans进行lambda表达式的调试效果也非常好。我正在使用NetBeans 8和JDK 8u5。

如果您在包含lambda的代码行上设置断点,实际上会在设置流管道时首先触发一次,然后每个流元素都会触发一次。以您的示例为例,第一次触发断点将是设置流管道的map()调用:

first breakpoint

您可以看到调用堆栈以及main方法的局部变量和参数值,就像您预期的那样。如果继续单步执行,将再次触发“相同”的断点,但这次是在调用lambda中:

enter image description here

请注意,这次调用堆栈深入了流处理机制,并且局部变量是lambda本身的局部变量,而不是包含main方法的局部变量。(我更改了naturals列表中的值,以使其清晰明了。)

正如Marlon Bernardes(+1)所指出的那样,您可以使用peek来检查在流处理过程中通过的值。但是,如果您从并行流中使用此方法,请小心。不同线程上的值可能以不可预测的顺序打印。如果您正在从peek中存储值到调试数据结构中,则该数据结构必须是线程安全的。

最后,如果您正在调试大量的lambda表达式(特别是多行语句lambda表达式),最好将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表达式太复杂,以至于需要逐步执行它,那么您可能需要为它编写一堆单元测试。


我的问题是我无法调试lambda主体,但是您使用方法引用的方法对我想要的东西帮助很大。您可以更新您的答案,使用Holger的方法也可以通过在不同行中添加{ int result=n * 2; return result; }来完美解决问题,因为两个答案都很有帮助。当然要点赞。 - Federico Piazza
1
@Fede 看起来其他答案已经更新了,所以不需要更新我的答案。反正我也讨厌多行 lambda。 :-) - Stuart Marks
是的,必须更改它,因为技术上问题要求lambda表达式,而方法引用不是lambda表达式...尽管可以用于相同的目的。如果您发布Holger的评论,我很乐意接受您的答案。这将是公平的,因为Holger已经回答了,而您的回答也有所帮助。 - Federico Piazza
1
@Stuart Marks:我也更喜欢单行lambda表达式。因此,通常在调试后删除换行符⟨这也适用于其他(普通的)复合语句⟩。 - Holger
1
@Fede 不用担心。作为提问者,您有权选择接受任何您喜欢的答案。感谢您的加一。 - Stuart Marks
2
我认为创建方法引用不仅让方法更容易单元测试,还可以使代码更易读。非常棒的回答!(+1) - Marlon Bernardes

11

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

当我们停在包含lambda的行时,按F7(步进)键,IntelliJ将突出显示将要调试的片段。我们可以使用Tab切换要调试的块,然后再次单击F7选择它。

以下是一些屏幕截图以说明:

1- 按下F7(步进)键,将显示高亮或选择模式 enter image description here

2- 使用Tab多次选择要调试的片段 enter image description here

3- 按下F7(步进)键进行步进 enter image description here


6

1
使用IDE进行调试总是很有帮助的,但是在流中对每个元素进行调试的理想方法是在终止方法操作之前使用peek(),因为Java Streams是惰性评估的,所以除非调用终止方法,否则相应的流将不会被评估。
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());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接