CodeGym /Blogue Java /Random-PT /Uma explicação das expressões lambda em Java. Com exemplo...
John Squirrels
Nível 41
San Francisco

Uma explicação das expressões lambda em Java. Com exemplos e tarefas. Parte 1

Publicado no grupo Random-PT
Para quem é este artigo?
  • É para pessoas que acham que já conhecem bem o Java Core, mas não têm ideia sobre expressões lambda em Java. Ou talvez eles tenham ouvido algo sobre expressões lambda, mas faltam detalhes
  • É para pessoas que têm uma certa compreensão das expressões lambda, mas ainda se assustam com elas e não estão acostumadas a usá-las.
Uma explicação das expressões lambda em Java.  Com exemplos e tarefas.  Parte 1 - 1Se você não se encaixa em nenhuma dessas categorias, pode achar este artigo chato, falho ou geralmente não é sua preferência. Nesse caso, fique à vontade para passar para outras coisas ou, se você for bem versado no assunto, dê sugestões nos comentários sobre como eu poderia melhorar ou complementar o artigo. O material não pretende ter nenhum valor acadêmico, muito menos novidade. Muito pelo contrário: tentarei descrever coisas que são complexas (para algumas pessoas) da maneira mais simples possível. Uma solicitação para explicar a Stream API me inspirou a escrever isso. Eu pensei sobre isso e decidi que alguns dos meus exemplos de stream seriam incompreensíveis sem uma compreensão das expressões lambda. Então, vamos começar com expressões lambda. O que você precisa saber para entender este artigo?
  1. Você deve entender programação orientada a objetos (OOP), a saber:

    • classes, objetos e a diferença entre eles;
    • interfaces, como diferem das classes e relacionamento entre interfaces e classes;
    • métodos, como chamá-los, métodos abstratos (ou seja, métodos sem implementação), parâmetros de método, argumentos de método e como passá-los;
    • modificadores de acesso, métodos/variáveis ​​estáticos, métodos/variáveis ​​finais;
    • herança de classes e interfaces, herança múltipla de interfaces.
  2. Conhecimento de Java Core: tipos genéricos (genéricos), coleções (listas), threads.
Bem, vamos ao que interessa.

Um pouco de história

As expressões lambda vieram para Java da programação funcional e para lá da matemática. Nos Estados Unidos, em meados do século 20, Alonzo Church, que gostava muito de matemática e todo tipo de abstração, trabalhava na Universidade de Princeton. Foi Alonzo Church quem inventou o cálculo lambda, que inicialmente era um conjunto de ideias abstratas totalmente não relacionadas à programação. Matemáticos como Alan Turing e John von Neumann trabalharam na Universidade de Princeton ao mesmo tempo. Tudo se encaixou: Church surgiu com o cálculo lambda. Turing desenvolveu sua máquina de computação abstrata, agora conhecida como "máquina de Turing". E von Neumann propôs uma arquitetura de computador que formou a base dos computadores modernos (agora chamada de "arquitetura von Neumann"). Naquela época, Alonzo Church' Suas ideias não se tornaram tão conhecidas quanto os trabalhos de seus colegas (com exceção do campo da matemática pura). No entanto, um pouco mais tarde, John McCarthy (também formado pela Universidade de Princeton e, na época de nossa história, funcionário do Instituto de Tecnologia de Massachusetts) se interessou pelas ideias de Church. Em 1958, ele criou a primeira linguagem de programação funcional, LISP, com base nessas ideias. E 58 anos depois, as ideias de programação funcional vazaram para o Java 8. Nem 70 anos se passaram... Sinceramente, esse não é o tempo que leva para uma ideia matemática ser aplicada na prática. um funcionário do Massachusetts Institute of Technology) interessou-se pelas ideias de Church. Em 1958, ele criou a primeira linguagem de programação funcional, LISP, com base nessas ideias. E 58 anos depois, as ideias de programação funcional vazaram para o Java 8. Nem 70 anos se passaram... Sinceramente, esse não é o tempo que leva para uma ideia matemática ser aplicada na prática. um funcionário do Massachusetts Institute of Technology) interessou-se pelas ideias de Church. Em 1958, ele criou a primeira linguagem de programação funcional, LISP, com base nessas ideias. E 58 anos depois, as ideias de programação funcional vazaram para o Java 8. Nem 70 anos se passaram... Sinceramente, esse não é o tempo que leva para uma ideia matemática ser aplicada na prática.

O coração da matéria

