JavaRush /Blogue Java /Random-PT /Lambdas e referências de método em ArrayList.forEach - co...

Lambdas e referências de método em ArrayList.forEach - como funciona

Publicado no grupo Random-PT
A introdução às expressões lambda na missão Java Syntax Zero começa com um exemplo muito específico:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Os autores da palestra analisam lambdas e referências de método usando a função forEach padrão da classe ArrayList. Pessoalmente, tive dificuldade em compreender o significado do que estava a acontecer, uma vez que a implementação desta função, bem como a interface a ela associada, permanecem “nos bastidores”. De onde vem o (s) argumento(s) , onde a função println() é passada são questões que teremos que responder nós mesmos. Felizmente, com o IntelliJ IDEA, podemos facilmente examinar o interior da classe ArrayList e desenrolar esse macarrão desde o início. Se você também não entende nada e quer descobrir, tentarei te ajudar pelo menos um pouco com isso. Expressão lambda e ArrayList.forEach – como funciona Da palestra já sabemos que uma expressão lambda é uma implementação de uma interface funcional . Ou seja, declaramos uma interface com uma única função e usamos um lambda para descrever o que essa função faz. Para fazer isso você precisa: 1. Criar uma interface funcional; 2. Crie uma variável cujo tipo corresponda à interface funcional; 3. Atribua a esta variável uma expressão lambda que descreva a implementação da função; 4. Chame uma função acessando uma variável (talvez eu esteja sendo grosseiro na terminologia, mas esta é a maneira mais clara). Darei um exemplo simples do Google, com comentários detalhados (obrigado aos autores do site metanit.com):
interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс,
    // который можно реализовать с помощью лямбды
}

public class LambdaApp {

    public static void main(String[] args) {

        // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
        Operationable operation;
        // Прописываем реализацию функции calculate с помощью лямбды, на вход подаём x и y, на выходе возвращаем их сумму
        operation = (x,y)->x+y;

        // Теперь мы можем обратиться к функции calculate через переменную operation
        int result = operation.calculate(10, 20);
        System.out.println(result); //30
    }
}
Agora vamos voltar ao exemplo da palestra. Vários elementos do tipo String são adicionados à coleção de listas . Os elementos são então recuperados usando a função forEach padrão , que é chamada no objeto de lista . Uma expressão lambda com alguns parâmetros estranhos é passada como argumento para a função .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Se você não entendeu imediatamente o que aconteceu aqui, então você não está sozinho. Felizmente, o IntelliJ IDEA tem um ótimo atalho de teclado: Ctrl+Left_Mouse_Button . Se passarmos o mouse sobre forEach e clicarmos nesta combinação, o código-fonte da classe ArrayList padrão será aberto, no qual veremos a implementação do método forEach :
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i));
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
Vemos que o argumento de entrada é uma ação do tipo Consumer . Vamos mover o cursor sobre a palavra Consumidor e pressionar novamente a combinação mágica Ctrl+LMB . Uma descrição da interface do Consumidor será aberta . Se removermos dele a implementação padrão (isso não é importante para nós agora), veremos o seguinte código:
public interface Consumer<t> {
   void accept(T t);
}
Então. Temos uma interface Consumer com uma única função de aceitação que aceita um argumento de qualquer tipo. Como existe apenas uma função, a interface é funcional e sua implementação pode ser escrita por meio de uma expressão lambda. Já vimos que ArrayList possui uma função forEach que utiliza uma implementação da interface Consumer como argumento de ação . Além disso, na função forEach encontramos o seguinte código:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
O loop for essencialmente itera por todos os elementos de um ArrayList. Dentro do loop vemos uma chamada para a função de aceitação do objeto de ação - lembra como chamamos operation.calculate? O elemento atual da coleção é passado para a função de aceitação . Agora podemos finalmente voltar à expressão lambda original e entender o que ela faz. Vamos coletar todo o código em uma pilha:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована нашей лямбдой {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i)); // Вызываем нашу реализацию функции accept интерфейса Consumer для каждого element коллекции
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

//...

list.forEach( (s) -> System.out.println(s) );
Nossa expressão lambda é uma implementação da função de aceitação descrita na interface Consumer . Usando um lambda, especificamos que a função accept recebe um argumento s e o exibe na tela. A expressão lambda foi passada para a função forEach como seu argumento de ação , que armazena a implementação da interface Consumer . Agora a função forEach pode chamar nossa implementação da interface Consumer com uma linha como esta:
action.accept(elementAt(es, i));
Assim, os argumentos de entrada na expressão lambda são outro elemento da coleção ArrayList , que é passado para nossa implementação da interface Consumer . Isso é tudo: analisamos a lógica da expressão lambda em ArrayList.forEach. Referência a um método em ArrayList.forEach - como funciona? A próxima etapa da palestra é examinar as referências de métodos. É verdade que eles entendem isso de uma forma muito estranha - depois de ler a palestra, não tive chance de entender o que esse código faz:
list.forEach( System.out::println );
Primeiro, um pouco de teoria novamente. Uma referência de método é, grosso modo, uma implementação de uma interface funcional descrita por outra função . Novamente, começarei com um exemplo simples:
public interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс
}

public static class Calculator {
    // Создадим статический класс Calculator и пропишем в нём метод methodReference.
    // Именно он будет реализовывать функцию calculate из интерфейса Operationable.
    public static int methodReference(int x, int y) {
        return x+y;
    }
}

public static void main(String[] args) {
    // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
    Operationable operation;
    // Теперь реализацией интерфейса будет не лямбда-выражение, а метод methodReference из нашего класса Calculator
    operation = Calculator::methodReference;

    // Теперь мы можем обратиться к функции интерфейса через переменную operation
    int result = operation.calculate(10, 20);
    System.out.println(result); //30
}
Voltemos ao exemplo da palestra:
list.forEach( System.out::println );
Deixe-me lembrá-lo de que System.out é um objeto do tipo PrintStream que possui uma função println . Vamos passar o mouse sobre println e clicar em Ctrl+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Observemos dois recursos principais: 1. A função println não retorna nada (void). 2. A função println recebe um argumento como entrada. Não te lembra nada?
public interface Consumer<t> {
   void accept(T t);
}
É isso mesmo - a assinatura da função accept é um caso mais geral da assinatura do método println ! Isso significa que o último pode ser usado com sucesso como referência a um método - ou seja, println se torna uma implementação específica da função accept :
list.forEach( System.out::println );
Passamos a função println do objeto System.out como argumento para a função forEach . O princípio é o mesmo do lambda: agora forEach pode passar um elemento de coleção para a função println por meio de uma chamada action.accept(elementAt(es, i)) . Na verdade, isso agora pode ser lido como System.out.println(elementAt(es, i)) .
public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована методом println {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        final Object[] es = elementData;
        final int size = this.size;
        for (int i = 0; modCount == expectedModCount && i < size; i++)
            action.accept(elementAt(es, i)); // Функция accept теперь реализована методом System.out.println!
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
Espero ter esclarecido pelo menos um pouco a situação para aqueles que são novos em lambdas e referências de métodos. Concluindo, recomendo o famoso livro "Java: A Beginner's Guide", de Robert Schildt - na minha opinião, lambdas e referências de funções são descritas de maneira bastante sensata.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION