JavaRush /Blogue Java /Random-PT /Interfaces Funcionais em Java

Interfaces Funcionais em Java

Publicado no grupo Random-PT
Olá! Na missão Java Syntax Pro, estudamos expressões lambda e dissemos que elas nada mais são do que uma implementação de um método funcional a partir de uma interface funcional. Em outras palavras, esta é a implementação de alguma classe anônima (desconhecida), seu método não realizado. E se nas aulas do curso nos aprofundamos nas manipulações com expressões lambda, agora consideraremos, por assim dizer, o outro lado: a saber, essas mesmas interfaces. Interfaces Funcionais em Java - 1A oitava versão do Java introduziu o conceito de interfaces funcionais . O que é isso? Uma interface com um método não implementado (abstrato) é considerada funcional. Muitas interfaces prontas para uso se enquadram nesta definição, como, por exemplo, a interface discutida anteriormente Comparator. E também interfaces que nós mesmos criamos, como:
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
Temos uma interface cuja tarefa é converter objetos de um tipo em objetos de outro (uma espécie de adaptador). Uma anotação @FunctionalInterfacenão é algo supercomplexo ou importante, pois seu objetivo é informar ao compilador que uma determinada interface é funcional e não deve conter mais de um método. Se uma interface com esta anotação tiver mais de um método não implementado (abstrato), o compilador não irá ignorar esta interface, pois a perceberá como um código errado. Interfaces sem essa anotação podem ser consideradas funcionais e funcionarão, mas @FunctionalInterfaceisso nada mais é do que um seguro adicional. Vamos voltar para a aula Comparator. Se você olhar seu código (ou documentação ), verá que ele possui muito mais de um método. Aí você pergunta: como então ela pode ser considerada uma interface funcional? Interfaces abstratas podem ter métodos que não estão dentro do escopo de um único método:
  • estático
O conceito de interfaces implica que uma determinada unidade de código não pode ter nenhum método implementado. Mas a partir do Java 8, tornou-se possível usar métodos estáticos e padrão em interfaces. Os métodos estáticos estão vinculados diretamente a uma classe e não requerem um objeto específico dessa classe para chamar tal método. Ou seja, esses métodos se enquadram harmoniosamente no conceito de interfaces. Como exemplo, vamos adicionar um método estático para verificar se um objeto é nulo à classe anterior:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
Tendo recebido este método, o compilador não reclamou, o que significa que nossa interface ainda está funcional.
  • métodos padrão
Antes do Java 8, se precisássemos criar um método em uma interface que fosse herdada por outras classes, poderíamos apenas criar um método abstrato que fosse implementado em cada classe específica. Mas e se esse método for o mesmo para todas as classes? Neste caso , as classes abstratas foram usadas com mais frequência . Mas a partir do Java 8, existe uma opção de usar interfaces com métodos implementados - os métodos padrão. Ao herdar uma interface, você pode sobrescrever esses métodos ou deixar tudo como está (deixar a lógica padrão). Ao criar um método padrão, devemos adicionar a palavra-chave - default:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий an object - " + t.toString());
   }
}
Novamente, vemos que o compilador não começou a reclamar e não ultrapassamos as limitações da interface funcional.
  • Métodos de classe de objeto
Na aula Comparando Objetos , falamos sobre o fato de que todas as classes herdam da classe Object. Isso não se aplica a interfaces. Mas se tivermos um método abstrato na interface que corresponda à assinatura com algum método da classe Object, tal método (ou métodos) não quebrará nossa restrição funcional da interface:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий an object - " + t.toString());
   }

   boolean equals(Object obj);
}
E novamente, nosso compilador não reclama, então a interface Converterainda é considerada funcional. Agora a questão é: por que precisamos nos limitar a um método não implementado em uma interface funcional? E então para que possamos implementá-lo usando lambdas. Vejamos isso com um exemplo Converter. Para fazer isso, vamos criar uma classe Dog:
public class Dog {
  String name;
  int age;
  int weight;

  public Dog(final String name, final int age, final int weight) {
     this.name = name;
     this.age = age;
     this.weight = weight;
  }
}
E um semelhante Raccoon(guaxinim):
public class Raccoon {
  String name;
  int age;
  int weight;

