Como depurar stream (). Map (...) com expressões lambda?

114

Em nosso projeto, estamos migrando para o java 8 e testando os novos recursos dele.

No meu projeto estou usando predicados e funções de Guava para filtrar e transformar algumas coleções usando Collections2.transforme Collections2.filter.

Nesta migração eu preciso mudar, por exemplo, o código de goiaba para java 8 mudanças. Então, as mudanças que estou fazendo são do 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);

Para...

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

Usando o goiaba fiquei muito confortável depurando o código, já que poderia depurar cada processo de transformação, mas minha preocupação é como depurar, por exemplo .map(n -> n*2).

Usando o depurador, posso ver alguns códigos 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];
}

Mas não é tão simples quanto Guava depurar o código, na verdade não consegui encontrar a n * 2transformação.

Existe uma maneira de ver essa transformação ou uma maneira de depurar facilmente esse código?

EDITAR: Adicionei respostas de diferentes comentários e postei respostas

Graças ao Holgercomentário que respondeu à minha pergunta, a abordagem de ter o bloco lambda me permitiu ver o processo de transformação e depurar o que aconteceu dentro do corpo lambda:

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

Graças à Stuart Marksabordagem de ter referências de método também me permitiu depurar o processo de transformação:

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

Graças à Marlon Bernardesresposta, percebi que meu Eclipse não mostra o que deveria e o uso de peek () ajudou a exibir os resultados.

Federico Piazza
fonte
Você não precisa declarar sua resultvariável temporária como Integer. Um simples intdeve servir também se você estiver mapexecutando ping intem um int...
Holger
Além disso, acrescento que existe o depurador aprimorado no IntelliJ IDEA 14. Agora podemos depurar o Lamdas.
Mikhail

Respostas:

86

Normalmente não tenho problemas para depurar expressões lambda ao usar Eclipse ou IntelliJ IDEA. Basta definir um ponto de interrupção e não inspecionar a expressão lambda inteira (inspecione apenas o corpo lambda).

Depurando Lambdas

Outra abordagem é usar peekpara inspecionar os elementos do fluxo:

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

ATUALIZAR:

Acho que você está ficando confuso porque mapé uma intermediate operation- em outras palavras: é uma operação preguiçosa que só será executada depois que a terminal operationfor executada. Então, quando você chama, stream.map(n -> n * 2)o corpo lambda não está sendo executado no momento. Você precisa definir um ponto de interrupção e inspecioná-lo depois que uma operação de terminal foi chamada ( collectneste caso).

Verifique o Stream Operations para mais explicações.

ATUALIZAÇÃO 2:

Citando o comentário de Holger :

O que torna isso complicado aqui é que a chamada para mapear e a expressão lambda estão em uma linha, então um ponto de interrupção de linha será interrompido em duas ações completamente não relacionadas.

