Skip to content

4. Lambda-functies in Java 8

4.1. Voorbeeld-01 - Functionele interfaces en lambda's

  

Laten we eens kijken naar de volgende code:


package dvp.java8.lambdas;

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

    // anonieme klassen
    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);
}
  • regels 41-44: definiëren een functionele interface I1. Een functionele interface is een interface met slechts één methode. Deze is niet gekoppeld aan de aanwezigheid van de annotatie [@FunctionalInterface] op regel 41, die optioneel is;
  • regels 46-49: een tweede functionele interface I2;
  • regels 6-19: de interfaces I1 en I2 worden geïmplementeerd met anonieme klassen, de meest gangbare oplossing vóór de introductie van lambda-functies;
  • regels 21-29: de interfaces I1 en I2 zijn geïmplementeerd met lambda-functies;
  • regels 7-12: implementatie van de interface I1 met een anonieme klasse. De syntaxis voor het implementeren van een interface I met een anonieme klasse is als volgt:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

waarbij m1, m2, ... de methoden zijn die door de interface I worden gedefinieerd.

  • regels 14-19: implementatie van de interface I2 met een anonieme klasse;
  • regel 22: implementatie van de interface I1 met een lambda-functie. Hier wordt gebruikgemaakt van het feit dat de functionele interface slechts één methode heeft. De lambda-functie implementeert dan deze enige methode M. De syntaxis is als volgt:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

De typen T1, T2 en Tn kunnen worden weggelaten als de compiler ze uit de context kan afleiden (type-inferentie).

  • regel 22: implementatie van de methode [I1.doSomething] met de volgende signatuur:

void doSomething();

[doSomething] is een methode zonder parameters die geen resultaat retourneert. De lambda-implementatie ervan kan worden geschreven zoals in regel 22 of zoals in de regels 24-26, d.w.z. dat er accolades rond de code van de lambda-functie kunnen worden geplaatst. Als deze code slechts één instructie bevat, zoals hier, kunnen deze accolades worden weggelaten;

  • regel 23: implementatie van de methode [I1.getSomething] met de volgende signatuur:

String getSomething(double value);

[getSomething] accepteert een parameter van het type [double] en retourneert een resultaat van het type [String]. De lambda-implementatie ervan kan die van regel 23 zijn of die van de regels 27-29. In de implementatie van regel 23:

  • wordt het type van de parameter [value] weggelaten. Er wordt dan het type [double] gebruikt dat in de handtekening van [getSomething] wordt gevonden;
  • de lambda-code staat niet tussen haakjes. Het resultaat van de lambda is dan de waarde van de enige uitdrukking in deze code, in dit geval: String.format("ib2.lambda(%s)", value);

In de implementatie op de regels 27-29:

  • wordt het type van de parameter [value] expliciet gedeclareerd;
  • Er wordt een [return] gebruikt om het resultaat van de lambda weer te geven. In dit geval moeten er accolades worden geplaatst;
  • regels 32-37: de verschillende anonieme functies en lambda's worden aangeroepen;

Het verkregen resultaat is als volgt:

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. Voorbeeld-02 - de functionele interface Predicate<T>

  

Meestal hebben we te maken met functionele interfaces uit bibliotheken in plaats van met functionele interfaces die we zelf definiëren. Hier richten we ons op de functionele interface [Predicate], gedefinieerd in het pakket [java.util.function], dat de meeste functionele interfaces van Java 8 bevat. Deze is als volgt gedefinieerd:

Image

We hebben gezegd dat een functionele interface slechts één methode heeft. Hier zijn er echter vier. Een andere innovatie van Java 8 was de introductie van het begrip ‘standaardmethode’ van een interface, gekenmerkt door het sleutelwoord [default]. We hebben hier drie van dergelijke methoden. Deze methoden hebben als bijzonderheid dat ze een standaardimplementatie hebben. Een klasse die een interface met standaardmethoden implementeert, is dus niet verplicht om deze methoden te implementeren. Zo hoeft een klasse die de interface [Predicate] wil implementeren, slechts één methode verplicht te implementeren: de methode [test]. De interface [Predicate] is dus inderdaad een functionele interface. We kunnen dus stellen dat een functionele interface een interface is waarvan de implementatie slechts één verplichte methode bevat. Als deze meer dan één methode heeft, moeten de andere methoden het sleutelwoord [default] hebben.

De methode [Predicate<T>.test] ontvangt een parameter van het type T en retourneert een booleaanse waarde. Deze interface wordt doorgaans gebruikt om verzamelingen te filteren. Om het gebruik ervan te illustreren, gebruiken we de volgende gegevens:


package dvp.data;

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

public class Personne {

  public enum Sexe {HOMME,FEMME};

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

  // constructors
  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 en setters
...

  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • regels 32-38: de methode [toString] retourneert de tekenreeks jSON van de persoon;

De klasse [Personnes] definieert een lijst van 3 personen:


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();
    }
  }
}
  • regels 10-11: de lijst met 3 personen;
  • regels 13-15: een statische methode om deze lijst te verkrijgen;
  • regels 17-23: een statische methode waarmee de tekenreeks jSON kan worden verkregen uit een lijst met personen die als parameter wordt doorgegeven;

Deze gegevens worden verwerkt door de volgende code:


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) {
    // predikaat geïmplementeerd door anonieme klasse
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predikaat geïmplementeerd door een lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // lijst met personen
    List<Personne> personnes = Personnes.get();
    // weergaven
    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] filtert de lijst [personnes]
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • regels 13-18: implementatie van de interface Predicate<Person> door middel van een anonieme klasse. Dit is een filter op het gewicht van de persoon;
  • regel 20: implementatie van de interface Predicate<Person> door middel van een lambda-functie. Dit is een filter op de leeftijd van de persoon. Zoals eerder vermeld, had deze ook als volgt geschreven kunnen worden:

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

maar de versie op regel 20 is beknopter. Het type van de parameter p wordt afgeleid uit de context. Hier wordt een type [Predicate<Personne>] geconstrueerd. De geïmplementeerde methode heeft dan de signatuur [boolean test(Personne param)]. Het impliciete type van p, regel 20, is dus het type [Personne];

  • regel 22: er wordt een vooraf gedefinieerde lijst met personen opgehaald;
  • regel 24: deze worden gefilterd op basis van hun leeftijd;
  • regel 25: we filteren ze op basis van hun gewicht. In beide gevallen geven we de tekenreeks jSON uit de aldus gefilterde lijst weer;
  • regels 28-37: een statische methode die
    • als parameters een lijst met te filteren personen en het filter accepteert. Dit filter is een instantie van de interface [Predicate<Personne>]. In de volksmond wordt hier en elders in het document met een instantie van een interface I een instantie van een klasse C bedoeld die I implementeert;
    • levert als resultaat de aldus gefilterde lijst op;
  • regel 32: we gebruiken de methode [test] van de interface [Predicate]. Afhankelijk van het filter dat aan de methode wordt doorgegeven, zal de methode [test]:

return personne.getPoids()<50;

of


return p.getAge() < 28

De uitvoering van de klasse [Exemple02] levert het volgende resultaat op:

[{"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. Voorbeeld-03 - de functionele interface Function<T,R>

  

De functionele interface Function<T,R> is als volgt gedefinieerd:

Image

De enige methode van de interface heeft de signatuur R apply(T t). Deze wordt doorgaans gebruikt om op basis van een type Collection<T> een nieuw type Collection<R> te creëren. Om deze interface te illustreren, gebruiken we de volgende code:


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 {
    // implementatie met anonieme klasse
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementatie met lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // lijst met personen
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // weergaven
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }

  // transformatie 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;
  }
}
  • regels 15-20: we implementeren de interface [Function<Personne, String>] met een anonieme klasse die de transformatie Personne --> String uitvoert;
  • regel 22: we implementeren de interface [Function<Personne, Integer>] met een lambda-functie die de transformatie Personne --> Integer uitvoert;
  • regels 33-39: een statische methode die
    • twee parameters accepteert: de eerste van het type List<Personne> is een lijst met personen die moeten worden omgezet. De tweede van het type Function<Personne, T> is een functie die op basis van elke persoon in de lijst een object van het type T aanmaakt;
    • en als resultaat een type List<T> oplevert, waarbij elk element afkomstig is van de transformatie Persoon -> T;
  • regels 35-37: de transformatie Personne -> T wordt toegepast. Als de tweede parameter van de methode het object [mapToName] is, wordt een transformatie Personne -> String uitgevoerd. Als het om het object [mapToAge] gaat, wordt een transformatie van Personne naar Integer uitgevoerd;

Het verkregen resultaat is als volgt:

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

4.4. Voorbeeld-04 - de functionele interface Consumer<T>

  

De functionele interface Consumer<T> wordt als volgt gedefinieerd:

Image

De enige methode van de interface heeft de volgende signatuur: void accept(T t). Deze methode verwerkt (verbruikt) haar parameter en levert geen resultaat op. Ter illustratie gebruiken we de volgende code:


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) {
        // lijst met personen
        List<Personne> personnes = Personnes.get();
        // anonieme implementatie
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // lambda-implementatie
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // weergaven
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • regels 14-19: de interface [Consumer<Personne>] wordt geïmplementeerd door een anonieme klasse waarvan de methode [accept] de naam en leeftijd van de persoon weergeeft;
  • regel 21: de interface [Consumer<Personne>] wordt geïmplementeerd door een lambda-functie waarvan de impliciete methode [accept] de naam en het gewicht van de persoon weergeeft;
  • regels 23-25: de lijst met personen wordt gebruikt door de implementatie [consumerAge];
  • regels 27-29: de lijst met personen wordt verwerkt door de implementatie [consumerPoids];

De resultaten zijn als volgt:

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. Voorbeeld-05 - de functionele interface BiConsumer<T,U>

  

De functionele interface BiConsumer<T,U> is als volgt gedefinieerd:

Image

De enige methode van de interface heeft de volgende signatuur: void accept(T t, U u). Deze methode gebruikt het type T met de aanvullende informatie U u. We illustreren het gebruik ervan met de volgende code:


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) {
    // lijst met personen
    List<Personne> personnes = Personnes.get();
    // anonieme implementatie
    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-implementatie
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // weergaven
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • regels 14-20: implementatie van de interface BiConsumer<T,U> met een anonieme klasse. De methode [apply] gebruikt de tweede parameter om de leeftijd bij te werken van de persoon die als eerste parameter wordt doorgegeven. Vervolgens wordt het resultaat weergegeven;
  • regels 22-25: implementatie van de interface BiConsumer<T,U> met een lambda-functie. De impliciete methode [apply] gebruikt de tweede parameter om het gewicht bij te werken van de persoon die als eerste parameter wordt doorgegeven. Vervolgens wordt het resultaat weergegeven;
  • regels 27-29: de lijst met personen wordt verwerkt met de implementatie [biconsumerAge];
  • regels 31-33: de lijst met personen wordt verwerkt met de implementatie [biconsumerPoids];

De verkregen resultaten zijn als volgt:

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. Voorbeeld-06 – de functionele interface BiFunction<T,U,R>

  

De functionele interface BiFunction<T,U,R> is als volgt gedefinieerd:

Image

De enige methode van de interface heeft de volgende signatuur: R apply(T t, U u). Deze methode lijkt op de methode [BiConsumer.apply], maar terwijl deze laatste geen resultaat retourneert, retourneert de methode [BiFunction.apply] wel een resultaat. We illustreren het gebruik ervan met de volgende code:


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) {
        // lijst met personen
        List<Personne> personnes = Personnes.get();
        // anonieme implementatie
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // lambda-implementatie
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // weergaven
        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));
        }
    }
}
  • regels 14-19: de interface BiFunction<Personne, Integer, Integer> wordt geïmplementeerd met een anonieme klasse. De methode [apply] van deze klasse retourneert de leeftijd van de persoon die als eerste parameter wordt doorgegeven, vermeerderd met de waarde van de tweede parameter;
  • regels 21-23: de interface BiFunction<Persoon, Integer, Double> wordt geïmplementeerd met een lambda-functie. De methode [apply] retourneert het gewicht van de persoon die als eerste parameter wordt doorgegeven, vermeerderd met de waarde van de tweede parameter;
  • regels 25-27: de implementatie [biFunctionAge] wordt toegepast op de personen;
  • regels 29-31: de implementatie [biFunctionPoids] wordt toegepast op de personen;

De verkregen resultaten zijn als volgt:

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

Naast lambda-functies heeft Java 8 het type Stream<T> geïntroduceerd, dat een stroom van elementen van het type T modelleert. Deze elementen kunnen opeenvolgende transformaties ondergaan die door lambda-functies worden geïmplementeerd. Wanneer dit mogelijk is en er meerdere processors beschikbaar zijn, kunnen deze transformaties soms parallel worden uitgevoerd.

4.7. Voorbeeld-07 - de functionele interface Supplier<T>

  

De functionele interface Supplier<T> wordt als volgt gedefinieerd:

Image

De enige methode van de interface heeft de signatuur: T get(), die tot taak heeft een object van het type T te leveren.

We zullen deze functionele interface illustreren met de volgende code:


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) {
        // anonieme implementatie
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // lijst met personen
            List<Personne> personnes = Personnes.get();

            // interface-implementatie
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // verwerking
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }

    // weergave persoon
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • regels 13-28: implementatie van een type Supplier<Personne>;
  • regels 31-33: de statische methode [affiche] verwacht een parameter van het type Supplier<Personne>;
  • regels 25-27: uitvoering van de instantie Supplier<Personne>;

Dit levert de volgende resultaten op:

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"}