Uma expressão lambda é um tipo de função. Você pode considerá-lo um método Java comum, mas com a capacidade distinta de ser passado para outros métodos como um argumento. Isso mesmo. Tornou-se possível passar não apenas números, strings e gatos para métodos, mas também outros métodos! Quando podemos precisar disso? Seria útil, por exemplo, se quisermos passar algum método de retorno de chamada. Ou seja, se precisamos que o método que chamamos tenha a capacidade de chamar algum outro método que passamos para ele. Em outras palavras, temos a capacidade de passar um callback em determinadas circunstâncias e um callback diferente em outras. E para que nosso método que recebe nossos callbacks os chame. A classificação é um exemplo simples. Suponha que estamos escrevendo algum algoritmo de classificação inteligente que se parece com isto:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
No ifenunciado, chamamos o compare()método, passando dois objetos a serem comparados, e queremos saber qual desses objetos é "maior". Assumimos que o "maior" vem antes do "menor". Coloquei "maior" entre aspas, pois estamos escrevendo um método universal que saberá ordenar não só em ordem crescente, mas também em ordem decrescente (neste caso, o objeto "maior" será na verdade o objeto "menor" , e vice versa). Para definir o algoritmo específico para nossa classificação, precisamos de algum mecanismo para passá-lo para nosso mySuperSort()método. Assim poderemos "controlar" nosso método quando ele for chamado. Claro, poderíamos escrever dois métodos separados - mySuperSortAscend()emySuperSortDescend()— para classificar em ordem crescente e decrescente. Ou poderíamos passar algum argumento para o método (por exemplo, uma variável booleana; se for verdadeiro, classifique em ordem crescente e, se for falso, em ordem decrescente). Mas e se quisermos classificar algo complicado, como uma lista de arrays de strings? Como nosso mySuperSort()método saberá como classificar esses arrays de strings? Por tamanho? Pelo comprimento cumulativo de todas as palavras? Talvez em ordem alfabética com base na primeira string da matriz? E se precisarmos classificar a lista de arrays pelo tamanho do array em alguns casos, e pelo tamanho cumulativo de todas as palavras em cada array em outros casos? Espero que você já tenha ouvido falar sobre comparadores e que, neste caso, simplesmente passaríamos para nosso método de classificação um objeto comparador que descreve o algoritmo de classificação desejado. Porque o padrãosort()O método é implementado com base no mesmo princípio que mySuperSort()usarei sort()em meus exemplos.

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 
arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

Comparator<;String[]> sortByLength = new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}; 

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() { 

    @Override 
    public int compare(String[] o1, String[] o2) { 
        int length1 = 0; 
        int length2 = 0; 
        for (String s : o1) { 
            length1 += s.length(); 
        } 

        for (String s : o2) { 
            length2 += s.length(); 
        } 

        return length1 - length2; 
    } 
};

arrays.sort(sortByLength);
Resultado:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Aqui as matrizes são classificadas pelo número de palavras em cada matriz. Uma matriz com menos palavras é considerada "menor". É por isso que vem primeiro. Uma matriz com mais palavras é considerada "maior" e é colocada no final. Se passarmos um comparador diferente para o sort()método, como sortByCumulativeWordLength, obteremos um resultado diferente:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Agora, as matrizes são classificadas pelo número total de letras nas palavras da matriz. Na primeira matriz, há 10 letras, na segunda — 12 e na terceira — 15. Se tivermos apenas um único comparador, não precisamos declarar uma variável separada para ele. Em vez disso, podemos simplesmente criar uma classe anônima no momento da chamada para o sort()método. Algo assim:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 

arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}); 
Teremos o mesmo resultado do primeiro caso. Tarefa 1. Reescreva este exemplo para que ele classifique os arrays não em ordem crescente do número de palavras em cada array, mas em ordem decrescente. Já sabemos de tudo isso. Sabemos como passar objetos para métodos. Dependendo do que precisamos no momento, podemos passar diferentes objetos para um método, que então invocará o método que implementamos. Isso levanta a questão: por que diabos precisamos de uma expressão lambda aqui?  Porque uma expressão lambda é um objeto que tem exatamente um método. Como um "objeto de método". Um método empacotado em um objeto. Ele só tem uma sintaxe um pouco desconhecida (mas falaremos mais sobre isso depois). Vamos dar outra olhada neste código:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Aqui pegamos nossa lista de arrays e chamamos seu sort()método, para o qual passamos um objeto comparador com um único compare()método (seu nome não importa para nós — afinal, é o único método deste objeto, então não podemos errar). Este método tem dois parâmetros com os quais trabalharemos. Se você estiver trabalhando no IntelliJ IDEA, provavelmente o viu oferecer uma condensação significativa do código da seguinte maneira:

arrays.sort((o1, o2) -> o1.length - o2.length);
Isso reduz seis linhas a uma única linha curta. 6 linhas são reescritas como uma curta. Alguma coisa sumiu, mas garanto que não foi nada importante. Esse código funcionará exatamente da mesma maneira que com uma classe anônima. Tarefa 2. Tente reescrever a solução para a Tarefa 1 usando uma expressão lambda (no mínimo, peça ao IntelliJ IDEA para converter sua classe anônima em uma expressão lambda).

Vamos falar sobre interfaces

Em princípio, uma interface é simplesmente uma lista de métodos abstratos. Quando criamos uma classe que implementa alguma interface, nossa classe deve implementar os métodos incluídos na interface (ou temos que tornar a classe abstrata). Existem interfaces com muitos métodos diferentes (por exemplo,  List) e interfaces com apenas um método (por exemplo, Comparatorou Runnable). Existem interfaces que não possuem um único método (as chamadas interfaces de marcador como Serializable). As interfaces que possuem apenas um método também são chamadas de interfaces funcionais . No Java 8, eles ainda são marcados com uma anotação especial:@FunctionalInterface. São essas interfaces de método único que são adequadas como tipos de destino para expressões lambda. Como eu disse acima, uma expressão lambda é um método envolvido em um objeto. E quando passamos tal objeto, estamos essencialmente passando este único método. Acontece que não nos importamos como o método é chamado. As únicas coisas que importam para nós são os parâmetros do método e, claro, o corpo do método. Em essência, uma expressão lambda é a implementação de uma interface funcional. Sempre que vemos uma interface com um único método, uma classe anônima pode ser reescrita como lambda. Se a interface tiver mais ou menos de um método, uma expressão lambda não funcionará e, em vez disso, usaremos uma classe anônima ou até mesmo uma instância de uma classe comum. Agora é hora de explorar um pouco os lambdas. :)

Sintaxe

A sintaxe geral é mais ou menos assim:

(parameters) -> {method body}
Ou seja, parênteses envolvendo os parâmetros do método, uma "seta" (formada por hífen e sinal de maior que) e depois o corpo do método entre colchetes, como sempre. Os parâmetros correspondem aos especificados no método de interface. Se os tipos de variáveis ​​podem ser determinados de forma inequívoca pelo compilador (no nosso caso, ele sabe que estamos trabalhando com arrays de strings, porque nosso Listobjeto é digitado usando String[]), então você não precisa indicar seus tipos.
Se forem ambíguos, indique o tipo. IDEA irá colori-lo de cinza se não for necessário.
Você pode ler mais neste tutorial da Oracle e em outros lugares. Isso é chamado de " digitação de destino ". Você pode nomear as variáveis ​​como quiser — não precisa usar os mesmos nomes especificados na interface. Se não houver parâmetros, basta indicar parênteses vazios. Se houver apenas um parâmetro, basta indicar o nome da variável sem parênteses. Agora que entendemos os parâmetros, é hora de discutir o corpo da expressão lambda. Dentro das chaves, você escreve o código exatamente como faria para um método comum. Se o seu código consiste em uma única linha, você pode omitir totalmente as chaves (semelhante às instruções if e loops for). Se o seu lambda de linha única retornar algo, você não precisa incluir umreturndeclaração. Mas se você usar chaves, então você deve incluir explicitamente uma returninstrução, assim como faria em um método comum.

Exemplos

Exemplo 1.

() -> {}
O exemplo mais simples. E o mais inútil :), já que não faz nada. Exemplo 2.

() -> ""
Outro exemplo interessante. Não leva nada e retorna uma string vazia ( returné omitido, porque é desnecessário). Aqui está a mesma coisa, mas com return:

() -> { 
    return ""; 
}
Exemplo 3. "Olá, Mundo!" usando lambdas

