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

Posted

技术标签:

【中文标题】如何使用 lambda 表达式调试 stream().map(...)?【英文标题】:How to debug stream().map(...) with lambda expressions? 【发布时间】:2014-08-23 20:58:36 【问题描述】:

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

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

在此迁移中,我需要将例如番石榴代码更改为 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());

使用番石榴调试代码我非常舒服,因为我可以调试每个转换过程,但我关心的是如何调试例如.map(n -&gt; 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转换。

有没有办法查看这种转换或轻松调试这段代码?

编辑:我添加了来自不同 cmets 的答案并发布了答案

感谢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。如果您将mapping 到intint,则一个简单的int 也应该这样做...... 另外我补充一点,在 IntelliJ IDEA 14 中有改进的调试器。现在我们可以调试 Lamdas。 【参考方案1】:

在使用 Eclipse 或 IntelliJ IDEA 时,我通常可以毫无问题地调试 lambda 表达式。只需设置一个断点并确保不要检查整个 lambda 表达式(仅检查 lambda 主体)。

另一种方法是使用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());

更新:

我认为您会感到困惑,因为mapintermediate operation - 换句话说:这是一个惰性操作,只有在执行terminal operation 之后才会执行。因此,当您调用 stream.map(n -&gt; n * 2) 时,lambda 主体目前没有被执行。您需要设置断点并在调用终端操作后对其进行检查(在本例中为collect)。

请查看Stream Operations 了解更多说明。

更新 2:

引用Holger's评论:

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

map( 之后插入换行符 将只允许您为 lambda 表达式设置断点。 调试器不显示中间值的情况并不少见 return 声明。将 lambda 更改为 n -&gt; int result=n * 2; return result; 将允许您检查结果。再次,插入行 逐行执行时会适当中断...

【讨论】:

感谢打印屏幕。你有什么版本的 Eclipse 或者你做了什么来得到那个对话框?我尝试使用inspectdisplay 并得到n cannot be resolved to a variable。顺便说一句,peek 也很有用,但它会一次打印所有值。我想查看每个迭代过程以检查转换。有可能吗? 我正在使用 Eclipse Kepler SR2(从 Eclipse 的市场安装了 Java 8 支持)。 你也在使用 Eclipse 吗?只需在.map 行设置断点并多次按 F8。 @Fede:这里的棘手之处在于对 map 的调用和 lambda 表达式在一行中,因此行断点将在两个完全不相关的操作上停止。在map( 之后插入换行符将只允许您为 lambda 表达式设置断点。调试器不显示return 语句的中间值并不罕见。将 lambda 更改为 n -&gt; int result=n * 2; return result; 将允许您检查 result。同样,在逐行执行时适当地插入换行符…… @Marlon Bernardes:当然,您可以将其添加到答案中,因为这就是 cmets 的目的:帮助改进内容。顺便说一句,我已经编辑了引用的文本,添加了代码格式……【参考方案2】:

IntelliJ 为这种情况提供了一个很好的插件,作为 Java Stream Debugger 插件。你应该看看:https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

它通过添加 Trace Current Stream Chain 按钮扩展了 IDEA 调试器工具窗口,当调试器在 Stream API 调用链内停止时,该按钮变为活动状态。

它有很好的界面来处理单独的流操作,并让你有机会遵循一些你应该调试的值。

您可以通过单击此处从“调试”窗口手动启动它:

【讨论】:

太糟糕了,这对可选项无效,它可以增长到相同的复杂性【参考方案3】:

调试 lambda 也适用于 NetBeans。我正在使用 NetBeans 8 和 JDK 8u5。

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

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

请注意,这次调用堆栈位于流机器的深处,并且局部变量是 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; 来完美地工作,我可以接受答案,因为两个答案都有帮助。当然 +1。 @Fede 看起来其他答案已经更新,所以不需要更新我的。无论如何,我讨厌多行 lambda。 :-) @Stuart Marks:我也更喜欢单行 lambda。所以通常我在调试后删除换行符⟨这也适用于其他(普通)复合语句⟩。 @Fede 不用担心。作为提问者,您有权接受您喜欢的任何答案。感谢您的 +1。 我认为创建方法引用,除了使方法更易于单元测试之外,还可以使代码更具可读性。很好的答案! (+1)【参考方案4】:

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

当我们停在包含 lambda 的行时,如果我们按下 F7(进入),那么 IntelliJ 将突出显示要调试的 sn-p。我们可以使用 Tab 切换要调试的块,一旦我们决定了,然后我们再次单击 F7

这里有一些截图来说明:

1- 按F7(步入)键,将显示高亮显示(或选择模式)

2-多次使用Tab选择sn-p进行调试

3- 按 F7 (step into) 键进入

【讨论】:

【参考方案5】:

Intellij IDEA 15 似乎让它变得更加容易,它允许在 lambda 所在行的一部分停止,请参阅第一个功能:http://blog.jetbrains.com/idea/2015/06/intellij-idea-15-eap-is-open/

【讨论】:

【参考方案6】:

使用 IDE 进行调试总是有帮助的,但是通过流中的每个元素进行调试的理想方法是在终端方法操作之前使用 peek(),因为 Java Steam 是延迟评估的,因此除非调用终端方法,否则不会评估相应的流。

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());

【讨论】:

以上是关于如何使用 lambda 表达式调试 stream().map(...)?的主要内容,如果未能解决你的问题,请参考以下文章

Lambda表达式和Stream介绍

Java8新特性Lambda表达式&Stream流&方法引用最全集锦

JAVA8之lambda表达式详解,及stream中的lambda使用

《Java8实战》读书笔记07:Lambda 重构测试和调试(设计模式实现)

《Java8实战》读书笔记07:Lambda 重构测试和调试(设计模式实现)

JAVA基础知识|lambda与stream