Guia da interface Java BiFunction

Guia da interface Java BiFunction

1. Introdução

Java 8 introduziufunctional style programming, permitindo-nos parametrizar métodos de uso geral passando funções.

Provavelmente estamos mais familiarizados com as interfaces funcionais Java 8 de parâmetro único comoFunction,Predicate,eConsumer.

In this tutorial, we’re going to look at functional interfaces that use two parameters. Essas funções são chamadas de funções binárias e são representadas em Java com a interface funcionalBiFunction.

2. Funções de parâmetro único

Vamos recapitular rapidamente como usamos um parâmetro único ou função unária, como fazemos emstreams:

List mapped = Stream.of("hello", "world")
  .map(word -> word + "!")
  .collect(Collectors.toList());

assertThat(mapped).containsExactly("hello!", "world!");

Como podemos ver, omap usaFunction, que recebe um único parâmetro e nos permite realizar uma operação naquele valor, retornando um novo valor.

3. Operações com dois parâmetros

The Java Stream library provides us with a reduce function that allows us to combine the elements of a stream. Precisamos expressar como os valores que acumulamos até agora são transformados adicionando o próximo item.

A funçãoreduce usa a interface funcionalBinaryOperator<T>, que usa dois objetos do mesmo tipo como suas entradas.

Vamos imaginar que queremos juntar todos os itens em nosso fluxo, colocando os novos na frente com um separador de traço. Vamos dar uma olhada em algumas maneiras de implementar isso nas seções a seguir.

3.1. Usando um Lambda

A implementação de um lambda para aBiFunction é prefixada por dois parâmetros, entre colchetes:

String result = Stream.of("hello", "world")
  .reduce("", (a, b) -> b + "-" + a);

assertThat(result).isEqualTo("world-hello-");

Como podemos ver, os dois valores,aeb sãoStrings. Escrevemos um lambda que os combina para obter a saída desejada, com o segundo primeiro e um traço no meio.

Devemos notar quereduce usa um valor inicial - neste caso, a string vazia. Assim, terminamos com um traço à direita com o código acima, à medida que o primeiro valor do nosso fluxo é associado a ele.

Além disso, devemos observar que a inferência de tipo de Java nos permite omitir os tipos de nossos parâmetros na maioria das vezes. Em situações em que o tipo de um lambda não é claro no contexto, podemos usar tipos para nossos parâmetros:

String result = Stream.of("hello", "world")
  .reduce("", (String a, String b) -> b + "-" + a);

3.2. Usando uma função

E se quiséssemos fazer com que o algoritmo acima não acabasse? We could write more code in our lambda, but that might get messy. Vamos extrair uma função ao invés:

private String combineWithoutTrailingDash(String a, String b) {
    if (a.isEmpty()) {
        return b;
    }
    return b + "-" + a;
}

E então chame:

String result = Stream.of("hello", "world")
  .reduce("", (a, b) -> combineWithoutTrailingDash(a, b));

assertThat(result).isEqualTo("world-hello");

Como podemos ver, o lambda chama nossa função, que é mais fácil de ler do que colocar a implementação mais complexa em linha.

3.3. Usando uma referência de método

Alguns IDEs solicitarão automaticamente que convertamos o lambda acima em uma referência de método, pois geralmente é mais claro de ler.

Vamos reescrever nosso código para usar uma referência de método:

String result = Stream.of("hello", "world")
  .reduce("", this::combineWithoutTrailingDash);

assertThat(result).isEqualTo("world-hello");

As referências de método geralmente tornam o código funcional mais autoexplicativo.

4. UsandoBiFunction

Até agora, demonstramos como usar funções em que ambos os parâmetros são do mesmo tipo. The BiFunction interface allows us to use parameters of different types, com um valor de retorno de um terceiro tipo.

Vamos imaginar que estejamos criando um algoritmo para combinar duas listas de tamanhos iguais em uma terceira lista, executando uma operação em cada par de elementos:

List list1 = Arrays.asList("a", "b", "c");
List list2 = Arrays.asList(1, 2, 3);

List result = new ArrayList<>();
for (int i=0; i < list1.size(); i++) {
    result.add(list1.get(i) + list2.get(i));
}

assertThat(result).containsExactly("a1", "b2", "c3");

4.1. Generalizar a função

We can generalize this specialized function using a BiFunction como o combinador:

private static  List listCombiner(
  List list1, List list2, BiFunction combiner) {
    List result = new ArrayList<>();
    for (int i = 0; i < list1.size(); i++) {
        result.add(combiner.apply(list1.get(i), list2.get(i)));
    }
    return result;
}

