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


114

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

在我的项目中,我使用Guava谓词和函数使用Collections2.transform和过滤和转换某些集合Collections2.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 -> 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,如果你是应该做的,以及map平安intint...
霍尔格

我还要补充一点,IntelliJ IDEA 14中有改进的调试器。现在我们可以调试Lamdas了。
米哈伊尔(Mikhail)

Answers:


86

我通常在使用Eclipse或IntelliJ IDEA时调试lambda表达式时没有问题。只需设置一个断点,并确保不要检查整个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());

更新:

我认为您会感到困惑,因为map它是intermediate operation-换句话说:这是一个懒惰的操作,只有在terminal operation执行a之后才会执行。因此,当您调用stream.map(n -> n * 2)lambda主体时,此主体当前未执行。您需要设置一个断点并在调用终端操作(collect在这种情况下)后检查它。

检查流操作以获取更多说明。

更新2:

引用霍尔格的评论:

这里使棘手的是,对map的调用和lambda表达式在一行中,因此行断点将在两个完全不相关的操作上停止。

在此之后插入换行符map( 将允许您仅为lambda表达式设置断点。调试器不显示return语句的中间值也很常见。将lambda更改为n -> { int result=n * 2; return result; } 可以检查结果。再次,逐行步进时,插入行会适当中断。


感谢您的打印屏幕。您拥有什么版本的Eclipse或进行了什么操作以获取该对话框?我尝试使用inspect,并display 和获得n cannot be resolved to a variable。顺便说一句,peek也很有用,但它会立即打印所有值。我想查看每个迭代过程以检查转换。有可能吗?
2014年

我正在使用Eclipse Kepler SR2(从Eclipse的市场安装了Java 8支持)。
马龙·伯纳德斯

您也在使用Eclipse吗?只需在线设置一个断点.map,然后多次按F8。
马龙·伯纳德斯

6
@Fede:这里的棘手之处在于对的调用map和lambda表达式在一行中,因此行断点将在两个完全不相关的操作上停止。在此之后插入换行符map(将允许您仅为lambda表达式设置断点。调试器不显示return语句的中间值也很常见。将lambda更改为n -> { int result=n * 2; return result; }可以进行检查result。同样,逐行步进时插入行会适当中断…
Holger 2014年

1
@Marlon Bernardes:当然,您可以将其添加到答案中,因为这是评论的目的:帮助改进内容。顺便说一句,我编辑了加引号的文本,添加了代码格式…
Holger 2014年

33

IntelliJ在这种情况下有一个很好的插件,例如Java Stream Debugger插件。您应该检查一下:https : //plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

它通过添加“跟踪当前流链”按钮扩展了IDEA调试器工具窗口,当调试器停止在流API调用链中时,该按钮将变为活动状态。

它具有用于处理单独的流操作的漂亮界面,并为您提供了一些您应该调试的值。

Java Stream调试器

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

在此处输入图片说明


23

调试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。
费德里科广场

1
@Fede看起来其他答案已经更新,因此无需更新我的。无论如何,我讨厌多行lambda。:-)
Stuart Marks 2014年

1
@Stuart Marks:我也更喜欢单行lambda。因此,通常我在调试之后删除换行符⟨这也适用于其他(普通)复合语句。
Holger 2014年

1
@Fede不用担心。接受您喜欢的答案是您的特权。感谢您的+1。
Stuart Marks 2014年

1
我认为创建方法引用除了使方法更易于进行单元测试外,还使代码更具可读性。好答案!(+1)
马龙·伯纳德斯


6

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

如果我们按F7(进入),然后在包含lambda的行处停止,则IntelliJ将突出显示要调试的代码段。我们可以切换要调试的块Tab,一旦确定了块,然后F7再次单击即可。

这里有一些截图说明:

1-按F7(进入)键,将显示高亮(或选择模式) 在此处输入图片说明

2- Tab多次选择要调试的代码段 在此处输入图片说明

3-按F7(进入)键进入 在此处输入图片说明


1

使用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());
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.