Wie debugge ich stream (). Map (…) mit Lambda-Ausdrücken?

114

In unserem Projekt migrieren wir auf Java 8 und testen die neuen Funktionen.

In meinem Projekt verwende ich Guava-Prädikate und -Funktionen, um einige Sammlungen mit Collections2.transformund zu filtern und zu transformieren Collections2.filter.

Bei dieser Migration muss ich zum Beispiel Guavencode in Java 8 ändern. Die Änderungen, die ich vornehme, sind folgende:

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

Zu...

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

Mit Guave war ich sehr komfortabel beim Debuggen des Codes, da ich jeden Transformationsprozess debuggen konnte, aber mein Anliegen ist beispielsweise das Debuggen .map(n -> n*2).

Mit dem Debugger kann ich folgenden Code sehen:

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

Aber es ist nicht so einfach wie Guava, den Code zu debuggen, eigentlich konnte ich die n * 2Transformation nicht finden .

Gibt es eine Möglichkeit, diese Transformation zu sehen oder diesen Code einfach zu debuggen?

BEARBEITEN: Ich habe Antworten aus verschiedenen Kommentaren hinzugefügt und Antworten gepostet

Dank eines HolgerKommentars, der meine Frage beantwortete, ermöglichte mir der Ansatz, einen Lambda-Block zu haben, den Transformationsprozess zu sehen und zu debuggen, was im Lambda-Körper passiert ist:

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

Dank Stuart Marksdes Ansatzes, Methodenreferenzen zu haben, konnte ich auch den Transformationsprozess debuggen:

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

Dank der Marlon BernardesAntwort bemerkte ich, dass meine Eclipse nicht anzeigt, was sie sollte, und die Verwendung von peek () half, Ergebnisse anzuzeigen.

Federico Piazza
quelle
Sie müssen Ihre temporäre resultVariable nicht als deklarieren Integer. Eine einfache intsollten auch tun , wenn Sie sind mapPing ein intzu einem int...
Holger
Außerdem füge ich hinzu, dass es in IntelliJ IDEA 14 den verbesserten Debugger gibt. Jetzt können wir die Lamdas debuggen.
Mikhail

Antworten:

86

Normalerweise habe ich kein Problem damit, Lambda-Ausdrücke zu debuggen, während ich Eclipse oder IntelliJ IDEA verwende. Setzen Sie einfach einen Haltepunkt und achten Sie darauf, nicht den gesamten Lambda-Ausdruck zu überprüfen (überprüfen Sie nur den Lambda-Körper).

Lambdas debuggen

Ein anderer Ansatz besteht darin peek, die Elemente des Streams zu untersuchen:

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

AKTUALISIEREN:

Ich denke, Sie werden verwirrt, weil mapes sich um eine intermediate operation- mit anderen Worten: es handelt sich um eine faule Operation handelt, die erst ausgeführt wird, nachdem a terminal operationausgeführt wurde. Wenn Sie also stream.map(n -> n * 2)den Lambda-Körper anrufen , wird er im Moment nicht ausgeführt. Sie müssen einen Haltepunkt festlegen und ihn überprüfen, nachdem eine Terminaloperation aufgerufen wurde ( collectin diesem Fall).

Überprüfen Sie Stream Operations auf weitere Erklärungen.

UPDATE 2:

Zitat von Holgers Kommentar:

Was es hier schwierig macht, ist, dass der Aufruf von map und der Lambda-Ausdruck in einer Zeile stehen, sodass ein Zeilenumbruchpunkt bei zwei völlig unabhängigen Aktionen stoppt.

Wenn Sie direkt danach einen Zeilenumbruch einfügen map( , können Sie einen Unterbrechungspunkt nur für den Lambda-Ausdruck festlegen. Und es ist nicht ungewöhnlich, dass Debugger keine Zwischenwerte einer returnAnweisung anzeigen. Wenn Sie das Lambda ändern, n -> { int result=n * 2; return result; } können Sie das Ergebnis überprüfen. Fügen Sie erneut Zeilenumbrüche ein, wenn Sie Zeile für Zeile fortschreiten.

Marlon Bernardes
quelle
Danke für den Druckbildschirm. Welche Version von Eclipse haben Sie oder was haben Sie getan, um diesen Dialog zu erhalten? Ich habe versucht, inspectund display und zu bekommen n cannot be resolved to a variable. Übrigens ist Peek auch nützlich, druckt aber alle Werte auf einmal. Ich möchte jeden Iterationsprozess sehen, um die Transformation zu überprüfen. Ist es möglich
Federico Piazza
Ich verwende Eclipse Kepler SR2 (mit Java 8-Unterstützung, die vom Eclipse-Marktplatz installiert wurde).
Marlon Bernardes
Verwenden Sie auch Eclipse? Setzen Sie einfach einen Haltepunkt .maponline und drücken Sie mehrmals F8.
Marlon Bernardes
6
@Fede: Was es hier schwierig macht, ist, dass der Aufruf von mapund der Lambda-Ausdruck in einer Zeile stehen, sodass ein Zeilenumbruchpunkt bei zwei völlig unabhängigen Aktionen stoppt. Wenn Sie direkt danach einen Zeilenumbruch einfügen map(, können Sie einen Unterbrechungspunkt nur für den Lambda-Ausdruck festlegen. Und es ist nicht ungewöhnlich, dass Debugger keine Zwischenwerte einer returnAnweisung anzeigen. Wenn Sie das Lambda ändern, n -> { int result=n * 2; return result; }können Sie es überprüfen result. Fügen Sie erneut Zeilenumbrüche entsprechend ein, wenn Sie Zeile für Zeile
Holger
1
@Marlon Bernardes: Sicher, Sie können es der Antwort hinzufügen, da dies der Zweck von Kommentaren ist: zur Verbesserung des Inhalts beizutragen. Übrigens habe ich den zitierten Text bearbeitet und Code-Formatierungen hinzugefügt…
Holger
33

IntelliJ hat für diesen Fall ein so schönes Plugin wie ein Java Stream Debugger- Plugin. Sie sollten es überprüfen: https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

Es erweitert das IDEA Debugger-Toolfenster durch Hinzufügen der Schaltfläche Aktuelle Stream-Kette verfolgen, die aktiv wird, wenn der Debugger innerhalb einer Kette von Stream-API-Aufrufen stoppt.

Es hat eine schöne Oberfläche für die Arbeit mit separaten Streams und bietet Ihnen die Möglichkeit, einige Werte zu befolgen, die Sie debuggen sollten.

Java Stream Debugger

Sie können es manuell über das Debug-Fenster starten, indem Sie hier klicken:

Geben Sie hier die Bildbeschreibung ein

Dmytro Melnychuk
quelle
23

Das Debuggen von Lambdas funktioniert auch gut mit NetBeans. Ich verwende NetBeans 8 und JDK 8u5.

Wenn Sie einen Haltepunkt in einer Zeile festlegen, in der sich ein Lambda befindet, werden Sie beim Einrichten der Pipeline tatsächlich einmal und dann für jedes Stream-Element einmal getroffen. In Ihrem Beispiel ist das erste Mal, dass Sie den Haltepunkt erreichen, der map()Aufruf, der die Stream-Pipeline einrichtet:

erster Haltepunkt

Sie können den Aufrufstapel sowie die lokalen Variablen und Parameterwerte mainwie erwartet anzeigen. Wenn Sie weitergehen, wird der "gleiche" Haltepunkt erneut erreicht, außer diesmal innerhalb des Aufrufs an das Lambda:

Geben Sie hier die Bildbeschreibung ein

Beachten Sie, dass sich der Aufrufstapel diesmal tief in der Streams-Maschinerie befindet und die lokalen Variablen die Einheimischen des Lambda selbst sind, nicht die einschließende mainMethode. (Ich habe die Werte in der naturalsListe geändert , um dies zu verdeutlichen.)

Wie Marlon Bernardes betonte (+1), können Sie damit peekWerte überprüfen, die in der Pipeline vergehen. Seien Sie jedoch vorsichtig, wenn Sie dies aus einem parallelen Stream verwenden. Die Werte können in einer unvorhersehbaren Reihenfolge über verschiedene Threads hinweg gedruckt werden. Wenn Sie Werte in einer Debugging-Datenstruktur aus speichern peek, muss diese Datenstruktur natürlich threadsicher sein.

Wenn Sie Lambdas häufig debuggen (insbesondere Lambdas mit mehrzeiligen Anweisungen), ist es möglicherweise vorzuziehen, das Lambda in eine benannte Methode zu extrahieren und dann mithilfe einer Methodenreferenz darauf zu verweisen. Beispielsweise,

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

Dies macht es möglicherweise einfacher zu sehen, was beim Debuggen vor sich geht. Darüber hinaus erleichtert das Extrahieren von Methoden auf diese Weise den Unit-Test. Wenn Ihr Lambda so kompliziert ist, dass Sie es in einem Schritt durchlaufen müssen, möchten Sie wahrscheinlich trotzdem eine Reihe von Unit-Tests dafür durchführen.

Stuart Marks
quelle
Mein Problem war, dass ich den Lambda-Körper nicht debuggen konnte, aber Ihr Ansatz, Methodenreferenzen zu verwenden, half mir sehr bei dem, was ich wollte. Sie können Ihre Antwort mit dem Holger-Ansatz aktualisieren, der auch perfekt funktioniert, indem Sie { int result=n * 2; return result; }verschiedene Zeilen hinzufügen , und ich kann die Antwort akzeptieren, da beide Antworten hilfreich sind. +1 natürlich.
Federico Piazza
1
@Fede Sieht so aus, als ob die andere Antwort bereits aktualisiert wurde, sodass meine nicht aktualisiert werden muss. Ich hasse sowieso mehrzeilige Lambdas. :-)
Stuart Marks
1
@ Stuart Marks: Ich bevorzuge auch einzeilige Lambdas. Normalerweise entferne ich die Zeilenumbrüche nach dem Debuggen, was auch für andere (gewöhnliche) zusammengesetzte Anweisungen gilt.
Holger
1
@Fede Keine Sorge. Es ist Ihr Vorrecht als Fragesteller, die von Ihnen bevorzugte Antwort zu akzeptieren. Danke für die +1.
Stuart Marks
1
Ich denke, dass das Erstellen von Methodenreferenzen nicht nur das Vereinfachen von Methoden für Unit-Tests erleichtert, sondern auch zu einem besser lesbaren Code führt. Gute Antwort! (+1)
Marlon Bernardes
6

Um weitere aktualisierte Details bereitzustellen (Oktober 2019), hat IntelliJ eine hübsche Integration hinzugefügt, um diese Art von Code zu debuggen, die äußerst nützlich ist.

Wenn wir an einer Zeile anhalten, die ein Lambda enthält, wenn wir drücken F7(eintreten), hebt IntelliJ das zu debuggende Snippet hervor. Wir können wechseln, mit welchem ​​Block Tabwir debuggen möchten, und sobald wir uns dafür entschieden haben, klicken wir F7erneut.

Hier einige Screenshots zur Veranschaulichung:

1- Drücken Sie die F7Taste (Schritt in), um die Markierungen (oder den Auswahlmodus) anzuzeigen. Geben Sie hier die Bildbeschreibung ein

2- Verwenden Tab Sie das zu debuggende Snippet mehrmals aus Geben Sie hier die Bildbeschreibung ein

3- Drücken Sie die F7Taste (Schritt hinein), um hineinzugehen Geben Sie hier die Bildbeschreibung ein

Federico Piazza
quelle
1

Das Debuggen mit IDEs ist immer hilfreich, aber die ideale Methode zum Debuggen durch die einzelnen Elemente in einem Stream besteht darin, peek () vor einer Terminalmethodenoperation zu verwenden, da Java-Steams träge ausgewertet werden. Wenn also keine Terminalmethode aufgerufen wird, wird der jeweilige Stream verwendet nicht ausgewertet werden.

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
quelle