Skip to content

4. As funções lambda do Java 8

4.1. Exemplo-01 - Interfaces funcionais e lambdas

  

Consideremos o código a seguir:


package dvp.java8.lambdas;

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

    // classes anônimas
    I1 ia1 = new I1() {
      @Override
      public void doSomething() {
        System.out.println("ia1.doSomething");
      }
    };

    I2 ia2 = new I2() {
      @Override
      public String getSomething(double value) {
        return String.format("ia2.getSomething(%s)", value);
      }
    };

    // lambdas
    I1 ib1 = () -> System.out.println("ib1.lambda");
    I2 ib2 = (value) -> String.format("ib2.lambda(%s)", value);
    I1 ib3 = () -> {
      System.out.println("ib3.lambda");
    };
    I2 ib4 = (double value) -> {
      return String.format("ib4.lambda(%s)", value);
    };

    // aplicativo
    ia1.doSomething();
    System.out.println(ia2.getSomething(4.3));
    ib1.doSomething();
    System.out.println(ib2.getSomething(5.8));
    ib3.doSomething();
    System.out.println(ib4.getSomething(10.1));
  }
}

@FunctionalInterface
interface I1 {
  void doSomething();
}

@FunctionalInterface
interface I2 {
  String getSomething(double value);
}
  • linhas 41-44: definem uma interface funcional I1. Uma interface funcional é uma interface que possui apenas um método. Ela não está vinculada à presença da anotação [@FunctionalInterface] na linha 41, que é opcional;
  • linhas 46-49: uma segunda interface funcional I2;
  • linhas 6-19: as interfaces I1 e I2 são implementadas com classes anônimas, a solução mais comum antes da introdução das funções lambda;
  • linhas 21-29: as interfaces I1 e I2 são implementadas com funções lambda;
  • linhas 7-12: implementação da interface I1 com uma classe anônima. A sintaxe para implementar uma interface I com uma classe anônima é a seguinte:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

onde m1, m2, ... são os métodos definidos pela interface I.

  • linhas 14-19: implementação da interface I2 com uma classe anônima;
  • linha 22: implementação da interface I1 com uma função lambda. Aqui, aproveita-se o fato de que a interface funcional possui apenas um método. A função lambda implementa, então, esse único método M. Sua sintaxe é a seguinte:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

Os tipos T1, T2 e Tn podem ser omitidos se o compilador puder deduzi-los a partir do contexto (inferência de tipo).

  • linha 22: implementação do método [I1.doSomething] com a seguinte assinatura:

void doSomething();

[doSomething] é um método que não possui parâmetros e não retorna nenhum resultado. Sua implementação lambda pode ser escrita como na linha 22 ou como nas linhas 24-26, ou seja, é possível colocar chaves ao redor do código da função lambda. Se esse código contiver apenas uma instrução, como neste caso, essas chaves podem ser omitidas;

  • linha 23: implementação do método [I1.getSomething] com a seguinte assinatura:

String getSomething(double value);

[getSomething] aceita um parâmetro do tipo [double] e retorna um resultado do tipo [String]. Sua implementação lambda pode ser a da linha 23 ou a das linhas 27-29. Na implementação da linha 23:

  • o tipo do parâmetro [value] é omitido. Nesse caso, será utilizado o tipo [double] encontrado na assinatura de [getSomething];
  • o código lambda não está entre parênteses. O resultado do lambda é, portanto, o valor da única expressão desse código, neste caso: String.format("ib2.lambda(%s)", value);

Na implementação das linhas 27 a 29:

  • declara-se explicitamente o tipo do parâmetro [value];
  • Utiliza-se um [return] para exibir o resultado da função lambda. Nesse caso, é preciso colocar chaves;
  • linhas 32-37: chamam-se as diversas funções anônimas e lambda;

O resultado obtido é o seguinte:

1
2
3
4
5
6
ia1.doSomething
ia2.getSomething(4.3)
ib1.lambda
ib2.lambda(5.8)
ib3.lambda
ib4.lambda(10.1)

4.2. Exemplo-02 – a interface funcional Predicate<T>

  

Na maioria das vezes, lidamos com interfaces funcionais de bibliotecas, em vez de interfaces funcionais que nós mesmos definimos. Aqui, estamos interessados na interface funcional [Predicate], definida no pacote [java.util.function], que reúne a maioria das interfaces funcionais do Java 8. Ela é definida da seguinte maneira:

Image

Mencionamos que uma interface funcional possui apenas um método. No entanto, aqui, há quatro. Outra inovação trazida pelo Java 8 foi a introdução do conceito de método padrão de uma interface, caracterizado pela palavra-chave [default]. Temos aqui três métodos desse tipo. Esses métodos têm a particularidade de possuir uma implementação padrão. Portanto, não há obrigação de uma classe que implemente uma interface com métodos padrão ter que implementar esses métodos. Assim, uma classe que deseje implementar a interface [Predicate] tem apenas um método que deve ser implementado obrigatoriamente: o método [test]. A interface [Predicate] é, portanto, uma interface funcional. Dir-se-á, portanto, que uma interface funcional é uma interface cuja implementação contém apenas um método obrigatório. Se ela tiver mais de um método, os demais devem ter a palavra-chave [default].

O método [Predicate<T>.test] recebe um parâmetro do tipo T e retorna um booleano. Essa interface é geralmente utilizada para filtrar coleções. Para ilustrar seu uso, utilizaremos os seguintes dados:


