Comment déboguer stream (). Map (…) avec des expressions lambda?

114

Dans notre projet, nous migrons vers java 8 et nous testons ses nouvelles fonctionnalités.

Sur mon projet, j'utilise des prédicats et des fonctions Guava pour filtrer et transformer certaines collections en utilisant Collections2.transformet Collections2.filter.

Sur cette migration, j'ai besoin de changer par exemple le code goyave en changements java 8. Donc, les changements que je fais sont du genre:

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

En utilisant guava, j'étais très à l'aise pour déboguer le code car je pouvais déboguer chaque processus de transformation, mais mon souci est de savoir comment déboguer par exemple .map(n -> n*2).

En utilisant le débogueur, je peux voir du code comme:

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

Mais ce n'est pas aussi simple que Guava de déboguer le code, en fait je n'ai pas trouvé la n * 2transformation.

Existe-t-il un moyen de voir cette transformation ou un moyen de déboguer facilement ce code?

EDIT: J'ai ajouté une réponse à partir de différents commentaires et des réponses publiées

Merci au Holgercommentaire qui a répondu à ma question, l'approche d'avoir un bloc lambda m'a permis de voir le processus de transformation et de déboguer ce qui s'est passé à l'intérieur du corps lambda:

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

Grâce à Stuart Marksl'approche d'avoir des références de méthode m'a également permis de déboguer le processus de transformation:

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

Grâce à la Marlon Bernardesréponse, j'ai remarqué que mon Eclipse ne montre pas ce qu'il devrait et l'utilisation de peek () a aidé à afficher les résultats.

Piazza Federico
la source
Vous n'avez pas besoin de déclarer votre resultvariable temporaire comme Integer. Un simple intdevrait faire aussi bien si vous envoyez mapun ping intà un int
Holger
J'ajoute également qu'il y a le débogueur amélioré dans IntelliJ IDEA 14. Maintenant, nous pouvons déboguer les Lamdas.
Mikhail

Réponses:

86

Je n'ai généralement aucun problème à déboguer les expressions lambda tout en utilisant Eclipse ou IntelliJ IDEA. Définissez simplement un point d'arrêt et assurez-vous de ne pas inspecter toute l'expression lambda (inspectez uniquement le corps lambda).

Débogage de Lambdas

Une autre approche consiste à utiliser peekpour inspecter les éléments du flux:

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

METTRE À JOUR:

Je pense que vous êtes confus parce que mapc'est un intermediate operation- en d'autres termes: c'est une opération paresseuse qui ne sera exécutée qu'après l' terminal operationexécution d'un. Ainsi, lorsque vous appelez, stream.map(n -> n * 2)le corps lambda n'est pas exécuté pour le moment. Vous devez définir un point d'arrêt et l'inspecter après l'appel d'une opération de terminal ( collectdans ce cas).

Consultez les opérations de flux pour plus d'explications.

MISE À JOUR 2:

Citant le commentaire de Holger :

Ce qui rend les choses délicates ici, c'est que l'appel à mapper et l'expression lambda sont sur une seule ligne, de sorte qu'un point d'arrêt de ligne s'arrête sur deux actions totalement indépendantes.

L'insertion d'un saut de ligne juste après map( vous permettrait de définir un point de rupture pour l'expression lambda uniquement. Et il n'est pas inhabituel que les débogueurs n'affichent pas les valeurs intermédiaires d'une returninstruction. Changer le lambda en n -> { int result=n * 2; return result; } vous permettrait d'inspecter le résultat. Encore une fois, insérez les sauts de ligne de manière appropriée lorsque vous avancez ligne par ligne…

Marlon Bernardes
la source
Merci pour l'écran d'impression. Quelle version d'Eclipse avez-vous ou qu'avez-vous fait pour obtenir ce dialogue? J'ai essayé d'utiliser inspectet display et d'obtenir n cannot be resolved to a variable. Btw, peek est également utile mais imprime toutes les valeurs à la fois. Je veux voir chaque processus d'itération pour vérifier la transformation. Est-ce possible?
Federico Piazza
J'utilise Eclipse Kepler SR2 (avec le support Java 8 installé à partir du marché Eclipse).
Marlon Bernardes
Utilisez-vous également Eclipse? Définissez simplement un point d'arrêt en .mapligne et appuyez plusieurs fois sur F8.
Marlon Bernardes
6
@Fede: ce qui rend les choses compliquées ici, c'est que l'appel à mapet l'expression lambda sont sur une seule ligne, donc un point d'arrêt de ligne s'arrêtera sur deux actions totalement indépendantes. L'insertion d'un saut de ligne juste après map(vous permettrait de définir un point de rupture pour l'expression lambda uniquement. Et il n'est pas inhabituel que les débogueurs n'affichent pas les valeurs intermédiaires d'une returninstruction. Changer le lambda en n -> { int result=n * 2; return result; }vous permettrait d'inspecter result. Encore une fois, insérez des sauts de ligne de manière appropriée lorsque vous avancez ligne par ligne…
Holger
1
@Marlon Bernardes: bien sûr, vous pouvez l'ajouter à la réponse car c'est le but des commentaires: aider à améliorer le contenu. Btw., J'ai édité le texte cité en ajoutant la mise en forme du code…
Holger
33

IntelliJ a un si bon plugin pour ce cas en tant que plugin Java Stream Debugger . Vous devriez le vérifier: https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

Il étend la fenêtre de l'outil IDEA Debugger en ajoutant le bouton Trace Current Stream Chain, qui devient actif lorsque le débogueur s'arrête à l'intérieur d'une chaîne d'appels d'API Stream.

Il a une interface agréable pour travailler avec des opérations de flux séparés et vous donne la possibilité de suivre certaines valeurs que vous devez déboguer.

Débogueur de flux Java

Vous pouvez le lancer manuellement depuis la fenêtre de débogage en cliquant ici:

entrez la description de l'image ici

Dmytro Melnychuk
la source
23

Le débogage des lambdas fonctionne également bien avec NetBeans. J'utilise NetBeans 8 et JDK 8u5.

Si vous définissez un point d'arrêt sur une ligne où il y a un lambda, vous frapperez en fait une fois lorsque le pipeline est configuré, puis une fois pour chaque élément de flux. En utilisant votre exemple, la première fois que vous atteignez le point d'arrêt sera l' map()appel qui configure le pipeline de flux:

premier point d'arrêt

Vous pouvez voir la pile d'appels et les variables locales et les valeurs des paramètres maincomme prévu. Si vous continuez à avancer, le "même" point d'arrêt est à nouveau atteint, sauf que cette fois, il fait partie de l'appel au lambda:

entrez la description de l'image ici

Notez que cette fois, la pile d'appels est profondément dans la machinerie des flux, et les variables locales sont les locals du lambda lui-même, pas la mainméthode englobante . (J'ai changé les valeurs de la naturalsliste pour que cela soit clair.)

Comme Marlon Bernardes l'a souligné (+1), vous pouvez utiliser peekpour inspecter les valeurs au fur et à mesure qu'elles passent dans le pipeline. Soyez prudent si vous l'utilisez à partir d'un flux parallèle. Les valeurs peuvent être imprimées dans un ordre imprévisible sur différents threads. Si vous stockez des valeurs dans une structure de données de débogage à partir de peek, cette structure de données devra bien sûr être thread-safe.

Enfin, si vous effectuez beaucoup de débogage de lambdas (en particulier les lambdas d'instructions multilignes), il peut être préférable d'extraire le lambda dans une méthode nommée, puis de s'y référer à l'aide d'une référence de méthode. Par exemple,

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

Cela peut vous permettre de voir plus facilement ce qui se passe pendant le débogage. De plus, l'extraction des méthodes de cette manière facilite le test unitaire. Si votre lambda est si compliqué que vous devez le parcourir en une seule étape, vous voudrez probablement avoir de toute façon un tas de tests unitaires.

Marques Stuart
la source
Mon problème était que je ne pouvais pas déboguer le corps lambda, mais votre approche d'utilisation des références de méthode m'a beaucoup aidé avec ce que je voulais. Vous pouvez mettre à jour votre réponse en utilisant l'approche Holger qui a également parfaitement fonctionné en ajoutant { int result=n * 2; return result; }différentes lignes et je pourrais accepter la réponse car les deux réponses étaient utiles. +1 bien sûr.
Federico Piazza
1
@Fede On dirait que l'autre réponse a déjà été mise à jour, donc pas besoin de mettre à jour la mienne. Je déteste les lambdas multilignes de toute façon. :-)
Stuart marque le
1
@Stuart Marks: Je préfère aussi les lambdas à une seule ligne. Donc, généralement, je supprime les sauts de ligne après le débogage «qui s'applique également à d'autres instructions composées (ordinaires)».
Holger
1
@Fede Pas de soucis. C'est votre prérogative en tant que demandeur d'accepter la réponse que vous préférez. Merci pour le +1.
Stuart marque le
1
Je pense que la création de références de méthode, en plus de rendre les méthodes plus faciles aux tests unitaires, permet également un code plus lisible. Très bonne réponse! (+1)
Marlon Bernardes
6

Juste pour fournir plus de détails mis à jour (octobre 2019), IntelliJ a ajouté une assez belle intégration pour déboguer ce type de code extrêmement utile.

Lorsque nous nous arrêtons à une ligne qui contient un lambda si nous appuyons sur F7(step into), IntelliJ mettra en évidence ce que sera l'extrait à déboguer. Nous pouvons changer le bloc avec lequel déboguer Tabet une fois que nous l'avons décidé, nous cliquons à F7nouveau.

Voici quelques captures d'écran pour illustrer:

1- Appuyez F7sur la touche (pas à pas), affichera les faits saillants (ou le mode de sélection) entrez la description de l'image ici

2- Utiliser Tabplusieurs fois pour sélectionner l'extrait à déboguer entrez la description de l'image ici

3- Appuyez F7sur la touche (step into) pour entrer entrez la description de l'image ici

Piazza Federico
la source
1

Le débogage à l'aide des IDE est toujours utile, mais le moyen idéal de déboguer chaque élément d'un flux est d'utiliser peek () avant une opération de méthode de terminal, car Java Steams est évalué paresseusement, donc à moins qu'une méthode de terminal ne soit appelée, le flux respectif sera ne pas être évalué.

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
la source