Vamos ver o que está acontecendo aqui. Existem três tipos de parâmetros:T para o tipo de item na primeira lista,U para o tipo na segunda lista e, em seguida,R para qualquer tipo que a função de combinação retornar.

We use the BiFunction provided to this function by calling its apply method para obter o resultado.

4.2. Chamando a função generalizada

Nosso combinador é umBiFunction, o que nos permite injetar um algoritmo, sejam quais forem os tipos de entrada e saída. Vamos experimentar:

List list1 = Arrays.asList("a", "b", "c");
List list2 = Arrays.asList(1, 2, 3);

List result = listCombiner(list1, list2, (a, b) -> a + b);

assertThat(result).containsExactly("a1", "b2", "c3");

E podemos usar isso para tipos completamente diferentes de entradas e saídas também.

Vamos injetar um algoritmo para determinar se o valor na primeira lista é maior que o valor na segunda e produzir um resultadoboolean:

List list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List list2 = Arrays.asList(0.1f, 0.2f, 4f);

List result = listCombiner(list1, list2, (a, b) -> a > b);

assertThat(result).containsExactly(true, true, false);

4.3. Referência do Método ABiFunction

Vamos reescrever o código acima com um método extraído e uma referência de método:

List list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List list2 = Arrays.asList(0.1f, 0.2f, 4f);

List result = listCombiner(list1, list2, this::firstIsGreaterThanSecond);

assertThat(result).containsExactly(true, true, false);

private boolean firstIsGreaterThanSecond(Double a, Float b) {
    return a > b;
}

Devemos notar que isso torna o código um pouco mais fácil de ler, pois o métodofirstIsGreaterThanSecond descreve o algoritmo injetado como uma referência de método.

4.4. Referências de métodoBiFunction usandothis

Vamos imaginar que queremos usar o algoritmo baseado emBiFunction- acima para determinar se duas listas são iguais:

List list1 = Arrays.asList(0.1f, 0.2f, 4f);
List list2 = Arrays.asList(0.1f, 0.2f, 4f);

List result = listCombiner(list1, list2, (a, b) -> a.equals(b));

assertThat(result).containsExactly(true, true, true);

Podemos realmente simplificar a solução:

List result = listCombiner(list1, list2, Float::equals);

Isso ocorre porque a funçãoequals emFloat tem a mesma assinatura deBiFunction. Leva um primeiro parâmetro implícito dethis, um objeto do tipoFloat. O segundo parâmetro,other, do tipoObject, é o valor a ser comparado.

5. CompondoBiFunctions

E se pudéssemos usar referências de método para fazer a mesma coisa que nosso exemplo de comparação de lista numérica?

List list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List list2 = Arrays.asList(0.1d, 0.2d, 4d);

List result = listCombiner(list1, list2, Double::compareTo);

assertThat(result).containsExactly(1, 1, -1);

Isso é próximo ao nosso exemplo, mas retorna umInteger, em vez doBoolean original. Isso ocorre porque o métodocompareTo emDouble retornaInteger.

Podemos adicionar o comportamento extra de que precisamos para atingir nosso original emusing andThen to compose a function. Isso produz umBiFunction que primeiro faz uma coisa com as duas entradas e depois executa outra operação.

A seguir, vamos criar uma função para forçar nossa referência de métodoDouble::compareTo emBiFunction:

private static  BiFunction asBiFunction(BiFunction function) {
    return function;
}

A lambda or method reference only becomes a BiFunction after it has been converted by a method invocation. Podemos usar esta função auxiliar para converter nosso lambda no objetoBiFunction explicitamente.

Agora, podemos usarandThen para adicionar comportamento no topo da primeira função:

List list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List list2 = Arrays.asList(0.1d, 0.2d, 4d);

List result = listCombiner(list1, list2,
  asBiFunction(Double::compareTo).andThen(i -> i > 0));

assertThat(result).containsExactly(true, true, false);

6. Conclusão

Neste tutorial, exploramosBiFunction eBinaryOperator em termos da biblioteca Java Streams fornecida e nossas próprias funções personalizadas. Vimos como passarBiFunctions usando lambdas e referências de método, e vimos como compor funções.

As bibliotecas Java fornecem apenas interfaces funcionais de um e dois parâmetros. Para situações que requerem mais parâmetros, consulte nosso artigo emcurrying para mais ideias.

Como sempre, os exemplos de código completos estão disponíveisover on GitHub.