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();
}
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 Timer
classe implementa a Runnable
interface, então ela deve declarar dentro de si todos os métodos que estão na Runnable
interface e implementá-los, ou seja, escrever código no corpo de um método. O mesmo vale para a Calendar
classe.
Mas agora Runnable
as variáveis podem armazenar referências a objetos que implementam a Runnable
interface.
Exemplo:
Código | Observação |
---|---|
|
O run() método da Timer classe será chamado O run() método da Timer classe será chamado O run() método da Calendar classe 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 Timer
e Calendar
, existem dois tipos: Object
e Runnable
.
Se você atribuir uma referência de objeto a uma Object
variável, poderá chamar apenas os métodos declarados na Object
classe. E se você atribuir uma referência de objeto a uma Runnable
variável, poderá chamar os métodos do Runnable
tipo.
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 Timer
e Calendar
executam 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 Runnable
interface é 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 compareTo
mé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 Collections
classe 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?
Comparator
interface
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);
}
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();
}
}
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();
}
}
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);
}
}
Você pode criar um objeto que implementa a Comparator
interface 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();
}
};
Comparator<String> comparator = new StringLengthComparator();
class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
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();
}
};
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();
}
};
Parece que nada de importante foi omitido. De fato, se a Comparator
interface 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 comparator
variável pelo valor que foi atribuído à comparator
variá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 obj2
sã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 comparator
variá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)
GO TO FULL VERSION