1. Interfaces

Para entender o que são funções lambda, primeiro você precisa entender o que são interfaces. Então, vamos relembrar os pontos principais.

Uma interface é uma variação do conceito de uma classe. Uma classe fortemente truncada, digamos. Ao contrário de uma classe, uma interface não pode ter suas próprias variáveis ​​(exceto as estáticas). Você também não pode criar objetos cujo tipo é uma interface:

  • Você não pode declarar variáveis ​​da classe
  • Você não pode criar objetos

Exemplo:

interface Runnable
{
   void run();
}
Exemplo de uma interface padrão

Usando uma interface

Então, por que uma interface é necessária? As interfaces são usadas apenas em conjunto com a herança. A mesma interface pode ser herdada por diferentes classes, ou como também é dito — as classes implementam a interface .

Se uma classe implementa uma interface, ela deve implementar os métodos declarados, mas não implementados pela interface. Exemplo:

interface Runnable
{
   void run();
}

class Timer implements Runnable
{
   void run()
   {
      System.out.println(LocalTime.now());
   }
}

class Calendar implements Runnable
{
   void run()
   {
      var date = LocalDate.now();
      System.out.println("Today: " + date.getDayOfWeek());
   }
}

A Timerclasse implementa a Runnableinterface, então ela deve declarar dentro de si todos os métodos que estão na Runnableinterface e implementá-los, ou seja, escrever código no corpo de um método. O mesmo vale para a Calendarclasse.

Mas agora Runnableas variáveis ​​podem armazenar referências a objetos que implementam a Runnableinterface.

Exemplo:

Código Observação
Timer timer = new Timer();
timer.run();

Runnable r1 = new Timer();
r1.run();

Runnable r2 = new Calendar();
r2.run();

O run()método da Timerclasse será chamado


O run()método da Timerclasse será chamado


O run()método da Calendarclasse será chamado

Você sempre pode atribuir uma referência de objeto a uma variável de qualquer tipo, desde que esse tipo seja uma das classes ancestrais do objeto. Para as classes Timere Calendar, existem dois tipos: Objecte Runnable.

Se você atribuir uma referência de objeto a uma Objectvariável, poderá chamar apenas os métodos declarados na Objectclasse. E se você atribuir uma referência de objeto a uma Runnablevariável, poderá chamar os métodos do Runnabletipo.

Exemplo 2:

ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());

for (Runnable element: list)
    element.run();

Esse código funcionará porque os objetos Timere Calendarexecutam métodos que funcionam perfeitamente bem. Então, chamá-los não é um problema. Se tivéssemos apenas adicionado um método run() a ambas as classes, não poderíamos chamá-los de maneira tão simples.

Basicamente, a Runnableinterface é usada apenas como um local para colocar o método run.



2. Classificação

Vamos passar para algo mais prático. Por exemplo, vejamos a classificação de strings.

Para classificar uma coleção de strings em ordem alfabética, Java tem um ótimo método chamadoCollections.sort(collection);

Este método estático classifica a coleção passada. E no processo de classificação, ele realiza comparações de pares de seus elementos para entender se os elementos devem ser trocados.

Durante a ordenação, essas comparações são realizadas usando o compareTométodo (), que todas as classes padrão possuem: Integer, String, ...

O método compareTo() da classe Integer compara os valores de dois números, enquanto o método compareTo() da classe String examina a ordem alfabética das strings.

Portanto, uma coleção de números será classificada em ordem crescente, enquanto uma coleção de strings será classificada alfabeticamente.

Algoritmos de classificação alternativos

Mas e se quisermos classificar strings não em ordem alfabética, mas por seu comprimento? E se quisermos classificar os números em ordem decrescente? O que você faz neste caso?

Para lidar com tais situações, a Collectionsclasse possui outro sort()método que possui dois parâmetros:

Collections.sort(collection, comparator);

Onde comparador é um objeto especial que sabe como comparar objetos em uma coleção durante uma operação de classificação . O termo comparador vem da palavra inglesa comparador , que por sua vez deriva de compare , que significa "comparar".

Então, o que é esse objeto especial?

Comparatorinterface

Bem, é tudo muito simples. O tipo do sort()segundo parâmetro do método éComparator<T>

Onde T é um parâmetro de tipo que indica o tipo dos elementos na coleção e Comparatoré uma interface que possui um único métodoint compare(T obj1, T obj2);

Em outras palavras, um objeto comparador é qualquer objeto de qualquer classe que implemente a interface Comparator. A interface do Comparator parece muito simples:

public interface Comparator<Type>
{
   public int compare(Type obj1, Type obj2);
}
Código para a interface do Comparador

O compare()método compara os dois argumentos que são passados ​​para ele.

Se o método retornar um número negativo, isso significa obj1 < obj2. Se o método retornar um número positivo, isso significa obj1 > obj2. Se o método retornar 0, isso significa obj1 == obj2.

Aqui está um exemplo de um objeto comparador que compara strings por seu comprimento:

public class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
Código da StringLengthComparatorclasse

Para comparar os comprimentos das strings, basta subtrair um comprimento do outro.

O código completo para um programa que classifica strings por comprimento ficaria assim:

public class Solution
{
   public static void main(String[] args)
   {
      ArrayList<String> list = new ArrayList<String>();
      Collections.addAll(list, "Hello", "how's", "life?");
      Collections.sort(list, new StringLengthComparator());
   }
}

class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
Classificando strings por comprimento


3. Açúcar sintático

O que você acha, esse código pode ser escrito de forma mais compacta? Basicamente, há apenas uma linha que contém informações úteis — obj1.length() - obj2.length();.

Mas o código não pode existir fora de um método, então tivemos que adicionar um compare()método e, para armazenar o método, tivemos que adicionar uma nova classe — StringLengthComparator. E também precisamos especificar os tipos das variáveis... Tudo parece estar correto.

Mas existem maneiras de tornar esse código mais curto. Temos um pouco de açúcar sintático para você. Duas colheres!

classe interna anônima

Você pode escrever o código do comparador dentro do main()método e o compilador fará o resto. Exemplo:

public class Solution
{
    public static void main(String[] args)
    {
        ArrayList<String> list = new ArrayList<String>();
        Collections.addAll(list, "Hello", "how's", "life?");

        Comparator<String> comparator = new Comparator<String>()
        {
            public int compare (String obj1, String obj2)
            {
                return obj1.length() – obj2.length();
            }
        };

        Collections.sort(list, comparator);
    }
}
Ordenar strings por comprimento

Você pode criar um objeto que implementa a Comparatorinterface sem criar explicitamente uma classe! O compilador irá criá-lo automaticamente e dar-lhe um nome temporário. Vamos comparar:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
classe interna anônima
Comparator<String> comparator = new StringLengthComparator();

class StringLengthComparator implements Comparator<String>
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
}
StringLengthComparatoraula

A mesma cor é usada para indicar blocos de código idênticos nos dois casos diferentes. As diferenças são muito pequenas na prática.

Quando o compilador encontra o primeiro bloco de código, ele simplesmente gera um segundo bloco de código correspondente e dá à classe um nome aleatório.


4. Expressões lambda em Java

Digamos que você decida usar uma classe interna anônima em seu código. Neste caso, você terá um bloco de código como este:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
classe interna anônima

Aqui combinamos a declaração de uma variável com a criação de uma classe anônima. Mas existe uma maneira de tornar esse código mais curto. Por exemplo, assim:

Comparator<String> comparator = (String obj1, String obj2) ->
{
    return obj1.length() – obj2.length();
};

O ponto e vírgula é necessário porque aqui não temos apenas uma declaração de classe implícita, mas também a criação de uma variável.

Uma notação como essa é chamada de expressão lambda.

Se o compilador encontrar uma notação como esta em seu código, ele simplesmente gerará a versão detalhada do código (com uma classe interna anônima).

Observe que ao escrever a expressão lambda, omitimos não apenas o nome da classe, mas também o nome do método.Comparator<String>int compare()

A compilação não terá problemas para determinar o método , porque uma expressão lambda pode ser escrita apenas para interfaces que possuem um único método . A propósito, existe uma maneira de contornar essa regra, mas você aprenderá sobre isso quando começar a estudar OOP com mais profundidade (estamos falando sobre métodos padrão).

Vejamos a versão detalhada do código novamente, mas esmaeceremos a parte que pode ser omitida ao escrever uma expressão lambda:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
};
classe interna anônima

Parece que nada de importante foi omitido. De fato, se a Comparatorinterface tiver apenas um compare()método, o compilador pode recuperar totalmente o código acinzentado do código restante.

Ordenação

A propósito, agora podemos escrever o código de classificação assim:

Comparator<String> comparator = (String obj1, String obj2) ->
{
   return obj1.length() – obj2.length();
};
Collections.sort(list, comparator);

Ou ainda assim:

Collections.sort(list, (String obj1, String obj2) ->
   {
      return obj1.length() – obj2.length();
   }
);

Simplesmente substituímos imediatamente a comparatorvariável pelo valor que foi atribuído à comparatorvariável.

inferência de tipo

Mas isso não é tudo. O código nesses exemplos pode ser escrito de forma ainda mais compacta. Primeiro, o compilador pode determinar por si mesmo que as variáveis obj1​​e obj2são Strings. E segundo, as chaves e a declaração de retorno também podem ser omitidas se você tiver apenas um único comando no código do método.

A versão abreviada seria assim:

Comparator<String> comparator = (obj1, obj2) ->
   obj1.length() – obj2.length();

Collections.sort(list, comparator);

E se, em vez de usar a comparatorvariável, usarmos imediatamente seu valor, obteremos a seguinte versão:

Collections.sort(list, (obj1, obj2) ->  obj1.length() — obj2.length() );

Bem, o que você acha disso? Apenas uma linha de código sem informações supérfluas — apenas variáveis ​​e código. Não tem como encurtar! Ou existe?



5. Como funciona

Na verdade, o código pode ser escrito de forma ainda mais compacta. Mas mais sobre isso mais tarde.

Você pode escrever uma expressão lambda onde usaria um tipo de interface com um único método.

Por exemplo, no código , você pode escrever uma expressão lambda porque a assinatura do método é assim:Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());sort()

sort(Collection<T> colls, Comparator<T> comp)

Quando passamos a ArrayList<String>coleção como o primeiro argumento para o método sort, o compilador conseguiu determinar que o tipo do segundo argumento é . E a partir disso, concluiu que essa interface possui um único método. Todo o resto é tecnicalidade.Comparator<String>int compare(String obj1, String obj2)