Interfaces Funcionais em Java 8

Interfaces Funcionais em Java 8

1. Introdução

Este artigo é um guia para diferentes interfaces funcionais presentes no Java 8, seus casos de uso geral e uso na biblioteca JDK padrão.

Leitura adicional:

Iterável para transmitir em Java

O artigo explica como converter um Iterable em Stream e por que a interface Iterable não oferece suporte direto.

Read more

Como usar a lógica if / else no Java 8 Streams

Aprenda como aplicar a lógica if / else ao Java 8 Streams.

Read more

2. Lambdas em Java 8

O Java 8 trouxe uma nova e poderosa melhoria sintática na forma de expressões lambda. Um lambda é uma função anônima que pode ser tratada como cidadão de primeira classe, por exemplo, passado ou retornado de um método.

Antes do Java 8, você normalmente criava uma classe para todos os casos em que precisava encapsular uma única peça de funcionalidade. Isso implicava muito código desnecessário para definir algo que servia como representação de função primitiva.

Lambdas, interfaces funcionais e melhores práticas para trabalhar com elas, em geral, são descritas no artigo“Lambda Expressions and Functional Interfaces: Tips and Best Practices”. Este guia concentra-se em algumas interfaces funcionais específicas que estão presentes no pacotejava.util.function.

3. Interfaces Funcionais

Recomenda-se que todas as interfaces funcionais tenham uma anotação informativa@FunctionalInterface. Isso não apenas comunica claramente o objetivo dessa interface, mas também permite que um compilador gere um erro se a interface anotada não atender às condições.

Any interface with a SAM(Single Abstract Method) is a functional interface, e sua implementação pode ser tratada como expressões lambda.

Observe que os métodosdefault do Java 8 não sãoabstracte não contam: uma interface funcional ainda pode ter vários métodosdefault. Você pode observar isso observandoFunction’sdocumentation.

4. 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 interfaceFunction que é parametrizada pelos tipos de seu argumento e um valor de retorno:

public interface Function { … }

Um dos usos do tipoFunction na biblioteca padrão é o métodoMap.computeIfAbsent que retorna um valor de um mapa por chave, mas calcula um valor se uma chave ainda não estiver presente em um mapa. Para calcular um valor, ele usa a implementação da função passada:

Map nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());

Um valor, nesse caso, será calculado aplicando uma função a uma chave, inserida em um mapa e também retornada de uma chamada de método. Já agora,we may replace the lambda with a method reference that matches passed and returned value types.

Lembre-se de que um objeto no qual o método é invocado é, na verdade, o primeiro argumento implícito de um método, que permite lançar uma referência do método de instâncialength para uma interfaceFunction:

Integer value = nameMap.computeIfAbsent("John", String::length);

A interfaceFunction também tem um métodocompose padrão que permite combinar várias funções em uma e executá-las sequencialmente:

Function intToString = Object::toString;
Function quote = s -> "'" + s + "'";

Function quoteIntToString = quote.compose(intToString);

assertEquals("'5'", quoteIntToString.apply(5));

A funçãoquoteIntToString é uma combinação da funçãoquote aplicada a um resultado da funçãointToString.

5. Especializações de função primitiva

Visto que um tipo primitivo não pode ser um argumento de tipo genérico, existem versões da interfaceFunction para os tipos primitivos mais usadosdouble,int,long e suas combinações em tipos de argumento e retorno:

  • IntFunction,LongFunction, argumentosDoubleFunction: são do tipo especificado, o tipo de retorno é parametrizado

  • ToIntFunction,ToLongFunction,ToDoubleFunction: tipo de retorno é do tipo especificado, os argumentos são parametrizados

  • DoubleToIntFunction,DoubleToLongFunction,IntToDoubleFunction,IntToLongFunction,LongToIntFunction,LongToDoubleFunction - tendo o argumento e o tipo de retorno definidos como tipos primitivos, conforme especificado por os nomes deles

Não existe uma interface funcional pronta para usar para, digamos, uma função que pegashorte retornabyte, mas nada impede que você escreva a sua própria:

@FunctionalInterface
public interface ShortToByteFunction {

    byte applyAsByte(short s);

}

Agora podemos escrever um método que transforma um array deshort em um array debyte usando uma regra definida porShortToByteFunction:

public byte[] transformArray(short[] array, ShortToByteFunction function) {
    byte[] transformedArray = new byte[array.length];
    for (int i = 0; i < array.length; i++) {
        transformedArray[i] = function.applyAsByte(array[i]);
    }
    return transformedArray;
}

Veja como podemos usá-lo para transformar uma matriz de curtos em matriz de bytes multiplicada 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);

6. Especializações em função de duas entidades

Para definir lambdas com dois argumentos, temos que usar interfaces adicionais que contêm “Bi” palavra-chave em seus nomes:BiFunction,ToDoubleBiFunction,ToIntBiFunction eToLongBiFunction .

BiFunction tem argumentos e um tipo de retorno generificados, enquantoToDoubleBiFunction e outros permitem que você retorne um valor primitivo.

Um dos exemplos típicos de uso dessa interface na API padrão está no métodoMap.replaceAll, que permite substituir todos os valores em um mapa por algum valor calculado.

Vamos usar uma implementaçãoBiFunction que recebe uma chave e um valor antigo para calcular um novo valor para o salário e retorná-lo.

Map 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);

7. Fornecedores

A interface funcionalSupplier é outra especializaçãoFunction que não aceita nenhum argumento. É normalmente usado para geração lenta de valores. Por exemplo, vamos definir uma função que eleva ao quadrado um valordouble. Ele receberá não um valor em si, mas umSupplier deste valor:

public double squareLazy(Supplier lazyValue) {
    return Math.pow(lazyValue.get(), 2);
}

Isso nos permite gerar preguiçosamente o argumento para invocação dessa função usando uma implementaçãoSupplier. Isso pode ser útil se a geração desse argumento levar um tempo considerável. Vamos simular isso usando o métodosleepUninterruptibly de Guava:

Supplier lazyValue = () -> {
    Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
    return 9d;
};

Double valueSquared = squareLazy(lazyValue);

Outro caso de uso para o Fornecedor é definir uma lógica para geração de sequência. Para demonstrar isso, vamos usar um métodoStream.generate estático para criar umStream de números de Fibonacci:

int[] fibs = {0, 1};
Stream fibonacci = Stream.generate(() -> {
    int result = fibs[1];
    int fib3 = fibs[0] + fibs[1];
    fibs[0] = fibs[1];
    fibs[1] = fib3;
    return result;
});

A função que é passada para o métodoStream.generate implementa a interface funcionalSupplier. Observe que, para ser útil como gerador, oSupplier geralmente precisa de algum tipo de estado externo. Nesse caso, seu estado é composto por dois últimos números de sequência de Fibonacci.

Para implementar esse estado, usamos uma matriz em vez de algumas variáveis, porqueall external variables used inside the lambda have to be effectively final.

Outras especializações da interface funcionalSupplier incluemBooleanSupplier,DoubleSupplier,LongSuppliereIntSupplier, cujos tipos de retorno são primitivas correspondentes.

8. Consumidores

Ao contrário deSupplier, oConsumer aceita um argumento generificado e não retorna nada. É uma função que representa efeitos colaterais.

Por exemplo, vamos cumprimentar todos na lista de nomes imprimindo a saudação no console. O lambda passado para o métodoList.forEach implementa a interface funcionalConsumer:

List names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));

Existem também versões especializadas deConsumer -DoubleConsumer,IntConsumereLongConsumer - que recebem valores primitivos como argumentos. Mais interessante é a interfaceBiConsumer. Um de seus casos de uso é iterativo pelas entradas de um mapa:

Map 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 versõesBiConsumer especializadas é composto porObjDoubleConsumer,ObjIntConsumer eObjLongConsumer que recebem dois argumentos, um dos quais é generificado e outro é do tipo primitivo.

9. Predicados

Na lógica matemática, um predicado é uma função que recebe um valor e retorna um valor booleano.

A interface funcionalPredicate é uma especialização de umFunction que recebe um valor generificado e retorna um booleano. Um caso de uso típico do lambdaPredicate é filtrar uma coleção de valores:

List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");

List namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

No código acima, filtramos uma lista usando a APIStream e mantemos apenas os nomes que começam com a letra “A”. A lógica de filtragem é encapsulada na implementaçãoPredicate.

Como em todos os exemplos anteriores, existem versõesIntPredicate,DoublePredicateeLongPredicate desta função que recebem valores primitivos.

10. Operadores

As interfacesOperator são casos especiais de uma função que recebe e retorna o mesmo tipo de valor. A interfaceUnaryOperator recebe um único argumento. Um de seus casos de uso na API Collections é substituir todos os valores em uma lista por alguns valores calculados do mesmo tipo:

List names = Arrays.asList("bob", "josh", "megan");

names.replaceAll(name -> name.toUpperCase());

A funçãoList.replaceAll retornavoid, pois substitui os valores no lugar. Para atender à finalidade, o lambda usado para transformar os valores de uma lista precisa retornar o mesmo tipo de resultado que recebe. É por isso queUnaryOperator é útil aqui.

Claro, em vez dename → name.toUpperCase(), você pode simplesmente usar uma referência de método:

names.replaceAll(String::toUpperCase);

Um dos casos de uso mais interessantes de aBinaryOperator é uma operação de redução. Suponha que desejemos agregar uma coleção de números inteiros em uma soma de todos os valores. Com a APIStream, poderíamos fazer isso usando um coletor,, mas uma forma mais genérica de fazer isso seria usar o métodoreduce:

List values = Arrays.asList(3, 5, 8, 9, 12);

int sum = values.stream()
  .reduce(0, (i1, i2) -> i1 + i2);

O métodoreduce recebe um valor inicial do acumulador e uma funçãoBinaryOperator. Os argumentos desta função são um par de valores do mesmo tipo e uma função em si contém uma lógica para uni-los em um único valor do mesmo tipo. Passed function must be associative, o que significa que a ordem de agregação de valor não importa, ou seja, a seguinte condição deve ser válida:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

A propriedade associativa de uma função de operadorBinaryOperator permite paralelizar facilmente o processo de redução.

Claro, também existem especializações deUnaryOperatoreBinaryOperator que podem ser usadas com valores primitivos, a saberDoubleUnaryOperator,IntUnaryOperator,LongUnaryOperator,DoubleBinaryOperator,IntBinaryOperator eLongBinaryOperator.

11. Interfaces Funcionais Legadas

Nem todas as interfaces funcionais apareceram no Java 8. Muitas interfaces de versões anteriores do Java estão em conformidade com as restrições deFunctionalInterfacee podem ser usadas como lambdas. Um exemplo importante são as interfacesRunnableeCallable que são usadas em APIs de simultaneidade. No Java 8, essas interfaces também são marcadas com uma anotação@FunctionalInterface. Isso nos permite simplificar bastante o código de concorrência:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();

12. Conclusão

Neste artigo, descrevemos diferentes interfaces funcionais presentes na API Java 8 que podem ser usadas como expressões lambda. O código-fonte do artigo está disponívelover on GitHub.