Skip to content

4. Expressões Lambda do Java 8

4.1. Exemplo-01 - Interfaces funcionais e lambdas

  

Considere o seguinte código:


package dvp.java8.lambdas;
 
public class Exemple01 {
  public static void main(String[] args) {
 
    // anonymous classes
    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);
    };
 
    // app
    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 com apenas um método. Não depende da 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 utilizando 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 utilizando 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, tiramos partido do facto de a interface funcional ter apenas um método. A função lambda implementa então este único método M. A sua sintaxe é a seguinte:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

Os tipos T1, T2, Tn podem ser omitidos se o compilador os conseguir inferir a partir do contexto (inferência de tipos).

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

void doSomething();

[doSomething] é um método que não tem parâmetros e não retorna nenhum resultado. A sua implementação lambda pode ser escrita como na linha 22 ou como nas linhas 24–26, ou seja, podem ser colocadas chaves à volta do código da função lambda. Se este código contiver apenas uma única instrução, como aqui, estas chaves podem ser omitidas;

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

String getSomething(double value);

[getSomething] recebe um parâmetro do tipo [double] e devolve um resultado do tipo [String]. A 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. Será então utilizado o tipo [double] encontrado na assinatura de [getSomething];
  • o código lambda não está entre parênteses. O resultado do lambda é, então, o valor da única expressão nesse código, aqui: String.format("ib2.lambda(%s)", value);

Na implementação das linhas 27–29:

  • o tipo do parâmetro [value] é explicitamente declarado;
  • usamos um [return] para devolver o resultado da lambda. Neste caso, devem ser utilizadas chaves;
  • Linhas 32–37: chamamos as várias funções anónimas e lambdas;

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 próprios definimos. Aqui, estamos interessados na interface funcional [Predicate] definida no pacote [java.util.function], que contém a maioria das interfaces funcionais do Java 8. Está definida da seguinte forma:

Image

Mencionámos que uma interface funcional tem apenas um método. Aqui, no entanto, existem quatro. Outra inovação introduzida pelo Java 8 foi o conceito de método padrão numa interface, marcado pela palavra-chave [default]. Aqui temos três métodos desse tipo. Estes métodos têm a particularidade de possuírem uma implementação padrão. Não há, portanto, qualquer exigência de que uma classe que implemente uma interface com métodos padrão os implemente. Assim, uma classe que pretenda implementar a interface [Predicate] tem apenas um método que deve implementar: o método [test]. A interface [Predicate] é, portanto, de facto, uma interface funcional. Podemos assim dizer que uma interface funcional é uma interface cuja implementação contém apenas um método obrigatório. Se tiver mais do que um método, os outros devem ter a palavra-chave [default].

O método [Predicate<T>.test] recebe um parâmetro do tipo T e devolve um booleano. Esta interface é geralmente utilizada para filtrar coleções. Para ilustrar a sua utilização, 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};
 
  // data
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // manufacturers
  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 and 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] devolve a cadeia JSON da pessoa;

A classe [People] 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 recuperar esta lista;
  • linhas 17–23: um método estático para obter a cadeia JSON de uma lista de pessoas passada como parâmetro;

Estes dados serão utilizados pelo código seguinte:


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) {
    // predicate implemented by anonymous class
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predicate implemented by a lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // list of persons
    List<Personne> personnes = Personnes.get();
    // displays
    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] filters the [people] list
    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<Person> utilizando uma classe anónima. Trata-se de um filtro baseado no peso da pessoa;
  • linha 20: implementação da interface Predicate<Person> utilizando uma função lambda. Trata-se de um filtro baseado na idade da pessoa. Com base no que foi dito, também poderia ter sido escrito da seguinte forma:

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

mas a versão na linha 20 é mais concisa. O tipo do parâmetro p é inferido a partir do contexto. Aqui, estamos a construir um tipo [Predicate<Person>]. O método implementado tem então a assinatura [boolean test(Person param)]. Portanto, o tipo implícito de p na linha 20 é o tipo [Person];

  • linha 22: recuperamos uma lista predefinida de pessoas;
  • linha 24: filtramo-las por idade;
  • linha 25: filtramo-las por peso. Em ambos os casos, exibimos a cadeia JSON da lista filtrada;
  • linhas 28–37: um método estático que
    • recebe como parâmetros: uma lista de pessoas a filtrar e o filtro. O filtro é uma instância da interface [Predicate<Person>]. Por conveniência, referimo-nos aqui e noutras partes do documento a uma instância de uma interface I como uma instância de uma classe C que implementa I;
    • retorna a lista filtrada como resultado;
  • linha 32: utilizamos 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 Função<T,R>

  

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