() -> System.out.println("Hello, World!")
Não leva nada e não retorna nada (não podemos colocar returnantes da chamada para System.out.println(), porque o println()tipo de retorno do método é void). Ele simplesmente exibe a saudação. Isso é ideal para uma implementação da Runnableinterface. O exemplo a seguir é mais completo:

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
Ou assim:

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Ou podemos até mesmo salvar a expressão lambda como um Runnableobjeto e passá-la para o Threadconstrutor:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Vamos dar uma olhada no momento em que uma expressão lambda é salva em uma variável. A Runnableinterface nos diz que seus objetos devem ter um public void run()método. De acordo com a interface, o runmétodo não aceita parâmetros. E não retorna nada, ou seja, seu tipo de retorno é void. Da mesma forma, esse código criará um objeto com um método que não recebe nem retorna nada. Isso combina perfeitamente com o método Runnableda interface run(). É por isso que conseguimos colocar essa expressão lambda em uma Runnablevariável.  Exemplo 4.

() -> 42
Novamente, não leva nada, mas retorna o número 42. Tal expressão lambda pode ser colocada em uma Callablevariável, porque esta interface possui apenas um método que se parece com isto:

V call(),
onde  V é o tipo de retorno (no nosso caso,  int). Assim, podemos salvar uma expressão lambda da seguinte forma:

Callable<Integer> c = () -> 42;
Exemplo 5. Uma expressão lambda envolvendo várias linhas

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Novamente, esta é uma expressão lambda sem parâmetros e um voidtipo de retorno (porque não há returninstrução).  Exemplo 6

x -> x
Aqui pegamos uma xvariável e a retornamos. Observe que, se houver apenas um parâmetro, você poderá omitir os parênteses ao redor dele. Aqui está a mesma coisa, mas com parênteses:

(x) -> x
E aqui está um exemplo com uma declaração de retorno explícita:

x -> { 
    return x;
}
Ou assim com parênteses e uma declaração de retorno:

(x) -> { 
    return x;
}
Ou com uma indicação explícita do tipo (e, portanto, entre parênteses):

(int x) -> x
Exemplo 7

x -> ++x
Pegamos xe devolvemos, mas somente depois de adicionar 1. Você pode reescrever esse lambda assim:

x -> x + 1
Em ambos os casos, omitimos os parênteses ao redor do parâmetro e do corpo do método, junto com a returninstrução, pois são opcionais. Versões com parênteses e uma instrução de retorno são fornecidas no Exemplo 6. Exemplo 8

(x, y) -> x % y
Pegamos xe ye retornamos o restante da divisão de xpor y. Os parênteses em torno dos parâmetros são necessários aqui. Eles são opcionais apenas quando há apenas um parâmetro. Aqui está com uma indicação explícita dos tipos:

(double x, int y) -> x % y
Exemplo 9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Pegamos um Catobjeto, um Stringnome e uma idade int. No próprio método, usamos o nome e a idade passados ​​para definir variáveis ​​no gato. Como nosso catobjeto é um tipo de referência, ele será alterado fora da expressão lambda (ele receberá o nome e a idade passados). Aqui está uma versão um pouco mais complicada que usa um lambda semelhante:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) { 
        s.set(entity, "Smokey", 3); 
    }
}

interface HasNameAndAge { 
    void setName(String name); 
    void setAge(int age); 
}

interface Settable<C extends HasNameAndAge> { 
    void set(C entity, String name, int age); 
}

class Cat implements HasNameAndAge { 
    private String name; 
    private int age; 

    @Override 
    public void setName(String name) { 
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age; 
    } 

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' + 
                ", age=" + age + 
                '}';
    }
}
Resultado:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Como você pode ver, o Catobjeto tinha um estado e o estado mudou depois que usamos a expressão lambda. As expressões lambda combinam perfeitamente com os genéricos. E se precisarmos criar uma Dogclasse que também implemente HasNameAndAge, podemos realizar as mesmas operações no Dogmétodo main() sem alterar a expressão lambda. Tarefa 3. Escreva uma interface funcional com um método que receba um número e retorne um valor booleano. Escreva uma implementação dessa interface como uma expressão lambda que retorne true se o número passado for divisível por 13. Tarefa 4.Escreva uma interface funcional com um método que receba duas strings e também retorne uma string. Escreva uma implementação dessa interface como uma expressão lambda que retorne a string mais longa. Tarefa 5. Escreva uma interface funcional com um método que receba três números de ponto flutuante: a, b e c e também retorne um número de ponto flutuante. Escreva uma implementação dessa interface como uma expressão lambda que retorne o discriminante. Caso você tenha esquecido, é D = b^2 — 4ac. Tarefa 6. Usando a interface funcional da Tarefa 5, escreva uma expressão lambda que retorne o resultado de a * b^c. Uma explicação das expressões lambda em Java. Com exemplos e tarefas. Parte 2
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION