I. Presentation et installation▲
I-A. Presentation▲
Le langage Kotlin est un langage développé par JetBrains, entreprise conceptrice de l'EDIEnvironement de Developement Integre Intellij Idea entre autres. Cependant il est open-source.
Ses principaux avantages, pour reprendre les principaux arguments du site officiel, résident dans la simplification du code pour de nombreuses taches courantes (par exemple, la création de POJOPlain Old Java Object), une sécurité de code accrue grâce a un contrôle plus stricte que Java (notamment pour la nullité des valeurs), et une facilite d'interaction vers et a partir du langage Java et avec la JVMJava Virtual Machine - ce qui permet notamment la réutilisation directe de la plupart des codes écrits en Java,
Le langage Kotlin est un langage qui a plusieurs déclinaisons :
- une version visant une interaction avec le langage Java, et qui est compile en bytecode afin d’être exécuté sur la JVMJava Virtual Machine ou sur les périphériques Android,
- une version visant a être compilée pour le langage Javascript,
- une version dite native, visant a être compilée directement en exécutable indépendant de toute plateforme interprétation.
Ce tutoriel a pour objectif l'enseignement de la version pour la JVMJava Virtual Machine, même s'il ne devrait pas être ardu de passer a l'une des deux autres versions.
Il vise principalement les développeurs étant familiers avec le langage Java et la POOProgrammation Orientee Objet : même si tout langage impératif et oriente objet devrait convenir.
I-B. Installation▲
Nous allons ici aborder l'environnement de développement.
Sachez également qu'il est possible de tester les codes directement en ligneEssai de Kotlin en ligne (y compris pour les projets incluant plusieurs fichiers).
Ce tutoriel se basera sur l’éditeur Intellij IdeaEditeur Intellij Idea (la version Community est gratuite), mais est adaptable a tout éditeur. Sachez aussi que pour mieux gérer les différentes versions et mises a jour d'Intellij Idea, vous disposez aussi du programme gratuit JetBrain Toolboxl'outil JetBrain Toolbox.
Par ailleurs, ce tutoriel est base sur la version 1.3.70 de Kotlin et la version d'Intellij Idea Community 2019.2.
Afin d'installer l'environnement de développement, nous pouvons procéder comme suit :
- Nous utiliserons Kotlin a travers la JVM : nous avons donc besoin d'installer Java correspondant a notre Systeme d'Exploitation, si ce n'est déjà fait : Page de telechargement du SDK Java 8Page de telechargement de Java 8 (la version 8 de Java devrait être amplement suffisante),
- Nous pouvons installer la version d'Intellij Idea Community correspondant a notre système (ou passer par la JetBrains Toolbox afin de mieux gérer les mises a jour),
- Afin de bénéficier de la coloration syntaxique et de la vérification syntaxique du code par l'EDI, nous pouvons aussi installer le plugin Kotlin. Pour cela cliquez d'abord sur le bouton Configure dans l'accueil (fig 1.1), et choisissez le menu Plugins. Il se peut que vous ne soyez pas dans la fenêtre d'accueil, mais qu'un projet soit déjà chargé : dans ce cas il suffit simplement de refermer le projet (menu File→Close project) pour revenir a l’accueil. Activez l'onglet « Marketplace », cherchez le plugin Kotlin et installez-le (fig 1.2) (dans mon cas, le plugin a déjà été installe), puis cliquez sur Restart Intellij IDEA (puis validez avec Ok puis Restart) une fois que cela est fait afin de prendre en compte les mises a jour.
I-C. Première prise en main▲
Nous allons créer un simple programme affichant « Bonjour, les développeurs ! » depuis Intellij Idea en Kotlin, sans rien installer de plus : grâce au logiciel Gradle intégré dans notre EDI.
I-C-1. Création du projet▲
- Démarrez un nouveau projet de Kotlin/JVM et passez a l’étape suivante (fig 1.3),
- Nommez le projet comme bon vous semble, pour ma part j'ai choisit « BonjourLesDevs ». Par contre, assurez-vous que le JDK est configure. (Sinon le bouton « New… » vous permet de le définir) (fig 1.4). Cela étant assure, vous pouvez valider la création du projet (bouton « Finish »).
I-C-2. Ajout du fichier source et exécution▲
- Une fois le projet généré et ouvert par l'IDE, accédez au menu contextuel du dossier src juste crée et sélectionnez le menu New → Kotlin File/Class… choisissez le type File, et entrez le nom du fichier Hello (sans extension) (fig 1.5),
- Entrez le code suivant dans le fichier Hello.kt :
Vous pouvez alors exécuter le programme en cliquant sur la flèche verte a cote de la fonction main (choisissez l'option Run), ou également sur la flèche verte dans la zone d’exécution, où est d'ailleurs affiche le message de notre programme (fig 1.6).
Voici quelques remarques :
- Il n'y a pas obligation de terminer une instruction par un point-virgule (contrairement au Java),
- La notion de package est beaucoup plus souple qu'en Java : pas d'obligation d'utiliser un package correspondant au dossier où réside le fichier : dans ce cas-la, le package sera com.developpez, bien que le fichier soit situe a la racine des fichiers sources (src/main/java).
- C'est le mot-clé fun qui permet de déclarer une fonction,
- La fonction main doit être déclarée comme acceptant un tableau de String et sans valeur de retour,
- Remarquez également qu'en Kotlin un tableau est une classe générique (Array),
- également une déclaration se constitue d'abord du nom de la variable et ensuite du type,
- La fonction println, disponible immédiatement et sans aucun import, permet d'afficher du texte a l’écran et sans aucun retour a la ligne. C'est l’équivalent de l'appel fastidieux System.out.
println
de Java, - Enfin, ce code n'a pas déclaré de classe : contrairement au Java où toute fonction doit être membre d'une classe. Il n'y aucun package déclaré non plus. Elle sera donc déclarée dans une classe nommée HelloKt, qui représente l'ensemble des déclarations au niveau du fichier Hello.kt. Pour vous en convaincre, vous pouvez regarder le contenu du dossier out, a la racine du projet.
Mais il nous reste cependant une notion essentielle de configuration a voir : comment réaliser un Jar autoexécutable ?
I-D. Création d'un Jar autoexécutable▲
Il faut d'abord créer la configuration nous permettant une telle compilation :
- Accédez d'abord au menu File→Project Structure…,
- Sélectionnez la section Artifacts, dans la zone gauche de la nouvelle fenêtre de configuration,
- Effectuez alors une command Add→Jar→From module with dependencies… (Fig 1.7),
- Dans la nouvelle fenêtre, sélectionnez la classe principale, c'est-a-dire dans notre situation la seule classe disposant d'une fonction main, et la seule classe du projet tout court. L'option « Extract to the target JAR » est correcte, veuillez la sélectionner si ce n'est pas le cas, de même l'option par défaut pour le chemin du Manifest est aussi correct (Fig 1.8),
- Vous pouvez alors nommer le Jar et valider la configuration.
Ensuite, il nous faut réaliser la compilation proprement dite :
- Exécutez le menu Buid→Build artifacts…,
- Dans le menu qui apparaît, choisissez le menu BonjourLesDevs:jar→Build (il y a aussi des options pour effacer le jar généré entre autres),
- Après avoir rafraîchit la liste des fichiers (menu contextuel Syncronize sur le nœud racine), vous pouvez trouver le jar autoexécutable dans le dossier out/artifacts/BonjourLesDevs_jar.
Il est possible de s'assurer que le jar produit fonctionne bien en accédant au terminal intégré (qui sera alors directement positionne sur le dossier racine du projet) (fig 1.9) et en y exécutant les commandes suivantes, dans l'ordre :
$
>
cd out/artifacts/BonjourLesDevs_jar
$
>
java -jar BonjourLesDevs.jar
Remarquez que les commandes exécutées dans le terminal intégré sont similaires aux commandes d'un terminal BASHBourne Against Shell y compris dans le format de dossiers. Une fois dans le dossier approprie (C'est la commande cd qui permet de le faire en environnement Linux), la commande effectuée pour lancer l'application est la commande standard java -
jar #NomJar.jar.
Vous disposez donc d'un fichier Jar autonome.
II. Bases du langage▲
Dans ce chapitre, nous allons voir les bases de la syntaxe.
II-A. Les commentaires▲
Les commentaires fonctionnent comme en Java
// Commentaire en une ligne
/*
Commentaire sur plusieurs lignes
*/
II-B. Déclarations de variables▲
En Kotlin, on distingue les variables dont on peut changer la valeur, et les variables immuables qui ne sont pas tout a fait des constantes :
- le mot-clé val permet de déclarer une variable immuable,
- le mots-clés var permet de déclarer une variable altérable.
val
nom:String
= "Toto"
// nom = "Dodo" // Interdit!!! Car nom a ete declare avec val
var
age = 10
age += 12
// aucun probleme car age est alterable.
Pour rappel, il faut préciser le type de variable après le nom, et pas avant comme en Java.
Évidemment, une variable immuable doit être initialisée lors de sa déclaration.
Cela fonctionne aussi bien pour les types « primitifs » (tout est objet en Kotlin) que pour les types personnels.
class
Personne(val
nom: String
, var
age: Int
= 10
)
val
jean:Personne = Personne("Jean"
, 25
)
Même si les classes en Kotlin seront expliquées ultérieurement, sachez qu'ici la classe Personne dispose d'une propriété nom en lecture-seule, et d'une propriété age en lecture-écriture, dont la valeur par défaut est 10. (Notez bien cependant qu'ici la classe Personne n'a pas défini de méthode modificatrice, mais si c’était le cas, il aurait aussi été probable que la variable jean puisse voir son état modifie bien qu'ayant été déclarée comme val ! Ici le mot-clé val empêche juste de changer l'objet pointe par jean.). Aussi, on n’utilise pas de mot-clé new, contrairement au Java.
II-C. Types de base▲
Les types numériques Byte, Short, Int, Long, Float, et Double sont équivalents aux types existants en Java : si ce n'est que, pour rappel, en Kotlin, tout est objet. De surcroît Kotlin dispose d'un mécanisme, dont nous reparlerons ultérieurement, permettant « d'ajouter » des méthodes aux classes existantes (donc y compris nos classes personnelles et les classes qui nous sont proposées par les librairies). Ainsi on peut écrire :
2.
toString()
10.
downTo(0
) // genere le range (c'est a dire l'intervalle) decroissant de 10 a 0.
Le type Boolean fonctionne aussi comme en Java :
val
moinsDe18 = age < 18
val
estLucas = nom == "Lucas"
val
trouve = moinsDe18 && estLucas
Le type String dispose des mêmes méthodes qu'en Java, mais aussi des méthodes supplémentaires (liste complète iciListe des methodes de la classe String). Ainsi on peut écrire :
println("martin"
.all{it
.isLowerCase()}) // Teste si le mot entier est en basse casse.
La méthode all, permettant de tester si tous les éléments d'une collection remplissent un critère, n'existe pas en Java : c'est une extension du langage Kotlin. Le morceau de code constitue par les accolades est une fonction anonyme (ou lambda) : il en sera question ultérieurement.
Kotlin ajoute aussi le type Range (intervalle), très utile.
Ainsi on peut notamment exécuter une boucle grâce a un objet Range (en effet, la syntaxe basique Java pour effectuer une boucle n'existe pas en Kotlin) :
for
(i in
3..7
){
println(i)
} // Affiche les chiffres de 3 a 7 sur plusieurs lignes.
Mais on peut aussi obtenir une intervalle décroissant avec downTo :
for
(i in
10
downTo 0
) {
println(i)
} // Compte a rebours de 10 a 0, sur plusieurs lignes.
Il est aussi possible de préciser une progression autre que 1 :
for
(i in
2..36
step 3
) println(i) // Affiche les nombres 2,5,8,…,35
Il est aussi possible de combiner un intervalle décroissant et une progression définie :
for
(i in
36
downTo 2
step 3
) println(i) // Affiche les nombres 36,33,…,3
Nous reviendrons plus tard dans ce chapitre sur la boucle for.
Sachez aussi, comme nous le verrons plus tard avec les fonctions, le type void n'existe pas en Kotlin, mais on utilise a la place le type Unit (qui ne s'applique pas qu'aux fonctions).
N'oubliez pas que vous pouvez vous rendre compte des méthodes disponibles pour un objet donne dans le playground Kotlin ou dans l'IDE Intellij Idea grâce a la commande CTRL+ESPACE après avoir saisi le point accole a l'objet.
Nous reviendrons plus loin sur les tableaux (array) ainsi que sur les tables d'associations (map) dans la prochaine section.
II-D. Les collections natives▲
En Kotlin, il n'y a pas besoin d'importer quoi que ce soit pour utiliser les collections. De plus, grâce au mécanisme de fonctions d'extension (que nous verrons ultérieurement lors du prochain chapitre sur les classes), les collections bénéficient de plusieurs méthodes supplémentaires par rapport a leurs équivalents Java.
II-D-1. Les tableaux▲
Tout d'abord, en Kotlin, les tableaux sont représentés par la classe générique Array : ainsi l'on peut déclarer par exemple, un Array<Int>, un Array<String>, voire même un Array<Personne> (type personnel défini peu avant dans ce chapitre). Évidement, un Array ne peut contenir que des valeurs d'un même type commun.
On peut construire un Array de deux manières différentes : soit par le constructeur Array, soit par la fonction arrayOf. La fonction arrayOf est plus proche de ce que l'on connaît en Java, tandis que le constructeur Array permet de bénéficier de plus de contrôle sur l'initialisation :
val
tableau1 = arrayOf(2
,10
,-1
,4
,9
)
val
tableau2 = Array(10
, { i -> i * 2
}) // utilisation d'une fonction lambda (fonction anonyme)
assert(tableau1[1
] == 10
) // On accede aux elements comme en Java
tableau1[0
] = 170
assert(tableau1[0
] == 170
) // De meme pour la modification
Ici j'utilise notamment l'instruction assert, dans un but purement illustratif : si l'expression contenue en son appel est fausse, une exception est lancée, sinon, il ne se passe rien de nouveau. Ainsi c'est un simple moyen d'illustrer la valeur de la variable ainsi testée au lecteur.
Plusieurs remarques au sujet de l'initialisation via le constructeur :
- le premier paramètre décrit le nombre d'éléments du tableau
- le deuxième est une fonction qui prend l'index de l’élément à initialiser et qui retourne une valeur. Bien qu'ici j'ai utilise une fonction anonyme pour la clarté du code, je pouvais aussi bien utiliser une fonction régulière. Même si nous verrons les fonctions ultérieurement, vous pouvez également remarquer qu'il s'agit d'une situation où l'on passe une fonction a une fonction (ici, la fonction anonyme au constructeur).
Rien ne nous empêche de déclarer des tableau multidimensionnels, que ce soit par le biais du constructeur ou de la fonction arrayOf :
val
tableau1 = arrayOf(arrayOf(1
,2
,3
), arrayOf(4
,5
))
val
tableau2 = Array(3
, {i -> Array(3
, {j -> i*j})})
Évidemment, les tableaux gardent une taille fixe.
Les tableaux disposent aussi de fonctions supplémentaires par rapport a leur version Java. Parmi la multitude de fonctionsla multitude de fonctions supplementaires pour Array, citons :
val tab = arrayOf(7,10,15,3,6,9,12)
tab.sum()
tab.sort()
tab.sorted() // idem que sort() mais se contente de retourner le resultat au lieu de modifier le tableau contenu dans tab
tab.reverse()
tab.reversed()
tab.min()
tab.max()
tab.first()
tab.last()
tab.take(3) // les 3 premiers elements du tableau sans le modifier
tab.drop(3) // le tableau sans ses 3 premiers elements sans le modifier
II-D-2. Les listes▲
Les listes sont similaires aux tableaux, mais sont plus puissantes, du moins dans leur version modifiable. Car en effet, car comme d'autres types de collections, il y a les listes immuables (non modifiables) et les listes modifiables. Tout dépend de la manière dont elles sont déclarées.
val
maListeNonModifiable = listOf(1
,2
,3
)
// Interdit !!! maListeNonModifiable[2] = 6;
val
maListeModifiable = mutableListOf(1
,2
,3
)
maListeModifiable[2
] = 6
;
maListeModifiable += 10
; // Equivalent a maListeModifiable.add(10)
print(maListeModifiable) // => [1, 2, 6, 10]
Ainsi :
- on peut créer une liste immuable grâce a la fonction listOf(), et créer une liste modifiable par l’intermédiaire de la fonction mutableListOf(). Ces deux méthodes sont par ailleurs génériques, mais l’inférence de type nous permet d'eviter d'en déclarer le type,
- vous pouvez constater que l'on ne peut pas modifier un élément d'un objet List, contrairement a un objet MutableList,
- on peut facilement ajouter des éléments aux listes modifiables.
Pourquoi aurait-on alors intérêt a utiliser les listes non modifiables ? Parmi les cas utiles, on peut citer la création d'algorithmes, notamment parce que les listes immuables garantissent l’immutabilité - notion très utile dans la programmation fonctionnelle - et qu'elles peuvent facilement être interprétées en tant que « schéma » via un mécanisme que l'on appelle la déconstruction, que nous verrons plus loin, et qui est aussi très utilise dans la programmation fonctionnelle.
Par ailleurs, rien ne nous empêche d’écrire du code tel que le suivant, générant ainsi une liste immuable a partir d'une autre.
val
maListeNonModifiable = listOf(1
,2
,3
)
val
maListe2 = maListeNonModifiable + 5
// maListeNonModifiable reste telle quelle.
Le type MutableList étant dérivé du type List, elles ont de nombreuses méthodes communes qui peuvent s’avérer très utiles. On peut notamment citer :
val
maListeImmuable = listOf(1
,2
,3
,4
,5
)
maListeImmuable.isEmpty() // Est-elle vide ?
maListeImmuable.contains(2
) // Le chiffre 2 y figure-t-il ?
maListeImmuable.all({it
% 2
== 0
}) // Ne contient que des chiffres pairs ?
maListeImmuable.chunked(2
) // => [[1, 2], [3, 4], [5]]
maListeImmuable.count({it
% 2
== 0
}) // Combien de chiffres pairs a-t-elle ?
maListeImmuable.take(3
) // Retourne une liste juste avec les 3 premiers elements
maListeImmuable.drop(2
) // Retourne une liste sans les 2 premiers elements
Certaines lignes du code précédent utilisent les lambdas, des fonctions anonymes que nous verrons ultérieurement.
N’hésitez pas a aller consulter les méthodes disponibles pour ListAPI pour le type List et MutableListAPI pour le type MutableList.
II-D-3. Les tables associatives▲
Kotlin supporte aussi les tables associatives : collections qui associent leurs valeurs a des clés, ou Map en anglais.
Comme pour les listes, il y a les tables associatives immuables (MapAPI table associative immuable) et les tables associatives modifiables (MutableMapAPI table associative modifiable), et on peut le instancier grâce aux fonctions mapOf() et mutableMapOf()
val
monDictionnaireImmuable = mapOf("Sandra"
to 27
, "Alex"
to 10
)
val
monDictionnaireModifiable = mutableMapOf("Sandra"
to "0102030405"
, "Alex"
to "0104050607"
)
monDictionnaireModifiable += ("Beatrice"
to "0809101112"
)
monDictionnaireModifiable["Sandra"
] = "0802030405"
val
monDictionnaireImmuableEnrichi = monDictionnaireImmuable + ("Beatrice"
to 30
)
Elles disposent elles aussi de nombreuses méthodes qui peuvent vous simplifier le développement :
val
monDictionnaireImmuable = mapOf("Sandra"
to 27
, "Alex"
to 10
)
monDictionnaireImmuable.getOrDefault("Beatrice"
, 33
) // Retourne l'age de Beatrice ou 33 si elle ne figure pas dans la table
monDictionnaireImmuable.all({ it
.value > 18
}) // Indique si toutes les personnes de la table sont majeur
monDictionnaireImmuable.any({ it
.key == "Sandra"
}) // Sandra est-elle dans la table ?
monDictionnaireImmuable.contains("Sandra"
) // Idem
monDictionnaireImmuable.filter({ it
.value > 18
}) // Retourne une table mais en ne gardant que les personnes majeures (ne modifie pas la table originale)
monDictionnaireImmuable.map({ it
.value - 10
}) // Retourne une liste des ages des personnes, auquels on aura retranche 10
monDictionnaireImmuable.count() // Le nombre d'associations de la table
Encore une fois, n’hésitez pas a aller regarder les documentations officielles pour ces deux classes, dont j'ai passe les liens plus haut.
II-D-4. Les sets▲
L'utilisation des Sets est elle aussi intuitive :
val
mesPiecesCapturees = setOf("Cavalier"
, "Dame"
, "Fou"
)
val
mesPiecesCaptureesV2 = mesPiecesCapturees - "Dame"
val
mesComics = mutableSetOf("Spiderman"
, "Batman"
, "Superman"
)
mesComics += "Green Lantern"
De même, vous pouvez retrouver les API pour les classes SetAPI pour les Set immuables (immuable) et MutableSetAPI pour les Set modifiables (modifiable). Quelques méthodes parmi tant d'autres :
val
mesPiecesCapturees = setOf("Cavalier"
, "Dame"
, "Fou"
)
mesPiecesCapturees.count() // Nombre d'elements
mesPiecesCapturees.filter{it
.startsWith("C"
)} // Set filtre ne retenant que les elements commençant par « C »
mesPiecesCapturees.map{it
.toLowerCase()} // Set où les elements ont ete convertis en minuscule
mesPiecesCapturees.isEmpty() // Est-il vide ?
mesPiecesCapturees.isNotEmpty() // A-t-il des elements ?
mesPiecesCapturees.maxBy{it
.length} // Quel element est le plus long ?
II-E. Les imports / espaces de nommage▲
Les imports et espaces de nommage fonctionnent dans l'ensemble comme en Java
package
com.monprojet.test // Definit un espace de nommage
import
com.projetimporte.* // Importe tout le paquetage
import
com.projet2.classe1 as
cls1 // Importe une classe et lui attribue un alias
A noter :
- un espace de nommage n'a pas pour obligation de respecter le chemin du fichier : cela peut être n'importe quelle valeur acceptable,
- il n'y a pas d'import statique en Kotlin : il faut simplement utiliser la syntaxe standard d'import sur l'Object en question (car -nous le verrons plus tard- il n'y a pas de méthode statique proprement dite en Kotlin, mais une classe de type Singleton, c'est a dire une classe de type Object),
- il est possible d'importer des fonctions globales (ne faisant partie d'aucune classe).
Kotlin importe déjà automatiquement les paquetages suivant :
- kotlin.*
- kotlin.annotation.*
- kotlin.collections.*
- kotlin.comparisons.*
- kotlin.io.*
- kotlin.ranges.*
- kotlin.sequences.*
- kotlin.text.*
- java.lang.* (seulement sur la version JVM de Kotlin)
- kotlin.jvm.* (même remarque)
II-F. Interpolation de chaînes▲
Cette fonctionnalité nous permet de concaténer des chaînes plus facilement. Si en java nous voulons écrire une séquence du type :
int a = 10
;
int b = 6
;
String
c = "abc"
;
System.out
.println("["
+ a + ", "
+ b*2
+ ", "
+ c + "]"
);
En Kotlin on pourra simplement écrire :
val
a = 10
val
b = 6
val
c = "abc"
println("[
$a
,
${b*2}
,
$c
]"
)
Ainsi :
- On utilise qu'une seule chaîne de caractères,
- $a permet de substituer la valeur de la variable a dans la chaîne, quel que soit son type (souvenez-vous qu'en Kotlin, tout est objet),
- ${b*2} s’écrit avec accolades étant donne qu'il s'agit d'une expression, et non plus d'une simple référence à une variable.
II-G. Inférence de type▲
Dans de nombreuses situations, le compilateur Kotlin est capable de reconnaître le type d'une variable grâce a l'expression qui a servi a l'initialiser :
val
age = 10
// age est declare comme Int
val
nom = "Toto"
// nom est declare comme String
var
prix = 10.2
// prix est declare comme Double
Il va de soi que pour une variable non initialisée de suite (donc de type var), il faut en préciser le type : l’inférence de type ne pouvant s'appliquer dans ce cas.
De même l’inférence de type fonctionne aussi avec les types complexes : qu'ils soient définis par Kotlin ou par nous-même.
class
Personne(val
name: String
)
val
jean = Personne("Jean"
) // jean est de type Personne.
Nous verrons par la suite d'autre inférence de type.
II-H. Expressions▲
Toute instruction qui retourne une valeur constitue une expression : simple valeur littérale (3, « Toto »…), expression arithmétique ou booléenne, appel de fonction (même une fonction ne retournant aucune valeur exploitable : c'est-a-dire une fonction définie comme retournant Unit), déclaration d'une fonction anonyme… Ces expressions peuvent donc être utilisées pour initialiser des variables.
val
message = if
(age < 18
) "C'est un mineur !"
else
"Il est majeur."
// Se referer a la section suivante sur les structures de controle.
Certaines instructions ne retournent jamais de valeur. C'est le cas pour la boucle while, comme nous le verrons dans une prochaine section du chapitre :
val
resultat = while
(i < 10
) {i += 1
} // Strictement interdit !!!
Par ailleurs les affectations de variables ne sont pas des expressions : il est donc impossible de les enchaîner au sein d'une même instruction :
var
b = 0
val
a = b = 10
// Strictement interdit !!!
II-I. Tuples et déconstruction▲
Kotlin dispose d'un support pour les tuples simples que sont les Paires (Pair) et Triples (Triple). Si un tableau ne peut contenir que des valeurs de types communs, les tuples permettent d’agréger plusieurs types de données : en Kotlin on peut simplement utiliser les Pair et Triple pour combiner respectivement deux ou trois éléments. On accède alors simplement aux différentes composantes par l’intermédiaire des méthodes component1(), component2(), component3(), où component1() retourne la 1re composante du tuple.
val
tuple1 = Pair('a', 25
) // de type Pair<Char, Int>
val
valeur1_1 = tuple1.component1()
val
valeur1_2 = tuple1.component2()
assert(valeur1_1 == 'a')
assert(valeur1_2 == 25
)
val
tuple2 = Triple('a', 10
, "Toto"
) // de type Triple<Char, Int, String>
val
valeur2_1 = tuple2.component1()
val
valeur2_2 = tuple2.component2()
val
valeur2_3 = tuple2.component3()
assert(valeur2_1 == 'a')
assert(valeur2_2 == 10
)
assert(valeur2_3 == "Toto"
)
Nous pouvons aussi accéder aux différentes composantes d'un tuple par le biais de ce que l'on appelle une déconstruction (destructuring) :
val
monTuple = Triple('a', 10
, "Toto"
)
val
(a, b, c) = monTuple // deconstruction !
assert(a == 'a')
assert(b == 10
)
assert(c == "Toto"
)
La déconstruction a ici lieu parce que l'on tente ici d'affecter, non pas une simple variable, mais une structure qui dispose de plusieurs variables (ici la structure (a,b,c)) a une structure qui a la même forme (soit ici une agrégation de trois valeurs). Remarquez qu'il est interdit de déclarer val Triple
(
a,b,c) =
Triple
(
'a'
, 10
, "Toto"
) : il faut simplement décrire la forme de la structure a gauche de l'affectation.
Sachez également qu'il est possible d'ignorer certaines valeurs lors de la déconstruction :
val
(a, _ , c) = Triple('a', 10
, "Toto"
)
assert (a == 'a')
assert (c == "Toto"
)
II-J. Structures de contrôle▲
Les structures de contrôle sont similaires a celles du java (if, for, while, switch…), a ceci près que la plupart des structures de Kotlin sont des expressions, et donc leur résultat peuvent être affectes a des variables, telles que nous l'avons vu dans la section précédente ; mais également, l’équivalent de la structure switch en Kotlin est beaucoup plus simple d'utilisation et beaucoup plus puissante.
II-J-1. L'instruction while▲
L'instruction while (ou do … while) en Kotlin, tout comme en Java, ne retourne pas d'expression. Elle s'utilise aussi comme en Java : rien de nouveau.
var
i = 0
while
(i < 10
) {
i += 1
println(i)
}
var
j = 0
do
{
j += 1
println(j)
} while
(j < 10
)
Les mots-clés break et continue fonctionnent comme en Java.
II-J-2. L'instruction if▲
En revanche, l'instruction if peut être utilisée aussi bien en tant que structure de contrôle traditionnelle qu'en tant qu'expression. Voici les deux cas de figures en situation :
if
(1
<2
) {
println("ok"
)
}
else
if
(2
!= 2
) {
println("ko 1"
)
}
else
{
println("ko 2"
)
}
Ça, c'est le cas de figure que nous connaissions déjà.
Et bien l'autre, c'est comme l’opérateur ternaire de Java, sauf que l’opérateur ternaire n'existe pas en Kotlin, il a simplement été remplace par l'expression if-else.
val
couleur = if
(caseBlanche) "white"
else
"black"
Ce qui évidemment, nous évite de créer une variable temporaire, mais a la place de créer directement un valeur immuable.
II-J-3. L'instruction for▲
L'instruction for, quand a elle, ne peut pas être utilisée en tant qu'expression.
L'utilisation de l'instruction for, telle que l'on connaît en Java (initialisation/condition/mise a jour accompagnée d'un bloc d'instructions) est strictement interdite en Kotlin. En effet, on ne peut utiliser la boucle for que sur des objets « iterable » : traditionnellement des ranges ou certains types de collections. Par « iterable » je veux simplement parler de types où l'on puisse obtenir les différentes valeurs d'une variable de ce type par appels successifs a la méthode d'extraction (appelons-la next() pour mieux illustrer).
Ce qui peut par exemple donner :
// i n'existe pas dans cette portee
for
(i in
0..10
) println(i)
Remarquez-bien ici qu'il ne faut utiliser ni le mot-clé val, ni var, afin de déclarer la variable i : qu'elle existe déjà ou non, il ne faut surtout pas les utiliser.
Un autre exemple avec simple tableau :
val
monTableau = arrayOf(2
,3
,5
,7
,11
,13
)
for
(premier in
monTableau) {
println("
$premier
est un nombre premier"
)
}
Remarquez aussi que je peux m'affranchir des accolades si je n'ai besoin que d'une simple expression pour la boucle for (ce qui est donc possible dans ce deuxième exemple).
Enfin, si l'on souhaite parcourir un tableau tout en conservant les valeurs des différents index, on peut utilise la méthode withIndex() mais avec une syntaxe un peu particulière pour les variables d'index et de valeur :
val
monTableau = arrayOf(2
,3
,5
,7
,11
,13
)
for
((index, valeur) in
monTableau.withIndex()) {
println("
$index
:
$valeur
"
)
}
Remarquez que la méthode withIndex() retourne une Pair où la première valeur est l'index, donc il faut empaqueter les variables d'index et de valeur entre parenthèses afin de procéder au mécanisme de déconstruction. Mais rien ne nous empêche de procéder en deux étapes :
val
monTableau = arrayOf(2
,3
,5
,7
,11
,13
)
for
(tupleCourant in
monTableau.withIndex()) {
val
index = tupleCourant.component1()
val
valeur = tupleCourant.component2()
println("
$index
:
$valeur
"
)
}
Bien évidemment, l'instruction for permet aussi de parcourir les tables associatives
val
monRepertoireTel = mapOf("Bea"
to "0123456789"
, "Lily"
to "08123456"
, "Max"
to "06123456"
)
for
(entree in
monRepertoireTel) {
println("
${entree.key}
=>
${entree.value}
"
)
}
// Version utilisant la deconstruction
for
((nom, numero) in
monRepertoireTel) {
println("
$nom
=>
$numero
"
)
}
Tout comme pour les boucles while (do…while), les mots-clés break et continue fonctionnent comme en Java.
II-J-4. L'instruction when▲
L'instruction est une instruction ultra puissante, basée sur une fonctionnalité communément appelée le « pattern-matching » dans un certain nombre de langages. Un concept plus ou moins lie au mécanisme de déconstruction que nous avons vu auparavant. D'ailleurs, avant de vous expliquer comment elle fonctionne et a quoi elle peut-être utile, je tiens a vous préciser qu'en plus de sa puissance et sa flexibilité, l'instruction when est aussi une expression.
II-J-4-a. Cas ultra simple▲
Pour commencer, un cas ultra simple : tester le signe d'une nombre
fun
main() {
val
a = 10
;
var
signeDeA: Int
;
when
{
a < 0
-> println("negatif"
)
a == 0
-> println("nul"
)
else
-> println("positif"
)
}
}
Ici, nous créons d'abord la variable évolutive signeDeA sans lui affecter de valeur, auquel cas il faut obligatoirement préciser un type.
Puis nous effectuons une instruction conditionnelle : ici la structure when parcourt chacun de ses tests dans l'ordre et s'interrompt (éventuellement) des que l'un des tests correspond, auquel cas elle exécute auparavant le code associe au test. Le test est situe avant la flèche (signe moins combine a une signe supérieur), et le code associe après. Remarquez qu'il n'y a pas d'instruction break : pas d'instruction break dans les instructions when !
Évidemment, dans le cas de figure présent, c'est l'instruction else – qui correspond a une branche default du switch de java – qui est exécutée. Car tous les tests précédents ont échoués.
D'ailleurs cette instruction else n'est pas obligatoire, nous aurions pu négliger le cas du signe positif :
fun
main() {
val
a = 10
;
var
signeDeA: Int
;
when
{
a < 0
-> println("negatif"
)
a == 0
-> println("nul"
)
}
}
Et pour être complet sur cet exemple, on peut inverse les trois tests, afin de bien se rendre compte que l'instruction when s'interrompt des que c'est nécessaire.
fun
main() {
val
a = 10
;
var
signeDeA: Int
;
when
{
a > 0
-> println("positif"
)
a == 0
-> println("nul"
)
else
-> println("negatif"
)
}
}
II-J-4-b. Test en fonction d'une variable en entree▲
On peut aussi baser les tests de l'instruction when sur une unique variable :
fun
main() {
val
de = java.util.Random()
val
valeurDe = de.nextInt(6
) + 1
val
texteValeur = when
(valeurDe)
{
1
-> "Un"
2
-> "Deux"
3
-> "Trois"
4
-> "Quatre"
5
-> "Cinq"
else
-> "Six"
}
println(texteValeur);
}
Plusieurs remarques :
- On vient de fournir la variable valeurDe a l'instruction when, ce qui aura pour conséquence que dans chaque test de l'instruction, valeurDe sera compare a l'expression du test.
- Ici, comme nous passons une variable a l'instruction when, celle-ci doit être testée de manière exhaustive. Ce qui est facile avec les énumérations (un type personnalise avec un nombre de valeurs définies, mais nous en reparlerons plus tard), mais pour un type numérique comme le type Int : il faudra obligatoirement fournir une branche else.
- Nous avons décidé d'utiliser l'instruction when en tant qu'expression et nous en affectons le résultat dans la variable texteValeur.
On aurait aussi bien pu se passer de créer la variable texteValeur :
val
de = java.util.Random()
val
valeurDe = de.nextInt(6
) + 1
println(when
(valeurDe) {
1
-> "Un"
2
-> "Deux"
3
-> "Trois"
4
-> "Quatre"
5
-> "Cinq"
else
-> "Six"
})
II-J-4-c. Pas d'obligation de tester des constantes▲
Augmentons le code précédant avec un texte a peine plus complexe (nous reviendrons ultérieurement sur la création de fonctions) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
fun
estPaire(texteValeur: String
): Boolean {
return
when
(texteValeur) {
"Deux"
, "Quatre"
, "Six"
-> true
else
-> false
}
}
fun
main(args: Array<String
>) {
val
de = java.util.Random()
val
valeurDe = de.nextInt(6
) + 1
val
texteValeur = when
(valeurDe) {
1
-> "Un"
2
-> "Deux"
3
-> "Trois"
4
-> "Quatre"
5
-> "Cinq"
else
-> "Six"
}
println(texteValeur)
println(when
{
estPaire(texteValeur) -> "Paire"
else
-> "Impaire"
})
}
La fonction estPaire indique si, pour les six valeurs textes du résultat d'un tirage de de, la valeur représentée est paire. Nous nous en servons donc pour savoir si la valeur de texteValeur est paire (ligne 21).
II-J-4-d. Exécution de bloc▲
Jusqu'ici nous n'avons retourne que de simples expressions pour chaque test de l'instruction when, en fait nous pouvons faire plus que cela :
when
(ageGuillaume) {
estMineur(ageGuillaume) -> {
println("Desole, le droit de vote est interdit aux mineurs"
)
repousserGuillaumeALEntree()
sermonerGuillaume()
}
else
-> {
println("Voici les bulletins de vote"
)
indiquerIsoloirAGuillaume()
}
}
II-J-4-e. Regrouper plusieurs valeurs de test▲
Nous avons vu auparavant que l'instruction break ne peut pas être utilisée avec l'instruction when, et que sur un test réussi, son instruction/expression est traitée et c'est la fin de la structure. Donc si l'on veut tester plusieurs valeurs a tester : ceci – qui était autorise avec un switch en java – est strictement interdit :
switch (a) {
2
:
3
:
5
:
6
: println("2,3,5 ou 6"
);
default: println("autre"
);
}
On écrira plutôt :
when
(a) {
2
, 3
, 5
, 6
-> println("2,3,5 ou 6"
)
else
-> "autre"
}
On peut même tester avec des intervalles de valeurs, que nous avons déjà vu auparavant :
when
(age) {
in
13..19
-> "adolescent"
else
-> "autre"
}
Remarquez l'utilisation du mot-clé in. On peut aussi inverse le test en écrivant !in début..fin .
II-J-4-f. Test du type de données▲
Grâce a l'instruction when, il devient facile de tester le type de données d'une variable :
when
(age) {
is
String
-> "Il faut traiter la valeur auparavant"
is
Int
-> "Ok"
else
-> "Impossible de faire quoi que ce soit avec"
}
Mais aussi, grâce au SmartCast, fonctionnalité dont nous reparlerons sous peu, il n'est pas nécessaire d'effectuer de casting dans les instructions associées aux tests.
when
(age) {
is
String
-> println(age.length) // age est un String donc la propriete length existe
is
Int
-> println(age.dec()) // affiche age - 1
else
-> println("Impossible quoi que ce soit avec"
)
II-K. La conversion de type▲
II-K-1. La conversion classique▲
On peut utiliser les mots-clés is et !is pour respectivement tester si un type est ou n’est pas d’un type donné.
if
(obj is
String
)
{
// Faire quelque chose
}
if
(a!is
Int
)
{
// Faire quelque chose
}
Le mot clé as permet d’effectuer une conversion de type.
val
s:String
= valeur as
String
Mais si valeur est null, cela lèvera une exception. (Car le type String ne peut prendre la valeur null, contrairement au type String ? : nous en reparlerons sous peu, ainsi que du mot-clé as?)
II-K-2. Le SmartCast▲
Tout d'abord examinez le code suivant :
if
(figureCourante is
Cercle)
{
println(figureCourante.rayon)
}
else
if
(figureCourante is
Rectangle)
{
println(figureCourante.largeur)
}
Admettons, qu'auparavant, son auteur ait prit soin de définir une hiérarchie de figures (classe Figure), dont la classe fille Cercle dispose de la propriété rayon et pas la classe fille Rectangle. Et de même la classe fille Rectangle dispose de la propriété largeur et pas la classe Cercle. Et donc que la classe Figure ne dispose d'aucune de ces deux propriétés.
Ce qui peut choquer de prime abord avec le code ci-dessus, c'est que, bien que figureCourante s'apparente a une instance de Figure, il n'y a a aucun moment quelconque cast avant d’accéder aux propriétés rayon (de la classe fille Cercle) et largeur (de la classe fille Rectangle). Mais juste deux branches if-else.
Et pourtant, il s'agit bien d'une fonctionnalité de Kotlin :
- dans chaque test, il est sûr que la variable est d'un type donne
- donc inutile d'y effectuer un cast : il est assez intelligent pour réduire le champ des possibles a l’intérieur du if.
Et nous avons vu plus haut, une façon bien plus élégante de réaliser cela : when !
when
(figureCourante) {
is
Cercle -> println(figureCourante.rayon)
is
Rectangle -> println(figureCourante.largeur)
else
-> println("Echec d'exploitation de la figure"
)
}
II-K-3. La conversion de types numériques▲
En Kotlin, il n’y a pas de conversion automatique des types numériques. Si l’on veut convertir un Int en Long, il faut passer par une méthode prévue à cet effet :
val
a = 10.0
// a est un Double
val
b = a.toLong()
val
c = 10.0
as
Int
// la conversion automatique entre types numeriques n’existe pas: une ClassCastException sera levee
val
d = 10
as
Long
// de meme
Les méthodes s’appellent simplement toInt(), toDouble(), toLong() … et font partie de l’API standard de Kotlin. (Vous pouvez toujours y accéder depuis le site officielRéférence officielle de la librairie standard.)
II-L. Null safety▲
En Kotlin, pour un type donne, il existe deux variantes :
- une qui n'acceptera jamais d’être associée a la valeur null,
- une autre qui le permettra
Par exemple : une variable de type Int ne vaudra jamais null, mais une variable de type Int ? Oui.
Cela a donc une conséquence directe : il est interdit d'attribuer une valeur de type Int ? a une valeur de type Int, a moins de la convertir auparavant dans le cas où elle vaudrait null.
On peut utiliser un smartCast pour cela :
val
a:Int
? = null
val
b:Int
= when
(a) {
null
-> 0
else
-> a
}
println(b)
Mais on peut aussi utiliser l’opérateur elvis :
val
a:Int
? = null
val
b:Int
= a ?: 0
println(b)
Ainsi la variable b vaudra 0 si a est null, et la valeur de a sinon.
Enfin, la il possible de convertir une valeur « nullable » en une autre (équivalente bien sûr) grâce au mot-clé as?
val s:String? = valeur as? String
Ainsi si valeur vaut null, s vaudra également null. On aurait très bien pu utiliser le mot-clé as, mais une valeur null aurait alors levé une exception.
III. Focus sur les fonctions▲
Dans ce chapitre, nous allons explorer les fonctions un peu plus en détail.
III-A. Les bases▲
III-A-1. Fonction sans valeur de retour ni paramètre▲
Voici notre première fonction
fun
direBonjour()
{
println("Bonjour"
)
}
fun
main(args: Array<String
>)
{
direBonjour()
direBonjour()
}
Plusieurs remarques :
- on utilise le mot-clé fun et c’est le premier mot de la définition de fonction
- ici, il n’y a aucune indication sur la valeur de retour : et pour cause, de cas-ci nous n’en avons pas besoin.
III-A-2. Fonction sans valeur de retour mais avec paramètre▲
Maintenant, personnalisons un peu l’invité d’accueil :
fun
direBonjour(nom: String
)
{
println("Bonjour
${nom}
!"
)
}
fun
main(args: Array<String
>)
{
direBonjour("Hubert"
)
direBonjour("Francis"
)
}
Alors :
- pour chaque paramètre de la fonction, il faut faire attention à préciser sous la forme nom: type ! Nous avons donc précisé « nom » avant le type « String », en pensant aux deux points (attention, il ne doit pas avoir d’espace entre le nom de la variable et les deux points),
- pour le message à afficher nous utilisons ici l’interpolation de chaîne dont nous avons déjà parlé. C’est ici une occasion idéale d’écrire un code concis et compréhensible grâce à cette fonctionnalité.
III-A-3. Fonction avec un paramètre ayant une valeur par défaut▲
Maintenant, pourquoi pas fournir une valeur par défaut à notre invité d’accueil ?
fun
direBonjour(nom: String
= "le monde"
) {
println("Bonjour
${nom}
!"
)
}
fun
main(args: Array<String
>)
{
direBonjour("Hubert"
)
direBonjour("Francis"
)
direBonjour()
}
En fait, on peut définir autant de paramètre avec valeur par défaut que souhaité (même si le nombre de paramètres d’une fonction doit toujours rester raisonnable) mais il faut veiller à toujours les placer en dernier dans la liste des paramètre.
III-A-4. Fonction avec une valeur de retour▲
Dans ce cas, il faut préciser le type de valeur de retour dans la signature de la fonction (c’est le type de retour associé aux éventuels paramètres de la fonction).
fun
additionner(a: Int
, b: Int
= 0
): Int
{
return
a + b
}
fun
multiplier(a: Int
, b: Int
= 1
): Int
{
return
a * b
}
fun
main(args: Array<String
>) {
println(multiplier(additionner(2
,3
), additionner(5
)))
}
Remarques
- c’est après la liste des paramètres que l’on précise le type de retour. Et, comme précédemment, il faut absolument éviter quelconque espace entre la parenthèse fermante et les deux points,
- c’est avec le mot clé return, obligatoire, que l’on précise la valeur de retour,
- rien ne nous empêche de combiner avec des valeurs par défauts : ici j’ai choisi les éléments neutres des deux opérations (ceux qui permettent d’obtenir exactement la valeur du premier paramètre).
Veuillez également remarquer que :
- pour une fonction sans valeur de retour, on peut très bien préciser le type Unit, équivalent de void en Java/C++,
- le paramètre de la fonction main correspond à un tableau de String : il s’agit donc de la valeur des paramètres d’exécution du programme.
III-A-5. La valeur de retour est une simple expression▲
Si la valeur de retour est une expression, alors on peut utiliser la syntaxe simplifiée
fun
additionner(a: Int
, b: Int
= 0
) = a+b
Cela fonctionne donc avec les expressions if et when notamment.
III-A-6. Appel avec un ordre de paramètres différent▲
Rien ne nous empêche d’appeler une fonction avec un ordre de paramètres différent
fun
additionner(a: Int
, b: Int
= 0
) = a + b
fun
soustraire(a: Int
, b: Int
= 0
) = a - b
fun
multiplier(a: Int
, b: Int
= 1
) = a * b
fun
main(args: Array<String
>) {
println(multiplier(additionner(2
), soustraire(b = 5
, a = 10
)))
}
Vous pouvez remarquer l’inversion des paramètres de l’appel à la fonction soustraire.
III-A-7. Fonction dans une fonction▲
En Kotlin, il est aussi possible de déclarer une fonction à l’intérieur d’une fonction.
fun
saluer() {
fun
obtenirNom() = "Philae"
println("Bonjour,
${obtenirNom()}
!"
)
}
fun
main(args: Array<String
>) {
saluer()
}
Et c’est notamment une des conséquences du fait qu’en Kotlin, les fonctions sont également des valeurs. Et cela devient particulièrement intéressant lorsque l’on les utilise en tant que fonctions d’ordre supérieurs.
III-A-8. Fonctions variadiques▲
Il est également possible de définir des fonctions avec un nombre variable d’arguments
fun
additionPersonnalisee(vararg
params: Double
): Double
{
var
total = 0.0
for
(value in
params) total += value
return
total
}
fun
main(args: Array<String
>) {
println(additionPersonnalisee(10.0
, 20.0
, 30.0
, 40.0
, 50.0
))
}
En fait grâce au mot-clé vararg que le paramètre params accèpte en fait un nombre indéterminé de valeurs. Ainsi params sera en quelque sorte un Array<Double>. La conséquence directe est qu’on ne peut utiliser qu’un paramètre variadique par fonction (mais il ne doit pas forcément être déclaré en dernier).
Remarquez aussi que passer un simple tableau en tant que paramètre ne fonctionnera pas, pour cela il faut utiliser une petite astuce :
fun
additionPersonnalisee(vararg
params: Double
): Double
{
var
total = 0.0
for
(value in
params) total += value
return
total
}
fun
main(args: Array<String
>) {
println(additionPersonnalisee(*arrayOf(10.0
, 20.0
, 30.0
, 40.0
, 50.0
).toDoubleArray()))
}
L’étoile ! Ajouter une simple étoile devant le tableau permet de l’interpréter en tant que liste d’arguments, et non plus comme un unique argument.
Remarquez aussi que params étant en fait un DoubleArray et non un Array<Double> (deux types différents !), il a fallu effectuer une conversion préalable.
III-B. Fonctions d’ordre supérieur▲
Une fonction d’ordre supérieur effectue au moins l’une des deux tâches suivantes :
- elle prend au moins une fonction en tant que paramètre,
- elle retourne une fonction.
Vous vous doutez bien que cela ajoute une flexibilité à nos possibilités de codage.
III-B-1. Les exemples simples de la librairie standard▲
Dans la librairie standard, de nombreuses méthodes associées aux collections sont écrites en tant que fonctions d’ordre supérieur, notamment en acceptant au moins une fonction en entrée.
III-B-1-a. La fonction map▲
Admettons que nous disposons des données des élèves d’une classe, et chaque donnée dispose notamment d’une propriété prénom. Nous souhaitons convertir ce tableau de données en un simple tableau de chaînes de caractères.
Une solution dite « naïve » serait codée comme suit :
var
resultat = mutableListOf<String
>()
for
(eleve in
listeEleves) {
resultats.add(eleve.prenom)
}
Mais il y a bien mieux, grâce à la méthode map, dont notamment notre Array d’élèves bénéficie :
fun
extrairePrenom(eleve: Eleve) = eleve.prenom
fun
main(args: Array<String
>)
{
// recuperation du tableau d’eleves volontairement omise
val
resultats = listeEleves.map(::extrairePrenom)
}
Ici j’ai créé une fonction qui permet d’extraire le prénom d’un élève de son objet le représentant (en supposant ici que la classe Eleve dispose de la propriété prénom), et ensuite je passe la référence de la fonction. Et cela grâce à la paire de signes deux-points précédent son nom et uniquement son nom.
En effet, la fonction map a besoin de savoir quelle fonction appliquer à chacun des éléments. Ici nous passons une référence sur une fonction existante, mais lorsque nous parlerons de fonctions anonymes nous verrons une alternative qui s’avère utile dans bien des cas.
Remarquez également que la fonction map ne modifie pas la collection originale, mais en retourne une nouvelle.
Voici un autre exemple, que vous pouvez cette fois tester directement dans le playground Kotlin, ou dans l’IDE Intellij Idea.
fun
maConversion(entree: Int
): Int
= entree * 50
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.map(::maConversion))
println(java.util.Arrays.toString(resultats))
}
Attention toutefois, les tableaux (array) en Kotlin étant basés sur les tableaux natifs de Java, lesquels n’ont pas de formatage de leurs affichage par défaut. A la place la méthode toString() par défaut affiche l’identifiant mémoire de la variable. C’est pour cela que je fais appel à java.util.Arrays.toString afin d’afficher les résultats. La méthode map retourne une liste, qui est basée sur la classe ArrayList de Java, et donc dispose déjà d’un affichage par défaut.
III-B-1-b. La méthode filter▲
Une autre méthode d’ordre supérieur souvent présente dans beaucoup de types de collection : la fonction filter. Elle permet simplement de retourner une nouvelle collection avec des éléments filtrés selon une fonction que l’on passe en tant que critère de sélection.
fun
estPair(entree: Int
): Boolean = entree % 2
== 0
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.filter(::estPair))
println(java.util.Arrays.toString(resultats))
}
Tout comme la méthode map, la méthode filter ne modifie pas la collection originale.
III-B-1-c. La méthode reduce▲
Cette méthode est un peu plus complexe, mais peut s’avérer très utile si elle est comprise.
En tant que premier exemple, nous allons réaliser le calcul de la somme des éléments d’un tableau de chiffres. Je donne le code, avant de le commenter.
fun
additionner(a: Int
, b: Int
): Int
= a + b
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.reduce(::additionner))
}
En fait, c’est comme si la méthode reduce effectuait l’algorithme suivant
- Créé une variable numérique valant 0
- Prends les deux premiers chiffres du tableau, dans l’ordre : 1 et 2.
- Passe-les à la fonction additionner que j’ai reçue et conserve le résultat
- Ajoute ce résultat à la valeur courante de la variable
- Prends la 1ere valeur du tableau qui n’a pas encore été exploitée (lors du 1er passage ce sera l’élément d’indice 2)
- Passe la valeur courante de la variable en 1er paramètre et la valeur que je viens de récupérer en 2e paramètre à la fonction additionner et conserve le résultat
- Recommence à l’étape 4 jusqu’à ce qu’il ne reste plus de valeur à exploiter
- Retourne la valeur de la variable.
On peut donc dire que le 1er paramètre de la fonction passée à la méthode reduce correspond à la valeur accumulée lors de l’appel, et le deuxième paramètre à la valeur nouvellement extraite du tableau. Pour être plus clair, j’aurais aussi pu définir la fonction additionner comme suit :
fun
additionner(accumulateur: Int
, courant: Int
): Int
= accumulateur + courant
Ceci est un type d’utilisation parmi tant d’autres.
III-B-2. Codage d’une fonction d’ordre supérieur▲
Voyons cela à travers un exemple simple. Je souhaite par exemple effectuer un calcul entre deux nombres, mais je veux, dans ma fonction de calcul, laisser à l’utilisateur le soin de préciser le calcul à effectuer.
fun
add(a: Int
, b: Int
) = a + b
fun
sub(a: Int
, b: Int
) = a - b
fun
mul(a: Int
, b: Int
) = a * b
fun
div(a: Int
, b:Int
) = a / b
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, ::add)
afficherCalcul(10
,5
, ::sub)
afficherCalcul(10
,5
, ::mul)
afficherCalcul(10
,5
, ::div)
}
Ici, les quatre fonctions d’opérations add, sub, mul et div ont la même signature (pour rappel, le nom des variables n’influe pas sur la définition d’une signature : manger(aliment : String) a exactement la même signature que manger(dessert : String)). Elles acceptent deux Int, et retournent un Int (type déduit par le compilateur d’après chaque expression de retour).
On peut donc passer cette signature à notre fonction de calcul, de sorte que les quatre fonctions sont interchangeables. La signature est on en peut plus simple à définir :
- la liste des paramètres,
- le type de retour mais précédé d’une flèche et non de deux-point.
III-C. Fonctions anonymes et lambdas▲
Parfois, il peut s’avérer fastidieux de devoir créer une fonction, juste pour l’utiliser que dans un seul appel à une fonction/méthode d’ordre supérieur. Pour résoudre cette problématique, on peut utiliser les fonctions anonymes ou les lambdas.
III-C-1. Fonction anonyme▲
Une fonction anonyme est une fonction qui, comme son nom l’indique, ne se voit pas attribuer d’identité, de nom. Il va de soi qu’une telle fonction ne peut pas être utiliser de manière classique :
- nous pouvons la stocker dans une variable (val ou var)
- ou la passer directement à une fonction/méthode d’ordre supérieur
L’exemple de la fonction de calcul développé plus haut pourrais aussi être adapté comme suit :
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, fun
(a: Int
, b: Int
) = a + b)
afficherCalcul(10
,5
, fun
(a: Int
, b: Int
) = a - b)
afficherCalcul(10
,5
, fun
(a: Int
, b: Int
) = a * b)
afficherCalcul(10
,5
, fun
(a: Int
, b:Int
) = a / b)
}
Rien de compliqué dans la définition d’une fonction anonyme.
III-C-2. Lambda▲
Les lambdas, en plus de présenter une syntaxe différente des fonctions anonymes, présentent d’autres particularités.
III-C-2-a. Utilisation simple▲
D’abord, voyons une manière de réutiliser l’exemple précédent avec des lambdas.
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a + b})
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a - b})
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a * b})
afficherCalcul(10
,5
, {a: Int
, b: Int
-> a / b})
}
Une lambda s’écrit simplement : { [paramètres] -> [expression/ liste d’instructions sans accolades] }
Si l’on souhaite réaliser plusieurs instructions dans le lambda, on les enchaîne simplement, sans mettre d’accolades.
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
, {a: Int
, b: Int
->
println("Dans le lambda de calcul"
)
a + b
})
}
Si la lambda doit retourner une valeur, elle correspondra alors à la valeur de la dernière instruction, qui donc doit dans ce cas précis aussi être une expression. (C’est le cas dans le code précédent.)
Tout comme les fonctions anonymes, elles peuvent être stockées dans des variables.
En revanche, on ne peut pas préciser le type de retour à une lambda: pour cela il faudra utiliser une fonction anonyme à la place. En général, le compilateur parvient lui-même à vérifier si le type de retour de la lambda correspond bien au type attendu.
III-C-2-b. Lambda en tant que dernier paramètre d’une fonction▲
Une caractéristique sympathique des lambdas, est que si une lambda est le dernier paramètre d’une fonction/méthode, alors on peut « l’extraire des paramètres » et la placer juste derrière la fonction.
fun
afficherCalcul(a: Int
, b: Int
, calcul: (a: Int
, b: Int
) -> Int
)
{
println("Le calcul de
${a}
et
${b}
vaut
${calcul(a,b)}
"
)
}
fun
main(args: Array<String
>) {
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a + b}
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a - b}
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a * b}
afficherCalcul(10
,5
) {a: Int
, b: Int
-> a / b}
}
Ici, le paramètre calcul, qui définit une fonction à passer un paramètre, est le dernier paramètre de la fonction. Cela implique que si c’est une lambda que je passe, je peux la sortir de la liste des paramètres et la coller juste après l’appel de la fonction. C’est une simplification syntaxique qui peut clarifier les choses, même si dans le cas présent ça ne saute pas aux yeux.
On peut en tous cas séparer la liste des valeurs, de la logique à y appliquer.
III-C-2-c. Lambda à paramètre unique▲
Vous souvenez-vous de l’exemple où l’on souhaitait trier un tableau en ne retenant que les nombres pairs ?
On pourrait commencer par le réécrire simplement avec une lambda
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.filter({a -> a % 2
== 0
}))
println(java.util.Arrays.toString(resultats))
}
Par contre je trouve bien dommage de devoir créer un paramètre a juste ensuite tester son modulo : pas-vous ?
Et bien il est possible de contourner cette bizarrerie :
fun
main(args: Array<String
>) {
var
resultats = arrayOf(1
,2
,3
,4
,5
,6
)
println(resultats.filter({it
% 2
== 0
}))
println(java.util.Arrays.toString(resultats))
}
Dans une lambda à unique argument, il est possible de faire appel à cet argument avec le mots-clé it. Une fois de plus, l’écriture du calcul est plus élégante.
IV. Les classes : syntaxe de base▲
IV-A. Classe vide▲
En kotlin, il est possible de déclarer et instancier des classes vides
class
ClasseVide
fun
main(args: Array<String
>) {
val
instance = ClasseVide()
}
Remarquez :
- les accolades ne sont pas obligatoires lors de la déclaration d’une classe vide,
- on créé une classe simplement en suffixant le nom de la classe avec des parenthèses : aucun mot-clé new.
Évidemment, dans le cas présent, une telle classe vide ne sert à rien.
IV-B. Classe englobant des propriétés et méthodes▲
Dès lors, nous pouvons ajouter des variables (une variable dans une classe est une propriété) et fonctions (une fonction dans une classe est plus communément appelée méthode).
class
Personne {
val
nom = "Jean"
var
age = 0
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne()
println(humain.nom)
humain.age += 1
println(humain.age)
humain.grandir()
println(humain.age)
}
IV-C. Visibilité des propriétés et méthodes dans les classes▲
Dans l’exemple précédent, nous pouvions accéder à la variable age d’être humain directement depuis la fonction principale. En effet, en Kotlin, par défaut, les éléments d’une classe sont public. C’est-à-dire que l’on peut y accéder aussi bien dans le code de définition de la classe qu’à l’extérieur (ici, dans la fonction principale).
Mais il est possible de restreindre tout ou partie des variables et méthodes de la classe à une utilisation interne :
class
Personne {
val
nom = "Jean"
private
var
age = 0
fun
grandir() {
age += 1
}
fun
getAge() = age
}
fun
main(args: Array<String
>) {
val
humain = Personne()
println(humain.nom)
// Interdit !!! humain.age += 1
// Interdit !! println(humain.age)
println(humain.getAge())
humain.grandir()
println(humain.getAge())
}
Ici nous avons restreint l’accès de la variable age à la classe, avec le mot-clé private. Cela permet notamment d’avoir un meilleur contrôle sur l’évolution de cette variable. Et pour permettre aux utilisateurs de continuer à pouvoir lire l’age, une méthode supplémentaire getAge() a été ajoutée.
IV-D. Personnalisation des propriétés mutables▲
Dans le code précédent, nous avons rendu la propriété age privée, et créé une méthode getAge() afin de pouvoir y accéder depuis l’extérieur.
Mais nous pouvons faire mieux : laisser la propriété age publique, et personnaliser son accesseur en écriture (setter).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
class
Personne {
val
nom = "Jean"
var
age = 0
private
set
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne()
println(humain.nom)
// Interdit !!! humain.age += 1
println(humain.age)
humain.grandir()
println(humain.age)
}
Ainsi :
- dans la classe Personne nous avons restreint l’accès du setter de la propriété age à la classe elle-même,
- dans la méthode principale nous pouvons simplement accéder à la propriété age, mais uniquement en lecture (ligne 11) : il est impossible de la modifier par un accès direct depuis la fonction principale (lignes 13 et 15).
Cette personnalisation de getter/setter est ici possible parce que la propriété age est variable (de type var).
V. Utilisation plus élaborée des classes▲
V-A. Constructeur primaire▲
Dès lors qu’une classe doive disposer d’un unique constructeur (nous verrons le cas de constructeurs multiples dans la section suivante), il faut respecter la syntaxe du constructeur primaire.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
class
Personne(nom: String
) {
val
nom: String
var
age = 0
private
set
init
{
this
.nom = nom
}
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne("Jean"
)
println(humain.nom)
println(humain.age)
humain.grandir()
println(humain.age)
}
Ainsi la liste des paramètres est précisée juste après le nom de classe, et entre parenthèses. En outre, pour chaque paramètre, telle la déclaration d’un paramètre de fonction, il faut préciser le nom de la variable avant son type.
Remarquez également que la propriété nom pouvait ne pas être attribuée de valeur immédiatement (ligne 2), parce que cela a été fait dans le bloc d’initialisation init (lignes 6-8).
Et donc, grâce au constructeur primaire, c’est lors de la construction de l’instance que nous attribuons une valeur pour le nom (ligne 17).
Mais nous pouvons simplifier ce code :
class
Personne(val
nom: String
) {
var
age = 0
private
set
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne("Jean"
)
println(humain.nom)
println(humain.age)
humain.grandir()
println(humain.age)
}
Ce que nous avons fait ici :
- nous avons préfixé le paramètre nom du constructeur primaire par le mot-clé val. De cette manière, Kotlin ajoutera automatiquement une propriété nom en lecture-seule, et dont la valeur sera celle passée en paramètre lors de la construction de l’instance (ligne 11),
- de ce fait, nous avons pu supprimer le bloc init, devenu inutile.
Et si nous faisions de même pour la propriété age ?
class
Personne(val
nom: String
, var
age: Int
= 0
) {
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne("Jean"
)
println(humain.nom)
println(humain.age)
humain.grandir()
println(humain.age)
val
deuxiemeHumain = Personne("Alice"
, 18
)
println(deuxiemeHumain.age)
}
Nous avons donc :
- ajouté le paramètre age dans le constructeur primaire avec une valeur par défaut,
- supprimé la propriété age de la classe
En revanche, rien ne nous empêche ici de modifier l’âge depuis l’extérieur. Et si nous avions voulu utiliser une propriété en lecture-seule à la place, la méthode grandir() n’aurait pu fonctionner.
C’est donc inconvénient de cette technique : l’impossibilité de restreindre l’accès au setter.
V-B. Constructeurs auxiliaires▲
Si nous voulons d’autres constructeurs, il faut les déclarer à l’intérieur de la classe, et les baser sur le constructeur principal.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
class
Personne(val
nom: String
, var
age: Int
) {
constructor
(nom: String
) : this
(nom, 0
) {
// Nous pouvons même effectuer d'autres tâches
// d'initialisation ici
println("Dans un constructeur auxiliaire"
)
}
fun
grandir() {
age += 1
}
}
fun
main(args: Array<String
>) {
val
humain = Personne("Jean"
)
println(humain.nom)
println(humain.age)
humain.grandir()
println(humain.age)
val
deuxiemeHumain = Personne("Alice"
, 18
)
println(deuxiemeHumain.age)
}
J’ai volontairement supprimé la valeur par défaut du paramètre age dans le constructeur principal afin que l’exemple soit plus pédagogique.
C’est donc le mot-clé constructor qui permet de déclarer un constructeur auxiliaire. De prime abord, sa signature ressemble énormément à la signature d’une fonction. Mais vous avez peut-être remarqué l’appel à this(nom, 0)juste avant les accolages. C’est l’équivalent de l’appel à super() du Java/C++, et qui permet d’abord d’initialiser l’instance en faisant appel au constructeur primaire (donc ici nous initialisons l’age à 0). Enfin il est également possible d’effectuer d’autres instructions après cet appel, grâce au code situé entre les accolades.
V-C. Classes imbriquées▲
En Kotlin, il est également possible de déclarer des classes imbriquées.
class
Externe(val
a: Int
) {
class
Interne(var
b: Int
) {
fun
augmenterB() {
b += 1
}
}
}
fun
main(args: Array<String
>) {
val
externe = Externe(3
)
val
interne = Externe.Interne(2
)
println(externe.a)
println(interne.b)
interne.augmenterB()
println(interne.b)
}
Prêtez bien attention d’ailleurs aux différentes casses (majuscule/minuscule) des différentes instances et classes, en particulier lors de la création de la variable interne (ligne 10).
V-D. Les Objets▲
Les Objets regroupent des fonctions que l’on peut appeler sans devoir instancier le modèle en question (toutes leurs méthodes et valeurs sont des équivalents des méthodes et valeurs statiques de Java/C++).
Un Objet est en quelque sorte, une classe Singleton, pour ceux qui connaissent ce patron de conception.
object
Math {
fun
moyenne(valeurs: Array<Int
>): Double
{
if
(valeurs.isEmpty()) return
0.0
return
(valeurs.sum() / valeurs.size).toDouble()
}
fun
min(valeurs: Array<Int
>): Int
{
return
valeurs.min() ?: 0
}
}
fun
main(args: Array<String
>) {
println(Math.moyenne(arrayOf(2
,10
,18
,13
)))
println(Math.min(arrayOf(3
,1
,10
)))
}
Ainsi, pour accéder aux méthodes moyenne et min, je n’ai pas eu à créer d’instance de la classe Math. En revanche, j’accède à ces méthodes « en tant que propriétés de la classe Math ».
Attention toutefois: un objet ne peut être imbriquée que dans un autre objet ou une classe non-interne.
V-E. Les Objets compagnons (companion object)▲
Il est possible d’ajouter des méthodes « statiques » dans une classe grâce à la fonctionnalité objet compagnon.
class
Personne(val
nom: String
, age: Int
)
{
var
age = age
private
set
fun
grandir()
{
age += 1
}
companion
object
Math {
fun
moyenne(valeurs: Array<Int
>): Double
{
if
(valeurs.isEmpty()) return
0.0
return
(valeurs.sum() / valeurs.size).toDouble()
}
fun
min(valeurs: Array<Int
>): Int
{
return
valeurs.min() ?: 0
}
}
}
fun
main(args: Array<String
>) {
println(Personne.moyenne(arrayOf(2
,10
,18
,13
)))
println(Personne.min(arrayOf(3
, 12
, 8
)))
val
guillaume = Personne("Guillaume"
, 15
)
println(guillaume.nom)
}
- Je déclare un objet compagnon dans la classe Personne : je peux donc directement appeler les méthodes moyenne et min de la classe Personne, sans même avoir instancié de variable du modèle Personne. Ce sont des méthodes « statiques » ajoutées à la classe Personne.
Nous pouvons même ignorer le nom du compagnon objet dans la classe Personne :
class
Personne(val
nom: String
, age: Int
)
{
var
age = age
private
set
fun
grandir()
{
age += 1
}
companion
object
{ // Il n’y a plus le nom Math
fun
moyenne(valeurs: Array<Int
>): Double
{
if
(valeurs.isEmpty()) return
0.0
return
(valeurs.sum() / valeurs.size).toDouble()
}
fun
min(valeurs: Array<Int
>): Int
{
return
valeurs.min() ?: 0
}
}
}
fun
main(args: Array<String
>) {
println(Personne.moyenne(arrayOf(2
,10
,18
,13
)))
println(Personne.min(arrayOf(3
, 12
, 8
)))
val
guillaume = Personne("Guillaume"
, 15
)
println(guillaume.nom)
}
V-F. Les fonctions d’extension▲
Les fonctions d’extensions permettent d’enrichir une classe déjà existante (mais juste dans le contexte du projet dans lequel on se situe, évidemment).
Vous connaissez déjà certaines fonctions d’extension : les fameuses fonctions map/filter/reduce que nous avons utilisées avec certaines collections.
Ainsi, le type Array est basé sur les tableaux natifs de Java, lesquels ne possèdent aucune des trois méthodes.
On peut même, de cette manière, « augmenter » le type String, pourtant à l’origine immuable en Java.
fun
String
.capitalize() : String
{
return
"
${this[0].toUpperCase()}${this.substring(1)}
"
}
fun
main(args: Array<String
>) {
println("hello"
.capitalize())
}
Ici j’ai défini une nouvelle méthode capitalize() sur la classe String :
- c’est presque une fonction classique : il faut juste penser à préfixer le nom de la méthode par le nom de la classe destinataire (y compris nos propres types) suivi d’un point,
- à l’intérieur de la fonction, this représente l’instance de l’objet sur lequel a été appelée la méthode (pour les non habitués à Java, c’est un concept de la programmation orientée objet dont nous reparlerons ultérieurement),
- évidemment, nous ne sommes pas limité à une fonction sans argument,
- de même nous pouvons utiliser la simple syntaxe d’une expression en valeur de retour.
Un autre exemple intéressant, c’est d’écrire une méthode pour les nombres premiers :
fun
Int
.estPremier() : Boolean {
return
(2
until this
).none{this
% it
== 0
}
}
fun
main(args: Array<String
>) {
for
(i in
1..20
)
{
println("
$i
est premier ?
${if (i.estPremier()) "oui" else "non"}
"
)
}
}
Remarquez :
- 2 until 5 représente une intervalle commençant à 2 mais n’incluant pas 5,
- la fonction d’ordre supérieur none est une méthode présente dans les intervalle et permettant de vérifier qu’aucun élément ne vérifie la condition énoncée.
Une fonction d’extension a évidemment accès à l’ensemble des membres et méthodes publiques de la classe en question.
Mais il est aussi possible de définir une fonction d’extension sur un type « nullable » !
fun
Int
?.estPair() : Boolean {
if
(this
== null
) return
false
return
this
% 2
== 0
}
fun
main(args: Array<String
>) {
val
i:Int
? = null
val
j = 3
println(i.estPair())
println(j.estPair())
}
Et donc, comme l’extrait de code précédent le démontre, nous pouvons éventuellement tester si la valeur est null afin de réaliser un traitement adéquat.
V-G. Les propriétés d’extension▲
Tout comme nous pouvons ajouter des méthodes aux classes existantes, nous pouvons ajouter des propriétés.
val
String
.demiTaille: Int
get
() = length / 2
fun
main(args: Array<String
>) {
println("Hello World !"
.demiTaille)
}
Il faut absolument respecter cette forme :
- on définit le type de la propriété sans l’initialiser
- on ajouter un getter pour la propriété (d’ailleurs ici il fait appel à une autre propriété de la classe String : length).
Cela fonctionne également avec les génériques, que nous verrons sous peut-être
val
<T> Array<T>.demiTaille : Int
get
() = size / 2
fun
main(args: Array<String
>) {
println(arrayOf(2
,3
,4
,5
,6
).demiTaille)
println(arrayOf(1.0
,2.0
,3.0
,4.0
,5.0
).demiTaille)
}
V-H. Les compagnons d’objet d’extensions▲
Tout compagnon d’objet de classe peut se voir ajouter des fonctions et valeurs. Le mécanisme est le même que pour les fonctions et propriétés d’extension.
class
Vecteur(val
dx: Double
, val
dy: Double
) {
companion
object
{
}
}
// Mince l'auteur original a oublie d'ajouter le vecteur nul en tant que valeur de classe
// Pas grave, je vais le faire moi-meme, localement
val
Vecteur.Companion.NUL: Vecteur
get
() = Vecteur(0.0
, 0.0
)
fun
main(args: Array<String
>) {
println(Vecteur.NUL.dx)
println(Vecteur.NUL.dy)
}
Ainsi j’ai appliqué une propriété d’extension sur le compagnon d’objet de la classe Vecteur. Ici le nom du compagnon d’objet s’appelle tout simplement Companion, étant donné qu’il est anonyme dans la classe que l’on souhaite étendre. Pour être plus complet, j’aurais d’ailleurs pu ajouter une extension de méthode sur la classe Vecteur elle-même afin d’en calculer la norme (la longueur du vecteur).
Nous reparlerons des objets compagnons ultérieurement.
V-I. Introduction basique aux génériques▲
V-I-1. Fonction générique▲
Dans sa forme la plus basique, une fonction générique est une fonction dont tout ou partie des paramètres fonctionne sans restriction sur le type d’argument.
fun
<T> afficher(a: T, b: T) {
println("A est
$a
, B est
$b
"
)
}
fun
main(args: Array<String
>) {
afficher(3
,2
)
afficher(2.0
,10.0
)
afficher("Hello"
, "World !"
)
}
- Donc, entre le mot-clé fun et la signature de la fonction, j’insère une liste d’identifiants de types. Ici, il n’y a qu’un réceptacle de type que l’on a nommé T,
- Ensuite j’ai pu déclarer chaque paramètre comme étant du type T,
- Nous avons pu utiliser la fonction avec plusieurs types de données.
Mais nous ne sommes pas limité à exiger un unique type pour la liste des paramètres, ni à exiger un type pour l’ensemble des paramètres.
fun
<T, U> afficher(a: T, b: U) {
println("A est
$a
, B est
$b
"
)
}
fun
main(args: Array<String
>) {
afficher(3
, 'a')
afficher(2.0
, "euros"
)
afficher("Bonjour"
, "les codeurs !"
)
}
L’utilisation de deux String dans le dernier exemple est purement pédagogique.
fun
<T> afficher(a: T, b: Int
) {
println("A est
$a
, B est
$b
"
)
}
fun
main(args: Array<String
>) {
afficher(3
, 10
)
afficher(2.0
, 20
)
afficher("Bonjour"
, 30
)
}
L’ordre de déclarations des types aussi est libre :
fun
<T, U> afficher(a: U, b: T) {
println("A est
$a
, B est
$b
"
)
}
fun
main(args: Array<String
>) {
afficher(3
, 'a')
afficher(2.0
, "euros"
)
afficher("Bonjour"
, "les codeurs !"
)
}
Enfin, le choix des noms T et U n’est nullement obligatoire : il s’agit juste d’une convention.
V-I-2. Classe générique▲
Les classes génériques se déclarent et fonctionnent de manière similaire
class
MaPaire<T, U>(val
prem: T, val
deux: U)
fun
main(args: Array<String
>) {
val
paire = MaPaire(10
, "euros"
)
println(paire.prem)
println(paire.deux)
}
Cette fois-ci, la liste des types à fournir est déclarée en pleine signature de classe : entre le nom de la classe et son constructeur primaire si définit.
V-I-3. Aller plus loin▲
Une couverture complète des génériques ne rentre pas dans le cadre de ce tutoriel. Toutefois, je ne peux que vous inviter à aller consulter la documentation officielleDocumentation officielle sur les génériques (en anglais).
VI. L’héritage de classes▲
En Kotlin, comme dans de nombreux langages, il est possible de bénéficier des fonctionnalités de la Programmation Orientée Objets afin de pouvoir dupliquer valeurs ou comportements entre plusieurs entités à moindre frais.
L’entité classique est la classe, mais pas seulement : il est par exemple possible de combiner plusieurs interfaces en une. (Nous arriverons très vite sur les interfaces.)
VI-A. Bases de l’héritage▲
VI-A-1. Héritage simple▲
Tout d’abord, un simple rappel sur l’héritage dans la programmation orientée objets.
Prenons, pour illustre avec un cas de la vie courante, le cas d’un véhicule. Quand nous parlons de véhicule, nous parlons de véhicule, notamment nous pensons à quelque chose
- avec des roues,
- une ou plusieurs places où s’installer,
- …, et j’en passe
Ce que je viens de décrire, c’est la représentation commune que l’on se fait d’un véhicule.
Maintenant, nous pouvons diviser les véhicules en plusieurs catégories, disons par exemple
-
les automobiles :
- disposant de moteurs,
- pouvant reculer
-
les vélos :
- disposant d’un pédalier
- disposant d’un guidon
Je viens donc de décrire une relation d’héritage :
- une automobile est un véhicule, avec certaines caractéristiques communes aux vélos, mais aussi des attributs et comportements propres
- on peut appliquer le même raisonnement aux vélos
Une automobile est un véhicule, un vélo est un véhicule.
En Kotlin, nous pouvons donc définir une classe de base Vehicule :
open
class
Vehicule(nombreRoues: Int
, nombrePlaces)
Nous avons ajouté le mots-clé open, qui nous autorisera à dériver la classe Vehicule, notamment par les classes Velo et Automobile. Justement, définissons-les :
class
Velo : Vehicule(2
,1
)
class
Automobile(nombreRoues: Int
, nombrePlaces: Int
) : Vehicule(nombreRoues, nombrePlaces)
Il a donc suffit, pour chacune des sous-classes, ajouter :
- les deux-points,
- le nom de classe de base,
- les valeurs à passer au constructeur primaire de la classe de base.
Ainsi, nous déclarons un Velo comme étant un Vehicule ayant 2 roues (afin de simplifier notre exemple) et une unique place. Par contre, nous laissons de la flexibilité en ce qui concerne la classe Automobile.
Ne l’oubliez pas, le mot-clé open est obligatoire afin de rendre une classe héritable !
Enfin, nous pouvons en créer des instances comme l’on a l’habitude
fun
main(args: Array<String
>) {
val
monVehicule = Vehicule(3
, 1
)
// println(monVehicule.nombreRoues): erreur ! Sera resolue plus tard
// println(monVehicule.nombrePlaces): erreur ! Sera resolue plus tard
val
monVelo = Velo()
// println(monVelo.nombreRoues) : erreur ! Sera resolue plus tard
// println(monVelo.nombrePlaces) : erreur ! Sera resolue plus tard
val
maVoiture = Automobile(4
,4
)
// println(maVoiture.nombreRoues) : erreur ! Sera resolue plus tard
// println(maVoiture.nombrePlaces) : erreur ! Sera resolue plus tard
}
Remarquez également qu’aucune de ces trois classes ne dispose des propriétés nombreRoues et nombrePlaces: ce qui est normal, étant donné que l’on a juste passé ces définitions en tant que valeurs du constructeur primaire de la classe Vehicule.
Par contre, il suffit de le rendre propriétés au sein de la classe Vehicule, soit en ajoutant des mots-clés val/var aux paramètres désirés du constructeur primaire (comme nous l’avons vu auparavant), soit en ajoutant des propriétés dédiées à l’intérieur de la classe Vehicule.
open
class
Vehicule(val
nombreRoues: Int
, val
nombrePlaces: Int
)
class
Velo : Vehicule(2
,1
)
class
Automobile(nombreRoues: Int
, nombrePlaces: Int
) : Vehicule(nombreRoues, nombrePlaces)
fun
main(args: Array<String
>) {
val
monVehicule = Vehicule(3
, 1
)
println(monVehicule.nombreRoues)
println(monVehicule.nombrePlaces)
val
monVelo = Velo()
println(monVelo.nombreRoues)
println(monVelo.nombrePlaces)
val
maVoiture = Automobile(4
,4
)
println(maVoiture.nombreRoues)
println(maVoiture.nombrePlaces)
}
Tout d’abord, remarquez que l’appel au constructeur primaire de la classe de base doit obligatoirement se faire avant le corps de la classe.
L’ajout des mots-clé val dans le constructeur primaire de la classe Vehicule a permit de disposer des propriétés voulues dans les trois classes liées hiérarchiquement. Ainsi, il n’a pas été nécessaire de déclarer des propriétés nombreRoues et nombrePlaces dans aucune des deux classes Velo et Vehicule.
En ce qui concerne l’autre méthode :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
open
class
Vehicule(nombreRoues: Int
, nombrePlaces: Int
)
{
val
nombreRoues: Int
val
nombrePlaces: Int
init
{
this
.nombreRoues = nombreRoues
this
.nombrePlaces = nombrePlaces
}
}
class
Velo : Vehicule(2
,1
)
class
Automobile(nombreRoues: Int
, nombrePlaces: Int
) : Vehicule(nombreRoues, nombrePlaces)
fun
main(args: Array<String
>) {
val
monVehicule = Vehicule(3
, 1
)
println(monVehicule.nombreRoues)
println(monVehicule.nombrePlaces)
val
monVelo = Velo()
println(monVelo.nombreRoues)
println(monVelo.nombrePlaces)
val
maVoiture = Automobile(4
,4
)
println(maVoiture.nombreRoues)
println(maVoiture.nombrePlaces)
}
Cette fois-ci nous avons ajouté manuellement les propriétés dans la classe Vehicule. Le mot-clé this fait référence à l’instance de la classe Vehicule (donc c’est éventuellement aussi une instance de la classe Velo ou Automobile) dans laquelle nous nous situons, ou plutôt que nous essayons d’initialiser, et permet ici d’éviter la confusion avec les paramètres du constructeur primaire.
Évidemment, parmi les automobiles, nous pouvons notamment distinguer :
- les voitures
- les cyclomoteurs
Et parmi les cyclomoteurs : les scooters, les motos… rien ne nous limite dans la profondeur de la hiérarchie.
open
class
Vehicule(val
nombreRoues: Int
, val
nombrePlaces: Int
)
class
Velo : Vehicule(2
,1
)
open
class
Automobile(nombreRoues: Int
, nombrePlaces: Int
) : Vehicule(nombreRoues, nombrePlaces)
class
Voiture: Automobile(4
,4
)
Évidemment, il a fallu ajouter le mot-clé open à la déclaration de la classe Automobile.
Enfin, note importante, une classe ne peut hériter que d’une seule classe à la fois. Plus tard, avec les interfaces, nous verrons comment un peu contourner cette limitation.
VI-A-2. La visibilité protected▲
Dans la section IV-C, nous avons vu les visibilités public et private. Lorsque nous définissons une relation d’héritage entre classes, la prise en compte des visibilités est légèrement plus complexe :
- un membre ou une fonction public dans une classe, le sera aussi dans l’ensemble des classes filles, Et ce, quelle que soit la profondeur d’héritage,
- un membre ou une fonction private dans une classe ne sera visible que dans la définition de la classe proprement dite : elle ne sera accessible ni de l’extérieur, ni des classes filles.
Il y a donc une troisième possibilité, plus souple que private, qui est protected :
- une propriété ou fonction déclarée protected ne sera pas accessible de l’extérieur,
- en revanche, elle sera accessible dans les classes filles, quelles qu’en soient la profondeur.
VI-A-3. Héritage et constructeurs secondaires▲
Dès lors que l’on ajoute des constructeurs secondaires dans une classe fille, la syntaxe simple dans laquelle nous avions simplement suffixé le nom de la classe-mère avec les paramètres n’est plus autorisée :
2.
3.
4.
open
class
Vehicule(val
nombreRoues: Int
, val
nombrePlaces: Int
)
class
Velo: Vehicule(4
,4
) {
constructor
(): super
(3
,3
)
}
La ligne 2 provoquera une erreur de compilation. Il y a deux remèdes possibles :
- ajouter un constructeur primaire à la classe fille Velo, et baser les constructeurs secondaires dessus comme nous l’avons déjà vu,
- ou bien, s’il n’y a pas de constructeur primaire dans la classe fille Velo, chaque constructeur secondaire devra faire appel au constructeur primaire de la classe mère grâce au mot-clé super.
(Exemple purement pédagogique.)
open
class
Vehicule(val
nombreRoues: Int
, val
nombrePlaces: Int
)
class
Velo: Vehicule {
constructor
(): super
(3
,3
)
constructor
(moitieNombrePlaces: Int
): super
(4
, moitieNombrePlaces * 2
)
}
fun
main(args: Array<String
>) {
val
velo1 = Velo()
println(velo1.nombrePlaces)
println(velo1.nombreRoues)
val
velo2 = Velo(2
)
println(velo2.nombrePlaces)
println(velo2.nombreRoues)
}
VI-B. Héritage plus élaboré▲
VI-B-1. Héritage de classes abstraites▲
Il se peut que, dans la définition de notre hiérarchie de classes, nous ne souhaitions pas que l’utilisateur puisse créer et utiliser des instances de la classe racine (la classe au sommet de la hiérarchie). Cela peut-être notamment utile dans l’utilisation du polymorphisme, que nous verrons ultérieurement.
Voici un cas concret: une hiérarchie de figures géométriques, appelons la racine Shape. Une figure, en soit, ne définit pas grand-chose, En revanche, des triangles, rectangles, carrés…, oui. Nous pouvons donc décider de rendre la classe Shape abstraite.
Pour cela, il faut:
- déclarer la classe comme abstract,
- déclarer ce qui doit absolument être redéfini dans les classes filles (propriétés ou fonctions) comme abstract.
En revanche, il est possible de définir des propriétés et fonctions par défaut : elles ne seront donc pas déclarées abstract.
Voici ce que cela pourrait donner en code:
Veuillez donc remarquer:
- il est possible, dans une classe abstraite, d’utiliser des propriétés/méthodes abstraites et non abstraites,
- il est impossible d’instancier la classe Figure
- il n’y a aucune obligation d’utiliser le mots-clé open, car tout élément abstrait doit obligatoirement être défini dans toute classe fille destinée à être instanciée. Et ce, une fois de plus, quelle que soit la profondeur. On appelle classe concrète, par opposition à une classe abstraite, une classe qui a l’ensemble de ses éléments définis, et qui donc dont l’on peut créer des instances,
- les classes filles n’ont donc pas pour obligation d’être toutes concrètes et de définir l’ensemble des propriétés/méthodes de leurs classes ancêtres. Seules celles dont on doit pouvoir créer des instances.
VI-B-2. Héritage d’interfaces▲
Outre les classes abstraites, une autre notion est fréquemment utilisée dans la programmation orientée objets: la notion d’interfaces.
Les interfaces sont en quelques sortes des classes abstraites, mais ayant l’interdiction de stocker des états. Par exemple, dans notre classe Shape de la section précédente, nous avons utilisé un état, la propriété name. Et bien dans une interface, cela aurait été impossible.
En revanche, il reste possible de définir des implémentations par défaut de méthodes.
Une version interface de la classe Shape pourrait donner:
Tout d’abord, veuillez remarquer:
- c’est avec le simple mot-clé interface que l’on déclare une interface,
- la propriété nom a été supprimée de l’interface Figure,
- il ne faut pas déclarer les parenthèses lorsque l’on hérite de la classe Figure, les interfaces n’ont pas de constructeur, et pour cause, elles n’ont pas d’état.
J’ajouterais même que, les interfaces n’ayant pas d’état, tout membre d’interface est automatiquement abstrait. Il devient donc superflu de déclarer les éléments abstract:
Mais ce qui est vraiment pratique avec les interfaces, c’est que une classe peut hériter de plusieurs interfaces:
VI-B-3. Héritage d’une classe et d’interfaces simultanée▲
Rien ne nous empêche d’hériter d’une classe (abstraite ou non) et d’interfaces en même temps.
Ainsi, nous pouvons adapter l’exemple précédent en définissant une classe commune Animal, que nous rendons ici abstraite, juste par choix:
Ainsi un canard peut et voler et nager, un poisson ne peut que nager. Et il n’a pas été nécessaire de définir les caractéristiques plusieurs fois. Juste deux interfaces qu’il a fallu hériter convenablement.
Pourquoi avoir rajouté une telle classe Animal à la fois abstraite et vide dans cette exemple ? En clair, qu’est-ce que cela nous a-t-il apporté ? Justement la section suivante traite du polymorphisme, où cela prendra plus de sens.
VI-B-4. Le polymorphisme▲
Encore une notion fréquente de la programmation orientée objets.
En Kotlin, tout comme en Java, il est possible de stocker une variable de classe fille dans une variable du type de l’une de ses classes mères. Plus concrètement, dans notre exemple précédent, nous avions des Animal de type Canard et Poisson. Et bien nous pouvons stocker une instance de Canard ou une instance de Poisson dans une variable de type Animal. Bien que Animal soit abstraite, il est possible de déclarer une variable de type Animal, c’est juste le fait de créer une instance de Animal qui est interdit.
Et cette possibilité nous permet d’effectuer du polymorphisme:
Tout d’abord, afin de bénéficier du polymorphisme, j’ai ajouté une méthode bouger() dans la classe Animal, qui est redéfinie par chaque Animal afin de bénéficier d’une fonctionnalité commune à l’ensemble des Animal.
Ensuite, dans la fonction main, j’ai crée un tableau d’Animal. Et donc, en parcourant chaque élément du tableau, c’est la fonctionnalité bouger() propre au type d’Animal qui est appelée.
VII. Autres notions liées aux classes▲
VII-A. Objet compagnon▲
Nous avons déjà rencontré les objets compagnons auparavant dans ce tutoriel lorsque nous souhaitions ajouter des fonctionnalités à des classes existantes(section V-H).
De manière générale, les objets compagnons permettent de définir des propriétés/méthodes comme appartenant à une classe, au lieu de les faire appartenir aux instances de cette classe:
Ainsi j’ai défini une classe Math, contenant une constante PI et une fonction carre. Elles sont directement accessibles via la syntaxe Math.. C’est l’équivalent des propriétés et méthodes statiques du langage Java.
Rien ne nous interdit de déclarer un objet compagnon dans une classe normalement prévue pour créer des instances:
On remarque donc que les éléments définis par un objet compagnon ne restent accessibles que par la syntaxe Classe..
VII-B. Classe anonyme▲
En pratique, de nombreuses fonctions attendent des instances de classes, qui implémentent une interface particulière, en paramètre. Et dans de nombreux cas, nous sommes amenés à créer des instances de telles classes, juste avant d’appeler lesdites fonctions, et sans nous en servir une fois cette fonction terminée. En clair, on a crée un objet temporaire juste pour pouvoir satisfaire la fonction.
Par exemple
Ici, nous avons crée une variable leo, juste pour pouvoir la passer à la fonction direBonjour (l’exemple n’est pas très représentatif de la problématique, je vous l’accorde).
Les classes anonymes, fonctionnalité qui existe notamment déjà en Java, permettent de contourner cette problématique.
Ainsi, nous avons pu appeler la fonction direBonjourA sans même avoir eu à créer un objet de type Personne (qui du coup n’est plus nécessaire). Il s’agit simplement d’un « héritage d’interface » local. Évidement, les interfaces n’ayant pas de constructeur, il ne faut pas de parenthèses lors de la définition de l’interface anonyme.
Il est possible de faire la même chose si le paramètre attendu est une classe:
Veuillez noter l’appel du constructeur parent lors de la définition de la classe anonyme. Tout comme l’on aurait fait pour définir une classe standard.
Dans les deux cas, classe ou interface, la dénomination officielle est expression objet. C’est important car dans la documentation officielle, c’est sous ce nom que l’on y fait référence.
Enfin, tout comme dans toute classe, on peut redéfinir de la même manière les méthodes et propriétés de la hiérarchie, voire même en ajouter d’autres.
VII-C. Les classes de données▲
Les classes de données (data classes) peuvent souvent nous simplifier beaucoup de choses.
Prenons un exemple simple:
Vous avez sans doute remarquez le mot-clé data.
Grâce à cela, nous avons un certain nombre de choses déjà implémentées pour nous, rien que par cet ajout. Entre autres:
- une définition automatique des méthodes equals et hashCode : ce qui nous permet d’utiliser directement l’opérateur == pour comparer des objets de type Personne,
- une définition automatique de la méthode toString(): ce qui nous permet d’afficher proprement les objets de type Personne, avec une commande println par exemple,
- une définition automatique des méthodes component1, component2 et component3. Ici jusqu’à 3 car nous avons 3 paramètres dans notre constructeur. Ces méthodes permettent de déconstruire les objets de type Personne, comme nous l’avons vu auparavant dans ce tutoriel (Section II-i).
En revanche, il y a des restrictions:
- le constructeur primaire doit avoir au moins un paramètre,
- tous les paramètres du constructeur primaire doivent être marqués comme soit val, soit var,
- ces classes ne peuvent être ni open, ni abstract (donc non héritables), ni inner (donc non internes), ni sealed. Nous verrons ce que sont les classes sealed dans la section suivante.
Il y a d’autres restrictions, je vous invite à consulter la documentation officielledocumentation sur les data classes pour approfondir le sujet.
VII-D. Les classes scellées▲
Lorsque l’on veut être certain qu’une classe ne soit pas héritable à l’infini, mais qu’en revanche seulement un certain nombre de classes en soient héritées, on peut la définir comme scellée.
De ce fait, toutes les classes filles doivent être déclarées dans le même fichier que cette classe mère:
Ici je me suis contenté d’adapter l’exemple officiel.
Bien évidemment, ici NaN est bien une classe object au sens propre du terme, et non une classe anonyme.
La classe expression ne peut avoir d’autre classe fille que les classes Constante, Somme et Nan. Et l’on peut utiliser le polymorphisme dans une fonction dédiée pour évaluer des instances de la classe Expression. Ce n’est pas difficile à imaginer, à l’aide d’un algorithme récursif.
VII-E. Les classes énumérations▲
Les classes énumérations sont des énumérations plus puissantes que dans un certain nombre de langages, dont Java.
Une énumération est un type dont l’ensemble des valeurs a été défini à l’avance. C’est donc équivalent aux classes scellées, mais pour des simples valeurs.
De bonnes notions candidates pour les énumérations peuvent être : les mois de l’année, les points cardinaux, les langues vivantes enseignées dans un établissement…
Un exemple simple pour commencer
Prenons maintenant une énumération RGB, qui permettrait d’obtenir du rouge/vert ou du bleu. Nous pouvons faire en sorte de pouvoir lire aussi la valeur (hexadécimale mais codée sous forme d’entier) de la couleur choisie:
Ainsi, grâce au constructeur primaire de l’énumération RVB, nous pouvons en personnaliser et lire la valeur associée.
Allons un peu plus loin:
Une énumération peut aussi avoir des éléments abstraits, que les différentes valeurs doivent alors redéfinir en tant classes anonymes, certes avec une syntaxe épurée:
Attention à ne pas oublier le point-virgule après la définition des valeurs !
Veuillez également remarquer que les éléments n’ont pas besoin de la syntaxe Object : {afin de redéfinir la base de l’énumération.
Sachez aussi qu’il est possible, de manière similaire, d’implémenter des interfaces dans une énumération. N’hésitez pas à aller consulter la documentation officielle si besoin.
VII-F. La surcharge d’opérateurs▲
Kotlin permet de surcharger un certain nombre d’opérateurs au sein de nos propres classes.
Ainsi, on pourrait additionner deux dates de calendrier directement avec la notation d1 + d2, on pourrait multiplier deux matrices à l’aide de m1 * m2 …
Comprenez bien que tout mécanisme permettant d’augmenter une classe : donc l’héritage ou les fonctions d’extension, permettent la surcharge d’opérateurs.
Évidemment, le but ne sera pas d’être exhaustif, mais simplement de vous présenter un simple exemple. Je vous invite donc à vous rendre à la documentation officielleSurcharge d'opérateurs si vous souhaitez approfondir le sujet.
Soit une classe Fraction, et une première tentative infructueuse d’additionner deux fractions:
Une manière élégante de résoudre ce problème consiste à définir l’opérateur + pour la classe Fraction. Mais attention, en Kotlin, les noms des fonctions à redéfinir ne correspondent aux noms des opérateurs ! D’où la bonne idée de regarder la documentation officielle.
En l’occurrence, la fonction à redéfinir pour l’addition s’appelle plus et prends la 2e opérande de l’addition en paramètre:
Et dans une version plus classique, avec une simple méthode:
Dans les deux cas, vous pouvez remarquer l’utilisation du mot-clé operator: obligatoire pour que la méthode soit reconnue en tant que surcharge d’opérateur !