Image

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


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 {
    // implementation with anonymous class
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementation with lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // list of persons
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // displays
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }
 
  // transformation List<Person> --> 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<Person, String>] com uma classe anónima que realiza a transformação de Pessoa → String;
  • linha 22: implementamos a interface [Function<Person, Integer>] com uma função lambda que realiza a transformação de Person para Integer;
  • Linhas 33–39: um método estático que
    • recebe dois parâmetros: o primeiro, do tipo List<Person>, é uma lista de pessoas a serem transformadas. O segundo, do tipo Function<Person, T>, é uma função que recebe cada pessoa da lista e cria um objeto do tipo T;
    • retorna uma List<T> onde cada elemento resulta da transformação de Pessoa para T;
  • linhas 35–37: a transformação Pessoa -> T é aplicada. Se o segundo parâmetro do método for o objeto [mapToName], é realizada uma transformação Pessoa -> String. Se for o objeto [mapToAge], é realizada uma transformação Pessoa -> Integer;

O resultado é 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). Este método processa (consome) o seu parâmetro e não devolve qualquer resultado. Para ilustrar isto, utilizaremos o seguinte código:


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) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // anonymous implementation
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // immplémentaton lambda
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // displays
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • linhas 14–19: a interface [Consumer<Person>] é implementada por uma classe anónima cujo método [accept] exibe o nome e a idade da pessoa;
  • linha 21: a interface [Consumer<Person>] é 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 é consumida pela implementação [consumerAge];
  • linhas 27–29: a lista de pessoas é consumida pela implementação [consumerWeight];

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 consome o tipo T com a informação adicional U u. Iremos ilustrar a sua utilização com o seguinte código:


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) {
    // list of persons
    List<Personne> personnes = Personnes.get();
    // anonymous implementation
    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());
      }
    };
    // lambda implementation
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // displays
    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> utilizando uma classe anónima. O método [apply] utiliza o seu segundo parâmetro para atualizar a idade da pessoa passada como primeiro parâmetro. Em seguida, apresenta o resultado;
  • linhas 22–25: implementação da interface BiConsumer<T,U> utilizando uma função lambda. O método implícito [apply] utiliza o seu segundo parâmetro para atualizar o peso da pessoa passada como primeiro parâmetro. Em seguida, apresenta o resultado;
  • linhas 27–29: a lista de pessoas é consumida utilizando a implementação [biconsumerAge];
  • linhas 31–33: a lista de pessoas é consumida utilizando a implementação [biconsumerWeight];

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). Este método é semelhante ao método [BiConsumer.apply], mas enquanto este último não devolve um resultado, o método [BiFunction.apply] devolve um resultado. Iremos ilustrar a sua utilização com o seguinte código:


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) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // anonymous implementation
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // lambda implementation
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // displays
        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<Person, Integer, Integer> é implementada utilizando uma classe anónima. O seu método [apply] devolve a idade da pessoa passada como primeiro parâmetro, aumentada pelo valor do segundo parâmetro;
  • linhas 21–23: A interface BiFunction<Person, Integer, Double> é implementada utilizando uma função lambda. O método [apply] devolve o peso da pessoa passada como primeiro parâmetro, acrescido do valor do segundo parâmetro;
  • linhas 25–27: a implementação [biFunctionAge] é aplicada às pessoas;
  • linhas 29–31: a implementação [biFunctionWeight] é aplicada à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

Para além das funções lambda, o Java 8 introduziu o tipo Stream<T>, que modela um fluxo de elementos do tipo T. Estes elementos podem ser submetidos a transformações sucessivas implementadas por funções lambda. Quando possível, e quando existem vários processadores, estas transformações podem, por vezes, ser realizadas em paralelo.

4.7. Exemplo-07 - A interface funcional Supplier<T>

  

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

Image

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

Iremos ilustrar esta interface funcional com o seguinte código:


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) {
        // anonymous implementation
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // list of persons
            List<Personne> personnes = Personnes.get();
 
            // interface implementation
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // operation
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }
 
    // person display
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • linhas 13–28: implementação de um tipo Supplier<Person>;
  • linhas 31–33: o método estático [display] espera um parâmetro do tipo Supplier<Person>;
  • linhas 25–27: utilização da instância Supplier<Person>;

São obtidos 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"}