¿Cómo depurar stream (). Map (…) con expresiones lambda?

114

En nuestro proyecto estamos migrando a java 8 y estamos probando las nuevas características del mismo.

En mi proyecto, estoy usando predicados y funciones de Guava para filtrar y transformar algunas colecciones usando Collections2.transformy Collections2.filter.

En esta migración, necesito cambiar, por ejemplo, el código de guayaba a los cambios de java 8. Entonces, los cambios que estoy haciendo son del tipo:

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

A...

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

Usando guayaba me sentí muy cómodo depurando el código ya que podía depurar cada proceso de transformación, pero mi preocupación es cómo depurar, por ejemplo .map(n -> n*2).

Usando el depurador puedo ver un código como:

@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];
}

Pero no es tan sencillo como Guava depurar el código, en realidad no pude encontrar la n * 2transformación.

¿Hay alguna forma de ver esta transformación o una forma de depurar fácilmente este código?

EDITAR: agregué respuestas de diferentes comentarios y publiqué respuestas

Gracias al Holgercomentario que respondió a mi pregunta, el enfoque de tener un bloque lambda me permitió ver el proceso de transformación y depurar lo que sucedió dentro del cuerpo lambda:

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

Gracias al Stuart Marksenfoque de tener referencias de métodos también me permitió depurar el proceso de transformación:

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

Gracias a la Marlon Bernardesrespuesta, noté que mi Eclipse no muestra lo que debería y el uso de peek () ayudó a mostrar los resultados.

Plaza Federico
fuente
No es necesario que declare su resultvariable temporal como Integer. Un simple intdebería funcionar también si está haciendo mapping inta un int
Holger
También agrego que hay un depurador mejorado en IntelliJ IDEA 14. Ahora podemos depurar las Lamdas.
Mikhail

Respuestas:

86

Por lo general, no tengo problemas para depurar expresiones lambda mientras uso Eclipse o IntelliJ IDEA. Simplemente establezca un punto de interrupción y asegúrese de no inspeccionar toda la expresión lambda (inspeccione solo el cuerpo lambda).

Depuración de Lambdas

Otro enfoque es utilizar peekpara inspeccionar los elementos de la secuencia:

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

ACTUALIZAR:

Creo que se está confundiendo porque mapes una intermediate operation, en otras palabras: es una operación perezosa que se ejecutará solo después de que terminal operationse haya ejecutado. Entonces, cuando llamas stream.map(n -> n * 2)al cuerpo lambda, no se está ejecutando en este momento. Debe establecer un punto de interrupción e inspeccionarlo después de llamar a una operación de terminal ( collect, en este caso).

Consulte Operaciones de transmisión para obtener más explicaciones.

ACTUALIZACIÓN 2:

Citando el comentario de Holger :

Lo que lo hace complicado aquí es que la llamada al mapa y la expresión lambda están en una línea, por lo que un punto de interrupción de línea se detendrá en dos acciones completamente no relacionadas.

Insertar un salto de línea justo después map( le permitiría establecer un punto de interrupción solo para la expresión lambda. Y no es inusual que los depuradores no muestren valores intermedios de una returndeclaración. Cambiar la lambda a n -> { int result=n * 2; return result; } le permitiría inspeccionar el resultado. Una vez más, inserte los saltos de línea de forma adecuada al pasar línea por línea ...

Marlon Bernardes
fuente
Gracias por la pantalla de impresión. ¿Qué versión de Eclipse tienes o qué hiciste para obtener ese diálogo? Intenté usar inspectand display y get n cannot be resolved to a variable. Por cierto, peek también es útil, pero imprime todos los valores a la vez. Quiero ver cada proceso de iteración para verificar la transformación. Es posible
Federico Piazza
Estoy usando Eclipse Kepler SR2 (con soporte Java 8 instalado desde el mercado de Eclipse).
Marlon Bernardes
¿También estás usando Eclipse? Simplemente establezca un punto de interrupción en .maplínea y presione F8 varias veces.
Marlon Bernardes
6
@Fede: lo que lo complica aquí es que la llamada ay mapla expresión lambda están en una línea, por lo que un punto de interrupción de línea se detendrá en dos acciones completamente no relacionadas. Insertar un salto de línea justo después map(le permitiría establecer un punto de interrupción solo para la expresión lambda. Y no es inusual que los depuradores no muestren valores intermedios de una returndeclaración. Cambiar la lambda a n -> { int result=n * 2; return result; }le permitiría inspeccionar result. Una vez más, inserte los saltos de línea de manera apropiada al pasar línea por línea ...
Holger
1
@Marlon Bernardes: claro, puedes agregarlo a la respuesta ya que ese es el propósito de los comentarios: ayudar a mejorar el contenido. Por cierto, he editado el texto citado agregando formato de código…
Holger
33

IntelliJ tiene un complemento tan agradable para este caso como un complemento Java Stream Debugger . Debería comprobarlo: https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

Extiende la ventana de la herramienta IDEA Debugger agregando el botón Trace Current Stream Chain, que se activa cuando el depurador se detiene dentro de una cadena de llamadas Stream API.

Tiene una interfaz agradable para trabajar con operaciones de secuencias separadas y le brinda la oportunidad de seguir algunos valores que debe depurar.

Depurador de secuencias de Java

Puede iniciarlo manualmente desde la ventana Depurar haciendo clic aquí:

ingrese la descripción de la imagen aquí

Dmytro Melnychuk
fuente
23

La depuración de lambdas también funciona bien con NetBeans. Estoy usando NetBeans 8 y JDK 8u5.

Si establece un punto de interrupción en una línea donde hay un lambda, en realidad golpeará una vez cuando se configure la canalización, y luego una vez para cada elemento de flujo. Usando su ejemplo, la primera vez que llegue al punto de interrupción será la map()llamada que está configurando el flujo de flujo:

primer punto de ruptura

Puede ver la pila de llamadas y las variables locales y los valores de los parámetros maincomo era de esperar. Si continúa avanzando, se vuelve a tocar el "mismo" punto de interrupción, excepto que esta vez está dentro de la llamada a la lambda:

ingrese la descripción de la imagen aquí

Tenga en cuenta que esta vez la pila de llamadas se encuentra en lo más profundo de la maquinaria de flujos, y las variables locales son las variables locales de la lambda en sí, no el mainmétodo adjunto . (Cambié los valores en la naturalslista para aclarar esto).

Como señaló Marlon Bernardes (+1), puede usarlo peekpara inspeccionar los valores a medida que pasan en la tubería. Sin embargo, tenga cuidado si está usando esto desde una transmisión paralela. Los valores se pueden imprimir en un orden impredecible en diferentes subprocesos. Si está almacenando valores en una estructura de datos de depuración de peek, esa estructura de datos, por supuesto, tendrá que ser segura para subprocesos.

Finalmente, si está haciendo mucha depuración de lambdas (especialmente lambdas de declaración de varias líneas), podría ser preferible extraer el lambda en un método con nombre y luego consultarlo usando una referencia de método. Por ejemplo,

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

Esto podría hacer que sea más fácil ver lo que sucede mientras está depurando. Además, extraer métodos de esta manera facilita la prueba unitaria. Si su lambda es tan complicado que necesita realizar un solo paso a través de él, probablemente desee tener un montón de pruebas unitarias para él de todos modos.

Stuart Marks
fuente
Mi problema fue que no pude depurar el cuerpo lambda, pero su enfoque de usar referencias de métodos me ayudó mucho con lo que quería. Puede actualizar su respuesta usando el enfoque de Holger que también funcionó perfectamente agregando { int result=n * 2; return result; }diferentes líneas y pude aceptar la respuesta ya que ambas respuestas fueron útiles. +1 por supuesto.
Federico Piazza
1
@Fede Parece que la otra respuesta ya se actualizó, por lo que no es necesario actualizar la mía. De todos modos, odio las lambdas multilínea. :-)
Stuart Marks
1
@Stuart Marks: Yo también prefiero lambdas de una sola línea. Por lo general, elimino los saltos de línea después de depurar, lo que también se aplica a otras declaraciones compuestas (ordinarias).
Holger
1
@Fede No te preocupes. Es su prerrogativa como solicitante aceptar la respuesta que prefiera. Gracias por el +1.
Stuart Marks
1
Creo que la creación de referencias a métodos, además de facilitar los métodos a las pruebas unitarias, también hace que el código sea más legible. ¡Gran respuesta! (+1)
Marlon Bernardes
6

Solo para proporcionar detalles más actualizados (octubre de 2019), IntelliJ ha agregado una integración bastante agradable para depurar este tipo de código que es extremadamente útil.

Cuando nos detenemos en una línea que contiene un lambda si presionamos F7(entrar), IntelliJ resaltará cuál será el fragmento para depurar. Podemos cambiar el fragmento con el que depurar Taby una vez que lo decidimos, hacemos clic de F7nuevo.

Aquí algunas capturas de pantalla para ilustrar:

1- Presione la F7tecla (entrar), se mostrarán los aspectos más destacados (o el modo de selección) ingrese la descripción de la imagen aquí

2- Use Tabvarias veces para seleccionar el fragmento para depurar ingrese la descripción de la imagen aquí

3- Presione la F7tecla (entrar) para entrar ingrese la descripción de la imagen aquí

Plaza Federico
fuente
1

La depuración con IDE siempre es útil, pero la forma ideal de depurar cada elemento en una secuencia es usar peek () antes de una operación de método de terminal, ya que los Steam de Java se evalúan de forma perezosa, por lo que, a menos que se invoque un método de terminal, la secuencia respectiva no ser evaluado.

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());
Sashank Samantray
fuente