  public Raccoon(final String name, final int age, final int weight) {
     this.name = name;
     this.age = age;
     this.weight = weight;
  }
}
Suponha que temos um objeto Doge precisamos criar um objeto baseado em seus campos Raccoon. Ou seja, Converterconverte um objeto de um tipo em outro. Como será:
public static void main(String[] args) {
  Dog dog = new Dog("Bobbie", 5, 3);

  Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);

  Raccoon raccoon = converter.convert(dog);

  System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
Quando o executamos, obtemos a seguinte saída no console:

Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
E isso significa que nosso método funcionou corretamente.Interfaces Funcionais em Java - 2

Interfaces funcionais básicas do Java 8

Bem, agora vamos dar uma olhada em várias interfaces funcionais que o Java 8 nos trouxe e que são usadas ativamente em conjunto com a API Stream.

Predicado

Predicate— uma interface funcional para verificar se uma determinada condição é atendida. Se a condição for atendida, retorna true, caso contrário - false:
@FunctionalInterface
public interface Predicate<T> {
   boolean test(T t);
}
Por exemplo, considere criar um Predicateque verifique a paridade de um número do tipo Integer:
public static void main(String[] args) {
   Predicate<Integer> isEvenNumber = x -> x % 2==0;

   System.out.println(isEvenNumber.test(4));
   System.out.println(isEvenNumber.test(3));
}
Saída do console:

true
false

Consumidor

Consumer(do inglês - “consumidor”) - uma interface funcional que recebe um objeto do tipo T como argumento de entrada, executa algumas ações, mas não retorna nada:
@FunctionalInterface
public interface Consumer<T> {
   void accept(T t);
}
Como exemplo, considere , cuja tarefa é enviar uma saudação ao console com o argumento de string passado: Consumer
public static void main(String[] args) {
   Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
   greetings.accept("Elena");
}
Saída do console:

Hello Elena !!!

Fornecedor

Supplier(do inglês - provedor) - uma interface funcional que não aceita nenhum argumento, mas retorna um objeto do tipo T:
@FunctionalInterface
public interface Supplier<T> {
   T get();
}
Por exemplo, considere Supplier, que produzirá nomes aleatórios de uma lista:
public static void main(String[] args) {
   ArrayList<String> nameList = new ArrayList<>();
   nameList .add("Elena");
   nameList .add("John");
   nameList .add("Alex");
   nameList .add("Jim");
   nameList .add("Sara");

   Supplier<String> randomName = () -> {
       int value = (int)(Math.random() * nameList.size());
       return nameList.get(value);
   };

   System.out.println(randomName.get());
}
E se executarmos isso, veremos resultados aleatórios de uma lista de nomes no console.

Função

Function— esta interface funcional pega um argumento T e o converte em um objeto do tipo R, que é retornado como resultado:
@FunctionalInterface
public interface Function<T, R> {
   R apply(T t);
}
Como exemplo, tomemos , que converte números do formato de string ( ) para o formato de número ( ): FunctionStringInteger
public static void main(String[] args) {
   Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
   System.out.println(valueConverter.apply("678"));
}
Quando o executamos, obtemos a seguinte saída no console:

678
PS: se passarmos não apenas números, mas também outros caracteres na string, será lançada uma exceção - NumberFormatException.

Operador Unário

UnaryOperator— uma interface funcional que recebe um objeto do tipo T como parâmetro, realiza algumas operações nele e retorna o resultado das operações na forma de um objeto do mesmo tipo T:
@FunctionalInterface
public interface UnaryOperator<T> {
   T apply(T t);
}
UnaryOperator, que usa seu método applypara elevar ao quadrado um número:
public static void main(String[] args) {
   UnaryOperator<Integer> squareValue = x -> x * x;
   System.out.println(squareValue.apply(9));
}
Saída do console:

81
Vimos cinco interfaces funcionais. Isso não é tudo o que está disponível para nós a partir do Java 8 - essas são as interfaces principais. O restante dos disponíveis são análogos complicados. A lista completa pode ser encontrada na documentação oficial da Oracle .

Interfaces funcionais no Stream

