As expressões lambda têm outra utilidade além de salvar linhas de código?

120

As expressões lambda têm outra utilidade além de salvar linhas de código?

Existem recursos especiais fornecidos pelas lambdas que resolveram problemas que não eram fáceis de resolver? O uso típico que eu vi é que, em vez de escrever isso:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

Podemos usar uma expressão lambda para reduzir o código:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());
Vikash
fonte
27
Observe que ele pode ser ainda mais curto: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen 21/11
88
ou ainda mais curto:Comparator.comparing(Developer::getName)
Thomas Kläger 21/11
31
Eu não subestimaria esse benefício. Quando as coisas são muito mais fáceis, é mais provável que você faça as coisas da maneira "certa". Geralmente, existe alguma tensão entre escrever código que é mais sustentável a longo prazo, ou mais fácil de anotar no curto prazo. As lambdas reduzem essa tensão e, assim, ajudam a escrever um código mais limpo.
yshavit
5
Lambdas são fechos, o que aumenta ainda mais a economia de espaço, pois agora você não precisa adicionar um construtor parametrizado, campos, código de atribuição, etc. à sua classe de substituição.
CodesInChaos

Respostas:

116

As expressões Lambda não alteram o conjunto de problemas que você pode resolver com Java em geral, mas definitivamente facilitam a solução de certos problemas, apenas pelo mesmo motivo de não estarmos mais programando em linguagem assembly. A remoção de tarefas redundantes do trabalho do programador facilita a vida e permite fazer coisas que você nem tocaria de outra maneira, apenas pela quantidade de código que você precisaria produzir (manualmente).

Mas expressões lambda não estão apenas salvando linhas de código. Expressões lambda permitem definir funções , algo para o qual você poderia usar classes internas anônimas como uma solução alternativa antes, é por isso que você pode substituir classes internas anônimas nesses casos, mas não em geral.

Mais notavelmente, expressões lambda são definidas independentemente da interface funcional para a qual serão convertidas, portanto, não há membros herdados aos quais eles possam acessar; além disso, eles não podem acessar a instância do tipo que implementa a interface funcional. Dentro de uma expressão lambda thise supercom o mesmo significado que no contexto circundante, consulte também esta resposta . Além disso, você não pode criar novas variáveis ​​locais que sombream variáveis ​​locais do contexto circundante. Para a tarefa pretendida de definir uma função, isso remove muitas fontes de erro, mas também implica que, para outros casos de uso, pode haver classes internas anônimas que não podem ser convertidas em uma expressão lambda, mesmo se estiver implementando uma interface funcional.

Além disso, a construção new Type() { … }garante produzir uma nova instância distinta (como newsempre). Instâncias anônimas de classe interna sempre mantêm uma referência a sua instância externa, se criadas em um não staticcontexto. Por outro lado, as expressões lambda capturam apenas uma referência thisquando necessário, ou seja, se elas acessam thisou não são staticmembros. E eles produzem instâncias de uma identidade intencionalmente não especificada, o que permite que a implementação decida em tempo de execução se deve reutilizar instâncias existentes (consulte também “ Uma expressão lambda cria um objeto no heap toda vez que é executado? ”).

Essas diferenças se aplicam ao seu exemplo. Sua construção anônima de classe interna sempre produzirá uma nova instância, também poderá capturar uma referência à instância externa, enquanto sua (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())é uma expressão lambda não capturadora que será avaliada para um singleton em implementações típicas. Além disso, ele não produz um .classarquivo no seu disco rígido.

Dadas as diferenças em termos de semântica e desempenho, as expressões lambda podem mudar a maneira como os programadores resolverão certos problemas no futuro, é claro, também devido às novas APIs que abraçam idéias de programação funcional utilizando os novos recursos da linguagem. Consulte também expressão lambda do Java 8 e valores de primeira classe .

