• 12 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 13/11/2023

Tirez parti des lambdas

Avec l’apparition de Java 8, vous avez peut-être déjà utilisé les "lambdas". En Kotlin, celles-ci sont aussi présentes et vous permettront de rendre votre code encore plus lisible et surtout concis.

Dansons la lambada

En tant que développeur Java, il est fort probable que vous ayez déjà utilisé la notion de classe anonyme ! Prenons l’exemple suivant en Java :

button.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onCLick(View v){
        System.out.println("User has clicked on button!")
    }
});

Dans cet exemple, nous avons créé une classe anonyme implémentant l'unique méthode  onClick  de l’interface View.OnClickListener  . Comme vous le remarquez, une classe anonyme est une classe qui n’a pas de nom ! (D’où son nom… :p.)

Nous utilisons généralement ce genre de classe lorsque nous devons passer en paramètre un objet qui ne sera utilisé nulle part ailleurs.

Grâce aux lambdas, nous allons pouvoir simplifier ce code de cette manière :

button.setOnClickListener ( { v: View -> println("User has clicked on button!") } ) 

... simplifions encore...

button.setOnClickListener { v: View -> println("User has clicked on button!") }

... simplifions encore...

button.setOnClickListener { v -> println("User has clicked on button!") }

... simplifions encore...

button.setOnClickListener { println("User has clicked on button!") }

Plutôt impressionnant, n’est-ce pas ? Ce sera l’une des utilisations les plus courantes des lambdas dans votre code. :)

Whoua ! Mais alors, c’est quoi les lambdas ? :euh:

Une lambda (parfois appelée "expression lambda") est une sorte de "mini-fonction" très concise, que l’on pourra directement utiliser en tant que valeur : en d’autres mots, une lambda est l’équivalent d’une fonction anonyme (une fonction qui n’a pas de nom) sans le mot-clé  fun .

// Lambda
{ println("Hello, everyone!") }

// Fonction anonyme
fun(){ println("Hello, everyone!") }

En Kotlin, une lambda sera traitée par le compilateur de la même façon qu’une fonction anonyme : ces deux syntaxes sont donc équivalentes. ;) Expliquons tout de même un peu plus la syntaxe d’une lambda :

Syntaxe d'une lambda en Kotlin : les paramètres et le corps sont placés entre accolades
Syntaxe d'une lambda en Kotlin : les paramètres et le corps sont placés entre accolades

Essayez vraiment de vous représenter une lambda comme étant une mini-fonction : cela vous aidera beaucoup à comprendre son fonctionnement. :) D’ailleurs, comme une fonction normale, une expression lambda disposera de paramètre(s) et d’un corps, le tout placé entre accolades.

De plus, une lambda est une "expression" et pourra ainsi retourner une valeur que nous pourrons stocker dans une variable :

val addition = { x: Int, y: Int -> x + y }
println(addition(1, 1))
Une lambda retourne une valeur
Une lambda retourne une valeur

Nous stockons ici notre expression lambda dans la variable  addition  que nous utiliserons ensuite de la même manière que nous pouvons le faire avec une fonction normale. ;)

D’accord ! Mais comment ce genre de syntaxe "magique" peut fonctionner en Java ? Surtout sur les versions inférieures à la version 8 (où les lambdas n’existaient pas !)...

Très bonne question ! En fait, lorsque vous créez une lambda en Kotlin, le compilateur, lui, va générer automatiquement les classes suivantes pour que la JVM puisse l’interpréter correctement, qu’importe sa version :

Classes Java générées pour une Lambda
Classes Java générées pour une lambda
Référence : Kaamelott
Référence : Kaamelott

N’ayez pas peur ! :D Comme on peut le voir ici, les lambdas seront en réalité traduites en des petites classes implémentant l’interface Function , et, dans notre exemple plus particulièrement, l’interface Function2  . Et si vous remarquez bien, les paramètres et le corps de la méthode  invoke()  correspondent respectivement aux paramètres et au corps de notre lambda. :D

Ainsi, notre précédente valeur  addition  contenant une lambda pourra être appelée en Kotlin ET en Java sans problème !

val result = Operator().addition(10, 2)
println(result)
int result = new Operator().getAddition().invoke(10, 2);
System.out.println(result);

Ils sont tout de même très intelligents chez JetBrains, n’est-ce pas ? :)

Manipulons les lambdas !

Il est probable que vous soyez amené un jour à utiliser les lambdas comme paramètres d’une méthode ! Imaginons par exemple que vous souhaitiez créer une super calculatrice uniquement grâce aux lambdas... :D Nous allons commencer par créer des variables contenant différentes opérations comme une addition, une soustraction et même une division !

val addition = { x: Int, y: Int -> x + y }
val substraction = { x: Int, y: Int -> x - y }
val division = { x: Int, y: Int -> x / y }

Bon jusque-là, cela devrait normalement aller. :) Maintenant, nous allons créer une fonction pouvant contenir en paramètre une autre fonction : on appelle ce genre de fonction les "fonctions d’ordre supérieur" (ou "Higher-Order Functions", en anglais).

Fonction avec Lambda en paramètre
Fonction d'ordre supérieur (avec lambda en paramètre)

Nous avons créé ici une fonction appelée  executeOperation  prenant trois paramètres en entrée : deux variables de type  Integer  et une fonction. Cette dernière sera exécutée dans le corps de la méthode.

Ainsi, grâce à cette fonction  executeOperation , nous allons pouvoir passer en paramètre une fonction pour obtenir un résultat de ce type :

executeOperation(10, 5, addition) // Result = 15
executeOperation(5, 2, substraction) // Result = 3
executeOperation(10, 2, division) // Result = 5

Génial, n’est-ce pas ? Le fait de pouvoir passer une fonction en paramètre d’une autre nous ouvre les portes d’un monde que nous ne connaissions pas vraiment : celui de la programmation fonctionnelle ! :)

D’ailleurs, nous passons actuellement une fonction (sous sa forme lambda) stockée dans une variable, mais sachez que vous pouvez également passer directement une lambda à la place :

// Multiplication
print(executeOperation(10, 5, {x, y -> x * y} )) //Result = 50

Tout cela est super ! Mais que veut dire le mot-clé  inline  de notre fonction  executeOperation  ?

Ah oui, pardon, j’avais oublié ! Quand nous créons une "fonction d’ordre supérieur" en Kotlin, le compilateur Kotlin va créer, comme nous l’avons vu plus haut, une ou plusieurs classes  Function  représentant notre ou nos lambdas. Cette instanciation a forcément un impact sur la mémoire car, pour chaque lambda créée, un objet de type  Function  le sera également…

Le mot-clé  inline  va permettre d’indiquer à Kotlin de ne pas créer d’objet  Function  représentant la lambda, mais plutôt de déplacer le code de celle-ci directement là où elle sera appelée :

Explication du mot-clé
Explication du mot-clé "inline"

Ainsi, de manière générale, si vous décidez de créer une "fonction d’ordre supérieur" dans votre programme, pensez toujours à rajouter le mot-clé  inline  pour optimiser la mémoire de votre programme.

Last night a lambda saved my life...

Bon, peut-être que vous trouvez les lambdas un peu compliquées ! Cela est tout à fait normal, et ces dernières vous demanderont au début une gymnastique intellectuelle assez intense.

Cependant, les lambdas vous permettront d’être vraiment plus productif dans votre manière de programmer. D’ailleurs, Kotlin vous offre nativement quelques méthodes et extensions très pratiques utilisant les lambdas.

Grouper des actions

Parfois, nous réalisons certaines actions les unes à la suite des autres sur un même objet, notamment dans un but de "construction" et de "configuration". En Java, nous étions habitués à utiliser le pattern "Builder".

Eh bien, en Kotlin, nous pourrons utiliser la fonction d’ordre supérieur apply()  afin de grouper des actions sur un même objet :

TextView textView = new TextView(this);
textView.setVisibility(View.VISIBLE);
textView.setText("Hello, world!");
textView.setTextSize(19f);
textView.setMaxLines(3);
val textView = TextView(this)
textView.apply {
    visibility = View.VISIBLE
    text = "Hello, world!"
    textSize = 19f
    maxLines = 3
}

Ne soyons pas nuls !

Afin de gérer un peu plus proprement le fait qu’une variable pourra possiblement contenir une valeur nulle, nous pouvons, comme nous l’avons vu dans un précédent chapitre, utiliser une condition  if  .

val message: String? = "Hello, OpenClassrooms students!"
if (message != null) println(message)

Cependant, grâce à la fonction d’ordre supérieur let , vous pourrez réaliser l’équivalent de manière beaucoup plus lisible

val message: String? = "Hello, OpenClassrooms students!"
message?.let { print(it) }

let()  exécute le corps de la lambda SI la variable précédente est différente de nulle. 
it  correspond à l'unique paramètre de la lambda, contenant la valeur de la variable "message".

Le mot-clé it  à l’intérieur d’une lambda fait toujours référence à l’unique paramètre de celle-ci. Vous l’utiliserez très souvent pour gagner en lisibilité !

Les feignants…

Peut-être avez-vous déjà été confronté au cas où le contenu d’une variable prend un espace assez important en mémoire, variable qui ne sera peut-être pas utilisée selon les cas d’utilisation de votre programme (par exemple, celle-ci pourrait être utilisée uniquement après que l’utilisateur a réalisé une certaine action). Quel gâchis de ressources !

Eh bien, en Kotlin, grâce à la fonction d’ordre supérieur lazy , vous allez pouvoir repousser le chargement en mémoire d’une variable jusqu’au moment où vous l’utiliserez réellement. En somme, tant que vous n’accédez pas à la variable, celle-ci n’utilisera aucune ressource de votre programme :

val veryBigArray: Array<Int> by lazy {
    Array(1000000){ it }
}

Vous remarquez également que nous avons créé un Array d’un million d’éléments grâce à une lambda… ;) Pratique, n’est-ce pas ?

Une histoire de collections…

Comme vous commencez à le voir, les lambdas nous facilitent grandement la vie, tout en rendant notre code plus concis et lisible. D’ailleurs, celles-ci sont extrêmement utilisées sur des collections, afin d’éviter d’avoir à réaliser des actions répétitives et surtout pas très lisibles…

Voici un extrait des principales lambdas que vous serez susceptible d’utiliser à travers des collections. Accrochez-vous, ça va boucler !

data class User(var email: String, var age: Int)

val users = listOf(
    User("toto@gmail.com", 20),
    User("hello@gmail.com", 18),
    User("oc@gmail.com", 35))
    
// Get the older user
users.maxBy { it.age }          // User("oc@gmail.com", 35)

// Get the younger user
users.minBy { User::age }      // User("hello@gmail.com", 18)

it.age  => Filtre en utilisant la valeur du paramètre.

User::age  => Filtre en utilisant la référence du membre.

Dans l'exemple ci-dessous, nous récupérons la plus petite ( minBy  ) et la plus grande valeurs (maxBy  ) de la liste d’utilisateurs en nous basant sur la propriété  age  .

data class User(var email: String, var age: Int)

val users = listOf(
    User("toto@gmail.com", 20),
    User("hello@gmail.com", 18),
    User("oc@gmail.com", 35))
    
// Get user(s) with age >= 20
users.filter { it.age >= 20 }  // [User("toto@gmail.com", 20), User("oc@gmail.com", 35)]

// Get all users' email addresses
users.map { it.email }      // [toto@gmail.com, hello@gmail.com, oc@gmail.com]

// Get email addresses of all users >= 20
print(users.filter{ it.age >= 20 }.map { it.email })    // [toto@gmail.com, oc@gmail.com]

filter  => Itère sur chaque valeur de la collection et retourne la valeur si celle-ci respecte la condition.

map  => Itère sur chaque valeur de la collection et applique une fonction à chacune des valeurs (ici, la fonction est un "get" sur la propriété "email").

Les fonctions filter  et map  sont aussi très utilisées, et vous permettront de réaliser des actions spécifiques sur chacune des valeurs d’une collection :

data class User(var email: String, var age: Int)

val users = listOf(
    User("toto@gmail.com", 20),
    User("hello@gmail.com", 18),
    User("oc@gmail.com", 35))
    
// Are all users >= 20 ?
users.all { it.age >= 20 }    // false

// Is any user >= 35 ?
users.any { it.age > 35 }      // true

// How many users are >= 20 ?
users.count { it.age > 2O }      // 2

// Find the first user who is >= 20
users.find{ it.age >= 20 }    // User(email="toto@gmail.com", age=20)

all  => Vérifie si chaque élément de la collection respecte la condition.

any  => Vérifie si  au moins UN élément de la collection respecte la condition.

count  => Retourne le nombre d'éléments de la collection qui respectent la condition.

find  => Retourne le premier élément de la collection respectant la condition.

Terminons sur ces fonctions très pratiques que sont  all  ,  any  ,  count  et  find . Pas besoin de les expliquer en détail, leur fonctionnement se trouve être assez explicite. :)

Practice Makes Perfect!

La théorie, c'est bien, mais la pratique, c'est encore mieux ! Justement, nous vous avons concocté un petit exercice interactif de fin de chapitre pour appliquer toutes ces nouvelles connaissances.

En résumé

  • Une lambda (parfois appelée "expression lambda") est une sorte de "mini-fonction" très concise, que l’on pourra directement utiliser en tant que valeur.

  • Une lambda est une "expression", et pourra ainsi retourner une valeur que nous pourrons stocker dans une variable, par exemple.

  • Une fonction prenant en paramètre une ou plusieurs autres fonctions (qui peuvent être des lambdas) est appelée une fonction d’ordre supérieur (Higher-Order Function).

  • Le mot-clé  it  à l’intérieur d’une lambda fait toujours référence à l’unique paramètre de celle-ci. Vous l’utiliserez très souvent pour gagner en lisibilité.

Pour aller plus loin

Crédits

Rendons à César ce qui est à César ! ^^

  • Certaines icônes utilisées dans les chapitres de ce cours sont sous license CC 3.0 (auteur : Twitter – distribution : Flaticon) et d’autres sous licence Apache 2 (auteur : Google – distribution : material.io).

Exemple de certificat de réussite
Exemple de certificat de réussite