Bem-vindo ao tutorial de exemplo de interfaces funcionais do Java 8. O Java sempre foi uma linguagem de programação Orientada a Objetos. Isso significa que tudo na programação java gira em torno de Objetos (exceto alguns tipos primitivos para simplicidade). Não temos apenas funções no java, elas são parte da Classe e precisamos usar a classe/objeto para invocar qualquer função.
Interfaces Funcionais do Java 8
Se olharmos para algumas outras linguagens de programação, como C++, JavaScript; elas são chamadas de linguagem de programação funcional porque podemos escrever funções e usá-las quando necessário. Algumas dessas linguagens suportam tanto a Programação Orientada a Objetos quanto a Programação Funcional. Ser orientado a objetos não é ruim, mas traz muita verbosidade ao programa. Por exemplo, digamos que temos que criar uma instância de Runnable. Geralmente, fazemos isso usando classes anônimas como abaixo.
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
Se você olhar para o código acima, a parte real que é útil é o código dentro do método run(). Todo o resto do código é por causa da forma como os programas java são estruturados. Interfaces Funcionais do Java 8 e Expressões Lambda nos ajudam a escrever códigos menores e mais limpos, removendo muito código repetitivo.
Interface Funcional do Java 8
Uma interface com exatamente um método abstrato é chamada de Interface Funcional. A anotação @FunctionalInterface
é adicionada para que possamos marcar uma interface como interface funcional. Não é obrigatório usá-la, mas é uma prática recomendada usá-la com interfaces funcionais para evitar a adição acidental de métodos extras. Se a interface for anotada com a anotação @FunctionalInterface
e tentarmos ter mais de um método abstrato, ocorrerá um erro de compilação. O principal benefício das interfaces funcionais do Java 8 é que podemos usar expressões lambda para instanciá-las e evitar o uso de implementações de classes anônimas volumosas. A API de Coleções do Java 8 foi reescrita e uma nova API de Stream foi introduzida, que utiliza muitas interfaces funcionais. O Java 8 definiu muitas interfaces funcionais no pacote java.util.function
. Algumas das interfaces funcionais úteis do Java 8 são Consumer
, Supplier
, Function
e Predicate
. Você pode encontrar mais detalhes sobre elas em Exemplo de Stream do Java 8. java.lang.Runnable
é um ótimo exemplo de interface funcional com um único método abstrato run()
. O trecho de código abaixo fornece algumas orientações para interfaces funcionais:
interface Foo { boolean equals(Object obj); }
// Não funcional porque equals já é um membro implícito (classe Object)
interface Comparator {
boolean equals(Object obj);
int compare(T o1, T o2);
}
// Funcional porque Comparator tem apenas um método abstrato não-Object
interface Foo {
int m();
Object clone();
}
// Não funcional porque o método Object.clone não é público
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Funcional: dois métodos, mas têm a mesma assinatura
interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Funcional: Y.m é uma subassinatura e substituível pelo tipo de retorno
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Não funcional: nenhum método tem uma subassinatura de todos os métodos abstratos
interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class > c); }
interface Z extends X, Y {}
// Não funcional: nenhum método tem uma subassinatura de todos os métodos abstratos
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Erro do compilador: nenhum método é substituível pelo tipo de retorno
interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// Erro do compilador: assinaturas diferentes, mesma eliminação
Expressão Lambda
Expressões Lambda são a maneira através da qual podemos visualizar a programação funcional no mundo orientado a objetos do Java. Objetos são a base da linguagem de programação Java e nunca podemos ter uma função sem um Objeto, é por isso que a linguagem Java oferece suporte para usar expressões lambda apenas com interfaces funcionais. Como só há uma função abstrata nas interfaces funcionais, não há confusão ao aplicar a expressão lambda ao método. A sintaxe das Expressões Lambda é (argumento) -> (corpo). Agora vamos ver como podemos escrever o Runnable anônimo acima usando a expressão lambda.
Runnable r1 = () -> System.out.println("My Runnable");
Vamos tentar entender o que está acontecendo na expressão lambda acima.
- A interface Runnable é uma interface funcional, é por isso que podemos usar uma expressão lambda para criar sua instância.
- Como o método run() não recebe argumentos, nossa expressão lambda também não tem argumento.
- Assim como nos blocos if-else, podemos evitar chaves ({}) já que temos uma única instrução no corpo do método. Para múltiplas instruções, teríamos que usar chaves como em qualquer outro método.
Por que precisamos de Expressão Lambda
-
Linhas de Código Reduzidas Um dos claros benefícios de usar expressão lambda é que a quantidade de código é reduzida, já vimos como podemos facilmente criar uma instância de uma interface funcional usando expressão lambda em vez de usar uma classe anônima.
-
Suporte à Execução Sequencial e Paralela Outra vantagem de usar expressões lambda é que podemos aproveitar o suporte às operações sequenciais e paralelas da API Stream. Para explicar isso, vamos usar um exemplo simples onde precisamos escrever um método para testar se um número passado é primo ou não. Tradicionalmente, escreveríamos o código assim:
//Abordagem tradicional private static boolean isPrime(int number) { if(number < 2) return false; for(int i=2; i
O problema com o código acima é que ele é sequencial por natureza, se o número for muito grande, levará uma quantidade significativa de tempo. Outro problema com o código é que existem tantos pontos de saída e não é legível. Vamos ver como podemos escrever o mesmo método usando expressões lambda e a API Stream.
//Abordagem declarativa private static boolean isPrime(int number) { return number > 1 && IntStream.range(2, number).noneMatch( index -> number % index == 0); }
IntStream
é uma sequência de elementos int primitivos que suporta operações de agregação sequenciais e paralelas. Esta é a especialização primitiva de int deStream
. Para mais legibilidade, também podemos escrever o método assim:private static boolean isPrime(int number) { IntPredicate isDivisible = index -> number % index == 0; return number > 1 && IntStream.range(2, number).noneMatch( isDivisible); }
Se você não está familiarizado com IntStream, o método range() retorna um IntStream ordenado sequencialmente de startInclusive (inclusive) a endExclusive (exclusivo) com um passo incremental de 1. O método noneMatch() retorna se nenhum dos elementos deste stream corresponde ao predicado fornecido. Pode não avaliar o predicado em todos os elementos se não for necessário para determinar o resultado.
-
Passar comportamentos para métodos Vamos ver como podemos usar expressões lambda para passar o comportamento de um método com um exemplo simples. Digamos que temos que escrever um método para somar os números em uma lista se eles corresponderem a um critério dado. Podemos usar o Predicate e escrever um método como abaixo.
public static int somaComCondição(List<Integer> números, Predicate<Integer> predicado) { return números.parallelStream() .filter(predicado) .mapToInt(i -> i) .sum(); }
Uso de exemplo:
// soma de todos os números somaComCondição(números, n -> true) // soma de todos os números pares somaComCondição(números, i -> i%2==0) // soma de todos os números maiores que 5 somaComCondição(números, i -> i>5)
-
Maior Eficiência com Preguiça Mais uma vantagem de usar expressões lambda é a avaliação preguiçosa, por exemplo, vamos dizer que precisamos escrever um método para descobrir o maior número ímpar no intervalo de 3 a 11 e retornar o quadrado dele. Geralmente escreveríamos o código para este método assim:
private static int encontrarQuadradoDoMaiorImpar(List<Integer> numeros) { int max = 0; for (int i : numeros) { if (i % 2 != 0 && i > 3 && i < 11 && i > max) { max = i; } } return max * max; }
O programa acima sempre será executado em ordem sequencial, mas podemos usar a API Stream para alcançar isso e obter o benefício da busca preguiçosa. Vamos ver como podemos reescrever este código de forma funcional usando a API Stream e expressões lambda.
public static int encontrarQuadradoDoMaiorImpar(List<Integer> numeros) { return numeros.stream() .filter(NumberTest::isImpar) // Predicate é uma interface funcional e .filter(NumberTest::isMaiorQue3) // estamos usando lambdas para inicializá-la .filter(NumberTest::isMenorQue11) // em vez de classes internas anônimas .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isImpar(int i) { return i % 2 != 0; } public static boolean isMaiorQue3(int i){ return i > 3; } public static boolean isMenorQue11(int i){ return i < 11; }
Se você está surpreso com o operador de dois pontos duplos (::), ele foi introduzido no Java 8 e usado para referências de método. O Compilador Java cuida de mapear os argumentos para o método chamado. É uma forma abreviada das expressões lambda
i -> isMaiorQue3(i)
oui -> NumberTest.isMaiorQue3(i)
.
Exemplos de Expressões Lambda
Abaixo, estou fornecendo alguns trechos de código para expressões lambda com pequenos comentários explicativos.
() -> {} // No parameters; void result
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
// Corpo de bloco complexo com múltiplos retornos
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
Referências de Método e Construtor
A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
Isso é tudo para o Tutorial de Interfaces Funcionais do Java 8 e Expressões Lambda. Eu sugeriria fortemente que você olhasse para utilizá-lo porque essa sintaxe é nova no Java e levará algum tempo para entendê-la. Você também deve conferir as Funcionalidades do Java 8 para aprender sobre todas as melhorias e mudanças no lançamento do Java 8.
Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces