CodeGym /Cursos /JAVA 25 SELF /Interfaces funcionais: Predicate, Consumer, Supplier, Fun...

Interfaces funcionais: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
Nível 49 , Lição 0
Disponível

1. Aprofundando em interfaces funcionais

Uma interface funcional é uma interface que possui EXATAMENTE um método abstrato (ou seja, não implementado). É justamente por isso que o Java entende: “opa, aqui dá para passar uma lambda!” ou uma referência de método.

Para evitar enganos, essas interfaces costumam ter a anotação @FunctionalInterface. Ela não é obrigatória, mas, se você a adicionar e acidentalmente declarar um segundo método abstrato, o compilador reclamará na hora.

Exemplo:

@FunctionalInterface
interface MyAction {
    void run();
}
MyAction action = () -> System.out.println("Olá da lambda!");
action.run(); // Saída: Olá da lambda!

Para que isso serve?

  • Permite usar expressões lambda e referências de método em vez de criar classes anônimas (menos boilerplate!).
  • Deixa claro para o compilador que a interface se destina à programação funcional.

Curiosidade: A biblioteca padrão do Java já traz dezenas dessas interfaces — não é preciso reinventar a roda!

2. Visão geral das interfaces funcionais padrão

No pacote java.util.function há dezenas de interfaces funcionais. Vamos ver as quatro mais populares (as mais “frequentadas” entre todas as interfaces do Java).

Interface O que recebe O que retorna Uso típico
Predicate<T>
T
boolean
Verificação de condição (filtragem)
Consumer<T>
T
void
Executar uma ação sobre o objeto
Supplier<T>
nada
T
Obter/gerar um objeto
Function<T, R>
T
R
Transformar T em R

Predicate<T>

Descrição: Uma função que recebe um objeto do tipo T e retorna true ou false. Exemplo típico: filtrar uma lista. Método principal — test.

Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java"));       // false
System.out.println(isLong.test("Functional")); // true

Consumer<T>

Descrição: Recebe um objeto do tipo T, realiza uma ação e não retorna nada. Método principal — accept.

Consumer<String> printer = s -> System.out.println("Imprimindo: " + s);
printer.accept("Hello, world!"); // Imprimindo: Hello, world!

Supplier<T>

Descrição: Não recebe nada e retorna um objeto do tipo T. Pode ser visto como um “gerador” de valores. Método principal — get.

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Por exemplo, 0.1234567

Function<T, R>

Descrição: Recebe um objeto do tipo T e retorna um objeto do tipo R. Exemplo típico: transformação de dados. Método principal — apply.

Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4

Resumo: UnaryOperator, BinaryOperator, BiFunction

  • UnaryOperator<T> — equivalente a Function<T, T>: recebe e retorna o mesmo tipo.
  • BinaryOperator<T> — equivalente a BiFunction<T, T, T>: recebe dois T e retorna um T.
  • BiFunction<T, U, R> — recebe dois tipos diferentes e retorna um terceiro.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

3. Exemplos de uso

Vamos ver como essas interfaces aparecem em tarefas reais, especialmente em coleções e no Stream API.

Passando para métodos de coleções e do Stream API

Exemplo 1: Predicate e filtragem

List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
    .filter(word -> word.length() > 4) // Predicate<String>
    .toList();
System.out.println(longWords); // [stream, lambda]

Exemplo 2: Consumer e forEach

words.forEach(word -> System.out.println("Palavra: " + word)); // Consumer<String>

Exemplo 3: Function e map

List<Integer> lengths = words.stream()
    .map(word -> word.length()) // Function<String, Integer>
    .toList();
System.out.println(lengths); // [4, 6, 6, 4]

Exemplo 4: Supplier e geração de valores

Supplier<String> greetingSupplier = () -> "Olá, Java!";
System.out.println(greetingSupplier.get()); // Olá, Java!

Comparação com classes anônimas

Antes, era preciso escrever assim:

