Lambdas

Lambdas et functional interfaces

Introduites en java 8, les lambdas expressions sont des fonctions (des méthodes) anonymes qui sont traitées comme des valeurs par le langage.
La syntaxe est :
(paramètres) -> { instructions }
Par exemple
Runnable r = () -> System.out.println("Hello World");
Une lambda expression est forcémément associée à une interface qui ne contient qu'une seule méthode dont la signature (paramètres + types de retour) correspond à la lambda.
La lambda expression permet de fournir à la volée l'implémentation de cette méthode.
Dans l'exemple précédent, correspond à l'interface java.lang.Runnable.
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
L'équivalence peut être trouvée sans ambiguité grâce aux règles suivantes : Dans ce cas, une instance d'une classe implémentant l'interface attendue est créée, et le corps de la lambda est utilisé pour créer la méthode obligatoire.
La lambda expression est convertie. On utilise parfois le terme de single abstract method (or SAM) pour désigner cette méthode.

Ces interfaces peuvent être signalées par l'annotation @FunctionalInterface.

Les lambdas peuvent donc être vues comme un raccourci syntaxique.
Runnable r = () -> System.out.println("Hello World");
est remplacé par le compilateur par :
Runnable r = new Runnable() {
    public void run() {
        System.out.println("Hello World");
    }
};
Pour faire afficher Hello World, on fait donc :
Runnable r = () -> System.out.println("Hello World");
r.run();
(code dans RunDemo.java)

Règles syntaxiques des lambdas

Ce sont les mêmes règles utilisées pour les fonctions fléchées de javascript (voir developer.mozilla.org).

Exemple

public class LambdaDemo {

    public static void main(String args[]) {
    
        MathOperation addition = (a, b) -> a + b; // syntaxe la plus concise
        MathOperation soustraction = (int a, int b) -> a - b;
        MathOperation multiplication = (int a, int b) -> { return a * b; };
        MathOperation division = new MathOperation(){
            public int operation(int a, int b){
                return a / b;
            }
        };
        
        // la ligne suivante ne passe pas à la compilation
        // int test = addition(3, 4);
        
        System.out.println("3 + 4 = " + addition.operation(3, 4));
        System.out.println("3 - 4 = " + soustraction.operation(3, 4));
        System.out.println("3 * 4 = " + multiplication.operation(3, 4));
        System.out.println("3 / 4 = " + division.operation(3, 4)); // attention, division entière
    }

    // on définit l'interface fonctionnelle comme une classe interne,
    // mais on aurait pu la définir à l'extérieur
    @FunctionalInterface
    private interface MathOperation {
        int operation(int a, int b);
    }
}
(code dans LambdaDemo.java - source : tutorialspoint.com)
Exercice : Print Lambda

Retour sur FilenameFilter

On reprend l'exemple des classes anonymes en allant voir l'interface FilenameFilter :
package java.io;

@FunctionalInterface

public interface FilenameFilter{

    boolean accept(File dir, String name);

}
Avec les lambda expressions, cela peut s'exprimer par :
import java.io.File;
class Lambda3{
    public static void main(String[] args){
        File dir = new File(args[0]);
        String[] files = dir.list((f,s) -> { return s.endsWith(".java"); });
        for(String file : files){
            System.out.println(file);
        }
    }
}
Ces 2 codes sont équivalents :
String[] files =
    dir.list(
        new FilenameFilter() {
            public boolean accept(File f, String s) {
                return s.endsWith(".java");
            }
        }
    );
String[] files =
    dir.list(
        (f,s) -> {
            return s.endsWith(".java");
        }
    );

Method reference

Raccourci syntaxique lorsqu'il n'y a qu'un seul paramètre et que le corps de la lambda ne contient qu'une instruction contenant une seule méthode.

Permet d'écrire :
MyClass::toString
au lieu de :
(MyClass myObj) -> myObj.toString()

ou :
s -> System.out.println(s);
peut être exprimé par :
System.out::println

java.util.function

On a déjà vu l'utilisation de java.lang.Runnable, dont la SAM (single abstract method) est run(), une fonction prenant zéro paramètre en entrée et ne renvoyant rien.

Le package java.util.function, fournit d'autres functional interfaces prédéfinies pour des usages courants.

Par exemple, l'interface java.util.functionFunction<T,R> est une functional interface qui prend un (un seul) paramètre de type T en renvoie une valeur de type R. Sa SAM est apply().

Exemple :
import java.util.function.Function;

/** Exemple en utilisant java.util.function.Function **/
class FunctionDemo1 {
    public static void main(String args[]) {
        Function<Integer, Integer> f = x -> x + 2;
        System.out.println(f.apply(6));
    }
}                          

/** Même chose que FunctionDemo1, en définissant la functional interface de manière classique **/
class FunctionDemo2 {
    public static void main(String args[]) {
        Ajouter f = (x) -> x + 2;
        System.out.println(f.apply(6));
    }
    private interface Ajouter {
        int apply(int x);
        // En utilisant l'autoboxing, on aurait aussi pu écrire :
        // Integer apply(Integer x);
    }        
}
(code dans FunctionDemo.java)

Contexte extérieur

Variables locales

Il est possible d'utiliser des variables déclarées à l'extérieur des lambdas, sous certaines conditions.
class ExterieurDemo1 {
    public static void main(String args[]) {
        String str = "Hello World!";
        Runnable r = () -> System.out.println(str); // Utilise str, déclarée hors de la lambda
        r.run();
    }
}
(code dans ExterieurDemo1.java)

Cela n'est possible que pour des variables effectively final.
Une variable effectively final est soit une constante (déclarée final), soit une variable dont la valeur n'a pas été modifiée après avoir été déclarée et initialisée.

Exemple :
class ExterieurDemo2 {
    public static void main(String args[]) {
        String str = "Hello World!";
        str = "Salut"; // Ici on modifie str, qui n'est plus "effectively final"
        Runnable r = () -> System.out.println(str);
        r.run();
    }
}
Ne passe pas à la compilation :
javac ExterieurDemo2.java
ExterieurDemo2.java:6: error: local variables referenced from a lambda expression must be final or effectively final
        Runnable r = () -> System.out.println(str);
                                              ^
1 error
(code dans ExterieurDemo2.java)

Variables de classe et d'instance

La règle de effectively final, qui s'applique pour l'utilisation des variables locales dans une lambda, ne s'applique pas pour les variables de classe et d'instance :
class ExterieurDemo3 {
    
    static int i1;  // variable de classe
    int i2;         // variable d'instance
    
    public static void main(String args[]) {
    
        // test pour variable de classe
        Runnable r = () -> System.out.println("i1 = " + i1);
        r.run();
        
        // test pour variable d'instance
        ExterieurDemo3 test = new ExterieurDemo3();
        test.instanceMethod();
    }
    
    void instanceMethod(){
        Runnable r = () -> System.out.println("i2 = " + i2);
        r.run();
    }
}
(code dans ExterieurDemo3.java)
java ExterieurDemo3
i1 = 0
i2 = 0
Rappel : lorsqu'une variable de classe ou d'instance est déclarée, elle est initialisée avec une valeur par défaut - mais ce n'est pas le cas pour une variable locale, qui a une valeur null.

Ces variables peuvent être modifiées et utilisées dans une lambda :
class ExterieurDemo4 {
    
    static int i1;  // variable de classe
    int i2;         // variable d'instance
    
    public static void main(String args[]) {
    
        // test pour variable de classe
        Runnable r = () -> System.out.println("i1 = " + i1);
        i1 = 5;
        r.run();
        i1 = 6;
        r.run();
        
        // test pour variable d'instance
        ExterieurDemo3 test = new ExterieurDemo3();
        test.instanceMethod();
        test.i2 = 3;
        test.instanceMethod();
    }
    
    void instanceMethod(){
        Runnable r = () -> System.out.println("i2 = " + i2);
        r.run();
    }
}
(code dans ExterieurDemo4.java)

Généralités

Une motivation importante ayant conduit à l'introduction des lambdas en java est leur utilité dans les collections.

Java reste un langage objet mais est décrit par Oracle comme "légèrement fonctionnel" depuis l'introduction des lambdas.
Il n'existe pas de définition précise de ce qu'est un langage fonctionnel, mais doit au moins permettre de représenter une fonction comme une valeur qu'on peut mettre dans une variable.
Bien réaliser que ce n'est pas anodin et correspond à un mouvement qui touche un grand nombre de langages procéduraux (par ex php 5.3, C++ 11, go, javascript).

Noter aussi que dans un "vrai" un langage fonctionnel, le type fonction ~existe. Mais ce n'est pas le cas en java, une lambda expression étant finalement un raccourci syntaxique.