Conforme discutido acima, essas interfaces funcionais estão fortemente acopladas à API Stream. Como, você pergunta? Interfaces Funcionais em Java - 3E de tal forma que muitos métodos Streamfuncionam especificamente com essas interfaces funcionais. Vejamos como as interfaces funcionais podem ser usadas no Stream.

Método com Predicado

Por exemplo, vamos pegar o método de classe Stream- filterque toma como argumento Predicatee retorna Streamapenas os elementos que satisfazem a condição Predicate. No contexto de Stream-a, isso significa que ele passa apenas pelos elementos que são retornados truequando usados ​​em um método testde interface Predicate. Este é o aspecto do nosso exemplo Predicate, mas para um filtro de elementos em Stream:
public static void main(String[] args) {
   List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
           .filter(x -> x % 2==0)
           .collect(Collectors.toList());
}
Como resultado, a lista evenNumbersconsistirá nos elementos {2, 4, 6, 8}. E, como lembramos, collectele reunirá todos os elementos em uma determinada coleção: no nosso caso, em List.

Método com Consumidor

Um dos métodos do Stream, que utiliza a interface funcional Consumer, é o peek. Esta é a aparência do nosso exemplo para Consumerin Stream:
public static void main(String[] args) {
   List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
           .peek(x -> System.out.println("Hello " + x + " !!!"))
           .collect(Collectors.toList());
}
Saída do console:

Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Mas como o método peekfunciona com Consumer, a modificação das strings in Streamnão ocorrerá, mas peekretornará Streamcom os elementos originais: os mesmos que vieram até ele. Portanto, a lista peopleGreetingsserá composta pelos elementos “Elena”, “John”, “Alex”, “Jim”, “Sara”. Há também um método comumente usado foreach, que é semelhante ao método peek, mas a diferença é que ele é terminal final.

Método com Fornecedor

Um exemplo de método Streamque usa a interface de função Supplieré generate, que gera uma sequência infinita com base na interface de função passada para ele. Vamos usar nosso exemplo Supplierpara imprimir cinco nomes aleatórios no console:
public static void main(String[] args) {
   ArrayList<String> nameList = new ArrayList<>();
   nameList.add("Elena");
   nameList.add("John");
   nameList.add("Alex");
   nameList.add("Jim");
   nameList.add("Sara");

   Stream.generate(() -> {
       int value = (int) (Math.random() * nameList.size());
       return nameList.get(value);
   }).limit(5).forEach(System.out::println);
}
E esta é a saída que obtemos no console:

John
Elena
Elena
Elena
Jim
Aqui usamos o método limit(5)para definir um limite no método generate, caso contrário o programa imprimiria nomes aleatórios no console indefinidamente.

Método com Função

Um exemplo típico de método com Streamargumento Functioné um método mapque pega elementos de um tipo, faz algo com eles e os repassa, mas estes já podem ser elementos de um tipo diferente. Como seria um exemplo com Functionin Stream:
public static void main(String[] args) {
   List<Integer> values = Stream.of("32", "43", "74", "54", "3")
           .map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
Como resultado, obtemos uma lista de números, mas em formato Integer.

Método com UnaryOperator

Como método que usa UnaryOperatorcomo argumento, vamos pegar um método de classe Stream- iterate. Este método é semelhante ao método generate: também gera uma sequência infinita, mas possui dois argumentos:
  • o primeiro é o elemento a partir do qual começa a geração da sequência;
  • o segundo é UnaryOperator, que indica o princípio de geração de novos elementos a partir do primeiro elemento.
Esta é a aparência do nosso exemplo UnaryOperator, mas no método iterate:
public static void main(String[] args) {
   Stream.iterate(9, x -> x * x)
           .limit(4)
           .forEach(System.out::println);
}
Quando o executamos, obtemos a seguinte saída no console:

9
81
6561
43046721
Ou seja, cada um dos nossos elementos é multiplicado por si mesmo e assim por diante para os primeiros quatro números. Interfaces Funcionais em Java - 4Isso é tudo! Seria ótimo se depois de ler este artigo você estivesse um passo mais perto de entender e dominar a API Stream em Java!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION