Conteúdo


Linguagens Java 8

Interfaces funcionais

Saiba como criar interfaces funcionais customizadas e entenda por que você deve usar integrações sempre que possível.

Comments

Conteúdos da série:

Esse conteúdo é a parte # de # na série: Linguagens Java 8

Fique ligado em conteúdos adicionais dessa série.

Esse conteúdo é parte da série:Linguagens Java 8

Fique ligado em conteúdos adicionais dessa série.

Qual é o tipo de uma expressão lambda? Algumas linguagens usam valores de função ou objetos de função para representar as expressões lambda, mas não o Java™ popular. Em vez disso, o Java usa interfaces funcionais para representar tipos de expressão lambda. Pode parecer estranho à primeira vista, mas na verdade é uma maneira eficiente de assegurar a compatibilidade reversa com versões anteriores do Java.

O pedaço de código a seguir deve ser familiar:

Thread thread = new Thread(new Runnable() {
  public void run() {
    System.out.println("In another thread");
  }
});

thread.start();

System.out.println("In main");

A classe Thread e seu construtor foram introduzidos no Java 1.0, 20 anos atrás. Desde então, o construtor não mudou. É uma tradição passar uma instância anônima do Runnable para o construtor. Mas, a partir do Java 8 existe a opção de passar uma expressão lambda no lugar:

Thread thread = new Thread(() -> System.out.println("In another thread"));

O construtor da classe Thread está esperando uma instância que implemente Runnable. Nesse caso, em vez de passar um objeto, passamos uma expressão lambda. Temos a opção de passar a expressão lambda com uma variedade de métodos e construtores, incluindo alguns criados antes do Java 8. Isso funciona porque as expressões lambda são representadas como interfaces funcionais no Java.

Há três regras importantes para interfaces funcionais:

  1. Uma interface funcional tem apenas um método abstrato.
  2. Um método abstrato que também for um método público na classe Object não será contado como esse método.
  3. Uma interface funcional pode ter métodos padrão e métodos estáticos.

Qualquer interface que satisfaça a regra do único método abstrato será considerada automaticamente uma interface funcional. Isso inclui interfaces tradicionais como Runnable e Callable, além das interfaces customizadas que você mesmo constrói.

Interfaces funcionais integradas

Além das interfaces únicas de método abstrato já mencionadas, o JDK 8 inclui várias novas interfaces funcionais. As mais comuns são Function<T, R>, Predicate<T> e Consumer<T>, que são definidas no pacote java.util.function. O método map do Stream usa Function<T, R> como um parâmetro. Da mesma forma, filter usa Predicate<T> e forEach usa Consumer<T>. O pacote também tem outras interfaces funcionais como Supplier<T>, BiConsumer<T, U> e BiFunction<T, U, R>.

É possível usar uma interface funcional integrada como um parâmetro para os nossos próprios métodos. Por exemplo, considere uma classe Device com métodos como checkout e checkin para indicar se um dispositivo está em uso. Quando um usuário solicita um novo dispositivo, o método getFromAvailable retorna um conjunto de dispositivos disponíveis ou cria um novo, caso seja necessário.

É possível implementar uma função para emprestar um dispositivo, da seguinte forma:

public void borrowDevice(Consumer<Device> use) {
  Device device = getFromAvailable();
  
  device.checkout();
  
  try {
    use.accept(device);      
  } finally {
    device.checkin();
  }
}

O método borrowDevice:

  • Usa um Consumer<Device> como parâmetro.
  • Obtém um dispositivo do conjunto (não estamos preocupados com a segurança do encadeamento neste exemplo).
  • Chama o método checkout para configurar o status do dispositivo como retirado.
  • Entrega o dispositivo ao consumidor.

Quando um dispositivo é retornado da chamada para o método accept do Consumer, seu status é alterado para registrado, chamando o método checkin.

Aqui está uma maneira de usar o método borrowDevice:

new Sample().borrowDevice(device -> System.out.println("using " + device));

Como o método recebe uma interface funcional como seu parâmetro, é aceitável passar uma expressão lambda como argumento.

Interfaces funcionais customizadas

Embora seja melhor usar uma interface funcional integrada sempre que possível, às vezes é necessário usar uma interface funcional customizada.

Para criar sua própria interface funcional, faça duas coisas:

  1. Anote a interface com @FunctionalInterface, que é a convenção do Java 8 para interfaces funcionais customizadas.
  2. Assegure-se de que a interface tenha apenas um método abstrato.

A convenção esclarece que a interface tem a intenção de receber expressões lambda. Quando o compilador identificar a anotação, ele verificará se a interface tem apenas um método abstrato.

Usar a anotação @FunctionalInterface assegura que se você violar acidentalmente a regra abstract-method-count durante uma mudança futura na interface, uma mensagem de erro seja recebida. Isso é útil porque você capturará o problema imediatamente, em vez de deixá-lo para que outro desenvolvedor lide com ele mais tarde. Ninguém deseja obter uma mensagem de erro ao passar uma expressão lambda para a interface customizada de outra pessoa.

Criando uma interface funcional customizada

Como exemplo, vamos criar uma classe Order que tem uma lista de OrderItems e um método para transformá-los e imprimi-los. Vamos começar com uma interface.

O código abaixo cria uma interface funcional Transformer.

@FunctionalInterface
public interface Transformer<T> {
  T transform(T input);
}

A interface é marcada com a anotação @FunctionalInterface, informando que ela é uma interface funcional. Como essa anotação faz parte do pacote java.lang, nenhuma importação é necessária. A interface tem um método chamado transform que usa um objeto do tipo parametrizado T e retorna um objeto transformado do mesmo tipo. A semântica da transformação será decidida pela implementação da interface.

Aqui está a classe OrderItem:

public class OrderItem {
  private final int id;
  private final int price;
  
  public OrderItem(int theId, int thePrice) {
    id = theId;
    price = thePrice;
  }
  
  public int getId() { return id; }
  public int getPrice() { return price; }
  
  public String toString() { return String.format("id: %d price: %d", id, price); }
}

OrderItem é uma classe simples que tem duas propriedades: id e price, e um método toString.

Agora, observe a classe Order.

import java.util.*;
import java.util.stream.Stream;

public class Order {
  List<OrderItem> items;
  
  public Order(List<OrderItem> orderItems) {
    items = orderItems;
  }
  
  public void transformAndPrint(
    Transformer<Stream<OrderItem>> transformOrderItems) {
    
    transformOrderItems.transform(items.stream())
      .forEach(System.out::println);
  }
}

O método transformAndPrint usa Transform<Stream<OrderItem> como parâmetro, chama o método transform para transformar os itens do pedido que pertencem à instância Order e imprime os itens do pedido na sequência transformada.

Aqui está uma amostra que usa esse método:

import java.util.*;
import static java.util.Comparator.comparing;
import java.util.stream.Stream;
import java.util.function.*;

class Sample {     
  public static void main(String[] args) {
    Order order = new Order(Arrays.asList(
      new OrderItem(1, 1225),
      new OrderItem(2, 983),
      new OrderItem(3, 1554)
    ));
    
    
    order.transformAndPrint(new Transformer<Stream<OrderItem>>() {
      public Stream<OrderItem> transform(Stream<OrderItem> orderItems) {
        return orderItems.sorted(comparing(OrderItem::getPrice));
      }
    });
  }
}

Passamos uma classe interna anônima como argumento para o método transformAndPrint. No método transform, chamamos o método sorted do fluxo determinado, que classificará os itens do pedido. Aqui está a saída do código, mostrando os itens do pedido classificados em ordem ascendente de preço:

id: 2 price: 983
id: 1 price: 1225
id: 3 price: 1554

O poder das expressões lambda

Em qualquer local que se espera uma interface funcional, temos três opções:

  1. Passar uma classe interna anônima.
  2. Passar uma expressão lambda.
  3. Passar uma referência de método em vez de uma expressão lambda, em alguns casos.

Passar uma classe interna anônima é algo detalhado, e a referência de método apenas pode ser passada como uma alternativa a uma expressão lambda de passagem. Considere o que ocorrerá se reescrevermos nossa chamada para que a função transformAndPrint use uma expressão lambda em vez de uma classe interna anônima:

order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));

É muito mais conciso e fácil de ler do que a classe interna anônima com a qual começamos.

Interfaces funcionais customizadas versus integradas

Nossa interface funcional customizada ilustra as vantagens e desvantagens de criar interfaces customizadas. Considere as vantagens primeiro:

  • É possível fornecer à interface customizada um nome descritivo que ajude outros desenvolvedores a modificá-la ou reutilizá-la. Nomes como Transformer, Validator e ApplicationEvaluator são específicos do domínio e podem ajudar alguém que estiver lendo os métodos da interface a deduzir o que se espera como argumento.
  • É possível fornecer ao método abstrato qualquer nome sinteticamente válido que desejar. O benefício é somente para o receptor da interface e somente nos casos em que um método abstrato está sendo passado. Um responsável pela chamada que estiver passando as expressões lambda ou as referências de método, não receberá esse benefício.
  • É possível usar tipos parametrizados na sua interface ou mantê-la simples e específica para poucos tipos. Nesse caso, seria possível escrever a interface Transformer para usar OrderItems em vez do tipo parametrizado T.
  • É possível escrever métodos padrão customizados e métodos estáticos, que podem ser usados por outras implementações da interface.

Obviamente, também há desvantagens de usar interfaces funcionais customizadas:

  • Imagine a criação de várias interfaces, todas tendo métodos abstratos com a mesma assinatura, usando String como parâmetro e retornando Integer. Embora os nomes dos métodos possam ser diferentes, geralmente eles são redundantes e podem ser substituídos por uma interface com um nome genérico.
  • Qualquer pessoa que deseje usar interfaces customizadas deverá fazer um esforço extra para aprender, entender e lembrar de tudo. Todos os programadores Java estão familiarizados com Runnable no pacote java.lang. Já o vimos muitas vezes, portanto, não é preciso se esforçar para lembrar de seu propósito. No entanto, se eu usar um Executor customizado, será necessário aprender detalhadamente sobre a interface antes de usá-la. Esse esforço vale a pena em alguns casos, mas será desperdiçado se o Executor for muito semelhante ao Runnable.

Qual é o melhor?

Sabendo os prós e contras das interfaces funcionais customizadas e das integradas, como decidir qual usar? Vamos revisar a interface Transformer para descobrir.

Lembre-se de que Transformer existe para transmitir a semântica de transformar um objeto em outro. Aqui, estamos o referenciando por nome:

public void transformAndPrint(Transformer<Stream<OrderItem>> transformOrderItems) {

O método transformAndPrint recebe um argumento que é encarregado da transformação. A transformação pode ressequenciar elementos na coleção OrderItems. Como alternativa, ela pode mascarar alguns detalhes de cada item de ordem. Ou a transformação pode decidir não fazer nada e simplesmente retornar a coleção original. A implementação é deixada para o responsável pela chamada.

O essencial é que o responsável pela chamada saiba que eles podem fornecer uma implementação de transformação como argumento para o método transformAndPrint. O nome da interface funcional e sua documentação devem fornecer esses detalhes. Nesse caso, isso também fica claro pelo nome do parâmetro (transformOrderItems) e deve ser incluído com a documentação da função transformAndPrint. Embora o nome da interface funcional seja útil, ele não é a única pista para seu propósito e uso.

Olhando de perto a interface Transformer e comparando seu propósito com as interfaces funcionais integradas do JDK, vemos que Function<T, R> poderia substituir Transformer. Para testar isso, vamos remover a interface funcional Transformer do código e alterar a função transformAndPrint, desta forma:

public void transformAndPrint(Function<Stream<OrderItem>, Stream<OrderItem>> transformOrderItems) {
  transformOrderItems.apply(items.stream())
    .forEach(System.out::println);
}

A mudança não causa grande impacto, — além de alterar Transformer<Stream<OrderItem>> para Function<Stream<OrderItem>>, Stream<OrderItem>>, nós mudamos a chamada de método de transform() para apply().

Se a chamada para transformAndPrint tivesse usado uma classe interna anônima, também seria necessário alterá-la. No entanto, nós já alteramos a chamada para usar uma expressão lambda:

order.transformAndPrint(orderItems -> orderItems.sorted(comparing(OrderItem::getPrice)));

O nome da interface funcional é irrelevante para a expressão lambda — ele apenas seria relevante para o compilador, que vincula o argumento da expressão lambda ao parâmetro de método. O nome do método transform versus apply é igualmente irrelevante para o responsável pela chamada.

O uso de uma interface funcional integrada nos deixou com uma interface a menos, e a chamada do método funciona da mesma forma. Além disso, a capacidade de leitura do código não foi comprometida. Este exercício nos mostra que seria possível substituir facilmente nossa interface funcional customizada por uma integrada. É necessário apenas fornecer uma documentação para o transformAndPrint (não mostrado) e nomear o argumento de forma mais descritiva.

Conclusão

A decisão de design para usar expressões lambda como um tipo de interface funcional facilita a compatibilidade com versões desde o Java 8 até as versões mais recentes do Java. É possível passar uma expressão lambda para qualquer função mais antiga que geralmente receberia uma única interface de método abstrato. Para receber expressões lambda, um tipo de parâmetro de método seria uma interface funcional.

Em alguns casos, faz sentido criar sua própria interface funcional, mas isso deve ser feito com cuidado. Considere uma interface funcional customizada somente se o aplicativo exigir métodos altamente especializados ou se nenhuma interface existente atender às suas necessidades. Sempre verifique se a funcionalidade existe em uma das interfaces funcionais integradas do JDK. Use interfaces funcionais integradas sempre que puder.


Recursos para download


Temas relacionados


Comentários

Acesse ou registre-se para adicionar e acompanhar os comentários.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=1050591
ArticleTitle=Linguagens Java 8: Interfaces funcionais
publish-date=10042017