Holger
fonte
1
Essencialmente, expressões lambda são um tipo de programação funcional. Dado que qualquer tarefa pode ser realizada com programação funcional ou programação imperativa, não há vantagem, exceto pela legibilidade e manutenção , que podem superar outras preocupações.
Draco18s não confia mais em SE
1
@ Trilarion: você pode preencher livros inteiros com a definição de função . Você também teria que decidir se deveria se concentrar no objetivo ou nos aspectos suportados pelas expressões lambda do Java. O último link da minha resposta ( este ) já discute alguns desses aspectos no contexto de Java. Mas, para entender minha resposta, já é suficiente entender que funções são um conceito diferente de classes. Para mais detalhes, já existem recursos existentes e Q & como ...
Holger
1
@ Trilarion: ambas as funções não permitem resolver mais problemas, a menos que você considere a quantidade de tamanho / complexidade do código ou soluções alternativas necessárias para outras soluções como inaceitáveis ​​(como dito no primeiro parágrafo), e já havia uma maneira de definir funções , como dito, as classes internas podem ser usadas como solução alternativa, na ausência de uma maneira melhor de definir funções (mas as classes internas não são funções, pois podem servir a muito mais propósitos). É o mesmo que com OOP - ele não permite fazer nada que você não possa fazer com a montagem, mas ainda assim, você pode imaginar um aplicativo como o Eclipse escrito na montagem?
Holger
3
@ EricDuminil: para mim, a Completude de Turing é muito teórica. Por exemplo, se o Java é Turing completo ou não, você não pode gravar um driver de dispositivo do Windows em Java. (Ou no PostScript, que também é Turing completo) A adição de recursos ao Java que permitem fazer isso mudaria o conjunto de problemas que você pode resolver com ele, no entanto, isso não acontecerá. Então, eu prefiro o ponto de montagem; Como tudo o que você pode fazer é implementado nas instruções executáveis ​​da CPU, não há nada que você não possa fazer no Assembly que suporte todas as instruções da CPU (também mais fácil de entender do que o formalismo da máquina de Turing).
Holger
2
@abelito é comum a todas as construções de programação de nível superior, fornecer menos "visibilidade do que realmente está acontecendo", pois é disso que se trata, menos detalhes, mais foco no problema de programação real. Você pode usá-lo para tornar o código melhor ou pior; é apenas uma ferramenta. Como o recurso de votação; é uma ferramenta que você pode usar da maneira pretendida, para fornecer um julgamento fundamentado sobre a qualidade real de uma postagem. Ou você a usa para rebaixar uma resposta elaborada e razoável, apenas porque você odeia lambdas.
Holger
52

Linguagens de programação não são para máquinas executadas.

Eles são para os programadores pensarem .

Os idiomas são uma conversa com um compilador para transformar nossos pensamentos em algo que uma máquina pode executar. Uma das principais queixas sobre Java de pessoas que vêm para ele de outras línguas (ou deixam -lo para outros idiomas) costumavam ser que obriga um determinado modelo mental sobre o programador (ou seja, tudo é uma classe).

Não vou avaliar se isso é bom ou ruim: tudo é trade-offs. Mas o Java 8 lambdas permite que os programadores pensem em termos de funções , algo que você não poderia fazer anteriormente em Java.

É a mesma coisa que um programador de procedimentos que aprende a pensar em termos de classes quando se trata de Java: você os vê gradualmente passar de classes que são estruturas glorificadas e ter classes 'auxiliares' com vários métodos estáticos e passar para algo que assemelha-se mais a um design racional de OO (mea culpa).

Se você pensar nelas como uma maneira mais curta de expressar classes internas anônimas, provavelmente não as achará muito impressionantes da mesma maneira que o programador de procedimentos acima provavelmente não achou que as classes fossem um grande aprimoramento.

