- Introdução
- leitura adicional:
- Iterable to Stream in Java
- How to Use if/else Logic in Java 8 Streams
- Lambdas in Java 8
- Interfaces funcionais
- funções
- especializações de funções primitivas
- Dois-Aridade da Função Especializações
- fornecedores
- consumidores
- predicados
- operadores
- Interfaces funcionais legadas
- conclusão
Introdução
Este tutorial é um guia de diferentes interfaces funcionais presentes no Java 8, bem como, em geral, casos de uso, e o uso padrão do JDK biblioteca.
leitura adicional:
Iterable to Stream in Java
How to Use if/else Logic in Java 8 Streams
Lambdas in Java 8
Java 8 brought a powerful new syntactic improvement in the form of lambda expressions. Um lambda é uma função anônima que podemos lidar como um cidadão de primeira classe. Por exemplo, podemos passá-lo ou devolvê-lo de um método.
antes do Java 8, normalmente criaríamos uma classe para cada caso em que precisássemos de encapsular uma única peça de funcionalidade. Isso implicou um monte de código de boilerplate desnecessário para definir algo que serviu como uma representação de funções primitivas.o artigo “Lambda Expressions and Functional Interfaces: Tips and Best Practices” descreve mais pormenorizadamente as interfaces funcionais e as melhores práticas de trabalho com lambdas. Este guia foca em algumas interfaces funcionais particulares que estão presentes no java.util.pacote de funções.
Interfaces funcionais
recomenda-se que todas as interfaces funcionais tenham uma anotação informativa @FunctionalInterface. Isto comunica claramente a finalidade da interface, e também permite que um compilador para gerar um erro se a interface anotada não satisfaz as condições.
qualquer interface com um SAM (método Abstracto único) é uma interface funcional, e sua implementação pode ser tratada como expressões lambda.
Note que os métodos padrão do Java 8 não são abstratos e não contam; uma interface funcional ainda pode ter vários métodos padrão. Podemos observar isso olhando para a documentação da função.
funções
o caso mais simples e geral de um lambda é uma interface funcional com um método que recebe um valor e retorna outro. Esta função de um único argumento é representada pela interface de função, que é parametrizada pelos tipos de seu argumento e um valor de retorno:
public interface Function<T, R> { … }
uma das utilizações do tipo de função na biblioteca padrão é o mapa.computeIfAbsent method. Este método retorna um valor de um mapa por chave, mas calcula um valor se uma chave não está já presente em um mapa. Para calcular um valor, ele usa a implementação da função passada:
Map<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
neste caso, vamos calcular um valor aplicando uma função a uma chave, colocar dentro de um mapa, e também retornado de uma chamada de método. Podemos substituir o lambda por uma referência de método que corresponde a tipos de valor passados e devolvidos.
lembre-se que um objeto que invocamos o método é, de fato, o primeiro argumento implícito de um método. Isto permite-nos lançar uma referência de tamanho do método de instância a uma interface de função:
Integer value = nameMap.computeIfAbsent("John", String::length);
A Função de interface também tem um padrão de compor método que nos permite combinar várias funções em um só e executá-los sequencialmente:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
O quoteIntToString função é uma combinação da função proposta aplicada ao resultado da intToString função.
especializações de funções primitivas
Uma vez que um tipo primitivo não pode ser um argumento de tipo genérico, existem versões da interface de função para os tipos primitivos mais usados double, int, long, e suas combinações em argumentos e tipos de retorno:
- IntFunction, LongFunction, DoubleFunction: arguments are of specified type, return type is parameterized
- ToIntFunction, ToLongFunction, ToDoubleFunction: return type is of specified type, arguments are parameterized
- DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction: tendo ambos os argumentos e tipo de retorno definidas como tipos primitivos, como especificado pelos seus nomes
Como exemplo, não há fora-da-caixa de interface funcional para uma função que leva um curto e retorna um byte, mas nada nos impede de escrever a nossa própria:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
Agora podemos escrever um método que transforma uma matriz de curto para uma matriz de bytes usando uma regra definida por uma ShortToByteFunction:
public byte transformArray(short array, ShortToByteFunction function) { byte transformedArray = new byte; for (int i = 0; i < array.length; i++) { transformedArray = function.applyAsByte(array); } return transformedArray;}
veja como poderíamos usá-lo para transformar uma matriz de calções para uma matriz de bytes multiplicado por 2:
short array = {(short) 1, (short) 2, (short) 3};byte transformedArray = transformArray(array, s -> (byte) (s * 2));byte expectedArray = {(byte) 2, (byte) 4, (byte) 6};assertArrayEquals(expectedArray, transformedArray);
Dois-Aridade da Função Especializações
Para definir lambdas com dois argumentos, temos que usar interfaces adicionais que contêm “Bi” de palavra-chave em seus nomes: BiFunction, ToDoubleBiFunction, ToIntBiFunction, e ToLongBiFunction.
BiFunction has both arguments and a return type generified, while ToDoubleBiFunction and others allow us to return a primitive value.
Um dos exemplos típicos de usar esta interface na API padrão está no mapa.método replaceAll, que permite substituir todos os valores em um mapa com algum valor calculado.
vamos usar uma implementação bifuncional que recebe uma chave e um valor antigo para calcular um novo valor para o salário e devolvê-lo.
Map<String, Integer> salaries = new HashMap<>();salaries.put("John", 40000);salaries.put("Freddy", 30000);salaries.put("Samuel", 50000);salaries.replaceAll((name, oldValue) -> name.equals("Freddy") ? oldValue : oldValue + 10000);
fornecedores
a interface funcional do Fornecedor é mais uma especialização de funções que não leva quaisquer argumentos. Usamo-lo tipicamente para a geração preguiçosa de valores. Por exemplo, vamos definir uma função que quadrada um valor duplo. Não receberá um valor em si, mas um fornecedor desse valor:
public double squareLazy(Supplier<Double> lazyValue) { return Math.pow(lazyValue.get(), 2);}
Isto permite-nos gerar preguiçosamente o argumento para invocação desta função usando uma implementação do Fornecedor. Isto pode ser útil se a geração do argumento levar um tempo considerável. Vamos simular que usando o método de sono intruptível De Goiaba:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
outro caso de uso para o fornecedor é definir a lógica para a geração de sequência. Para demonstrá-lo, vamos usar um fluxo estático.gerar método para criar um fluxo de números Fibonacci:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
a função que passamos para o fluxo.o método gerar implementa a interface funcional do Fornecedor. Observe que para ser útil como um gerador, o fornecedor geralmente precisa de algum tipo de estado externo. Neste caso, o seu estado compreende os dois últimos números de sequência de Fibonacci.
para implementar este estado, nós usamos um array em vez de um par de variáveis porque todas as variáveis externas usadas dentro do lambda têm que ser efetivamente finais.
outras especializações da interface funcional do Fornecedor incluem BooleanSupplier, DoubleSupplier, LongSupplier e IntSupplier, cujos tipos de retorno são primitivas correspondentes.
consumidores
ao contrário do Fornecedor, o consumidor aceita um argumento generificado e não devolve nada. É uma função que representa efeitos secundários.
Por exemplo, vamos cumprimentar todos em uma lista de nomes, imprimindo a saudação no console. O lambda passou para a lista.forEach método implementa o Consumidor interface funcional:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
Há também versões especializadas do Consumidor — DoubleConsumer, IntConsumer e LongConsumer — que recebem valores primitivos como argumentos. Mais interessante é a interface BiConsumer. Um de seus casos de uso é iterar através de entradas de um mapa:
Map<String, Integer> ages = new HashMap<>();ages.put("John", 25);ages.put("Freddy", 24);ages.put("Samuel", 30);ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
Outro conjunto de empresas especializadas BiConsumer versões é composto de ObjDoubleConsumer, ObjIntConsumer, e ObjLongConsumer, que recebe dois argumentos; um dos argumentos é generified, e o outro é um tipo primitivo.
predicados
na lógica matemática, um predicado é uma função que recebe um valor e retorna um valor booleano.
a interface funcional de predicados é uma especialização de uma função que recebe um valor generificado e retorna um booleano. Um caso típico de Uso do lambda predicado é filtrar uma coleção de valores:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
no código acima, filtramos uma lista usando a API Stream e mantemos apenas os nomes que começam com a letra”A”. A implementação de predicados encapsula a lógica de filtragem.
Como em todos os exemplos anteriores, há Intpredicato, Doublepredicato e versões LongPredicate desta função que recebem valores primitivos.as interfaces de operador
operadores
São casos especiais de uma função que recebe e devolve o mesmo tipo de valor. A interface UnaryOperator recebe um único argumento. Um de seus casos de uso na API de coleções é substituir todos os valores de uma lista com alguns valores computados do mesmo tipo:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
a lista.a função replaceAll retorna void à medida que substitui os valores no lugar. Para se ajustar à finalidade, o lambda usado para transformar os valores de uma lista tem que retornar o mesmo tipo de resultado que recebe. É por isso que o unary operator é útil aqui.
claro, em vez de nome -> nome.toUpperCase (), podemos simplesmente usar uma referência de método:
names.replaceAll(String::toUpperCase);
um dos casos de uso mais interessantes de um operador binário é uma operação de redução. Suponha que queremos agregar uma coleção de inteiros em uma soma de todos os valores. Com a API Stream, poderíamos fazer isso usando um coletor, mas uma maneira mais genérica de fazê-lo seria usar o método de redução:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
o método de redução recebe um valor inicial de acumulador e uma função BinaryOperator. Os argumentos desta função são um par de valores do mesmo tipo; a função em si também contém uma lógica para juntá-los em um único valor do mesmo tipo. O passado função deve ser associativa, o que significa que a ordem de agregação de valor não importa, por exemplo, a seguinte condição deve conter:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
associativa propriedade de um BinaryOperator função de operador permite-nos facilmente paralelizar o processo de redução.
é claro, há também especializações da UnaryOperator e BinaryOperator que pode ser usado com valores primitivos, nomeadamente DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, e LongBinaryOperator.
Interfaces funcionais legadas
nem todas as interfaces funcionais apareceram no Java 8. Muitas interfaces de versões anteriores de Java estão em conformidade com as restrições de uma interface funcional, e nós podemos usá-las como lambdas. Exemplos proeminentes incluem as interfaces Runnable e Callable que são usadas em APIs concurrencie. No Java 8, estas interfaces também são marcadas com uma anotação @ FunctionalInterface. Isso nos permite simplificar muito o código de concorrência:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();