JavaRush /Blogue Java /Random-PT /Popular sobre expressões lambda em Java. Com exemplos e t...
Стас Пасинков
Nível 26
Киев

Popular sobre expressões lambda em Java. Com exemplos e tarefas. Parte 1

Publicado no grupo Random-PT
Para quem é este artigo?
  • Para quem pensa que já conhece bem o Java Core, mas não tem ideia sobre expressões lambda em Java. Ou talvez você já tenha ouvido algo sobre lambdas, mas sem detalhes.
  • para quem tem algum entendimento de expressões lambda, mas ainda tem medo e é incomum de usá-las.
Se você não se enquadra em uma dessas categorias, você pode achar este artigo chato, incorreto e geralmente “nada legal”. Nesse caso, fique à vontade para passar, ou, se você for bem versado no assunto, sugira nos comentários como posso melhorar ou complementar o artigo. O material não reivindica nenhum valor acadêmico e muito menos novidade. Pelo contrário: nele tentarei descrever coisas complexas (para alguns) da forma mais simples possível. Fui inspirado a escrever por um pedido para explicar a API do stream. Pensei sobre isso e decidi que sem entender as expressões lambda, alguns dos meus exemplos sobre “streams” seriam incompreensíveis. Então, vamos começar com lambdas. Popular sobre expressões lambda em Java.  Com exemplos e tarefas.  Parte 1 - 1Que conhecimento é necessário para entender este artigo:
  1. Compreensão da programação orientada a objetos (doravante denominada OOP), a saber:
    • conhecimento do que são classes e objetos, qual a diferença entre eles;
    • conhecimento do que são interfaces, como se diferenciam das classes, qual a ligação entre elas (interfaces e classes);
    • conhecimento do que é um método, como chamá-lo, o que é um método abstrato (ou um método sem implementação), quais são os parâmetros/argumentos de um método, como passá-los para lá;
    • modificadores de acesso, métodos/variáveis ​​estáticos, métodos/variáveis ​​finais;
    • herança (classes, interfaces, herança múltipla de interfaces).
  2. Conhecimento de Java Core: genéricos, coleções (listas), threads.
Bem, vamos começar.

Um pouco de história

As expressões lambda vieram para Java da programação funcional e, daí, da matemática. Em meados do século 20, na América, um certo Alonzo Church trabalhava na Universidade de Princeton, que gostava muito de matemática e de todo tipo de abstração. Foi Alonzo Church quem criou o cálculo lambda, que inicialmente era um conjunto de algumas ideias abstratas e não tinha nada a ver com programação. Ao mesmo tempo, matemáticos como Alan Turing e John von Neumann trabalharam na mesma Universidade de Princeton. Tudo deu certo: Church criou o sistema de cálculo lambda, Turing desenvolveu sua máquina de computação abstrata, agora conhecida como “máquina de Turing”. Bem, von Neumann propôs um diagrama da arquitetura dos computadores, que formou a base dos computadores modernos (e agora é chamada de “arquitetura de von Neumann”). Naquela época, as ideias de Alonzo Church não ganharam tanta fama quanto o trabalho de seus colegas (com exceção do campo da matemática “pura”). Porém, um pouco mais tarde, um certo John McCarthy (também formado pela Universidade de Princeton, na época da história - funcionário do Instituto de Tecnologia de Massachusetts) interessou-se pelas ideias de Church. Com base neles, em 1958 criou a primeira linguagem de programação funcional, Lisp. E 58 anos depois, as ideias de programação funcional vazaram para Java como número 8. Nem mesmo 70 anos se passaram... Na verdade, este não é o período mais longo para aplicar uma ideia matemática na prática.

A essência