Jared Smith
fonte
5
Mas tenha cuidado com isso, eu costumava pensar que OOP era tudo, e então fiquei terrivelmente confuso com as arquiteturas de sistema de componentes de entidade (o que significa que você não tem uma classe para cada tipo de objeto de jogo ?! e cada objeto de jogo não é um objeto OOP ?!)
user253751 22/11
3
Em outras palavras, pensando em OOP, em vez de pensar apenas * o f * classes como outra ferramenta, y restrito meu pensamento de uma maneira ruim.
user253751
2
@immibis: existem muitas maneiras diferentes de "pensar em POO"; portanto, além do erro comum de confundir "pensar em POO" com "pensamento em sala de aula", há muitas maneiras de pensar de maneira contraproducente. Infelizmente, isso acontece com muita frequência desde o início, pois até livros e tutoriais ensinam os inferiores, mostram exemplos antipadrões e espalham esse pensamento. A suposta necessidade de “ter uma classe para cada tipo de” seja o que for, é apenas um exemplo, contradizendo o conceito de abstração. Da mesma forma, parece haver o mito “OOP significa escrever o máximo possível”, etc
Holger
@immibis, embora nesse caso não estivesse ajudando você, essa anedota realmente apoia o argumento: a linguagem e o paradigma fornecem uma estrutura para estruturar seus pensamentos e transformá-los em um design. No seu caso, você se deparou com as bordas da estrutura, mas em outro contexto isso pode ter ajudado a desviar-se de uma grande bola de lama e produzir um design feio. Por isso, suponho, "tudo é trade-offs". Manter o último benefício e evitar o problema anterior é uma questão importante ao decidir o quão "grande" criar um idioma.
Leushenko
@immibis É por isso que é importante aprender um monte de paradigma bem : cada um você atinge cerca as insuficiências dos outros.
Jared Smith
36

Salvar linhas de código pode ser visto como um novo recurso, se permitir que você escreva um pedaço substancial de lógica de maneira mais curta e clara, o que leva menos tempo para que outras pessoas leiam e entendam.

Sem expressões lambda (e / ou referências de método), os Streampipelines seriam muito menos legíveis.

Pense, por exemplo, como Streamseria o pipeline a seguir se você substituísse cada expressão lambda por uma instância de classe anônima.

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

Seria:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

É muito mais difícil escrever do que a versão com expressões lambda e é muito mais suscetível a erros. Também é mais difícil de entender.

E este é um pipeline relativamente curto.

Para tornar isso legível sem expressões lambda e referências a métodos, você teria que definir variáveis ​​que contêm as várias instâncias de interface funcional que estão sendo usadas aqui, o que teria dividido a lógica do pipeline, dificultando a compreensão.

Eran
fonte
17
Eu acho que essa é uma visão importante. Pode-se argumentar que algo é praticamente impossível de ser feito em uma linguagem de programação se a sobrecarga sintática e cognitiva for grande demais para ser útil, de modo que outras abordagens serão superiores. Portanto, reduzindo a sobrecarga sintática e cognitiva (como as lambdas fazem em comparação com as classes anônimas), pipelines como o acima se tornam possíveis. Mais explícito, se escrever um pedaço de código faz com que você seja demitido do seu emprego, pode-se dizer que isso não é possível.
Sebastian Redl
Nitpick: Você realmente não precisa do Comparatorfor, sortedpois ele se compara em ordem natural de qualquer maneira. Talvez altere para comparar o comprimento das strings ou similar, para tornar o exemplo ainda melhor?
tobias_k
@tobias_k não há problema. Eu mudei compareToIgnoreCasepara fazer com que ele se comportasse diferente de sorted().
Eran
1
compareToIgnoreCaseainda é um mau exemplo, pois você pode simplesmente usar o String.CASE_INSENSITIVE_ORDERcomparador existente . A sugestão de @ tobias_k de comparar por comprimento (ou qualquer outra propriedade que não possua um comparador embutido) se encaixa melhor. A julgar pelos exemplos de código de perguntas e respostas SO, as lambdas causaram muitas implementações desnecessárias de comparadores ...
Holger
Eu sou o único que acha que o segundo exemplo é mais fácil de entender?
Clark Battle
8

Iteração interna

Ao iterar o Java Collections, a maioria dos desenvolvedores costuma obter um elemento e depois processá- lo. Ou seja, retire esse item e, em seguida, use-o ou reinsira-o etc. Com as versões pré-8 do Java, você pode implementar uma classe interna e fazer algo como:

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

Agora, com o Java 8, você pode fazer melhor e menos detalhado com:

numbers.forEach((Integer value) -> System.out.println(value));

ou melhor

numbers.forEach(System.out::println);

Comportamentos como argumentos

Adivinhe o seguinte caso:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

Com a interface do Java 8 Predicate, você pode fazer melhor assim:

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

Chamando assim:

sumAll(numbers, n -> n % 2 == 0);

Fonte: DZone - Por que precisamos de expressões lambda em Java

também
fonte
Provavelmente, o primeiro caso é uma maneira de salvar algumas linhas de código, mas facilita a leitura do código
também em
4
Como a iteração interna é um recurso lambda? De fato, tecnicamente, as lambdas não permitem que você faça nada que não poderia fazer antes do java-8. É apenas uma maneira mais concisa de transmitir o código.
Ousmane D.
@ Aominè Neste caso, numbers.forEach((Integer value) -> System.out.println(value));a expressão lambda é composta de duas partes, a da esquerda do símbolo de seta (->), listando seus parâmetros e a da direita, contendo o corpo . O compilador descobre automaticamente que a expressão lambda tem a mesma assinatura do único método não implementado da interface Consumer.
alseether
5
Não perguntei o que é uma expressão lambda, estou apenas dizendo que a iteração interna não tem nada a ver com expressões lambda.
Ousmane D.
1
@ Aominè Entendo o seu ponto, ele não tem nada com lambdas em si , mas, como você ressalta, é mais uma maneira mais limpa de escrever. Eu acho que em termos de eficiência é tão bom quanto as versões pré-8
21/11
4

Existem muitos benefícios em usar lambdas em vez da classe interna a seguir:

  • Torne o código mais compacto e expressivo sem introduzir mais semânticas de sintaxe de idioma. você já deu um exemplo em sua pergunta.

  • Ao usar lambdas, você pode programar com operações de estilo funcional em fluxos de elementos, como transformações de redução de mapa em coleções. veja java.util.function & java.util.stream pacotes de documentação.

  • Não há arquivo de classes físicas gerado para lambdas pelo compilador. Assim, diminui os aplicativos entregues. Como Memory atribui a lambda?

  • O compilador otimizará a criação de lambda se o lambda não acessar variáveis ​​fora de seu escopo, o que significa que a instância lambda será criada apenas uma vez pela JVM. para obter mais detalhes, você pode ver a resposta de @ Holger à pergunta A referência de método ao cache é uma boa idéia no Java 8? .

  • O Lambdas pode implementar interfaces com vários marcadores além da interface funcional, mas as classes internas anônimas não podem implementar mais interfaces, por exemplo:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};
holi-java
fonte
2

Lambdas são apenas açúcar sintático para classes anônimas.

Antes das lambdas, classes anônimas podem ser usadas para obter a mesma coisa. Toda expressão lambda pode ser convertida em uma classe anônima.

Se você estiver usando o IntelliJ IDEA, ele poderá fazer a conversão:

  • Coloque o cursor na lambda
  • Pressione alt / opção + enter

insira a descrição da imagem aqui

Vassoura
fonte
3
... Pensei que o @StuartMarks já esclareceu a açucar sintética (sp? Gr?) Das lambdas? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan
2

Para responder sua pergunta, o fato é que as lambdas não permitem que você faça nada que não pudesse fazer antes do java-8; ao contrário, permite escrever um código mais conciso . Os benefícios disso são que seu código será mais claro e flexível.

Ousmane D.
fonte
Claramente, o @Holger dissipou qualquer dúvida em termos de eficiência lambda. Não é apenas uma maneira de tornar o código mais limpo, mas se o lambda não capturar nenhum valor, será uma classe Singleton (reutilizável)
também em
Se você se aprofundar, cada recurso de idioma tem uma diferença. A questão em questão está em um nível alto, portanto, escrevi minha resposta em um nível alto.
Ousmane D.
2

Uma coisa que ainda não vi mencionada é que um lambda permite definir a funcionalidade onde é usado .

Portanto, se você tem alguma função de seleção simples, não precisa colocá-la em um local separado com um monte de clichê, basta escrever um lambda que seja conciso e relevante localmente.

Carlos
fonte
1

Sim, existem muitas vantagens.

  • Não há necessidade de definir toda a classe, podemos passar a implementação da função como referência.
    • Internamente, a criação da classe criará o arquivo .class; se você usar o lambda, a criação da classe será evitada pelo compilador, porque no lambda você está passando a implementação da função em vez da classe.
  • A reutilização do código é maior do que antes
  • E como você disse, o código é mais curto que a implementação normal.
Sunil Kanzar
fonte