package dvp.data;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Personne {

  public enum Sexe {HOMME,FEMME};

  // dados
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // construtores
  public Personne() {

  }

  public Personne(String nom, Sexe sexe, int age, double poids) {
    this.nom = nom;
    this.sexe=sexe;
    this.age = age;
    this.poids = poids;
  }

  // getters e setters
...

  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • linhas 32-38: o método [toString] retorna a string jSON da pessoa;

A classe [Personnes] define uma lista de 3 pessoas:


package dvp.data;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

public class Personnes {
  private static List<Personne> personnes = Arrays.asList(new Personne("jean", Personne.Sexe.HOMME, 20, 70),
    new Personne("marie", Personne.Sexe.FEMME, 10, 30), new Personne("camille", Personne.Sexe.FEMME, 30, 55));

  public static List<Personne> get() {
    return personnes;
  }

  public static String toString(List<Personne> liste) {
    try {
      return new ObjectMapper().writeValueAsString(liste);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • linhas 10-11: a lista de 3 pessoas;
  • linhas 13-15: um método estático para obter essa lista;
  • linhas 17 a 23: um método estático que permite obter a string jSON a partir de uma lista de pessoas passada como parâmetro;

Esses dados serão utilizados pelo código a seguir:


package dvp.java8.lambdas;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple02 {
  public static void main(String[] args) {
    // predicado implementado por classe anônima
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predicado implementado por um lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // lista de pessoas
    List<Personne> personnes = Personnes.get();
    // exibições
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterAge)));
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterPoids)));
  }

  private static List<Personne> filterPersonnes(List<Personne> personnes, Predicate<Personne> filter) {
      // [filter] filtra a lista [personnes]
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • linhas 13-18: implementação da interface Predicate<Pessoa> por meio de uma classe anônima. Trata-se de um filtro com base no peso da pessoa;
  • linha 20: implementação da interface Predicate<Pessoa> por meio de uma função lambda. Trata-se de um filtro com base na idade da pessoa. De acordo com o que foi dito, ela também poderia ter sido escrita da seguinte maneira:

        Predicate<Personne> filterAge = (Personne p) -> {
            return (p.getAge() < 28);
        };

mas a versão da linha 20 é mais concisa. O tipo do parâmetro p é deduzido a partir do contexto. Aqui, constrói-se um tipo [Predicate<Personne>]. O método implementado tem, então, a assinatura [boolean test(Personne param)]. Portanto, o tipo implícito de p, na linha 20, é o tipo [Personne];

  • linha 22: recupera-se uma lista pré-definida de pessoas;
  • linha 24: elas são filtradas de acordo com a idade;
  • linha 25: filtra-se as pessoas de acordo com o peso. Nos dois casos, exibe-se a sequência jSON da lista assim filtrada;
  • linhas 28-37: um método estático que
    • aceita como parâmetros: uma lista de pessoas a ser filtrada e o filtro. Este é uma instância da interface [Predicate<Personne>]. Por convenção, designamos, aqui e em outras partes do documento, como instância de uma interface I, uma instância de uma classe C que implementa I;
    • retorna como resultado a lista assim filtrada;
  • linha 32: utiliza-se o método [test] da interface [Predicate]. Dependendo do filtro passado ao método, o método [test] será:

return personne.getPoids()<50;

ou


return p.getAge() < 28

A execução da classe [Exemple02] produz o seguinte resultado:

[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

4.3. Exemplo-03 — a interface funcional Function<T,R>

  

A interface funcional Function<T,R> é definida da seguinte maneira:

Image

O único método da interface tem a assinatura R apply(T t). Ele é geralmente utilizado para criar, a partir de um tipo Collection<T>, um novo tipo Collection<R>. Para ilustrar essa interface, utilizaremos o código a seguir:


package dvp.java8.lambdas;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class Exemple03 {
  public static void main(String[] args) throws JsonProcessingException {
    // implementação com classe anônima
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementação com lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // lista de pessoas
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // visualizações
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }

  // transformação de List<Pessoa> para List<T>
  private static <T> List<T> mapPersonnes(List<Personne> personnes, Function<Personne, T> mapper) {
    List<T> maps = new ArrayList<>();
    for (Personne p : personnes) {
      maps.add(mapper.apply(p));
    }
    return maps;
  }
}
  • linhas 15-20: implementamos a interface [Function<Personne, String>] com uma classe anônima que fará a transformação Pessoa --> String;
  • linha 22: implementamos a interface [Function<Personne, Integer>] com uma função lambda que fará a transformação Pessoa --> Integer;
  • linhas 33-39: um método estático que
    • aceita dois parâmetros: o primeiro, do tipo List<Personne>, é uma lista de pessoas a serem transformadas. O segundo, do tipo Function<Pessoa, T>, é uma função que, a partir de cada pessoa da lista, cria um objeto do tipo T;
    • retorna como resultado um tipo List<T>, em que cada elemento provém da transformação Pessoa -> T;
  • linhas 35-37: aplica-se a transformação Pessoa -> T. Se o segundo parâmetro do método for o objeto [mapToName], será realizada uma transformação Pessoa -> String. Se for o objeto [mapToAge], será realizada uma transformação Pessoa -> Integer;

O resultado obtido é o seguinte:

["jean","marie","camille"]
[20,10,30]

4.4. Exemplo-04 - a interface funcional Consumer<T>

  

A interface funcional Consumer<T> é definida da seguinte forma:

Image

O único método da interface tem a assinatura: void accept(T t). Esse método utiliza (consome) seu parâmetro e não retorna nenhum resultado. Para ilustrar isso, usaremos o código a seguir:


package dvp.java8.lambdas;

import java.util.List;
import java.util.function.Consumer;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple04 {
    public static void main(String[] args) {
        // lista de pessoas
        List<Personne> personnes = Personnes.get();
        // implementação anônima
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // implementação lambda
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // visualizações
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • linhas 14-19: a interface [Consumer<Personne>] é implementada por uma classe anônima cujo método [accept] exibe o nome e a idade da pessoa;
  • linha 21: a interface [Consumer<Personne>] é implementada por uma função lambda cujo método implícito [accept] exibe o nome e o peso da pessoa;
  • linhas 23-25: a lista de pessoas é utilizada pela implementação [consumerAge];
  • linhas 27-29: a lista de pessoas é utilizada pela implementação [consumerPoids];

Os resultados são os seguintes:

1
2
3
4
5
6
7
 age de jean = 20
 age de marie = 10
 age de camille = 30
--------
 poids de jean = 70.0
 poids de marie = 30.0
poids de camille = 55.0

4.5. Exemplo-05 — a interface funcional BiConsumer<T,U>

  

A interface funcional BiConsumer<T,U> é definida da seguinte forma:

Image

O único método da interface tem a assinatura: void accept(T t, U u). Ele recebe o tipo T com a informação adicional U u. Ilustraremos seu uso com o código a seguir:


package dvp.java8.lambdas;

import dvp.data.Personne;
import dvp.data.Personnes;

import java.util.List;
import java.util.function.BiConsumer;

public class Exemple05 {
  public static void main(String[] args) {
    // lista de pessoas
    List<Personne> personnes = Personnes.get();
    // implementação anônima
    BiConsumer<Personne, Integer> biconsumerAge = new BiConsumer<Personne, Integer>() {
      @Override
      public void accept(Personne personne, Integer integer) {
        personne.setAge(personne.getAge() + integer);
        System.out.printf("age de %s = %s%n", personne.getNom(), personne.getAge());
      }
    };
    // implementação lambda
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // visualizações
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • linhas 14-20: implementação da interface BiConsumer<T,U> com uma classe anônima. O método [apply] utiliza seu segundo parâmetro para atualizar a idade da pessoa passada como primeiro parâmetro. Em seguida, ele exibe o resultado;
  • linhas 22-25: implementação da interface BiConsumer<T,U> com uma função lambda. O método implícito [apply] utiliza seu segundo parâmetro para atualizar o peso da pessoa passada como primeiro parâmetro. Em seguida, exibe o resultado;
  • linhas 27-29: a lista de pessoas é processada com a implementação [biconsumerAge];
  • linhas 31-33: a lista de pessoas é processada com a implementação [biconsumerPoids];

Os resultados obtidos são os seguintes:

1
2
3
4
5
6
7
age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

4.6. Exemplo-06 — a interface funcional BiFunction<T,U,R>

  

A interface funcional BiFunction<T,U,R> é definida da seguinte forma:

Image

O único método da interface tem a assinatura: R apply(T t, U u). Esse método é semelhante ao método [BiConsumer.apply], mas enquanto este último não retorna nenhum resultado, o método [BiFunction.apply] retorna um resultado. Ilustraremos seu uso com o código a seguir:


package dvp.java8.lambdas;

import java.util.List;
import java.util.function.BiFunction;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple06 {
    public static void main(String[] args) {
        // lista de pessoas
        List<Personne> personnes = Personnes.get();
        // implementação anônima
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // implementação lambda
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // visualizações
        for (Personne p : personnes) {
            System.out.printf("age de %s = %s%n", p.getNom(), biFunctionAge.apply(p, 100));
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            System.out.printf("poids de %s = %s%n", p.getNom(), biFunctionPoids.apply(p, 200));
        }
    }
}
  • linhas 14-19: a interface BiFunction<Pessoa, Inteiro, Inteiro> é implementada com uma classe anônima. O método [apply] dessa classe retorna a idade da pessoa passada como primeiro parâmetro, aumentada pelo valor do segundo parâmetro;
  • linhas 21-23: a interface BiFunction<Pessoa, Inteiro, Duplo> é implementada com uma função lambda. O método [apply] retorna o peso da pessoa passada como primeiro parâmetro, aumentado pelo valor do segundo parâmetro;
  • linhas 25-27: aplica-se a implementação [biFunctionAge] às pessoas;
  • linhas 29-31: aplica-se a implementação [biFunctionPoids] às pessoas;

Os resultados obtidos são os seguintes:

age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

Além das funções lambda, o Java 8 introduziu o tipo Stream<T>, que modela um fluxo de elementos do tipo T. Esses elementos podem passar por transformações sucessivas implementadas por funções lambda. Quando possível, e se houver vários processadores, essas transformações podem, às vezes, ser realizadas em paralelo.

4.7. Exemplo-07 — a interface funcional Supplier<T>

  

A interface funcional Supplier<T> é definida da seguinte maneira:

Image

O único método da interface tem a assinatura: T get(), cuja função é fornecer um objeto do tipo T.

Ilustraremos essa interface funcional com o código a seguir:


package dvp.java8.lambdas;

import java.util.List;
import java.util.Random;
import java.util.function.Supplier;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple07 {
    public static void main(String[] args) {
        // implementação anônima
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // lista de pessoas
            List<Personne> personnes = Personnes.get();

            // implementação de interface
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // análise
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }

    // exibição de pessoa
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • linhas 13-28: implementação de um tipo Supplier<Personne>;
  • linhas 31-33: o método estático [affiche] espera um parâmetro do tipo Supplier<Personne>;
  • linhas 25-27: execução da instância Supplier<Personne>;

Obtêm-se os seguintes resultados:

1
2
3
4
5
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}