Interfaces Funcionais em Java
Interfaces com um único método abstrato em Java que servem como alvos para lambdas, marcadas com @FunctionalInterface.
Uma interface funcional é uma interface com exatamente um método abstrato. Esse único método é o alvo para o qual uma lambda ou referência de método é compilada. Runnable, Comparator<T>, Callable<V>, Supplier<T>, Function<T, R>, Predicate<T>, Consumer<T>, ActionListener, FileFilter — todas são interfaces funcionais. Há dezenas já no JDK e você escreverá as suas próprias quando nenhuma delas se encaixar.
O capítulo anterior mostrou lambdas como () -> 42 e s -> s.length() "compilando para qualquer interface que o contexto precisar." Este capítulo responde o que torna uma interface um alvo válido — a regra do método abstrato único (SAM) — e como @FunctionalInterface permite dizer "sim, esta é uma, e quero que o compilador a imponha."
A regra SAM, com precisão
Para ser funcional, uma interface deve declarar exatamente um método que precisa de implementação. A formulação importa: não "exatamente um método no total," mas "exatamente um método abstrato." Três categorias de métodos não contam contra o único:
- Métodos
default— eles já têm um corpo, portanto um implementador não precisa fornecer um. - Métodos
static— pertencem à própria interface, não aos implementadores. - Métodos abstratos
publicque sobrescrevem um método emjava.lang.Object— por exemplo,equals,hashCode,toString. Toda classe já herda implementações deObject, então redeclará-los em uma interface não adiciona um novo requisito.
O terceiro surpreende as pessoas. Comparator<T> declara boolean equals(Object), mas ainda é funcional porque esse método vem de Object. O método abstrato real é int compare(T, T).
@FunctionalInterface
interface MyComparator<T> {
int compare(T a, T b); // the one SAM
boolean equals(Object other); // Object override — doesn't count
default MyComparator<T> reversed() { // default — doesn't count
return (a, b) -> compare(b, a);
}
static <T extends Comparable<T>> MyComparator<T> natural() { // static — doesn't count
return (a, b) -> a.compareTo(b);
}
}@FunctionalInterface — verificação em tempo de compilação opcional
A anotação é opcional. Uma interface é funcional com base em sua forma, não em ser anotada. Mas anotar traz dois benefícios:
- Erro de compilação se a interface deixar de ser funcional. Adicione acidentalmente um segundo método abstrato e o compilador o interromperá imediatamente — na interface, não em cada ponto de chamada que a usa como alvo de lambda.
- Documentação. A anotação sinaliza "esta interface foi criada para ser usada como alvo de lambda," o que vale dizer em qualquer caso não óbvio.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
boolean isInvalid(T value); // <-- compile error: not a functional interface
}Sem a anotação, esse segundo método silenciosamente tornaria Validator<T> uma interface não funcional, e o primeiro ponto de chamada com estilo lambda que a usasse falharia ao compilar com uma mensagem confusa longe da causa.
A anotação também é a convenção para as próprias interfaces funcionais do JDK — Function, Predicate, Consumer, Supplier, Runnable, Callable — todas a carregam.
Lambdas, referências de métodos e classes anônimas são intercambiáveis
Uma interface funcional aceita três tipos de valor, e eles são livremente intercambiáveis:
Predicate<String> blank1 = s -> s.trim().isEmpty(); // lambda
Predicate<String> blank2 = String::isBlank; // method reference (since Java 11)
Predicate<String> blank3 = new Predicate<>() { // anonymous class
@Override public boolean test(String s) { return s.trim().isEmpty(); }
};Os três implementam a mesma interface Predicate<String> e produzem valores equivalentes no ponto de chamada. A lambda e a referência de método são dramaticamente mais curtas; a classe anônima é reservada para os casos raros listados no capítulo anterior (mais de um método necessário, estado local ao método, this referindo-se à nova instância).
Interfaces funcionais genéricas
A interface pode ser parametrizada — é assim que uma única declaração de Function<T, R> pode ser usada para toda transformação:
@FunctionalInterface
interface Mapper<T, R> {
R map(T input);
}
Mapper<String, Integer> length = s -> s.length();
Mapper<Integer, String> hex = n -> Integer.toHexString(n);Os parâmetros podem ser limitados, podem incluir múltiplas variáveis de tipo e podem ser reutilizados entre interfaces — a biblioteca padrão usa todas as variações.
Escrevendo sua própria interface funcional
Na maioria das vezes você deve usar as interfaces integradas em java.util.function — o próximo capítulo as apresenta todas. Escreva a sua própria quando:
- A semântica merece um nome.
Validator<T>é mais legível em um ponto de chamada do queFunction<T, ValidationResult>, mesmo que a forma corresponda. - Você precisa de uma exceção verificada.
Function.applynão lança nada verificado; se sua operação lançaIOException, escreva um SAM que a declare. - A forma não está na biblioteca padrão. Um método que recebe três argumentos (uma função tri) não tem interface integrada — escreva uma quando precisar.
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T input) throws IOException;
}
IOFunction<Path, String> readAll = Files::readString; // declared exception — built-in Function can'tUma quantidade surpreendentemente grande de "devo escrever isso?" se resume a legibilidade ou propagação de exceções.
Métodos default valem o esforço
O lugar onde você escreverá sua própria interface funcional e também adicionará métodos default é quando quiser que os chamadores possam compor instâncias:
@FunctionalInterface
interface Filter<T> {
boolean keep(T value);
default Filter<T> and(Filter<T> other) {
return v -> keep(v) && other.keep(v);
}
default Filter<T> negate() {
return v -> !keep(v);
}
}
Filter<Integer> positive = n -> n > 0;
Filter<Integer> even = n -> n % 2 == 0;
Filter<Integer> posOdd = positive.and(even.negate());Essa é exatamente a receita que o JDK usa para Predicate.and / or / negate, Function.andThen / compose e Comparator.thenComparing. O método abstrato único é o comportamento; os métodos default são a álgebra de composição que o rodeia.
Um exemplo completo: escrevendo, anotando e compondo
O programa abaixo define uma interface funcional Filter<T> com dois métodos default, demonstra a regra SAM (um método abstrato extra não compilaria) e mostra lambdas, referências de métodos e uma classe anônima, todos implementando o mesmo SAM.
O que observar na execução:
notBlank1(lambda),notBlank2(cadeia de referência de método) enotBlank3(classe anônima) implementam a mesma interfaceFilter<String>— de forma intercambiável. A lambda é a mais curta; a classe anônima é reservada para casos que lambdas não conseguem tratar.positive.and(even.negate())compôs três filtros em um sem declarações de métodos extras. Os métodosdefaultandenegatena interface são a álgebra de composição — é por isso que o JDK os adiciona aPredicate,FunctioneComparatortambém.SafelyFunctional<T>declara tantoapply(T)quantoboolean equals(Object), e ainda assim compilou com@FunctionalInterface. A sobrescrita deequalsé herdada deObject, portanto não conta contra a regra do método abstrato único.- Se você remover uma palavra-chave
defaultemFilter(transformando um default em um segundo método abstrato), a anotação@FunctionalInterfaceforça um erro de compilação imediato na declaração da interface — muito antes de qualquer ponto de chamada lambda ver falhas de inferência confusas.
O que vem a seguir
Você já sabe reconhecer uma interface funcional, escrever uma quando o JDK não tem o que você precisa e deixar o compilador impor sua forma. Quase sempre, porém, a resposta certa é "use o que já existe." O próximo capítulo, Interfaces Funcionais Integradas do Java, percorre java.util.function — Function, Predicate, Consumer, Supplier, suas variantes bi e as especializações primitivas que existem para evitar boxing.