Predicate<String> isShort = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.length() < 5;
    }
};

Com lambdas, fica muito mais agradável:

Predicate<String> isShort = s -> s.length() < 5;

4. Prática: escrevendo expressões lambda para cada interface

Vamos implementar um pequeno aplicativo — uma lista de usuários. Cada usuário será representado pela classe User:

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Vamos criar uma lista de usuários:

List<User> users = List.of(
    new User("Anna", 23),
    new User("Boris", 17),
    new User("Vika", 31),
    new User("Gosha", 15)
);

Predicate: filtrando adultos

Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
    .filter(isAdult)
    .toList();

System.out.println("Adultos: " + adults); // Adultos: [Anna (23), Vika (31)]

Consumer: imprimindo usuários

Consumer<User> printUser = user -> System.out.println("Usuário: " + user);
adults.forEach(printUser);

Supplier: gerando usuários

Supplier<User> randomUserSupplier = () -> {
    String[] names = {"Dima", "Katya", "Lyosha"};
    int randomAge = 10 + (int)(Math.random() * 30);
    String randomName = names[(int)(Math.random() * names.length)];
    return new User(randomName, randomAge);
};

User randomUser = randomUserSupplier.get();
System.out.println("Usuário aleatório: " + randomUser);

Function: obtendo o nome do usuário

Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
    .map(getName)
    .toList();

System.out.println("Nomes: " + names); // Nomes: [Anna, Boris, Vika, Gosha]

5. Detalhes úteis

Uso no Stream API: filter, map, forEach etc.

Vamos juntar tudo e escrever uma cadeia de transformações:

users.stream()
     .filter(user -> user.getAge() >= 18)         // Predicate<User>
     .map(user -> user.getName().toUpperCase())    // Function<User, String>
     .forEach(name -> System.out.println("Adulto: " + name)); // Consumer<String>

Resultado:

Adulto: ANNA
Adulto: VIKA

Tabela de referência: o que usar onde

Onde é usado Qual interface usar Exemplo de uso
filter (Stream)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

Por que é importante conhecer as interfaces funcionais?

  • Elas estão na base de todas as expressões lambda no Java.
  • Permitem escrever código genérico, reutilizável e conciso.
  • Simplificam o trabalho com coleções, streams e tarefas assíncronas.

Quando usar cada interface?

  • Predicate — quando você precisa verificar uma condição (filtrar, buscar).
  • Consumer — quando você precisa realizar uma ação com o objeto (imprimir, gravar, enviar).
  • Supplier — quando você precisa obter ou gerar um objeto (fábricas, geradores).
  • Function — quando você precisa transformar um objeto de um tipo em outro.

6. Erros comuns ao trabalhar com interfaces funcionais

Erro nº 1: escolha incorreta da interface. Às vezes iniciantes confundem Predicate e Function — por exemplo, tentam retornar boolean de Function, e não de Predicate. Lembre-se: Predicate sempre retorna boolean; Function, qualquer outro tipo.

Erro nº 2: não usar as interfaces padrão. Muitas vezes cria-se uma interface própria como “Checker” com o método boolean check(T t) em vez de usar Predicate. É melhor usar as padrão — elas são amplamente suportadas e tornam o código mais claro para outros desenvolvedores.

Erro nº 3: lambda complexa demais. Se a lambda vira um mini-romance de 10 linhas, vale extraí-la para um método ou classe. Lambda é sobre concisão e legibilidade.

Erro nº 4: esquecer a anotação @FunctionalInterface. Se você estiver escrevendo sua própria interface funcional, não se esqueça da anotação. Ela protege contra erros acidentais (como adicionar um segundo método abstrato).

Erro nº 5: uso de estado mutável dentro de lambdas. Se a lambda modifica variáveis externas ou coleções, isso pode levar a bugs inesperados, especialmente ao trabalhar com threads. É melhor evitar efeitos colaterais.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION