IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction au langage Kotlin

Ce tutoriel est une introduction au langage Kotlin pour les développeurs connaissant déjà le langage Java (ou autre langage oriente objet).

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

  1. 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),
  2. 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),
  3. 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.
Bouton « configure »
Fig 1.1 : bouton « configure »
Installation du plugin Kotlin
Fig 1.2 : installation du plugin Kotlin

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

  1. Démarrez un nouveau projet de Kotlin/JVM et passez a l’étape suivante (fig 1.3),
  2. 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 »).
Projet de type Kotlin/JVM
Fig 1.3: projet de type Kotlin/JVM
Creation du projet en precisant le SDK Java
Fig 1.4 : création du projet en précisant le SDK Java

I-C-2. Ajout du fichier source et exécution

  1. 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),
  2. Entrez le code suivant dans le fichier Hello.kt :
Hello.kt
TéléchargerCacher/Afficher le codeSélectionnez

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).

Creation du fichier source
Fig 1.5: création du fichier source
Execution du programme
Fig 1.6: exécution du programme

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.printlnde 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 :

  1. Accédez d'abord au menu File→Project Structure…,
  2. Sélectionnez la section Artifacts, dans la zone gauche de la nouvelle fenêtre de configuration,
  3. Effectuez alors une command Add→Jar→From module with dependencies… (Fig 1.7),
  4. 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),
  5. Vous pouvez alors nommer le Jar et valider la configuration.
Creation de l'artifact pour le jar
Fig 1.7: création de l'artifact pour le jar
Options de l'artifact
Fig 1.8 : options de l'artifact

Ensuite, il nous faut réaliser la compilation proprement dite :

  1. Exécutez le menu Buid→Build artifacts…,
  2. 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),
  3. 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 :

Test du jar autoexécutable
Sélectionnez
$> cd out/artifacts/BonjourLesDevs_jar
$> java -jar BonjourLesDevs.jar
Test du jar dans le terminal
Fig 1.9: test du jar dans le terminal

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

 
Sélectionnez
// 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.
 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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) :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
for (i in 2..36 step 3) println(i) // Affiche les nombres 2,5,8,&#8230;,35

Il est aussi possible de combiner un intervalle décroissant et une progression définie :

 
Sélectionnez
for (i in 36 downTo 2 step 3) println(i) // Affiche les nombres 36,33,&#8230;,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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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()

 
Sélectionnez
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 :

 
Sélectionnez
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&#160;?
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 :

 
Sélectionnez
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 :

 
Sélectionnez
val mesPiecesCapturees = setOf("Cavalier", "Dame", "Fou")
mesPiecesCapturees.count() // Nombre d'elements
mesPiecesCapturees.filter{it.startsWith("C")} // Set filtre ne retenant que les elements commen&#231;ant par &#171;&#160;C&#160;&#187;
mesPiecesCapturees.map{it.toLowerCase()} // Set o&#249; les elements ont ete convertis en minuscule
mesPiecesCapturees.isEmpty() // Est-il vide&#160;?
mesPiecesCapturees.isNotEmpty() // A-t-il des elements&#160;?
mesPiecesCapturees.maxBy{it.length} // Quel element est le plus long&#160;?

II-E. Les imports / espaces de nommage

Les imports et espaces de nommage fonctionnent dans l'ensemble comme en Java

 
Sélectionnez
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 :

 
Sélectionnez
int a = 10;
int b = 6;
String c = "abc";
System.out.println("[" + a + ", " + b*2 + ", " + c + "]");

En Kotlin on pourra simplement écrire :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
val message = if (age < 18) "C'est un mineur&#160;!" 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 :

 
Sélectionnez
val resultat = while(i < 10) {i += 1} // Strictement interdit&#160;!!!

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 :

 
Sélectionnez
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.

 
Sélectionnez
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) :

 
Sélectionnez
val monTuple = Triple('a', 10, "Toto")
val (a, b, c) = monTuple // deconstruction&#160;!
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
    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.

 
Sélectionnez
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 :

 
Sélectionnez
// 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

  1. 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.
  2. 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.
  3. 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 :

 
Sélectionnez
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) :

 
Cacher/Afficher le codeSélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
switch (a) {
2:
3:
5:
6: println("2,3,5 ou 6");
default: println("autre");
}

On écrira plutôt :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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é.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 !

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
val a:Int? = null
val b:Int = when (a) {
    null -> 0
    else -> a
}
    
println(b)

Mais on peut aussi utiliser l’opérateur elvis :

 
Sélectionnez
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?

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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 ?

 
Sélectionnez
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).

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
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.

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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

  1. Créé une variable numérique valant 0
  2. Prends les deux premiers chiffres du tableau, dans l’ordre : 1 et 2.
  3. Passe-les à la fonction additionner que j’ai reçue et conserve le résultat
  4. Ajoute ce résultat à la valeur courante de la variable
  5. 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)
  6. 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
  7. Recommence à l’étape 4 jusqu’à ce qu’il ne reste plus de valeur à exploiter
  8. 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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

  1. la liste des paramètres,
  2. 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 :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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

 
Sélectionnez
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).

 
Sélectionnez
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 :

 
Sélectionnez
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).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 ?

 
Sélectionnez
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.

 
Sélectionnez
1.
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&#234;me effectuer d'autres t&#226;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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
class Personne(val nom: String, age: Int)
{
    var age = age
        private set
    
    fun grandir()
    {
        age += 1
    }
    
    companion object { // Il n&#8217;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.

 
Sélectionnez
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 :

 
Sélectionnez
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 » !

 
Sélectionnez
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.

 
Sélectionnez
val String.demiTaille: Int
    get() = length / 2

fun main(args: Array<String>) {
   println("Hello World !".demiTaille)
}

Il faut absolument respecter cette forme :

  1. on définit le type de la propriété sans l’initialiser
  2. 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

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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 !")
}
  1. 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,
  2. Ensuite j’ai pu déclarer chaque paramètre comme étant du type T,
  3. 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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
class Velo : Vehicule(2,1)
class Automobile(nombreRoues: Int, nombrePlaces: Int) : Vehicule(nombreRoues, nombrePlaces)

Il a donc suffit, pour chacune des sous-classes, ajouter :

  1. les deux-points,
  2. le nom de classe de base,
  3. 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

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
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 :

 
Sélectionnez
1.
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.)

 
Sélectionnez
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:

Classe abstraite
Cacher/Afficher le codeSélectionnez

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:

La version interface de la classe Figure
Cacher/Afficher le codeSélectionnez

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:

Version simplifiée de l'interface Figure
Cacher/Afficher le codeSélectionnez

Mais ce qui est vraiment pratique avec les interfaces, c’est que une classe peut hériter de plusieurs interfaces:

Heriter de plusieurs interfaces
Cacher/Afficher le codeSélectionnez

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:

Héritage de classe et d'interfaces simultanée
Cacher/Afficher le codeSélectionnez

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:

Polymorphisme
Cacher/Afficher le codeSélectionnez

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:

Companion Object
Cacher/Afficher le codeSélectionnez

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:

Classe standard avec objet compagnon
Cacher/Afficher le codeSélectionnez

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

Création d'une instance de classe quasi-inutile
Cacher/Afficher le codeSélectionnez

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.

« Interface anonyme »
Cacher/Afficher le codeSélectionnez

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:

« Classe anonyme »
Cacher/Afficher le codeSélectionnez

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:

 
Cacher/Afficher le codeSélectionnez

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:

Classe scellée
Cacher/Afficher le codeSélectionnez

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

Enumération points cardinaux
Cacher/Afficher le codeSélectionnez

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:

Enumération avec constructeur primaire
Cacher/Afficher le codeSélectionnez

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:

Enumération avec constructeur primaire complexe
Cacher/Afficher le codeSélectionnez

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:

Enumération avec élément abstrait
Cacher/Afficher le codeSélectionnez

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:

Tentative infructueuse d'additionner deux fractions
Cacher/Afficher le codeSélectionnez

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:

Correction avec une extension de fonction
Cacher/Afficher le codeSélectionnez

Et dans une version plus classique, avec une simple méthode:

Correction avec une simple méthode
Cacher/Afficher le codeSélectionnez

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 !

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2020 Laurent Bernabe. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.