Inserir uma quebra de linha logo após map( permitiria definir um ponto de interrupção apenas para a expressão lambda. E não é incomum que os depuradores não mostrem valores intermediários de uma returninstrução. Alterar o lambda para n -> { int result=n * 2; return result; } permitiria que você inspecionasse o resultado. Mais uma vez, insira as quebras de linha apropriadamente ao avançar linha por linha ...

Marlon Bernardes
fonte
Obrigado pela tela de impressão. Qual versão do Eclipse você tem ou o que fez para obter esse diálogo? Tentei usar inspectand display and get n cannot be resolved to a variable. Aliás, peek também é útil, mas imprime todos os valores de uma vez. Quero ver cada processo de iteração para verificar a transformação. É possível?
Federico Piazza
Estou usando o Eclipse Kepler SR2 (com suporte a Java 8 instalado no mercado do Eclipse).
Marlon Bernardes
Você também está usando o Eclipse? Basta definir um ponto de interrupção on- .mapline e pressionar F8 várias vezes.
Marlon Bernardes
6
@Fede: o que torna isso complicado aqui é que a chamada para mape a expressão lambda estão em uma linha, então um ponto de interrupção de linha irá parar em duas ações completamente não relacionadas. Inserir uma quebra de linha logo após map(permitiria definir um ponto de interrupção apenas para a expressão lambda. E não é incomum que os depuradores não mostrem valores intermediários de uma returninstrução. Alterar o lambda para n -> { int result=n * 2; return result; }permitiria que você inspecionasse result. Mais uma vez, insira quebras de linha apropriadamente ao avançar linha por linha ...
Holger
1
@Marlon Bernardes: claro, você pode adicionar na resposta, pois esse é o propósito dos comentários: ajudar a melhorar o conteúdo. A propósito, editei o texto citado adicionando formatação de código…
Holger
33

O IntelliJ tem um plugin tão bom para este caso como um plugin Java Stream Debugger . Você deve conferir: https://plugins.jetbrains.com/plugin/9696-java-stream-debugger?platform=hootsuite

Ele estende a janela da ferramenta IDEA Debugger adicionando o botão Trace Current Stream Chain, que se torna ativo quando o depurador para dentro de uma cadeia de chamadas de API de fluxo.

Ele tem uma interface agradável para trabalhar com operações de fluxos separados e dá a você a oportunidade de seguir alguns valores que você deve depurar.

Java Stream Debugger

Você pode iniciá-lo manualmente na janela de depuração clicando aqui:

insira a descrição da imagem aqui

Dmytro Melnychuk
fonte
23

A depuração de lambdas também funciona bem com o NetBeans. Estou usando o NetBeans 8 e o JDK 8u5.

Se você definir um ponto de interrupção em uma linha onde há um lambda, você realmente atingirá uma vez quando o pipeline for configurado e, em seguida, uma vez para cada elemento de fluxo. Usando seu exemplo, a primeira vez que você atingir o ponto de interrupção será a map()chamada que está configurando o pipeline de fluxo:

primeiro ponto de interrupção

Você pode ver a pilha de chamadas e as variáveis ​​locais e os valores dos parâmetros mainconforme o esperado. Se você continuar avançando, o "mesmo" ponto de interrupção será atingido novamente, mas desta vez dentro da chamada ao lambda:

insira a descrição da imagem aqui

Observe que, desta vez, a pilha de chamadas está profundamente dentro do mecanismo dos streams e as variáveis ​​locais são os locais do próprio lambda, não o mainmétodo envolvente . (Eu mudei os valores na naturalslista para deixar isso claro.)

Como Marlon Bernardes apontou (+1), você pode usar peekpara inspecionar valores à medida que eles passam no pipeline. Porém, tenha cuidado se estiver usando isso de um fluxo paralelo. Os valores podem ser impressos em uma ordem imprevisível em diferentes threads. Se você estiver armazenando valores em uma estrutura de dados de depuração peek, essa estrutura de dados terá que ser segura para threads.

Por fim, se você estiver depurando muito os lambdas (especialmente lambdas de instruções de várias linhas), pode ser preferível extrair o lambda em um método nomeado e, em seguida, referir-se a ele usando uma referência de método. Por exemplo,

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

Isso pode tornar mais fácil ver o que está acontecendo durante a depuração. Além disso, extrair métodos dessa maneira facilita o teste de unidade. Se o seu lambda é tão complicado que você precisa executá-lo em uma única etapa, você provavelmente deseja ter um monte de testes de unidade para ele de qualquer maneira.

Stuart Marks
fonte
Meu problema era que eu não conseguia depurar o corpo lambda, mas sua abordagem de usar referências de método me ajudou muito com o que eu queria. Você poderia atualizar sua resposta usando a abordagem de Holger que também funcionou perfeitamente adicionando { int result=n * 2; return result; }linhas diferentes e eu poderia aceitar a resposta, pois ambas as respostas foram úteis. 1 claro.
Federico Piazza
1
@Fede Parece que a outra resposta já foi atualizada, então não há necessidade de atualizar a minha. Eu odeio lambdas de várias linhas de qualquer maneira. :-)
Stuart Marks
1
@Stuart Marks: Eu também prefiro lambdas de uma linha. Normalmente, eu removo as quebras de linha após a depuração ⟨que se aplica a outras instruções compostas (comuns) também.
Holger
1
@Fede Não se preocupe. É sua prerrogativa como solicitante aceitar a resposta que preferir. Obrigado pelo +1.
Stuart Marks
1
Acho que criar referências de método, além de tornar os métodos mais fáceis para os testes de unidade, também torna o código mais legível. Ótima resposta! (+1)
Marlon Bernardes
6

Apenas para fornecer detalhes mais atualizados (outubro de 2019), o IntelliJ adicionou uma boa integração para depurar esse tipo de código que é extremamente útil.

Quando paramos em uma linha que contém um lambda, se pressionarmos F7(entrar em), o IntelliJ destacará qual será o trecho a ser depurado. Podemos mudar o bloco para depurar Tabe, uma vez que decidimos, clicamos F7novamente.

Aqui estão algumas imagens para ilustrar:

1- Pressione a tecla F7(entrar) para exibir os destaques (ou modo de seleção) insira a descrição da imagem aqui

2- Use Tabvárias vezes para selecionar o snippet para depurar insira a descrição da imagem aqui

3- Pressione a tecla F7(entrar) para entrar insira a descrição da imagem aqui

Federico Piazza
fonte
1

Depurar usando IDE é sempre útil, mas a maneira ideal de depurar por meio de cada elemento em um fluxo é usar peek () antes de uma operação de método de terminal, pois os Java Steams são avaliados lentamente, então, a menos que um método de terminal seja invocado, o respectivo fluxo não ser avaliado.

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
fonte