Uma expressão lambda é uma dessas funções. Você pode pensar nisso como um método regular em Java, a única diferença é que ele pode ser passado para outros métodos como argumento. Sim, tornou-se possível passar não apenas números, strings e gatos para métodos, mas também outros métodos! Quando poderemos precisar disso? Por exemplo, se quisermos passar algum retorno de chamada. Precisamos que o método que chamamos seja capaz de chamar algum outro método que passamos para ele. Ou seja, para que tenhamos a oportunidade de transmitir um callback em alguns casos e outro em outros. E nosso método, que aceitaria nossos retornos de chamada, os chamaria. Um exemplo simples é a classificação. Digamos que escrevemos algum tipo de classificação complicada parecida com esta:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Onde, ifchamamos o método compare(), passamos lá dois objetos que comparamos, e queremos descobrir qual desses objetos é “maior”. Colocaremos o que é “mais” antes do que é “menor”. Escrevi “mais” entre aspas porque estamos escrevendo um método universal que será capaz de ordenar não apenas em ordem crescente, mas também decrescente (neste caso, “mais” será o objeto que é essencialmente menor, e vice-versa) . Para definir a regra exatamente como queremos classificar, precisamos de alguma forma passá-la para o nosso arquivo mySuperSort(). Neste caso, seremos capazes de “controlar” de alguma forma nosso método enquanto ele está sendo chamado. Claro, você pode escrever dois métodos separados mySuperSortAsc()para mySuperSortDesc()classificação em ordem crescente e decrescente. Ou passe algum parâmetro dentro do método (por exemplo, booleanif true, classifique em ordem crescente e if falseem ordem decrescente). Mas e se quisermos classificar não uma estrutura simples, mas, por exemplo, uma lista de arrays de strings? Como nosso método mySuperSort()saberá como classificar essas matrizes de strings? Para dimensionar? Pelo comprimento total das palavras? Talvez em ordem alfabética, dependendo da primeira linha da matriz? Mas e se, em alguns casos, precisarmos classificar uma lista de arrays pelo tamanho do array e, em outro caso, pelo comprimento total das palavras no array? Acho que você já ouviu falar sobre comparadores e que nesses casos simplesmente passamos um objeto comparador para o nosso método de ordenação, no qual descrevemos as regras pelas quais queremos ordenar. Como o método padrão sort()é implementado de acordo com o mesmo princípio que , mySuperSort()nos exemplos usarei o método padrão sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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[]> sortByWordsLength = 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. mamãe lavou a moldura
  2. paz O trabalho pode
  3. Eu realmente amo 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 no início. Aquele onde há mais palavras é considerado “mais” e vai para o final. Se sort()passarmos outro comparador para o método (sortByWordsLength), então o resultado será diferente:
  1. paz O trabalho pode
  2. mamãe lavou a moldura
  3. Eu realmente amo java
Agora as matrizes são classificadas pelo número total de letras nas palavras dessa matriz. No primeiro caso são 10 letras, no segundo 12 e no terceiro 15. Se usarmos apenas um comparador, não poderemos criar uma variável separada para ele, mas simplesmente criar um objeto de uma classe anônima logo no hora de chamar o método sort(). Assim:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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;
    }
});
O resultado será o mesmo do primeiro caso. Tarefa 1 . Reescreva este exemplo para que ele classifique as matrizes não em ordem crescente do número de palavras na matriz, mas em ordem decrescente. Já sabemos tudo isso. Sabemos como passar objetos para métodos, podemos passar este ou aquele objeto para um método dependendo do que precisamos no momento, e dentro do método onde passamos tal objeto, será chamado o método para o qual escrevemos a implementação . Surge a pergunta: o que as expressões lambda têm a ver com isso? Dado que um lambda é um objeto que contém exatamente um método. É como um objeto de método. Um método envolvido em um objeto. Eles apenas têm uma sintaxe um pouco incomum (mas falaremos mais sobre isso mais tarde). Vamos dar uma outra olhada nesta entrada
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Aqui pegamos nossa lista arrayse chamamos seu método sort(), onde passamos um objeto comparador com um único método compare()(não nos importa como ele é chamado, porque é o único neste objeto, não vamos sentir falta dele). Este método usa dois parâmetros, com os quais trabalharemos a seguir. Se você trabalha no IntelliJ IDEA , provavelmente já viu como ele oferece este código para encurtar significativamente:
arrays.sort((o1, o2) -> o1.length - o2.length);
Foi assim que seis linhas se transformaram em uma curta. 6 linhas foram reescritas em uma curta. Algo desapareceu, mas garanto que nada de importante desapareceu, e esse código funcionará exatamente da mesma forma que uma classe anônima. Tarefa 2 . Descubra como reescrever a solução para o problema 1 usando lambdas (como último recurso, peça ao IntelliJ IDEA para transformar sua classe anônima em um lambda).

Vamos falar sobre interfaces

Basicamente, uma interface é apenas uma lista de métodos abstratos. Quando criamos uma classe e dizemos que ela implementará algum tipo de interface, devemos escrever em nossa classe uma implementação dos métodos que estão listados na interface (ou, como último recurso, não escrevê-la, mas tornar a classe abstrata ). Existem interfaces com muitos métodos diferentes (por exemplo List), existem interfaces com apenas um método (por exemplo, o mesmo Comparator ou Runnable). Existem interfaces sem um único método (as chamadas interfaces de marcador, por exemplo Serializable). Aquelas interfaces que possuem apenas um método também são chamadas de interfaces funcionais . No Java 8 eles são marcados com uma anotação especial @FunctionalInterface . São interfaces com um único método adequadas para uso por expressões lambda. Como eu disse acima, uma expressão lambda é um método envolvido em um objeto. E quando passamos esse objeto em algum lugar, nós, de fato, passamos esse único método. Acontece que não nos importa como esse método é chamado. Tudo o que importa para nós são os parâmetros que esse método utiliza e, de fato, o próprio código do método. Uma expressão lambda é, essencialmente. implementação de uma interface funcional. Onde vemos uma interface com um método, significa que podemos reescrever essa classe anônima usando um lambda. Se a interface tiver mais/menos de um método, então a expressão lambda não nos servirá e usaremos uma classe anônima, ou mesmo regular. É hora de aprofundar os lambdas. :)

Sintaxe

A sintaxe geral é mais ou menos assim:
(параметры) -> {тело метода}
Ou seja, parênteses, dentro deles estão os parâmetros do método, uma “seta” (são dois caracteres seguidos: menos e maior), após o qual o corpo do método fica entre chaves, como sempre. Os parâmetros correspondem aos especificados na interface ao descrever o método. Se o tipo das variáveis ​​​​pode ser claramente definido pelo compilador (no nosso caso, sabe-se com certeza que estamos trabalhando com arrays de strings, porque é Listdigitado precisamente por arrays de strings), então o tipo das variáveis String[]​​​​não precisa ser escrito.
Se não tiver certeza, especifique o tipo e o IDEA o destacará em cinza se não for necessário.
Você pode ler mais no tutorial do Oracle , por exemplo. Isso é chamado de "digitação de destino" . Você pode dar qualquer nome às variáveis, não necessariamente aqueles especificados na interface. Se não houver parâmetros, apenas parênteses. Se houver apenas um parâmetro, apenas o nome da variável sem parênteses. Resolvemos os parâmetros, agora sobre o corpo da expressão lambda em si. Dentro das chaves, escreva o código como se fosse um método normal. Se todo o seu código consistir em apenas uma linha, você não precisará escrever chaves (como acontece com ifs e loops). Se o seu lambda retornar algo, mas seu corpo consistir em uma linha, returnnão será necessário escrever. Mas se você tiver chaves, então, como no método usual, você precisa escrever explicitamente return.

Exemplos

Exemplo 1.
() -> {}
A opção mais simples. E o mais sem sentido :) Porque não faz nada. Exemplo 2.
() -> ""
Também é uma opção interessante. Não aceita nada e retorna uma string vazia ( returnomitida como desnecessária). O mesmo, mas com return:
() -> {
    return "";
}
Exemplo 3. Olá, mundo usando lambdas
() -> System.out.println("Hello world!")
Não recebe nada, não retorna nada (não podemos colocar returnantes da chamada System.out.println(), pois o tipo de retorno no método println() — void), simplesmente exibe uma inscrição na tela. Ideal para implementar uma interface Runnable. O mesmo exemplo é 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é salvar a expressão lambda como um objeto do tipo Runnablee depois passá-la para o construtor thread’а:
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 mais de perto no momento de salvar uma expressão lambda em uma variável. A interface Runnablenos diz que seus objetos devem ter um método public void run(). De acordo com a interface, o método run não aceita nada como parâmetro. E não retorna nada (void). Portanto, ao escrever desta forma, será criado um objeto com algum método que não aceita nem retorna nada. O que é bastante consistente com o método run()na interface Runnable. É por isso que conseguimos colocar essa expressão lambda em uma variável como Runnable. Exemplo 4
() -> 42
Novamente, ele não aceita nada, mas retorna o número 42. Essa expressão lambda pode ser colocada em uma variável do tipo Callable, pois essa interface define apenas um método, que é mais ou menos assim:
V call(),
onde Vestá o tipo do valor de retorno (no nosso caso int). Conseqüentemente, podemos armazenar tal expressão lambda da seguinte maneira:
Callable<Integer> c = () -> 42;
Exemplo 5. Lambda em 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 seu tipo de retorno void(já que não existe return). Exemplo 6
x -> x
Aqui pegamos algo em uma variável хe o retornamos. Observe que se apenas um parâmetro for aceito, os parênteses ao redor dele não precisarão ser escritos. O mesmo, mas com colchetes:
(x) -> x
E aqui está a opção explícita return:
x -> {
    return x;
}
Ou assim, com colchetes e return:
(x) -> {
    return x;
}
Ou com indicação explícita do tipo (e, consequentemente, entre parênteses):
(int x) -> x
Exemplo 7
x -> ++x
Aceitamos хe devolvemos, mas por 1mais. Você também pode reescrever assim:
x -> x + 1
Em ambos os casos, não indicamos parênteses em torno do parâmetro, corpo do método e palavra return, pois isso não é necessário. As opções com colchetes e retorno estão descritas no exemplo 6. Exemplo 8
(x, y) -> x % y
Aceitamos alguns хe уretornamos o restante da divisão xpor y. Parênteses em torno dos parâmetros já são obrigatórios aqui. Eles são opcionais apenas quando há apenas um parâmetro. Assim com indicação explícita de tipos:
(double x, int y) -> x % y
Exemplo 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Aceitamos um objeto Cat, uma string com um nome e uma idade inteira. No próprio método, definimos o nome e a idade passados ​​para o método Cat. catComo nossa variável é um tipo de referência, o objeto Cat fora da expressão lambda será alterado (receberá o nome e a idade passados ​​dentro). 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 print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

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

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

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

class Cat implements WithNameAndAge {
    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='Murzik', age=3} Como você pode ver, a princípio o objeto Cat tinha um estado, mas depois de usar a expressão lambda, o estado mudou . Expressões lambda funcionam bem com genéricos. E se precisarmos criar uma classe Dog, por exemplo, que também irá implementar WithNameAndAge, então no método main()podemos fazer as mesmas operações com Dog, sem alterar em nada a própria 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 de tal interface na forma de uma expressão lambda que retorna se o número passadotrue for divisível por 13 sem resto . Escreva uma interface funcional com um método que receba duas strings e retorne a mesma string. Escreva uma implementação de tal interface na forma de um lambda que retorne a string mais longa. Tarefa 5 . Escreva uma interface funcional com um método que aceite três números fracionários: , e retorne o mesmo número fracionário. Escreva uma implementação dessa interface na forma de uma expressão lambda que retorne um discriminante. Quem esqueceu, D = b^2 - 4ac . Tarefa 6 . Usando a interface funcional da tarefa 5, escreva uma expressão lambda que retorne o resultado da operação . Popular sobre expressões lambda em Java. Com exemplos e tarefas. Parte 2.abca * b^c
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION