Jak debugować stream (). Map (…) z wyrażeniami lambda?

114

W naszym projekcie przeprowadzamy migrację do java 8 i testujemy jej nowe funkcje.

W moim projekcie używam predykatów i funkcji Guava do filtrowania i przekształcania niektórych kolekcji przy użyciu Collections2.transformi Collections2.filter.

Podczas tej migracji muszę zmienić na przykład kod guava na zmiany java 8. A więc zmiany, które wprowadzam, to:

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

Do...

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

Używając guawy, czułem się bardzo komfortowo debugując kod, ponieważ mogłem debugować każdy proces transformacji, ale martwię się, jak na przykład debugować .map(n -> n*2).

Używając debuggera, widzę kod, taki jak:

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

Ale debugowanie kodu nie jest tak proste jak guawa, właściwie nie mogłem znaleźć n * 2transformacji.

Czy istnieje sposób, aby zobaczyć tę transformację lub sposób na łatwe debugowanie tego kodu?

EDYCJA: Dodałem odpowiedź z różnych komentarzy i opublikowanych odpowiedzi

Dzięki Holgerkomentarzowi, który odpowiedział na moje pytanie, podejście z blokiem lambda pozwoliło mi zobaczyć proces transformacji i debugować, co się stało wewnątrz ciała lambda:

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

Dzięki Stuart Markspodejściu posiadania referencji do metod umożliwiło mi również debugowanie procesu transformacji:

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

Dzięki Marlon Bernardesodpowiedzi zauważyłem, że moje Eclipse nie pokazuje, co powinno, a użycie peek () pomogło wyświetlić wyniki.

Federico Piazza
źródło
Nie musisz deklarować swojej tymczasowej resultzmiennej jako Integer. Prosty też intpowinien wystarczyć, jeśli mapwysyłasz ping intdo int
Holger.
Dodam również, że w IntelliJ IDEA 14 jest ulepszony debugger. Teraz możemy debugować Lamdas.
Michaił

Odpowiedzi:

86

Zwykle nie mam problemu z debugowaniem wyrażeń lambda podczas korzystania z Eclipse lub IntelliJ IDEA. Po prostu ustaw punkt przerwania i nie sprawdzaj całego wyrażenia lambda (sprawdzaj tylko treść lambda).

Debugowanie lambd

Innym podejściem jest użycie peekdo inspekcji elementów strumienia:

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

AKTUALIZACJA:

Myślę, że jesteś zdezorientowany, ponieważ mapjest to intermediate operation- innymi słowy: jest to leniwa operacja, która zostanie wykonana dopiero po terminal operationwykonaniu a. Więc kiedy wywołujesz stream.map(n -> n * 2)ciało lambda, w tej chwili nie jest wykonywane. Musisz ustawić punkt przerwania i sprawdzić go po wywołaniu operacji terminalowej ( collectw tym przypadku).

Sprawdź Operacje na strumieniu, aby uzyskać dalsze wyjaśnienia.

AKTUALIZACJA 2:

Cytując komentarz Holgera :

To, co sprawia, że ​​jest to trudne, polega na tym, że wywołanie map i wyrażenie lambda znajdują się w jednym wierszu, więc punkt przerwania wiersza zatrzyma się na dwóch zupełnie niezwiązanych ze sobą akcjach.

