APIs
8
min de leitura
11 de março de 2021

Interfaces funcionais - Java 8

Gabriel Babler
Java Backend Developer
Eu tenho construído minha carreira como desenvolvedor software . Trabalhando principalmente com JAVA e Spring Framework, assim como com a plataforma AWS. Compartilhar conhecimentos e ajudar outros profissionais são alguns de meus objetivos profissionais. Anfitrião do PODCAST 20.21, que fala sobre tecnologia focada em compartilhar idéias, experiências e muito mais com a ajuda de outros profissionais.
Mais sobre o autor

Você já pensou em Interfaces Funcionais e como elas funcionam em Java? Vamos vê-las agora!

Primeiro de tudo, você sabe o que é uma Interface Funcional?

Interfaces funcionais são interfaces que têm um método a ser implementado, em outras palavras, um método abstrato. Isso significa que toda interface criada que respeite esta premissa, tornando-se automaticamente uma interface funcional.

O compilador reconhece essas interfaces e permite que elas estejam disponíveis para que os desenvolvedores trabalhem, por exemplo, com expressões lambda.

Hoje, vamos falar sobre as principais Interfaces Funcionais apresentadas no JDK, que são

  • Fornecedor
  • Consumidor e BiConsumidor
  • Predicado e BiPredicado
  • Função e BiFunção
  • UnaryOperator e BinaryOperator

Fornecedor

Verificando sua classe, podemos ver isso abaixo:

 

O que podemos concluir com isso?

A letra T significa que é genérica (genérico significa que pode ser de qualquer tipo), e neste caso significa que a operação get( ), quando executada, vai devolver algo para nós e pode ser de qualquer tipo.

Por outro lado, não precisamos passar um argumento. Em resumo, chamamos isso e recebemos algo - como um fornecedor (você entendeu o nome agora?).

Vamos ver um exemplo:

Aqui estamos chamando o método generate() do Stream API, que precisa de um Fornecedor para ser executado.

Assim, não estamos passando nada para o método usando os parênteses vazios '()', depois usando lambda '->' e finalmente executando o método - novo Random().nextInt() - que vai retornar algo para nós (neste caso, um número aleatório).

Aqui, nós acabamos de adicionar o limite e paraEach para mostrar o resultado no console.

Portanto, se executarmos o código, nós o obteremos:

É assim que o Fornecedor funciona - não precisamos fornecer nada e recebemos uma resposta.

Consumidor e BiConsumidor

Vamos verificar sua classe:

É uma interface simples, o oposto de Fornecedor. Ela recebe uma variável genérica, faz algo com ela e depois, não devolve nada.

Exemplo:

Exemplo

Temos uma lista da Integers e para imprimir estes números no console, podemos usar o .forEach para nos ajudar com isso.

Ele está recebendo uma variável - número - e fazendo algo com ela. Neste caso, ela está imprimindo no console e não devolvendo nada para o usuário.

Assim como um consumidor. Recebe algo, faz algo e é tudo.

O BiConsumer segue as mesmas regras, mas recebe dois argumentos.

Exemplo

No exemplo acima, estamos recebendo dois valores inteiros e apenas imprimindo-os no console e devolvendo nada.

Predicado e BiPredicado

Agora, vamos dar uma olhada nas classes Predicado e BiPredicado:

Exemplo

A classe Predicado tem um método chamado teste, que recebe um argumento, e retorna um booleano.

Podemos concluir que este método é utilizado para validar hipóteses. Vamos ver no código:

Para este exemplo, temos uma Lista de Integers que é composta com os números - 1, 2, 3, 4 e 5.

E mais uma vez, vamos utilizar o Stream API. Vamos converter nossa lista para um Stream através do .stream() - então vamos usar o .filter() - e é neste pequenote que a magia acontece com o Predicado.

Nosso filtro de método precisa de um Predicado para ser executado e, como vimos antes, ele validará uma hipótese e retornará Verdadeiro ou Falso para nós.

Neste método, estamos obtendo o número e verificando se ele é divisível por 2 - o que significa que é um número par. Se for verdade, então executaremos o forEach, caso contrário, ele será ignorado.

É assim que os métodos Predicate funcionam: eles testam hipóteses e retornam a você se forem verdadeiros ou falsos.

Se executarmos este código, obtemos este resultado:

Exemplo

Acabou de imprimir os números pares, como esperado.

E em relação ao BiPredicate, temos o mesmo comportamento, a única diferença é que vamos receber dois parâmetros a serem verificados em vez de um. Confira abaixo:

Exemplo:

Exemplo

Aqui temos um BiPredicado recebendo dois parâmetros - palavra e tamanho - e verificando se eles têm o mesmo valor.

No primeiro teste, vamos receber o Verdadeiro, e no segundo, vamos receber o Falso.

Função e BiFunção

A função da interface funcional é a mais genérica. Ela tem a definição mais básica de uma função - ela recebe algo e retorna algo.

Vamos dar uma olhada na classe:

Exemplo

Vamos começar com a Função. Aqui podemos ver que o método aplicado recebe uma variável genérica - lembre-se que genérica significa que pode ser de qualquer tipo - e retornará outra variável genérica.

Uma coisa importante aqui é que mesmo que as palavras genéricas sejam diferentes - T e R - isso não significa que o método apply() não possa receber e devolver o mesmo tipo.

Então, vamos ao código - Mais uma vez, vamos usar o Stream API para este exemplo.

Aqui vamos usar o método .map(), que precisa de uma Função para ser executada.

Portanto, neste código:

Exemplo

Estamos recebendo o número, que é um valor Inteiro, e devolvendo um Duplo. Portanto, estamos passando um argumento para o mapa e recebendo uma resposta.

*Neste caso, estamos dando um valor Inteiro e recebendo como resposta um valor Duplo.

Mas, por exemplo, nós poderíamos fazer isso:

Exemplo

É um valor Inteiro como argumento e seu retorno é um valor Inteiro também. A função permite isso. Com relação à BiFunção, ela tem o mesmo comportamento, exceto que recebe dois argumentos assim como o BiPredicado.

Exemplo

Vamos verificar nos exemplos abaixo:

Exemplo

No primeiro exemplo (sumNumbers), temos uma BiFunção que recebe dois argumentos - tipo Inteiro - e retorna também um tipo Inteiro. Estamos fazendo uma soma e quando usamos o apply() resulta no número 3 (1 + 2).

A seguir, temos outro exemplo, mas, desta vez, ele está devolvendo um valor duplo. Obtivemos 2 números inteiros e, depois de chamar o Math.pow(), ele retornou um valor Duplo. Uma vez executado o método apply(), obteremos o resultado: 4.0 (Duplo)

Resumindo - damos um ou dois argumentos e recebemos algo em troca. O significado básico de uma função.

UnaryOperator e BinaryOperator

Vamos dar uma olhada em sua classe:

Exemplo

O UnaryOperator estende uma função - mas em seu construtor está definido o mesmo tipo de argumentos, você notou isso?

Temos UnaryOperator <T>estendendo-se à Função <T, T>.</T,></T>

Está definindo o tipo permitido para esta interface, e como todos estes genéricos são a mesma letra (T), isso significa que só nos é permitido trabalhar com o mesmo tipo de variáveis - nossos argumentos e retornos devem ser do mesmo tipo para trabalhar no UnaryOperator.

UnaryOperator tem o mesmo comportamento que a Função, mas só funciona com os mesmos tipos.

Por exemplo:

Exemplo

Você pode apenas definir um tipo em seu construtor, e ele será estendido à Função, e como podemos ver no código acima, podemos apenas trabalhar com variáveis do mesmo tipo.

É a mesma coisa que as outras.

Ela se estende à BiFunction. Portanto, vamos receber dois argumentos e devolver um - todos com o mesmo tipo.

Vamos checar isto:

Exemplo

Usamos novamente o Stream API.

Agora, vamos usar o método .reduce(). Ele receberá dois argumentos e retornará um valor com o mesmo tipo.

Em nosso caso, temos um conjunto de números - 1 a 5 - e vamos somar todos os valores.

*Como reduzir os retornos um valor Opcional, vamos usar o .ifPresent() para imprimir nosso resultado.

Portanto, como resultado, teremos aqui: 15 (1 + 2 + 3 + 4 + 5)

*Vai se repetir até que toda a matriz passe por ela.

Para embrulhar as coisas, vejamos um exemplo de tudo trabalhando em conjunto:

  • Aqui estamos obtendo apenas os números pares;
  • Em seguida, transformando-os em Duplo;
  • Em seguida, somando todas elas;
  • E se houver um número no final, nós o imprimimos no console.

Apenas uma última coisa - as interfaces funcionais também aceitam Referência de Método - o código poderia ser assim:

Exemplo

É isso aí! Espero que isso possa ajudá-lo a criar um código melhor e mais limpo! Assim como ajudar você a entender como funcionam as funções.


Obrigado pela leitura!