Executando verificação de segurança...
6

Introdução ao uso de interfaces funcionais

Este artigo visa demonstrar de forma prática o uso de algumas interfaces funcionais presentes a partir do JDK 8.

Predicate

@FunctionalInterface
public interface Predicate<T> {
  boolean test(T t);
}

A interface java.util.function.Predicate define um método abstrato chamado test(T t) que aceita um tipo genérico T e retornar um booleano, você pode usar essa interface para representar uma expressão booleana que utilize um objeto de um tipo T.

Por exemplo vamos filtrar um tipo do nosso domínio por meio de um atributo desse tipo:

Vamos começar modelando nosso domínio:

---
title: Modelagem Inicial
---
classDiagram
direction RD
class Car {
    color: Color
    model: String
    manufacturer: String
}

class Color{
    <<enumeration>>
    RED,
    YELLOW,
    GRAY,
    BLACK
}


Car .. Color

Código correspondente:

public record Car(
  Color color,
  String model,
  String manufacturer,
  Integer hp
) {
}
public enum Color {
  RED,
  YELLOW,
  GRAY,
  BLACK
}

Agora vamos utilizar a interface Predicate pra criar o nosso filtro:

public class Validate {
  public <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    return list.stream().filter(predicate).toList();
  }
}

Por fim basta criar o predicado e utilizar como parâmetro no nosso método:

public class Main {
  public static void main(String[] args) {

    Validate validate = new Validate();

    List<Car> cars = asList(
      new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
      new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
      new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
      new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
      new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
      new Car(Color.GRAY, "Audi R8", "Audi", 532),
      new Car(Color.RED, "Ford GT", "Ford", 660),
      new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
      new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
      new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
    );

    Predicate<Car> redCarsPredicate = (Car c) -> c.color().equals(Color.RED);
    List<Car> redCars = validate.filter(cars, redCarsPredicate);

    System.out.println("CARS: " + cars);
    System.out.println("TOTAL OF CARS: " + cars.size());
    System.out.println("RED CARS LIST: " + redCars);
    System.out.println("TOTAL OF RED CARS: " + redCars.size());
  }
}

Note que a variável redCarsPredicate foi utilizada apenas uma vez, sendo assim posso passar o predicado diretamente como parâmetro da lambda pois Predicate é uma interface funcional:

public class Main {
  public static void main(String[] args) {

    Validate validate = new Validate();

    List<Car> cars = asList(
      new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
      new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
      new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
      new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
      new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
      new Car(Color.GRAY, "Audi R8", "Audi", 532),
      new Car(Color.RED, "Ford GT", "Ford", 660),
      new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
      new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
      new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
    );

    List<Car> redCars = validate.filter(cars, (Car c) -> c.color().equals(Color.RED));

    System.out.println("CARS: " + cars);
    System.out.println("TOTAL OF CARS: " + cars.size());
    System.out.println("RED CARS LIST: " + redCars);
    System.out.println("TOTAL OF RED CARS: " + redCars.size());
    
  }
}

Consumer

@FunctionalInterface
public interface Consumer<T> {
  void accept(T t);
}

A interface funcional java.util.function.Consumer define um método abstrato chamado accept(T t) que recebe como parâmetro um tipo genérico e retorna void. Você pode utilizar essa interface funcional quando precisa acessar um tipo T e fazer algumas operações nele.

Observe esse código:

public class Main {
  public static void main(String[] args) {

    List<Car> cars = asList(
      new Car(Color.RED, "Ferrari 488", "Ferrari", 661),
      new Car(Color.YELLOW, "Lamborghini Huracan", "Lamborghini", 602),
      new Car(Color.GRAY, "Porsche 911 GT3", "Porsche", 500),
      new Car(Color.BLACK, "McLaren 720S", "McLaren", 710),
      new Car(Color.RED, "Chevrolet Corvette", "Chevrolet", 495),
      new Car(Color.GRAY, "Audi R8", "Audi", 532),
      new Car(Color.RED, "Ford GT", "Ford", 660),
      new Car(Color.GRAY, "Bugatti Chiron", "Bugatti", 1479),
      new Car(Color.YELLOW, "Ferrari LaFerrari", "Ferrari", 950),
      new Car(Color.RED, "Lamborghini Aventador", "Lamborghini", 730)
    );

    cars.forEach(car -> System.out.println(car.manufacturer()));
  }
}

Isso deve imprimir as marcas de todos os carros, já parou pra pensar em como funciona o método forEach() ? Olhe mais de perto:

public interface Iterable<T> {
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

Veja que o método forEach pertence a interface Iterable<T> e que recebe como parâmetro um Consumer, no caso da nossa chamada car -> System.out.println(car.manufacturer()) é o consumer, veja que esse método verifica o consumer não é nulo. Se o consumer passado como argumento for nulo, o será lançada uma exceção NullPointerException. Isso é útil para garantir que não se tente usar objetos nulos quando eles não são esperados, o que pode levar a erros ou falhas de execução.

Na sequencia é chamada um for each normal como estamos acostumados a ver, esse foreach itera sobre os elementos da própria instância que implementa a interface Iterable. O tipo T representa o tipo dos elementos na coleção e dentro do loop, chamamos o método accept do Consumer passando o elemento atual t. Isso permite que o Consumer execute alguma operação no elemento, no nosso exemplo imprimir a fabricante do carro.

Function

A interface funcional java.util.function.Function<T, R> define um método abstrato chamado apply que recebe como argumento um objeto de tipo genérico T e retornar um objeto genérico do tipo R. Pode ser utilizada para definir uma lambda que mapeia a informação a partir de um objeto de entrada para um objeto de saída.

Vamos dizer que eu queira saber quantas letras tem o nome de cada fabricante dos nossos carros:

O primeiro passo será extrar somente o nome dos fabricantes para uma lista

List<String> manufacturers = cars.stream().map(Car::manufacturer).toList();

Aqui estamos vendo 2 features das quais ainda não falamos ainda o método map() e method references, veremos adiante.

Vamos escrever um método map que funciona mais ou menos como o map ali em cima:

public class Mapper {

  public <T, R> List<R> map(List<T> list, Function<T, R> function) {
    List<R> result = new ArrayList<>();
    for (T t : list) {
      result.add(function.apply(t));
    }
    return result;
  }
}

Agora basta utilizar passando a lista de Strings (nomes dos fabricantes) que será nosso objeto de entrada e vamos ter como objeto de saída uma lista de inteiros que representam a quantidade de caracteres do nome de cada fabricante.

public class Main {
  public static void main(String[] args) {

    List<String> manufacturers = asList("Ferrari",
      "Lamborghini",
      "Porsche",
      "McLaren",
      "Chevrolet",
      "Audi",
      "Ford",
      "Bugatti",
      "Ferrari",
      "Lamborghini");

    Mapper mapper = new Mapper();

    System.out.println(manufacturers);

    List<Integer> lengths = mapper.map(manufacturers, String::length);

    System.out.println(lengths);
  }
}

Pra finalizar segue uma lista de possíveis casos de uso, exemplos de lambdas e interfaces funcionais que pode podem ser utilizados:

Caso de uso: Uma expressão booleana

Exemplo de lambda:

(List<String> list) -> list.isEmpty();

Interface Funcional:

Predicate<List<String>>


Casos de Uso: Criar Objetos

Exemplo de lambda:

() -> new Car(Color.RED, "Model Y", "Tesla", 200)

Interface Funcional:

Supplier<Car>


Casos de Uso: Consumindo de um objeto

Exemplo de lambda:

(Car c) -> System.out.println(c.manufacturer())

Interface Funcional:

Consumer<Car>


Casos de Uso: Extrair ou selecionar de um objeto

Exemplo de lambda:

(Car c) -> c.hp();

Interface Funcional:

Function<Car, Integer>  ou ToIntFunction<Car>

Exemplo de lambda:

(Car c) -> c.color();

Interface Funcional:

Function<Car, Color>


Casos de Uso: Combinar dois valores

Exemplo de lambda:

(int x, int y) -> x / y

Interface Funcional:

IntBinaryOperator


Casos de Uso: Comparar dois objetos

Exemplo de lambda:

(Car c1, Car c2) -> c1.hp().compareTo(c2.hp())

Interface Funcional:

Comparator<Car>
BiFunction<Car, Car, Integer>
ToIntBiFunction<Car, Car>
Carregando publicação patrocinada...
2

Ficou ótimo, leve meus pontos. Só não esqueça de explicar o que é objetos de primeira classe (ou valores de primeira classe), interface funcional, generics e método abstrato antes, senão os iniciantes vão boiar ⛵, vamos criar haters de Java e vão achar que somos apenas um bando de malucos falando sobre coisas desconexas.

1

Pultz verdade mano, valeu pelo feedback espero que seja útil tenho aprendido essas coisas e to usando aqui pra compartilhar e tbm pra fixar, meio que eu pego o exemplo e invento um explicando com as minhas palavras manja ?

Valeu pelo feedback vou incluir exemplos explicando Generics e metodo abstrato, ou se ficar grande linkar um outro post.