Wstawienie map( końca wiersza zaraz po nim pozwoliłoby ustawić punkt przerwania tylko dla wyrażenia lambda. I nie jest niczym niezwykłym, że debuggery nie pokazują pośrednich wartości returninstrukcji. Zmiana lambda na n -> { int result=n * 2; return result; } umożliwiłaby sprawdzenie wyniku. Ponownie, wstaw odpowiednio podziały wierszy podczas przechodzenia wiersz po wierszu…

Marlon Bernardes
źródło
Dzięki za sitodruk. Jaką masz wersję Eclipse lub co zrobiłeś, aby uzyskać to okno dialogowe? Spróbowałem użyć inspecti display i dostać n cannot be resolved to a variable. Przy okazji, peek też jest przydatny, ale wyświetla wszystkie wartości naraz. Chcę zobaczyć każdy proces iteracji, aby sprawdzić transformację. Czy to możliwe?
Federico Piazza
Używam Eclipse Kepler SR2 (z obsługą Java 8 zainstalowaną z platformy handlowej Eclipse).
Marlon Bernardes
Czy też używasz Eclipse? Po prostu ustaw punkt przerwania w .maplinii i naciśnij kilka razy klawisz F8.
Marlon Bernardes
6
@Fede: tym, co sprawia, że ​​jest to trudne, jest to, że wywołanie mapi wyrażenie lambda znajdują się w jednej linii, więc punkt przerwania linii zatrzyma się na dwóch zupełnie niezwiązanych ze sobą akcjach. Wstawienie map(końca wiersza zaraz po nim pozwoliłoby ustawić punkt przerwania tylko dla wyrażenia lambda. I nie jest niczym niezwykłym, że debuggery nie pokazują pośrednich wartości returninstrukcji. Zmiana lambda na n -> { int result=n * 2; return result; }umożliwiłaby sprawdzenie result. Ponownie wstaw odpowiednio znaki końca wiersza, przechodząc wiersz po wierszu…
Holger
1
@Marlon Bernardes: oczywiście, możesz dodać to do odpowiedzi, ponieważ taki jest cel komentarzy: pomoc w ulepszaniu treści. Przy okazji, zredagowałem cytowany tekst dodając formatowanie kodu…
Holger
33

IntelliJ ma tak fajną wtyczkę do tego przypadku, jak wtyczka Java Stream Debugger . Powinieneś to sprawdzić: https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

Rozszerza okno narzędzia IDEA Debugger, dodając przycisk Śledź bieżący łańcuch strumienia, który staje się aktywny, gdy debugger zatrzymuje się w łańcuchu wywołań interfejsu API Stream.

Ma ładny interfejs do pracy z oddzielnymi operacjami na strumieniach i daje możliwość śledzenia niektórych wartości, które należy debugować.

Java Stream Debugger

Możesz uruchomić go ręcznie z okna Debug, klikając tutaj:

wprowadź opis obrazu tutaj

Dmytro Melnychuk
źródło
23

Debugowanie lambd działa również dobrze z NetBeans. Używam NetBeans 8 i JDK 8u5.

Jeśli ustawisz punkt przerwania w linii, w której znajduje się lambda, faktycznie trafisz raz podczas konfigurowania potoku, a następnie raz dla każdego elementu strumienia. Korzystając z naszego przykładu, pierwszym uderzeniem w punkt przerwania będzie map()wywołanie, które konfiguruje potok strumienia:

pierwszy punkt przerwania

Możesz zobaczyć stos wywołań oraz zmienne lokalne i wartości parametrów mainzgodnie z oczekiwaniami. Jeśli będziesz kontynuować przechodzenie po krokach, „ten sam” punkt przerwania zostanie ponownie trafiony, z wyjątkiem tego, że tym razem znajduje się on w wywołaniu lambda:

wprowadź opis obrazu tutaj

Zauważ, że tym razem stos wywołań znajduje się głęboko w maszynerii strumieni, a zmienne lokalne są lokalnymi zmiennymi samej lambda, a nie otaczającą mainmetodą. (Zmieniłem wartości na naturalsliście, aby to wyjaśnić).

Jak zauważył Marlon Bernardes (+1), możesz używać peekdo sprawdzania wartości w trakcie ich przebiegu. Uważaj jednak, jeśli używasz tego z równoległego strumienia. Wartości mogą być drukowane w nieprzewidywalnej kolejności w różnych wątkach. Jeśli przechowujesz wartości w strukturze danych debugowania z peek, ta struktura danych będzie oczywiście musiała być bezpieczna wątkowo.

Na koniec, jeśli robisz dużo debugowania lambd (szczególnie wyrażeń wieloliniowych), może być lepiej wyodrębnić lambdę do nazwanej metody, a następnie odwołać się do niej za pomocą odwołania do metody. Na przykład,

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

Może to ułatwić sprawdzenie, co się dzieje podczas debugowania. Ponadto wyodrębnianie metod w ten sposób ułatwia testowanie jednostkowe. Jeśli twoja lambda jest tak skomplikowana, że ​​musisz przejść przez nią jeden krok, prawdopodobnie i tak chcesz mieć dla niej kilka testów jednostkowych.

Stuart Marks
źródło
Mój problem polegał na tym, że nie mogłem debugować treści lambda, ale twoje podejście do korzystania z odwołań do metod bardzo mi pomogło w tym, czego chciałem. Możesz zaktualizować swoją odpowiedź, używając podejścia Holgera, które również działało doskonale, dodając { int result=n * 2; return result; }różne linie i mogłem zaakceptować odpowiedź, ponieważ obie odpowiedzi były pomocne. +1 oczywiście.
Federico Piazza
1
@Fede Wygląda na to, że druga odpowiedź została już zaktualizowana, więc nie ma potrzeby aktualizowania mojej. I tak nienawidzę wieloliniowych lambd. :-)
Stuart Marks
1
@Stuart Marks: Ja też wolę lambdy jednowierszowe. Dlatego zazwyczaj usuwam podziały wierszy po debugowaniu - co dotyczy również innych (zwykłych) instrukcji złożonych.
Holger,
1
@Fede Nie martw się. Jako osoba pytająca masz prawo zaakceptować dowolną odpowiedź. Dzięki za +1.
Stuart Marks
1
Myślę, że tworzenie odwołań do metod, oprócz ułatwienia testów jednostkowych, sprawia, że ​​kod jest bardziej czytelny. Świetna odpowiedź! (+1)
Marlon Bernardes,
6

Aby zapewnić bardziej aktualne szczegóły (październik 2019 r.), IntelliJ dodał całkiem niezłą integrację do debugowania tego typu kodu, który jest niezwykle przydatny.

Kiedy zatrzymamy się na linii zawierającej lambdę, jeśli naciśniemy F7(step into), wówczas IntelliJ podświetli, co będzie fragmentem do debugowania. Możemy zmienić fragment do debugowania, Taba kiedy już zdecydujemy, klikamy F7ponownie.

Oto kilka zrzutów ekranu do zilustrowania:

1- Naciśnij przycisk F7(wejdź do), wyświetli się podświetlenie (lub tryb wyboru) wprowadź opis obrazu tutaj

2- Użyj Tabwiele razy, aby wybrać fragment do debugowania wprowadź opis obrazu tutaj

3- Naciśnij przycisk F7(step into), aby wejść wprowadź opis obrazu tutaj

Federico Piazza
źródło
1

Debugowanie przy użyciu IDE jest zawsze pomocne, ale idealnym sposobem debugowania przez każdy element w strumieniu jest użycie funkcji peek () przed operacją metody terminalowej, ponieważ Java Steams są leniwie oceniane, więc jeśli nie zostanie wywołana metoda terminalowa, odpowiedni strumień będzie nie podlegać ocenie.

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
źródło