Le Langage
- 1. Présentation & Origines
- 2. Comment coder du Kotlin ?
- 3. Avant-propos
- 4. Eléments de programmation impérative avec Kotlin
- 4.1. Point d’entrée du programme
- 4.2. Les variables (déclaration & affectation)
- 4.3. Apperçu des principaux types :
- 4.4. Les déclarations & affectations sur les variables
- 4.4.1. Rappel sur la notion de référence
- 4.4.2. Déclarations & affectations du type Boolean
- 4.4.3. Déclarations & affectations des types associés aux valeurs numériques
- 4.4.4. Déclarations & affectations du type Char et String
- 4.4.5. Conversions de types (transtypage)
- 4.4.6. Déclaration & affectations du type Array
- 4.4.7. Déclarations & affectations sur les collections
- 4.4.8. L’inférence de type
- 4.4.9. Cas de l’initialisation d’une variable avec null
- 4.4.10. Déclaration des variables constantes
- 4.4.11. L’entrée standard avec readLine() et readln()
- 5. Les structures/expressions conditionnelles
- 6. Les structures itératives
- 7. Programmation procédurale
- 7.1. Définition & appel d’une fonction
- 7.1.1. Avant propos
- 7.1.2. En-tête de la fonction
- 7.1.3. Corps de la fonction
- 7.1.4. Appel des fonctions
- 7.1.5. Les fonctions "Higher-order functions" (fonction d’ordre supérieur)
- 7.1.6. Les fonctions lambdas et les fonctions anonymes :
- 7.1.7. Visibilité des fonctions
- 7.1.8. Le smartCast (transtypage intelligent)
- 7.1.9. Les fonctions génériques
- 7.1. Définition & appel d’une fonction
- 8. La P.O.O. (Programmation Orientée Objet)
- 8.1. Les classes
- 8.2. Les attributs & méthodes de classe (Les objets compagnons)
- 8.3. L’héritage
- 8.3.1. Principes de base de l’héritage en Kotlin
- 8.3.2. Ouvrir une classe à l’héritage
- 8.3.3. Héritage d’une classe de base vers une classe dérivée
- 8.3.4. Mise en relation d’une classe dérivée avec une classe de base ayant un constructeur AVEC arguments
- 8.3.5. Enrichissement d’une classe dérivée : Ajout d’attributs et de méthodes
- 8.3.6. Redéfinition des attributs et/ou méthodes d’une classe de base
- 8.3.7. Utilisation du mot clé super
- 8.3.8. Les classes abstraites
- 8.3.9. Les interfaces
- 8.3.10. Définition d’une interface en Kotlin
- 8.4. Les extensions
- 8.5. Allons plus loin avec les Classes
- 8.5.1. Redéfinition de la méthode toString() de la classe Any
- 8.5.2. Les Data Class (classe de données)
- 8.5.3. Classes anonymes (expressions objets)
- 8.5.4. Les énumérations
- 8.5.5. Les classes scellées
- 8.5.6. La surcharge d’opérateurs
- 8.5.7. Surcharge des opérateurs d’incrémentation/décrémentation
- 8.5.8. Les classes génériques
1. Présentation & Origines
Kotlin est un langage relativement relativement récent (2011). Il est dévellopé par l’éditeur JetBrains. Le nom du langage vient de l’île de Kotlin en Russie car l’équipe de développement était justement basé à St-Pétesbourg.
L’objectif de Kotlin est d’offrir une alternative au langage Java tout en permettant le fonctionnement du code Java existant.
Pour ce faire, Kotlin va être compilé en byte code pour la JVM. L’avantage est double :
-
Kotlin hérite naturellement du caractère multiplateforme de Java;
-
Les anciens programmes compilés à partir demeurent opérationnels, ce qui augure une existante parallèle des 2 langages.
Sources : Page Wikipédia de Kotlin
1.1. Pourquoi choisir Kotlin ?
Kotlin se veut être un langage moderne afin de faciliter le développement d’application. Il garde les bases syntaxiques de Java mais en offrant les facilités de langages comme C# ou Python entres autres.
Kotlin est appréciable par ses qualités intrinsèques :
Programmation sur plusieurs paradigmes :
-
Procédural;
-
Orienté Objet (POO);
-
Fonctionnel.
La syntaxe est plus concise là ou Java était un peu verbeu.
Simplification du développement car Kotlin permet de réaliser des automatisations comme par exemple la génération des accesseurs et mutateurs pour les attributs des classes.
Kotlin renforce les bonnes pratiques et la fiabilité du code développé.
Kotlin est déjà un langage de référence :
En 2017 Google adopte officiellement Kotlin comme second langage de dévellopement pour son OS Android. En 2019 Google fait passer Kotlin comme langage recommandé pour le développement sous Android.
Du côté serveur, le framework Spring supporte officiellement Kotlin en 2017.
1.2. Références
-
Le site officiel : Accueil Kotlin
-
Documentation officielle : Documentation Kotlin
-
Le playground pour tester du code en ligne : Playground
2. Comment coder du Kotlin ?
Le plus simple est d’utiliser IntelliJ IDEA de JetBrains. C’est l’IDE de prélidiction pour Java et encore plus pour développer Kotlin.
Il existe 2 version de cet IDE, la version gratuite (Community Edition) et la version payante (Ultimate).
Le téléchargement des 2 versions est disponible sur la page d’accueille d’IntelliJ : IntelliJ IDEA
Le compilateur sera fourni et prise en charge par IntelliJ. Android Studio prend également en charge Kotlin pour le développement d’application
En cas d’utilisation d’un autre IDE ou éditeur de texte, il faudra installer le compilateur qui est mis à disposition sur GitHub. La documentation officielle consacre un partie sur l’utilisation de compilateur.
-
Compilateur Kotlin : Releases sur GitHub
-
Documentation du compilateur : Doc du compilateur
3. Avant-propos
Kotlin est un langage multi-paradigmes (procédurale, POO et fonctionnel), cependant il ne laisse pas totalement le choix du paradigme. La logique globale était d’avoir un langage permettant de produire du code plus rapidement (syntaxe plus courte que Java) mais sécurisé. Pour arriver à ses fins Kotlin s’appuie sur une typage fort et un logique de programmation fonctionnelle.
Il en résulte qu’il n’est pas aisé de segmenter totalement l’étude du langage, il faut aborder de front plusieurs éléments du langage pour comprendre certains mécanisme de ses instructions.
4. Eléments de programmation impérative avec Kotlin
4.1. Point d’entrée du programme
Le paradigme de la POO n’étant pas imposé en Kotlin, nous ne sommes pas/plus obligé de créer une classe accueillant la méthode statique main().
Tout comme en C/C++ c’est la fonction main() qui est sert de point d’entrée.
La définition d’une fonction se fait avec le mot clé fun
fun main(){
println("Hello World !")
}
Comparaison avec la version Java :
class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World !");
}
}
On constate les différences suivantes :
-
Pas de classe et donc pas de modificateur static
-
On ne précise pas que la fonction main() ne retourne rien (absence de void)
-
Pas de point virgule ; pour marquer la fin d’une instruction, c’est le saut de ligne qui s’en charge.
-
La fonction println() est directement accessible.
4.2. Les variables (déclaration & affectation)
4.2.1. Un langage orienté objet
En Java nous avions la cohabitation de classes/objets avec des types primitifs.
En Kotlin TOUTES les variables sont des objets. Même les données élémentaires comme les entiers, les flottans, les chaînes de caractères ou les booléens sont des instances de classes.
Il en résulte que TOUS les types commencent par une majuscule afin de respecter la convention qu’un identifiant de classe commence par une majuscule.
Le type Any : Toutes les classes héritent de la classe Any. Donc tous les objets instanciés en Kotlin auront pour type commun le type Any. |
4.3. Apperçu des principaux types :
Nous vous proposons de faire un petit tour d’horizon des principaux types de Kotlin avant d’explorer plus en détail les mécanismes de déclaration et d’affectation de chaque type.
4.3.1. Codage des valeurs numériques :
Tous les types numériques héritent de classe Number. |
MOT CLE DU TYPE |
TAILLE MEMOIRE (bits) |
VALEUR MINI |
VALEUR MAXI |
Byte |
8 |
-128 |
127 |
Short |
16 |
-32 768 |
32 767 |
Int |
32 |
-231 |
231 - 1 |
Long |
64 |
-263 |
263 - 1 |
Rappel : En Java/Kotlin l’occupation maximale en mémoire (taille) ne dépend pas de la cible (OS & machine).
MOT CLE DU TYPE |
TAILLE MEMOIRE (bits) |
BITS MANTISSE |
BITS EXPOSANT |
Float |
32 |
24 |
8 |
Double |
64 |
53 |
11 |
La comparaison de valeurs numériques en Kotlin ne peut se faire que si les deux valeurs sont strictement du même type ! Deux valeurs indentiques mais de types différents ne seront pas considérés comme égaux ! Voir exemple ci-dessous. |
val quinze_Int: Int = 15
val quinze_Long: Long = 15
// Vérification des types :
println("quinze_Int is Int : ${quinze_Int is Int}")
println("quinze_Long is Long : ${quinze_Long is Long}")
Tout se passe bien et on obtient le bon résultat dans la console.
quinze_Int is Int : true quinze_Long is Long : true Process finished with exit code 0
Ajoutons maintenant une instruction de comparaison d’égalité stricte.
println("quinze_Int == quinze_Long : ${quinze_Int == quinze_Long}")
Et là nous obtenons une erreur. .Console :
Kotlin: Operator '==' cannot be applied to 'Int' and 'Long'
On ne peut réaliser de comparaison d’égalité entre un objet de la classe Int et un objet de la classe Long.
Par contre on peut appliquer des comparaisons <, <=, > et >=.
Quels types privilègier pour les valeurs numériques ? : Pour les valeurs entières il est conseillé d’utiliser le type Int et pour les flottants le type Double. |
Les attributs MIN_VALUE; MAX_VALUE et SYZE_BYTES
Nous pouvons facilement retrouver les valeurs minimales et maximales que peut atteindre chaque type avec les attributs : MIN_VALUE et MAX_VALUE.
Exemple :
fun main(){
println(Int.MIN_VALUE)
println(Int.MAX_VALUE)
println(Long.MIN_VALUE)
println(Long.MAX_VALUE)
}
-2147483648 2147483647 -9223372036854775808 9223372036854775807 Process finished with exit code 0
De même nous pouvons retrouver le nombre d’octets utilisés pour chaque type avec l’attribut SYZE_BYTES.
Exemple :
fun main(){
println(Int.SIZE_BYTES) // 4
println(Int.SIZE_BITS) // 32
}
4 32 Process finished with exit code 0
4.3.2. Booléens & caractères
On trouve 2 autres types que sont les Booléens et les Caractères :
MOT CLE DU TYPE |
DESCRIPTION |
Boolean |
Ne prend que 2 valeurs dites booléennes : true ou false |
Char |
Stock un caractère unique. |
En raison du typage fort de Kotlin on ne peut pas réaliser de tests logiques comme True == 1 ou False == 0 Le compilateur considéra que c’est une erreur. |
4.3.3. Les chaînes de caractères
Les chaînes de caractères sont immuables (non mutables) c’est-à-dire que vous ne pouvez pas modifier le contenu d’une chaînes de caractères une fois que la variable a reçu sa 1ère affectation. Comme nous verrons plus tard, nous pourrons accéder aux différents caractères soit par une opération d’indexation ou avec une boucle d’itération (boucle de type foreach).
4.3.4. Les tableaux(arrays) :
Les tableaux/arrays sont quasi équivalents aux tableaux de Java. Par contre les tableaux de Kotlin sont itérables (on peut les parcourir avec une boucle de type foreach).
Principales caractéristiques :
-
Les valeurs sont mémorisées dans des zones contiguës en mémoire, ce qui permet d’avoir un accès rapide aux valeurs.
-
La taille ne peut être modifiée (tableau statique).
-
Les valeurs sont ordonnées et indexées (accessibles par indice).
4.3.5. Les collections
Kotlin nous offrent des collections qui facilitent le traitement des données. Nous verrons que les collections pourront être déclarées comme mutables ou non mutables.
Toutes les collections de de Kotlin sont des types itérables.
TYPE DE COLLECTION |
DESCRIPTION |
List |
Les listes ont un comportement similaire aux Arrays mais avec l’avantage d’être dynamique, c’est-à-dire que la taille est variable. En contre partie les temps d’accès sont plus importants. |
Set |
Les sets sont équivalents aux ensembles mathématiques. C’est-à-dire que les valeurs sont non ordonnées et donc non indexées. 2 sets sont considérés égaux s’ils contiennent les mêmes valeurs indépendement de l’ordre des valeurs. |
Map |
Les maps sont des tableaux associatifs. Chaque valeur stockée est associée à une clé. Il est possible de vérifier la présence d’une clé et/ou valeur. Les maps sont itérables par contre ils sont non ordonnées (l’ordre est instable). |
4.4. Les déclarations & affectations sur les variables
Avant-propos :
Kotlin se distingue principalement avec son mécanisme de déclaration des variables qui le singularise d’autres langages :
-
2 choix de déclaration :
-
Déclaration de valeurs mutables avec le mot clé var
-
Déclaration de valeurs non mutables avec le mot clé val
-
-
Typage statique mais possibilité d’inférence du type lors de la déclaration. C’est-à-dire que
-
La déclaration de variables null devra être "forcée".
4.4.1. Rappel sur la notion de référence
En Kotlin comme en Java chaque objet est accessible par une ou plusieurs références.
Définition :
Référence : Variable contenant l’adresse d’un objet afin d’en permettre l’accès.
Alias : Variable contenant l’adresse d’un objet déjà référencé par une 1ère variable.
Ainsi lorsqu’on déclare une variable, celle-ci permettra de référencer un objet. La qualité de "mutabilité" d’une variable ne concernera pas les données mais les références des objets accuillant les données. Les données peuvent être modifiées si les méthodes des objets le permette. |
4.4.2. Déclarations & affectations du type Boolean
Le type booléen est le type le plus simple et sera le parfait candidat pour nous initier aux régles d’affectation des variables/références.
var idVar: typeIdVar
fun main(){
var varBinaire: Boolean // Déclaration d'une référence mutable
varBinaire = true // Initialisation de la variable
println(varBinaire)
println(varBinaire is Boolean)
}
true true Process finished with exit code 0
La variable varBinaire de notre exemple peut référencer que 2 objets Boolean : true ou false
fun main(){
var varBinaire: Boolean // Déclaration d'une référence mutable
varBinaire = true // Initialisation de la variable
println(varBinaire)
println(varBinaire is Boolean)
varBinaire = false // Maintenant varBinaire référence un autre objet
println(varBinaire)
}
true true false Process finished with exit code 0
val idVar: typeIdVar
La déclaraction d’une variable peut se faire sans initialisation. L’initialisation sera reportée à la 1ère affectation. Par contre toute modification après la 1ère affectation déclenche une erreur.
fun main(){
val varBinaire: Boolean // Déclaration d'une référence NON mutable
varBinaire = true // Initialisation de la variable
println(varBinaire)
}
fun main(){
val varBinaire: Boolean // Déclaration d'une référence NON mutable
varBinaire = true // Initialisation de la variable
varBinaire = false // Tentative de réaffectation...
}
Kotlin: Val cannot be reassigned
On verra plus en détail les mécanismes d’affectation par la suite. Avec notamment le mot clé lateinit qui permet de différer l’initialisation d’une variable de sa déclaration. |
4.4.3. Déclarations & affectations des types associés aux valeurs numériques
La déclaration de variables numérique ne pose aucun problème. Voici la syntaxe à respecter pour déclarer une variable numérique mutable et non mutable :
var idVariable: type
var unByte: Byte // Entier signé sous 8 bits.
var unSshort: Short // Entier signé sous 16 bits.
var unEntierInt: Int // Entier signé sous 32 bits.
var unEntierLong: Long // Entier signé sous 64 bits
var unFlottant: Float // Flottant signé sous 32 bits.
var UnDouble: Double // Flottant signé sous 64 bits.
fun main(){
var nombreTest: Long
nombreTest = 165
nombreTest = 54
println(nombreTest)
}
54 Process finished with exit code 0
val idVariable: type
val unByte: Byte // Entier signé sous 8 bits.
val unSshort: Short // Entier signé sous 16 bits.
val unEntierInt: Int // Entier signé sous 32 bits.
val unEntierLong: Long // Entier signé sous 64 bits
val unFlottant: Float // Flottant signé sous 32 bits.
val UnDouble: Double // Flottant signé sous 64 bits.
fun main(){
val nombreTest: Long
nombreTest = 165
nombreTest = 54
}
La réaffectation de la variable est naturellement refusé et génère une erreur d’exécution.
Kotlin: Val cannot be reassigned
Cas de l’affectation du type Float avec une chaîne numérique
L’affectation de valeur/chaînes numériques peut poser quelques soucis. En effet Kotlin affecte un type pour les constantes numériques (valeur/chaîne numériques). Il en découle que certaines affectations peuvent donc être refusées par le compilateur ! C’est notamment le cas lorsque l’on désire affecter une constante numérique à une variable déclarée comme un type Float !
En effet Kotlin considère que les constantes numériques sont de type Double. Or affecter un Double dans un Float peut conduire à une potentielle troncature, d’où l’erreur levée à la compilation.
var unFlottant: Float
unFlottant = 43.2
Kotlin: The floating-point literal does not conform to the expected type Float
Pour que Kotlin autorise l’affectation, il faut indiquer que le littérale représente une valeur correspondant au type Float. Pour ce faire il faut rajouter un F ou un f après le dernier chiffre du littéral.
var unFlottant: Float
unFlottant = 43.2f
Signature des chaînes littérales numériques
Faison un petit tour des différentes possiblités d’écriture des chaînes numériques.
Signature des Floats : Nous vennons de le voir précédemmment, pour signer une valeur numérique en qualité de Float il faut utiliser :
-
F ou f
479.7825F 54.2089f
Signature des Longs : Les valeurs numériques sont implicitement au format Int. Pour désigner un Long, on terminera la chaîne numérique par un L :
-
L
Cette signature est moins utile que la précédente avec les Float. En effet ici nous pouvons affecter une chaîne littérale entière aussi bien un type Int (type d’origine de la chaîne numérique) et qui peut plus, peut le moins, donc le compilateur ne voit pas de problème à affecter un Int dans un Long
On ne pourra rencontrer une erreur que si on signe effectivement notre chaîne numérique comme ci-dessous :
var unInt: Int
unInt = 13L
Nous nous retrouvons comme prévu avec une erreur :
Kotlin: The integer literal does not conform to the expected type Int
Ecriture scientifique/ingénieur et séparateur de millier :
Pour faciliter la lecture des valeurs numérique Kotlin permet d’utiliser une écriture en puissance de 10 en utilisant le symbole e :
var unDouble: Double
unDouble = 12.607e-6
De même, il est possible d’utilise le tiret du bas (underscore) comme séparateur des milliers :
var unDouble: Double
unDouble = 3_987_124_051.0
Signature des valeurs au format binaire et hexadécimal :
Vous pouvez saisir des chaînes numériques au format binaire ou hexadécimal.
Il faut juste penser à les signer à gauche de la valeur :
* 0b pour le format binaire
* 0x pour le format hexadécimal
println(0b1111_1110)
println(0x00_FE)
4.4.4. Déclarations & affectations du type Char et String
Le type Char
Le type char permet de mémoriser un unique caractère unicode en mémoire.
La déclaration est sans particularité.
var carMutable: Char // Déclaration d'un caractère mutable.
val carImmuable: Char // Déclaration d'un caractère immuable.
Pour procéder à une affectation sur un Char nous pouvons :
-
Placer un caractère entre une paire de strophe ' '
-
Utiliser le code Unicode du caractère au format '\u numUnicode '.
var charAnd: Char = '&'
var charAndUnicode: Char = '\u0026'
println("charAnd = $charAnd")
println("charUnicode = $charAndUnicode")
charAnd = & charUnicode = & Process finished with exit code 0
Le type String
Le type String n’a pas de particularité sur son processus de déclaration et d’affectation. Le seul fait remarquable est qu’une chaîne de caractères ne peut-être modifiée, en pratique on réaffecte une nouvelle chaîne.
Dans la partie qui suit nous nous concentrons sur les processus de déclaration, d’initilisation d’accès et affectation à un caractère. Une autre partie est consacrée au boucle de parcours et principales méthodes de la classe String.
var uneLigneDeTexte: String // String mutable
val unParagraphe: String // String immuable
Pour l’affectation nous avons la syntaxe classique consistant à placer la chaîne de caractère entre 2 guillemets " "
uneLigneDeTexte = "Bonjour à tous."
unParagraphe = """ |Pour faciliter les sauts de lignes dans un String
|nous pouvons utiliser la méthode trimMargin().
|Fin du paragraphe.""".trimMargin()
Remarque : La méthode trimMargin() est facultative. Elle nous permet de supprimer les espaces et/ou tabulations placés avant le caractère |.
Accès à un caractère :
Les objets de type String sont naturellement ordonnés et disposent d’un accès par indexation. Le 1er indice commence par 0.
fun main(){
var texte = "Un, deux, trois, quatre, cinq"
println(texte[4])
//texte[4] = "D" // Ne fonctionne pas car les Strings sont toujours immuables
// Il faut réaffecter une nouvelle chaîne si nous désirons la modifier :
texte = texte.substring(0..3) + 'D' + texte.substring(5..28)
println(texte)
}
4.4.5. Conversions de types (transtypage)
Conversions entre types numériques
Kotlin permet le transtypage entre les différents types numériques (Byte, Short, Int, Long, Float et Double).
Evidemment un transtypage d’un type plus grand vers un plus petit pourra engendrer une erreur de débordement. De même la conversion d’un type de floattant (Float et Double) vers un type entier entraînera une troncature.
Kotlin encourage à favoriser les types Int, Long et Double.
Kotlin ne proposera plus les conversions avec les méthodes toShort() et toByte() dans ses prochaines version… Leur utilisation est découragée depuis la version 1.4 de Kotlin. |
// Les méthodes de transtypage/cast :
var data01: Byte = 3
var data02: Short = data01.toShort()
var data03: Int = data01.toInt()
var data04: Long = data01.toLong()
var data05: Double = data01.toDouble()
Conversions depuis et vers le type String
Conversions vers le type String
Nous pouvons convertir vers le type String les types suivants :
-
Char
-
Int
-
Double
-
Boolean
val n = 8 // Int
val d = 10.09 // Double
val c = '@' // Char
val b = true // Boolean
val s1 = n.toString() // "8"
val s2 = d.toString() // "10.09"
val s3 = c.toString() // "@"
val s4 = b.toString() // "true"
Conversion depuis le type String
Une valeur type String peut être convertie en nombre ou en booleen. Pour la conversion en booleen, toute chaîne de caractères sera systématique convertien à l’état false sauf si la chaîne contient la chaîne "true" peut importe qu’une partie des lettres soient en majuscules/minuscules.
fun main() {
println("abcd...".toBoolean())
println("true".toBoolean())
println("TRUE".toBoolean())
println("True".toBoolean())
println("tRuE".toBoolean())
}
false true true true true Process finished with exit code 0
La conversion de String vers Char n’est pas possible en Kotlin. |
4.4.6. Déclaration & affectations du type Array
Déclaration d’un Array
Les Arrays doivent contenir des données qui auront tous le même type. L’instruction de déclaration des Array en Kotlin impose de préciser le type.
var idArray: Array<type>
Le type placé entre les chevrons < > permet d’indiquer au compilateur le type des données à stocker.
var tableau: Array<Int> // Declaration d'un Array de Int
var tableauFloat: Array<Float> // Declaration d'un Array de Float
var tableauChar: Array<Char> // Declaration d'un Array de Char
var tableauString: Array<String> // Declaration d'un Array de String
// Non exhaustif...
Initialisation d’un Array :
La phase d’initialisation permet de définir la taille du tableau qui sera soit renseignée en dur, soit déduite d’un nombre d’éléments/valeurs à affecter.
Kotlin n’autorisera pas une instantiation sans initialisation avec des valeurs.
Nous avons trois manières de procéder :
-
Utiliser le constructeur Array() de la classe Array;
-
Utiliser la fonction d’initialisation dédiée arrayOf();
-
Convertir un autre type de collection en Array.
Intialisation à partir du constructeur Array() :
Prérequis : L’utilisation du constructeur Array() nécessite de savoir définir une fonction lambda et de connaitre la variable d’itération it est un plus. |
Le constructeur Array() à besoin de 2 arguments pour réaliser l’initisation :
-
La taille du tableau;
-
Une fonction lambda qui permettra pour chaque indice de déterminer la valeur à renseigner.
idRefTableau = Array(taille, lambda)
idRefTableau = Array(taille)lambda
// Déclaration de la référence :
var tableauInt: Array<Int>
// Initialisation d'un table de taille 5 contenant la valeur 9:
tableauInt = Array(5, {9})
// Affichage :
println(tableauInt.contentToString())
[9, 9, 9, 9, 9] Process finished with exit code 0
//Déclaration & initialisation (le type est déduit par inférence) :
var tableauInt = Array(5, {it}) // it est une variable d'itération générée automatiquement par Kotlin
// Affichage :
println(tableauInt.contentToString())
[0, 1, 2, 3, 4] Process finished with exit code 0
//Déclaration & initialisation :
var tableauInt: Array<Int> = Array(5){3 * it}
// Affichage :
println(tableauInt.contentToString())
[0, 3, 6, 9, 12] Process finished with exit code 0
Initialisation à partir de la fonction arrayOf() :
Ici nous allons initialiser à partir d’une série de valeurs passées en argument de la fonction arrayOf().
//Déclaration & initialisation :
var tableauInt = arrayOf("zéro", "un", "deux", "trois", "quatre")
// Affichage :
println(tableauInt.contentToString())
[zéro, un, deux, trois, quatre] Process finished with exit code 0
Initialisation à partir de la conversion d’une autre collection :
Voici un exemple où l’on génère une série numérique à l’aide d’une plage. Puis nous convertissons la plage en liste et la liste en tableau.
Une plage est une série de valeurs entières générées par l’instruction (valDepart..valFin). |
//Déclaration & initialisation :
var tableauInt = (5..10).toList().toTypedArray()
// Affichage :
println(tableauInt.contentToString())
[5, 6, 7, 8, 9, 10] Process finished with exit code 0
Tableau à plusieurs dimensions (tableau de tableaux)
Il est possible de générer des tableaux à n dimensions.
//Déclaration & initialisation d'un tableau de tableaux :
var tableauDeTableaux = arrayOf(arrayOf(0, 1, 2, 3), arrayOf(4, 5, 6, 7), arrayOf(8, 9, 10, 11))
// Affichage du contenu (attention il faut utiliser la méthode contentDeepToString ()):
println(tableauDeTableaux.contentDeepToString())
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]] Process finished with exit code 0
//Déclaration & initialisation d'un tableau de tableaux :
var tableauDeTableaux = Array(3){ Array(4){it} }
// Affichage du contenu (attention il faut utiliser la méthode contentDeepToString ()):
println(tableauDeTableaux.contentDeepToString())
[[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]] Process finished with exit code 0
Affectation des valeurs d’un Array
Il y a aucune difficulté pour affecter au coup par coup des valeurs.
idArray[i] = nouvelleValeur
//Déclaration & initialisation d'un tableau de tableaux :
val tableau = arrayOf("Zéro", "Un", "Deux")
// Modification d'une valeur :
tableau[1] = "1"
// Affichage du contenu (attention il faut utiliser la méthode contentDeepToString ()):
println(tableau.contentDeepToString())
[Zéro, 1, Deux] Process finished with exit code 0
On a ici une bonne illustration que le mot clé val protège la référence mais pas les attributs d’un objet puisque nous avons pu modifier une valeur d’un tableau. |
4.4.7. Déclarations & affectations sur les collections
Tour d’horizon sur les collections
Les collections nous permettent d’accéder à des structures de données dynamiques (la taille peut varier) et pourvus de méthodes permettant une étendu de traitement supérieur à celui des objets de la classe Array. Mais nous allons pouvoir pour chaque type de collection (list, set ou Map) l’initialiser comme mutable ou immuable les valeurs. Une collection immuable est une collection où il n’est pas possible de modifier les valeurs après leur initialisation.
Le type List (listes)
Ce type de collection est la plus proche du classique Array, elles ont donc quelques simularités.
Déclaration & initialisation d’un type List
A la différence des Array, les listes pourront être initialisée comme étant immuable ou mutable. C’est la fonction d’initialisation qui va affecter ce caractère à la liste instanciée. Ici le caractère de mutabilité concerne bien les valeurs stockée dans la liste. La mutabilité de la référence quant à elle repose toujours sur les mots clés var et val. Nous opterons pour des références immuables dans nos prochains exemples.
val listeImmuableString = listOf("zero", "un", "deux", "trois")
val listeImmuableInt = listOf(0, 1, 2, 3)
val listeImmuableVideInt = listOf<Int>()
Il existe une fonction spécifique pour initialiser une liste immuable vide :
val listeImmuableVideString = emptyList<String>()
Nous pouvons également utiliser le constructeur :
val listeConstruite = List(5){it} // Instance d'une liste de 5 éléments
L’affichage du contenu d’une liste peut se faire en passant directement l’identifiant de la liste en argument de la fonction print() ou println()
println(listeImmuableString)
println(listeImmuableInt)
println(listeImmuableVideInt)
println(listeConstruite)
[zero, un, deux, trois] [0, 1, 2, 3] [] [0, 1, 2, 3, 4] Process finished with exit code 0
val listeMutableString = mutableListOf("zero", "un", "deux", "trois")
val listeMutableInt = mutableListOf(0, 1, 2, 3)
val listeMutableVideInt = mutableListOf<Int>()
Mais nous pouvons aussi procéder à l’initialisation avec le constructeur MutableList() qui fonctionne comme sa version immuable List().
val listeConstruireMutable = MutableList(5){it}
Accès en lecture & écriture aux valeurs
Le principe est identique à celui des Arrays. On peut accéder à chacun des éléments par son indice. On peut modifier la valeur
val listeImmuable = listOf("zéro", "un", "deux", "trois")
val listeMutable = mutableListOf("zéro", "un", "deux", "trois")
println(listeImmuable[2])
listeMutable[2] = "DEUX"
println(listeMutable[2])
deux DEUX Process finished with exit code 0
Ajout et suppression de valeurs d’une liste mutable
Le 1er intérêt des listes par rapport au type Array est la possibilité de rajouter ou supprimer des valeurs.
val listeMutable = mutableListOf("zéro", "un", "deux", "trois")
// Ajout d'une valeur avec l'opérateur +=
listeMutable += "QUATRE"
// Ajout d'une valeur avec la méthode add()
listeMutable.add("SIX")
listeMutable.add(5, "CINQ") // Insertion à l'indice 5
println(listeMutable)
// Suppression d'une valeur
listeMutable.removeAt(2) // Suppression de la valeur à l'indice 2
listeMutable.remove("QUATRE") // Suppression de la valeur "QUATRE"
println(listeMutable)
[zéro, un, deux, trois, QUATRE, CINQ, SIX] [zéro, un, trois, CINQ, SIX] Process finished with exit code 0
Le type Set (ensembles)
Le type Set est très semblable au type List, la différence vient du fait que l’on respecte les principes d’un ensemble :
-
L’ordre des valeurs ne doit pas être pris en compte (structure non ordonnée);
-
On ne doit pas accepter de doublons.
val setImmuable = setOf("un", "deux", "deux","trois", "quatre", "un") // Les 2 doublons seront éliminés
val setDesordre = setOf("deux", "trois", "quatre", "un")
println(setImmuable)
println("trois est-il dans l'ensemble : ${"trois" in setImmuable}")
println("Les 2 ensembles sont-ils égaux : ${setDesordre == setImmuable}")
// setImmuable[2] est interdit, mais nous pouvons utiliser elementAt() :
println(setImmuable.elementAt(2))
[un, deux, trois, quatre] trois est-il dans l'ensemble : true Les 2 ensembles sont-ils égaux : true trois Process finished with exit code 0
Les Sets mutables vont permettre d’ajouter des valeurs comme avec une liste. Mais la présence de doublons reste impossible pour respecter l’unicité des valeurs.
val setMutable = mutableSetOf("un", "deux", "deux","trois", "quatre", "un") // Les 2 doublons seront éliminés
setMutable += "un"
setMutable.add("cinq")
println(setMutable)
setMutable.remove("trois")
println(setMutable)
println(setMutable.elementAt(2))
[un, deux, trois, quatre, cinq] [un, deux, quatre, cinq] quatre Process finished with exit code 0
Le type Map (tableau associatif)
Le type Map est un tableau associatif, c’est-à-dire que dans un tableau sont stockées des valeurs. Chaque valeur est associée à une clé pour permettre son accès au lieu d’utiliser un numéro d’indice.
// Création d'un map :
var tabAssoImmuable = mapOf(0 to "Zero", 1 to "Une", 2 to "Deux", 3 to "Trois")
var tabAssoMutable = mutableMapOf(0 to "Zero", 1 to "Une", 2 to "Deux", 3 to "Trois")
// Exploitation :
println("Exemple avec tabAssoImmuable : $tabAssoImmuable")
println("tabAssoImmuable.keys : ${tabAssoImmuable.keys}")
println("tabAssoImmuable.values : ${tabAssoImmuable.values}")
// Accès par clé :
println("Valeur à la clée 2 : tabAssoImmuable[2] = ${tabAssoImmuable[2]}")
// Vérification présence clé :
println("La clée 1 est-elle dans tabAssoImmuable : ${1 in tabAssoImmuable}")
println("La clée 7 est-elle dans tabAssoImmuable : ${7 in tabAssoImmuable}")
// Vérification présence valeur :
println("La valeur \"Deux\" est-elle dans tabAssoImmuable : ${"Deux" in tabAssoImmuable.values}")
Exemple avec tabAssoImmuable : {0=Zero, 1=Une, 2=Deux, 3=Trois} tabAssoImmuable.keys : [0, 1, 2, 3] tabAssoImmuable.values : [Zero, Une, Deux, Trois] Valeur à la clée 2 : tabAssoImmuable[2] = Deux La clée 1 est-elle dans tabAssoImmuable : true La clée 7 est-elle dans tabAssoImmuable : false La valeur "Deux" est-elle dans tabAssoImmuable : true Process finished with exit code 0
4.4.8. L’inférence de type
Kotlin offre un mécanisme d’inférence de type. L’inférence de type consiste à laisser déterminer le type d’une variable par le compilateur.
Le compilateur exploite le contexte pour déterminer le type. Par exemple il pourra déduire le type avec les valeurs d’intialisation.
L’inférence de type diffère du typage dynamique que l’on retrouve dans des langages comme Python ou Java Script. L’inférence de type et le typage dynamique permettent de ne pas indiquer le type au moment de sa déclaration. Cependant en typage dynamique, le type de la variable pourra évoluer durant l’exécution du programme. L’inférence de type respecte le typage statique, le type est affecté au moment de la compilation et ne pourra plus changer par la suite. |
4.4.9. Cas de l’initialisation d’une variable avec null
Initialiser une variable comme étant null peut mener à des erreurs durant le déroulement de nos programmes.
Kotlin décourage à cette pratique mais ne l’interdit pas. Pour ce faire il refusera qu’une variable soit affectée avec * null*.
fun main(){
var uneVariable: Int = null
}
Kotlin: Null can not be a value of a non-null type Int
Cependant nous pouvons forcer les choses, pour ce faire il faut rajouter le symbole ? après la déclaration du type de la variable.
De même, si la variable est utilisée sans que le compilateur puisse s’assurer que la variable prendre bien une valeur quelconque mais différente de null, alors une erreur sera levée. Nous pouvons choisir de faire ignorer cette vérification en plaçant un double !! après l’identifiant de la variable initialisée à null.
fun demo(uneVariable: Int){println("La variable initialisée à null vaut maintenant : ${uneVariable}")}
fun main(){
var uneVariable: Int? = null
if (true) uneVariable = 33 // La structure conditionnelle rend incertain pour le compilateur de la valeur de la variable au moment de l'exécution.
demo(uneVariable!!)
}
4.4.10. Déclaration des variables constantes
Nous avons vu que nous pouvions déclarer des variables comme étant immuables avec le mot clé val.
Cependant une "vraie" constante doit être initialisée au moment de la compilation selon les régles de l’art. Kotlin permet d’imposer ce mode de fonctionnement avec le mot clé const.
La variable sera alors déclarée endehors du corps de la fonction main().
const val ID_CONSTANTE = valeurInitialisation
const val MAXIMUM = 37
fun main(){
println("La constante MAXIMUM : $MAXIMUM")
}
La constante MAXIMUM : 37 Process finished with exit code 0
4.4.11. L’entrée standard avec readLine() et readln()
Kotlin dispose de la fonction readLine() pour permettre la lecture de valeur dans la console. La fonction readLine() retourne systématique la saisie sous la forme d’un String.
La fonction readLine() peut retourner le type null si la touche entrée est frappée sans aucune entrée préalable.
Il existe depuis la version 1.6 de Kotlin une variante court de readLine() qui est readln().
fun main() {
val line = readLine()!!
println(line)
}
Résultat :
Coucou Coucou Process finished with exit code 0
5. Les structures/expressions conditionnelles
5.1. Introduction aux Expressions et Structures Conditionnelles
Kotlin n’utilise pas des structures conditionnelles mais des expressions conditionnelles. Les deux concepts sont très proches mais également subtilement différents. Prenons un petit moment pour les expliquer.
Définition d’une structure conditionnelle : Une structure conditionnelle conditionne une instruction ou un bloc d’instructions selon qu’une ou plusieurs condition(s) logique(s) (prédicat(s)) soit vérifiée(s) ou non (proposition vraie ou fausse).
La condition logique peut être par exemple l’état d’une variable booléenne, le résultat d’une expression logique, l’état d’une entrée logique d’un système.
Définition d’une expression conditionnelle : Une expression conditionnelle conditionne une le résultat d’une valeur selon l’état d’une ou plusieurs condition(s) logique(s).
En d’autres termes, une expression conditionnelle pourra être placée en argument d’une fonction ou à l’affectation d’une variable par exemple.
Une expression conditionnelle peut également être utilisée en remplacement d’une structure conditionnelle alors que la réciproque n’est pas vraie.
5.2. Syntaxe des expressions conditionnelles if/else en Kotlin
Les mots clés utilisés sont identiques que dans les langages de type C/C++/Java : if /else
Voici un exemple illustrant la différent entre une expression et une structure conditionnelle :
val decision: Boolean = false
// Structure conditionnelle :
if(decision){
println("Oui !")
}
else{
println("Non !")
}
Ici nous avons une structure conditionnelle classique, avec le choix de 2 instructions println("Oui !") ou println("Non !").
Nous pouvons remplacer cette structure conditionnelle par une expression conditionnelle :
// Expression conditionnelle :
val decision: Boolean = false
println(if(decision) "Oui !" else "Non!")
La version en expression conditionnelle est plus élégante. En effet ici l’élément variant est bien la chaîne de caractère "Oui !"/"Non !". La fonction println() est redondante dans la version structure conditionnelle.
Une expression conditionnelle peut également comporter plusieurs instructions regroupées en bloc. Il faut juste que la dernière instruction corresponde à une expression.
var a = 1
var b = 2
var resultat = if (a < b){
a = b
2 * b
}
else{
b = a
2 * a
}
println(resultat)
5.3. Expression conditionnelle when
L’expression conditionnelle remplace la structure conditionnelle switch case que nous retrouvions dans de nombreux langage. On obtient le même résultat avec une syntaxe plus concise.
var varCandidate: String = when(y){
1 -> "un"
2 -> "deux"
7 -> "sept"
else ->{ // Le else est obligatoire et se place dans le bloc du when.
"On ne sait pas !"
}
}
print("varCandidate = $varCandidate")
5.4. Expression conditionnelle when avec des plages
Kotlin introduit une instruction nommée plage qui permet de réaliser des itérations avec une boucle (voir structures itératives), mais également de définir des intervalles comme conditions. Nous pouvons également mixer valeurs et plages.
var cible = -8
var resultat = when(cible) {
in -10..-5 -> "Trop négatif"
in -4..-1 -> "Faiblement négatif"
0 -> "Nul"
in 1..5 -> "Faiblement positif"
in 6..10 -> "Trop positif"
else -> {"Hors valeurs !"} // Attention le else est dans le bloc principal du when
}
println(resultat)
6. Les structures itératives
6.1. Généralités sur les structures itératives en Kotlin
Kotlin propose plusieurs possibilités pour réaliser des structures itératives. On retrouve les instructions suivantes :
-
La boucle while et do while (boucle non bornée) : Kotlin ne propose pas d’innovation, c’est une classique boucle dont la fin dépend d’une proposition logique.
-
La boucle for(…in…) : C’est une boucle de type for each qui permet d’itérer sur des collections et des plages par exemple. Par contre la boucle bornée for(int = 0; i < n; i++) disparait et est remplacée par l’utilisation de la boucle for avec une plage.
-
La boucle forEach : C’est une boucle spécifique pour les collections, on peut aussi l’utiliser avec des plages.
6.2. Boucle while
Rien de particulier pour ce type de structure itérative. La syntaxe est conventionnelle :
while(condition){
// Instructions à itérer
}
var i: Int = 0
while( i < 10) {
println("Iteration while n°$i")
i++
}
6.3. Boucle do while
On reste encore classique au niveau de la syntaxe :
do{
// Instruction à itérer...
}while (condition)
var i: Int = 0
do{
println("Iteration do while n°$i")
i++
} while (i < 10)
6.4. Boucle for(…in…)
C’est la boucle de prélidiction pour réaliser des itérations bornées. On peut également l’appliquer au contenu de collections, mais dans ce cadre le boucle forEach offre de meilleures performances.
for(item in itérable){
// Instruction à itérer
}
val liste = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
val ensemble = setOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
val tabAssociatif = mapOf(1 to "un", 2 to "deux", 3 to "trois", 4 to "quatre")
for (elmt in liste) {
println(elmt)
}
for (elmt in ensemble){
println(elmt)
}
for(elmt in tabAssociatif){
println(elmt)
Nous pouvons appliquer la boucle for(…in…) à une plage.
for(i in 0..10){print(i)}
6.5. Boucle forEach
La boucle forEach est particulièrement intéressante avec les collections.
iterable.foreach{lambda}
val liste = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
val ensemble = setOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
val tabAssociatif = mapOf(1 to "un", 2 to "deux", 3 to "trois", 4 to "quatre")
liste.forEach { println(it) }
ensemble.forEach { println(it) }
tabAssociatif.forEach { println(it) }
(0..10).forEach { println(it) }
7. Programmation procédurale
7.1. Définition & appel d’une fonction
7.1.1. Avant propos
Nous avons pris le parti de segmenter en 3 parties les composantes de la programmation procédurales :
-
En-tête d’une fonction
-
Corps d’une fonction
-
Appel d’une fonction
Cette présentation permet de mettre en avant les éléments classiques que nous pouvons retrouver dans d’autres langages : Java, C, C++, Python, etc. et de mettre en avant les éléments plus spécifiques ou du moins originaux de Kotlin comme par exemple :
-
Les fonctions expressions
-
Les fonction génériques
Kotlin s’inspire pour ne pas dire impose des éléments de la programmation "fonctionnelle". Les fonctions et procédures sont utilisées pour réduire le nombre d’instructions à rédiger en appliquant des fondements de la programmation fonctionnelle.
7.1.2. En-tête de la fonction
En-tête de base
L’en-tête contient les principales informations de la fonction :
-
Identifiant;
-
Paramètres et types associés (éventuellement des valeurs par défaut);
-
Le type retourné par la fonction.
Syntaxe de l’en-tête avec retour :
L’en-tête commence par le mot clé fun, suivi par les éventuels paramètres placés entre parenthèse et en fin de ligne l’éventuel type retourné.
fun idFonction(idArg01: typeArg01, idArg02: typeArg02, ...): typeRetour
fun somme(operandeGauche: Double, operandeDroit: Double): Double
Si la fonction ne retourne rien (procédure), alors on ne renseigne rien pour le type de retour.
fun idFonction(idArg01: typeArg01, idArg02: typeArg02, ...)
fun procedure(message: String)
En réalité une procédure en Kotlin est une fonction qui retourne un type particulier Unit. Nous avons par besoin de spécifier dans l’en-tête le retour de type Unit le compilateur s’en charge à notre place. A ce stade on peut considérer que Unit est l’équivalent de void que nous pouvons retrouver dans d’autres langages comme Java. |
En-tête avec argument par défaut
Kotlin permet de fournir des valeurs par défaut pour l’initialisation des paramètres. Si un argument est donné pour le paramètre, alors l’argument fourni est utilisé pour initialiser le paramètre. Par contre en absence d’argument, c’est la valeur par défaut qui sera utilisée.
La syntaxe est très simple, il suffit d’indiquer la valeur par défaut au niveau de l’en-tête :
fun idFonction(idArg01: typeArg01 = valDefaut01: , idArg02: typeArg02 = valDefaut02, ...)
On peut naturellement mixer des paramètres avec et sans valeur par défaut.
fun produit(x: Double = 2.0, y: Double = 4.0) : Double {
En-tête avec nombre d’arguments variable :
Il y a des situations où le nombre d’arguments qui sera fourni n’est pas connu à l’avance. Il est possible de définir des fonctions qui recevrons un nombre variable d’arguments.
La mise en oeuvre est assez aisée, il suffit d’utiliser le mot clé vararg
fun idFonction(vararg idVar: typeArguments): typeRetour
Prenons en exemple une fonction somme qui réalisera la somme des arguments fournis à l’appel de la fonction :
fun somme(vararg termes: Double): Double
Mutabilité des paramètres d’une fonction
Nous pouvons constater que nous n’avons pas préciser dans les différentes en-têtes le status de "mutabilité" des paramètres de la fonction… En Kotlin :
-
les paramètres d’une fonction SONT TOUJOURS IMMUABLES;
-
Il n’est pas possible d’utiliser les modificateurs val et var sur les paramètres.
Ce mode de comportement sur les paramètres d’une fonction permet de limiter les effets de bords, du moins pour des types simples. Pour des types structurés il sera toujours possible de modifier les valeurs de la structure de données si cette dernière n’a pas été initialisée comme immuable. |
7.1.3. Corps de la fonction
Corps d’une fonction de base
Kotlin en prime abord utilise une forme conventionnelle pour la forme du corps des fonctions, on retrouve donc les éléments classiques :
-
Une accolade ouvrante { pour marquer le début du bloc d’instructions et une accolade fermante } pour signaler la fin du bloc d’instruction.
-
Le mot clé return pour les éventuels retours. Si la fonction ne retourne rien, il suffit de ne rien indiquer.
fun idFonction(idArg01: Type01 = valDefaut01, vararg idVar: typeArguments): typeRetour{
// Corps de la fonction contenant l'ensemble des instructions.
return varRetour
fun exemple(mot1: String = "Bonjour", vararg mots: String): String{
var phrase: String = mot1
for (mot in mots)
phrase += " " + mot
return phrase
}
fun main(){
println(exemple("Coucou", "comment", "allez-vous ?"))
}
Coucou comment allez-vous ? Process finished with exit code 0
Les fonctions expressions
Kotlin propose une alternative pour rédiger les corps des fonctions tout particulièrement quand le corps est constitué d’une seule ligne d’instruction. En effet, il ser alors possible de placer sur la même ligne l’en-tête suivi de l’expression réalisée par la fonction.
fun idFoncExpression(arg01: Type01,...): TypeRetour = expression
fun produit(facteurGauche: Double, facteurDroit: Double): Double = facteurGauche * facteurDroit
fun main(){
println(produit(3.0, 15.0))
}
7.1.4. Appel des fonctions
Pas de difficultés particulière pour l’appel des fonctions. La formulation est classique, on utilise l’identifiant de la fonction et nous fournissons les arguments entre une paire de parenthèses.
fun somme(operandeGauche: Double, operandeDroit: Double): Double{
return operandeGauche + operandeDroit
}
fun main(){
var resultat = somme(5.0, -2.0)
println(resultat)
}
3.0 Process finished with exit code 0
L’uilisation du nom des paramètres permet de passer les arguments dans n’importe quel ordre et sourtout de laisser les valeurs par défaut pour les arguments qui ne sont pas renseignés !
fun exemple(mot1: String = "Bonjour", vararg mots: String): String {
var phrase: String = mot1
for (mot in mots)
phrase += " " + mot
return phrase
}
fun main(){
var retour = exemple(mots = arrayOf("tout", "le", "monde !"))
println(retour)
}
Bonjour tout le monde ! Process finished with exit code 0
7.1.5. Les fonctions "Higher-order functions" (fonction d’ordre supérieur)
Définition d’une fonction d’ordre supérieur
Les Higher-order function que nous pouvons traduire comme "fonctions d’odre supérieur" sont des fonctions qui vont justement prendre en argument une fonction ou en retourner une.
Ce sont donc des fonctions où l’on pourra passer des fonctions lambdas. Nous allons voir ici comment les construire.
Pour les définir nous aurons besoins de caractériser la fonction paramètre, pour ce faire nous utiliserons une paire de parenthèse ( ) pour signaler que l’argument attendu est la référence d’une fonction. Nous devrons préciser les types des éventuels arguments de la fonction passé en argument à l’intérieur des parenthèses. On devra impérativement préciser le type retourné par la fonction donnée en argument. On utiliser le mot clé Unit si la fonction ne retourne rien.
fun idFoncOrdreSup(argFonc: () -> Unit): typeRetour{
// Corps de la fonction...
}
fun idFoncOrdreSup(argFonc: () -> typeRetour): typeRetour{
// Corps de la fonction...
}
fun idFoncOrdreSup(argFonc: (typeRetour) -> typeRetour): typeRetour{
// Corps de la fonction...
}
fun fonctionOrdreSuperieur_01(argFonc: () -> Unit){
argFonc()
}
fun fonctionOrdreSuperieur_02(argFonc: () -> String): String{
return argFonc()
}
fun fonctionOrdreSuperieur_03(argFonc: (String) -> String): String{
return argFonc("Hello")
}
Appel d’une fonction d’ordre supérieur
Une fonction d’odre supérieur prendra en argument une fonction prédéfinie. Cette fonction peut être :
-
Une fonction lambda : nous expliquons dans la partie suivante la définition et l’utilisation d’une fonction lambda comme paramètre d’une fonction d’ordre supérieur.
-
Une fonction régulière : Le passage de la référence comme argument requiert l’utilisation du symbole ::
idFoncSuperieur(::idArgFonc)
// Définition d'une fonction d'ordre supérieur :
fun cobaye(argFonc: (String) ->String): String{
return argFonc("Message original")
}
// Définition d'une fonction argument :
fun argument(mot: String): String = mot + " (Ok, message lu)"
fun main(){
var retour: String
retour = cobaye(::argument) // Le passage de la référence s'accompagne des ::
println(retour)
}
Message original (Ok, message lu) Process finished with exit code 0
7.1.6. Les fonctions lambdas et les fonctions anonymes :
Les lambdas
Pour rappel une fonction lambda est une fonction qui peut être définie et appelée sans identifiant. Typiquement l’instruction d’une fonction lambda sera utilisée dans un paramètre d’une autre fonction/méthode.
Les lambdas permettent du gagner du temps en nous évitant de définir spécifiquement une fonction régulière qui ne sera utilisée qu’une seule fois ou très ponctuellement et dont le corps se réduit le plus souvent à une simple expression.
La syntaxe est la suivante :
{arg01: typeArg01, arg02: typeArg02, ...-> expression}
fun main() {
fonctionOrdreSuperieur_01({println("Appel de lambda_01")})
println( fonctionOrdreSuperieur_02( {"Appel de lambda_02"} ) )
println( fonctionOrdreSuperieur_03 ( { chaine: String -> chaine + " Fin !" } ) )
// Utilisation d'une référence :
val f = { chaine: String -> chaine + " Ok !" }
println( fonctionOrdreSuperieur_03(f))
}
// Définition des fonctions d'ordre supérieur
fun fonctionOrdreSuperieur_01(argFonc: () -> Unit){
argFonc()
}
fun fonctionOrdreSuperieur_02(argFonc: () -> String): String{
return argFonc()
}
fun fonctionOrdreSuperieur_03(argFonc: (String) -> String): String{
return argFonc("Exécution de la fonction d'ordre supérieur n°3")
Appel de lambda_01 Appel de lambda_02 Exécution de la fonction d'ordre supérieur n°3 Fin ! Exécution de la fonction d'ordre supérieur n°3 Ok ! Process finished with exit code 0
Variante syntaxique :
Si une fonction lambda est placée comme DERNIER (ou unique) paramètre d’une fonction d’ordre supérieur, il est alors possible de placer la fonction lambda JUSTE APRES la fonction d’ordre supérieur.
fonctionOrdreSuperieur_01() {println("Appel de lambda_01")}
println( fonctionOrdreSuperieur_02() {"Appel de lambda_02"} )
println( fonctionOrdreSuperieur_03 () { chaine: String -> chaine + " Fin !" } )
Utilisation du paramètre it :
Kotlin met à disposition un paramètre it qui nous épargne de déclarer justement un paramètre dans une fonction lambda. Cela est possible seulement si la fonction lambda requiert un seul paramètre.
// Définition de la fonction d'ordre supérieur :
fun doublerValeur(valeur: Double, lambda: (Double) -> Double) {println("Le double de $valeur = ${lambda(valeur)}")}
fun main(){
// Appels de la fonction d'ordre supérieur avec une fonction lambda :
doublerValeur(16.0, {x: Double -> 2 * x}) // Forme complète avec la déclaration du paramètre
doublerValeur(16.0, {2 * it}) // Forme utilisant le paramètre it
Les fonctions anonymes
Les fonction anonymes sont au niveau de leur syntaxe et utilisation un mix entre les fonctions lambdas et les fonctions expressions.
Outre la différence syntaxique, il y a également une différence à la compilation. Le type de retour n’est pas préciser pour une fonction lambda, c’est le compilateur qui met en oeuvre le mécanisme d’inférence pour fournir le type d’une fonction lambda. Dans une fonction anonyme nous pourrons préciser le type retourné.
// Syntaxe sous forme d'expression :
val anoynyme01 = fun(arg01: Type01, arg02: Type02, ...): typeRetour = expression
// Syntaxe avec bloc d'instructions :
val anoynyme02 = fun(arg01: Type01, arg02: Type02, ...): typeRetour {// Bloc d'instructions avec return}
// Définition de 2 fonctions anonymes selon 2 variantes syntaxiques :
val somme01 = fun(x: Double, y: Double): Double {return x + y} // Cette variante peut s'écrire sur plusieurs lignes
val somme02 = fun(x: Double, y: Double): Double = x + y // Doit s'écrire sur 1 ligne.
// Fonction d'ordre supérieur :
fun fonctionOrdreSup(x: Double, y: Double, somme: (Double, Double) -> Double) = println("$x + $y = ${somme(x, y)}")
// Fonction principale :
fun main(){
fonctionOrdreSup(5.0, 3.0, somme01)
fonctionOrdreSup(-10.0, 7.0, somme02)
}
5.0 + 3.0 = 8.0 -10.0 + 7.0 = -3.0 Process finished with exit code 0
7.1.7. Visibilité des fonctions
Par défaut toutes les fonctions sont visibles par tous. Pour rendre inaccessibles nos fonctions il faut de rajouter au début de leur en-tête le mot clé private.
private fun idFonction(idArg01: typeArg01, idArg02: typeArg02, ...): typeRetour
7.1.8. Le smartCast (transtypage intelligent)
Illustration du besoin de transtypage :
Avant de détailler les posibilités de transtypage intelligent de Kotlin, illustrons nos besoin avec la fonction somme() que nous avons vu en début :
fun somme(operandeGauche: Double, operandeDroit: Double): Double{
return operandeGauche + operandeDroit
}
Cette fonction est peu commode à l’usage car nous devons impérativement fournir 2 arguments de type Double. L’usage du type Int se solde par un échec !
fun main() {
println(somme(2, 3))
}
Kotlin: The integer literal does not conform to the expected type Double
Pour donner plus de souplesse, nous allons modifier le type des paramètres de la fonction somme(). Nous allons utiliser le type Number. En effet avec le système d’héritage, touts les types numériques (Float, Double, etc…) de Kotlin héritent du type Number.
Par contre la somme de deux variables du type Number n’est pas autorisée. Il faut donc procéder à un transtypage vers le type Double par exemple pour pouvoir réaliser la somme.
private fun somme(operandeGauche: Number, operandeDroit: Number): Double{
return operandeGauche.toDouble() + operandeDroit.toDouble()
Test d’un type et smartCast avec is
Nous serons mener à déterminer le type d’un objet afin d’agir en conséquence. Le mot clé is nous permet de tester le type d’une donnée. Illustons cela avec l’exemple suivant : La fonction taille(Any) ci-dessous prend en argument un objet et retourne sa taille si l’objet est une Collection (List, Set ou Map) ou une chaîne de caractères String.
fun taille(argObj: Any): Int{
if (argObj is Collection<*>) return argObj.size
if (argObj is String) return argObj.length
return 0
}
fun main(){
val listeTest = listOf(0, 1 ,2, 3)
val setTest = setOf(4, 5, 6)
val texte = "Kotlin"
println("Taille de listeTest : ${taille(listeTest)}")
println("Taille de setTest : ${taille(setTest)}")
println("Taille de texte : ${taille(texte)}")
}
Taille de listeTest : 4 Taille de setTest : 3 Taille de texte : 6 Process finished with exit code 0
On parle smartCast car dans d’autres langages la ligne suivante : if (argObj is String) return argObj.length aurait dû s’écrire if (argObj is String) return argObj.toString().length Ici c’est Kotlin qui a pris en charge la transtypage après l’utilisation de is.
Le code reste tout de même un poil rébarbatif avec l’utilisation du if, on est succeptible de conduire un certain nombre de tests suivant les types visés. Une structure/expression avec when devrait être plus optimal… Et c’est la cas, voici ce que cela donne :
fun taille(argObj: Any) = when(argObj){
is Collection<*> -> argObj.size
is String -> argObj.length
else -> 0
}
Transtypage avec as
Avec is nous procédions à un test et à une conversion à la volée en cas de succès d’appartenance au type ciblé. Avec as l’approche est différente, nous allons procéder à une conversion sans procéder à un test préalable. Mais attention il faut que le type de destination soit cohérent
var y: Number = 3L // La constante affectée est un Long
var z: Long = y as Long
var k: Number = 3 // La constante affectée est un Int
var l: Int = k as Int
var bidule: Any = "Kotlin" // La constante affectée est un String
var texte: String = bidule as String
8. La P.O.O. (Programmation Orientée Objet)
8.1. Les classes
8.1.1. Généralités
Les classes en Kotlin disposent d’un constructeur dit primaire dont la syntaxe à la particularité d’être intégré dans l’en-tête de la classe. Cette manière de faire permet de faciliter la déclaration des attributs. Les accesseurs (getters) et mutateurs (setters) sont par ailleurs générés automatiquement. D’autres constructeurs dits secondaires peuvent également être déclarés. C’est leur signature (nombre et types des arguments) qui permet de sélectionner le constructeur idoine pour la phase d’instanciation.
MODIFICATEUR |
VISIBILITE |
public (ou rien) |
C’est le status de visibilité par défaut si aucun modificateur n’est spécifié. La propriété ou fonction est visible de tous. |
private |
La propriété ou fonction n’est visible qu’au niveau de la classe. |
protected |
Propriété ou fonction visible uniquement dans la classe et ses sous classes. |
internal |
Propriété ou fonction visible par tous les éléments du module. |
8.1.2. Définition, déclaration et instanciation d’une classe
-
La définition d’une classe se réalise avec le mot clé class.
-
L’instruction de déclaration est classique étant donné qu’en Kotlin tous les types sont déjà des classes.
-
L’instanciation d’une classe se fait tout simplement en procédant à l’appel du constructeur de la classe.
C’est le retour du constructeur qui permet d’associer la référence de l’objet fraîchement instancié à l’identifiant (déclaré préalablement ou à la volée en même temps que l’instanciation).
---
class IdClasse
---
// Déclaration simple :
val objIdClass: IdClasse
// Instanciation de la classe après déclaration
objIdClass = IdClasse()
// Déclaration & instanciation combinées :
val instanceIdClasse = IdClasse()
8.1.3. Définition du constructeur primaire d’une classe
Philosophie d’approche de la POO avec Kotlin
Les classes acceptent des constructeurs secondaires grâce à la surcharge des méthodes. Cependant il préférable quand c’est possible d’utiliser les paramètres par défaut au niveau des paramètres du constructeur principal.
Le constructeur primaire
En Kotlin le constructeur primaire est associé à l’en-tête de la classe. Le corps du constructeur est optionnel, il correspond à un bloc d’instructions identifié par le nom init.
Pour rappel l’objectif premier du constructeur est de déclarer et d’initialiser les attributs de l’objet instancié.
Nous allons mettre en place dans cette partie une classe Cercle. Puis nous allons faire évoluer cette classe afin de voir tous les éléments associés au constructeur primaire :
-
Etape 1 : Déclaration des attributs à partir de l’en-tête du constructeur principal;
-
Etape 2 : Appel d’une méthode privée à partir du corps du constructeur principal;
-
Etape 3 : Personnalisation des getters (accesseurs) et setters (mutateurs) des attributs.
Nous devons définir la localisation et la dimension du cercle :
-
La localisation est assurée par les coordonnées du centre : coordCentreX et coordCentreY
-
La dimension est définie par le rayon
Des caractéristiques secondaires nous intéressent également :
-
Le périmètre
-
La surface
Etape 1 : Création de la classe et de l’en-tête du construction principal
class Cercle(val coordCentreX: Number = 0, val coordCentreY:Number = 0, val rayon: Number)
En une seule ligne nous avons défini notre classe ! Avec Kotlin il n’est pas forcement nécessaire de définir dans le corps de la classe les attributs et de les initialiser avec les arguments fournis au constructeur. Nous serons menés à le faire sur l’étape 3 afin de personnaliser les getters (accesseurs) et setters (mutateurs).
La création des getters/setters est liée aux mots clés var et val des identifiants (coordCentreX; coordCentreY et rayon) :
-
Absence de val ou var : L’identifiant est un simple paramètre/argument, aucun attribut ne sera généré.
-
val ou var associé à l’identificateur :
-
val ou public val : Création d’un attribut et de son getter (accesseur). L’accès en modification n’est pas autorisé.
-
var ou public var : Création d’un attribut avec son getter (accesseur) et setter (mutateur).
-
-
private val ou var : Création d’un attribut privé accessible uniquement au code de la classe en lecture.
-
protected val ou var : Création d’un attribut avec accès au code de sa classe et des sous-classes.
-
internal val ou var : Création d’une attribut visible uniquement par le code du même package.
Procédons à des essais sur notre classe Cercle.
Pour ce faire :
-
Modifions temporairement l’attribut rayon en le rendant private;
-
Instancions un objet objCercle
-
Accédons aux attributs coordCentreX et coordCentreY;
-
Constatons qu’une tentative d’accès à l’attribut rayon se solde par une erreur (action du modificateur private);
class Cercle(val coordCentreX: Number = 0, val coordCentreY:Number = 0, private val rayon: Number)
fun main(){
val objCercle = Cercle(rayon = 1)
println("Coordonnées centre : (${objCercle.coordCentreX}, ${objCercle.coordCentreY})")
println("Rayon :${objCercle.rayon}")
println(objCercle.rayon) // Echec, attribut privé
objCercle.coordCentreX = 1 // Echec, accès en lecture
}
Etape 2 : Ajout d’une méthode privée pour le calcul du périmètre et de la surface
Remettons l’en-tête dans sa forme initiale :
class Cercle(val coordCentreX: Number = 0, val coordCentreY:Number = 0, val rayon: Number)
Nous allons ajouter 3 éléments :
-
Déclaration des attributs suivants :
-
perimetre
-
surface
-
-
2 méthodes privées :
-
calculerPerimetre()
-
calculerSurface()
-
-
Appel des méthodes par le constructeur primaire des méthodes :
-
calculerPerimetre()
-
calculerSurface
-
En préalable, rajouter l’import de la constante PI et la méthode pow en rajoutant les instructions suivantes :
import kotlin.math.pow
import kotlin.math.PI
Voici ce que cela donne pour notre classe.
class Cercle(val coordCentreX: Number = 0, val coordCentreY: Number = 0, val rayon: Number){
// Attributs :
var perimetre: Double = 0.0
var surface: Double = 0.0
// Méthodes :
private fun calculerPerimetre(){perimetre = 2 * PI * rayon.toDouble()}
private fun calculerSurface(){surface = PI * rayon.toDouble().pow(2)}
// Corps du constructeur principal :
init{
calculerPerimetre()
calculerSurface()
}
}
Nous pouvons vérifier le comportement de notre classe :
val objCercle = Cercle(rayon = 1)
println("Coordonnées centre : (${objCercle.coordCentreX}, ${objCercle.coordCentreY})")
println("Rayon : ${objCercle.rayon}")
println("Périmètre : ${objCercle.perimetre}")
println("Surface : ${objCercle.surface}")
Coordonnées centre : (0, 0) Rayon : 1 Périmètre : 6.283185307179586 Surface : 3.141592653589793 Process finished with exit code 0
Etape 3 : Personnalisation des getters et setters
Nous vous proposons d’implémenter des setters pour les attributs :
-
coordCentreX
-
coordCentreY
-
rayon
Pour coordCentreX et coordCentreY cela se fait très simplement, il suffit de remplacer val par var et le tour est joué.
Par contre pour l’attribut rayon cela n’est pas suffisant… En effet le problème est le suivant, modifier le rayon du cercle implique également un changement des attributs perimetre et surface.
Pour mettre à jour les attributs perimetre et surface il suffit de refaire un appel des méthodes privées calculerPerimetre et calculerSurface. Sauf que durant l’instanciation c’était le constructeur primaire qui assurait ce rôle. Le plus simple en cas de modification de l’attribut rayon est de demander au setters de rayon d’appeler cette méthode !
Procédons donc aux modifications suivantes :
-
Passage de val à var pour les attributs coordCentreX et coordCentreY.
-
Suppression de val pour rayon, à ce moment rayon dans l’en-tête du constructeur principal N’EST PLUS UN ATTRIBUT mais un simple PARAMETRE.
-
Ajout de la déclaration de l’attribut rayon dans le corps de classe.
-
Initialisation de l’attribut rayon avec le paramètre associé rayon.
-
Personnalisation du getter de rayon, on affichera un simple message dans la console, cela est juste un prétexte ici pour montrer comment nous pouvons personnaliser un getter.
-
Personnalisation du setter de rayon afin qu’il procède à l’appel des méthodes : calculerRayon et calculerPerimetre.
class Cercle(var coordCentreX: Number = 0, var coordCentreY:Number = 0, rayon: Number){
// Attributs :
var perimetre: Double = 0.0
var surface: Double = 0.0
var rayon: Number = 0
get() {
println("Exécution du getter du rayon !")
return field // field est l'attribut, ici rayon
}
set(valUpDate) {
field = valUpDate
calculerPerimetre()
calculerSurface()
}
// Méthodes :
private fun calculerPerimetre(){perimetre = 2 * PI * rayon.toDouble()}
private fun calculerSurface(){surface = PI * rayon.toDouble().pow(2)}
// Corps du constructeur principal :
init{
this.rayon = rayon // Initialisation de l'attribut à partir du paramètre du constructeur principal
calculerPerimetre()
calculerSurface()
}
}
On rajoute un appel au setter de rayon pour modifier sa valeur.
fun main(){
val objCercle = Cercle(rayon = 1)
objCercle.rayon = 2.0
println("Coordonnées centre : (${objCercle.coordCentreX}, ${objCercle.coordCentreY})")
println("Rayon : ${objCercle.rayon}")
println("Périmètre : ${objCercle.perimetre}")
println("Surface : ${objCercle.surface}")
}
Exécution du getter du rayon ! Exécution du getter du rayon ! Exécution du getter du rayon ! Exécution du getter du rayon ! Exécution du getter du rayon ! Exécution du getter du rayon ! Coordonnées centre : (0, 0) Exécution du getter du rayon ! Rayon : 2.0 Périmètre : 12.566370614359172 Surface : 12.566370614359172 Process finished with exit code 0
8.1.4. Les constructeurs secondaires
Avant propos
Avant d’aborder l’héritage, nous allons étudier le fonctionnement des constructeurs secondaires dans une classe de base (sans héritage).
Ordre d’appel des constructeurs
Le constructeur primaire qu’il soit défini ou pas est TOUJOURS appelé. Même si la signature utilisée pour l’instanciation cible un constructeur secondaire, ce dernier devra obligatoirement appeler le constructeur primaire (on utilisera le mot clé this)
Voici quelques exemples illustrant notre propos :
class Exemple01{
init {
println("Constructeur primaire")
}
constructor(){
println("Constructeur secondaire")
}
}
fun main(){
val obj01 = Exemple01()
}
Constructeur primaire Constructeur secondaire Process finished with exit code 0
Dans Exemple01 le constructeur primaire n’est pas explicitement défini, le mot clé dans ce cas exceptionnel n’est pas obligatoire pour le constructeur secondaire. Cependant nous constatons que bloc init du constructeur primaire est bien appelé.
Définissons le constructeur primaire (ajout d’une paire de parenthèses) et distingons la signature du constructeur secondaire en lui ajoutant un paramètre.
class Exemple02(){
init {
println("Constructeur primaire")
}
constructor(bourage: Int): this(){
println("Constructeur secondaire")
}
}
fun main(){
val obj01 = Exemple02(0)
}
Constructeur primaire Constructeur secondaire Process finished with exit code 0
Commentaires :
-
Le paramètre bourage du constructeur secondaire nous permet pour notre exemple de distinguer la signature du constructeur secondaire de celle du constructeur primaire.
-
Le mot clé this est ici OBLIGATOIRE à partir du moment que le constructeur primaire est défini pour permettre son appel tout en lui transmettant d’éventuels arguments.
8.1.5. Structure d’une classe avec constructeurs secondaires
Nous vous proposons ici un exemple de classe avec des constructeurs secondaires.
class Exemple03(){
// Attributs :
// (Tous les attributs sont déclarés avec var pour pouvoir
// les initialiser avec les constructeurs secondaires)
var attrib01: Int = 1
private set // On rend la modification impossible
var attrib02: Int = 2
var attrib03: Int = 3
init{
println("constructeur primaire")
}
// Constructeurs secondaires :
constructor( attrib01: Int ): this(){
this.attrib01 = attrib01
println("constructeur 01")
}
constructor( attrib02: Int, attrib03: Int ): this() {
this.attrib02 = attrib02
this.attrib03 = attrib03
println("constructeur 02")
}
}
constructeur primaire attrib01 = 1 constructeur primaire constructeur 01 attrib01 = 5 constructeur primaire constructeur 02 attrib02 = 20 et attrib03 = 30 Process finished with exit code 0
L’utilisation de val n’est pas possible car à ce moment la valeur n’acceptera pas de changement à partir du constructeur secoondaire. La combinaison des mots clés private set permettent de d’interdire la modification des valeurs par le "getteur".
8.2. Les attributs & méthodes de classe (Les objets compagnons)
Dans des langages purement orientés objets comme Java nous somme obligés de déclarer des méthodes statiques pour accéder à la méthode sans passer par une instance de la classe définissant la méthode.
Ce type de méthodes accessiblent directement d’une classe sont communément désignées des méthodes de classe.
Il peut également être intéressant que des instances d’une classe puissent partager des attributs ayant des valeurs communes à tous les objets, c’est ce que l’on appelle des attributs de classe.
En Kotlin il est possible d’obtenir ce genre de comportement de la part d’attributs et de méthodes. Cependant le moyen pour y parvenir diffère de ce que nous trouvons habituellement dans d’autres langages. En effet Kotlin va utiliser des objets dits compagnon pour implémenter les attributs et méthodes de classe.
class Math(){
init{compteur += 1}
companion object{
// Définition d'un attribut de classe constant :
val PI = 3.1415927
// Définition d'un attribut mutable :
var compteur = 0
// Définition d'une méthode de classe :
fun puissance2(x: Double) = x*x
}
}
fun main(){
println("Le nombre PI : ${Math.PI}")
println("4 puissance 2 : ${Math.puissance2(4.0)}")
val obj01 = Math()
val Obj02 = Math()
println("Nombre d'instances de la classe Math : ${Math.compteur}")
}
Le nombre PI : 3.1415927 4 puissance 2 : 16.0 Nombre d'instances de la classe Math : 2 Process finished with exit code 0
8.3. L’héritage
8.3.1. Principes de base de l’héritage en Kotlin
L’héritage est un concept puissant notamment pour factoriser du code. Cependant cela peut égalementconduire à des dérives. En effet des sous-classes (classes filles) peuvent avec le mot clé override redéfinir des méthodes de la classe parente. Or cette redéfinition n’est pas forcément cohérente ou ne respecte pas les comportements initialement attendus. Cela est en contradiction avec les principes de la POO. Ainsi il est conseillé de fermer à l’héritage des classes qui ne sont pas pensées pour cet objectif.
En Kotlin la statégie avec l’héritage est en opposition avec celle de Java. En effet, en Java par défaut toutes les classes et leurs méthodes sont ouvertes à l’héritage. En Kotlin c’est l’inverse, les classes et méthodes sont fermées à l’héritage par défaut !
8.3.2. Ouvrir une classe à l’héritage
Comme expliqué précédement, par défaut il n’est pas possible d’utiliser l’héritage sur une classe ne le permettant pas. Il faut que la classe l’autorise explicitement avec le mot clé open.
open class IdClasse(paramètres & attributs du constructeur...) {
// Corps de la classe
}
MODIFICATEUR ACCES |
ROLE |
final |
Elément (classe, méthode ou propriété) ne pouvant pas être redéfini par l’héritage. C’est l'état par défaut de tous les éléments. |
open |
Elément accessible par l’héritage et POUVANT être redéfini. |
abstract |
Elément DEVANT être redéfini. Utilisable unique pour les classes abstraites. |
override |
Précède tout élément redéfini dans une classe enfant. |
8.3.3. Héritage d’une classe de base vers une classe dérivée
Mise en relation d’une classe dérivée avec une classe de base ayant un constructeur SANS arguments
L’instruction est simple à mettre en oeuvre, la classe dérivée aura dans son en-tête un lien avec la classe de base (classe mère) en faisaint appel au construteur de la classe mère :
// Définition de la classe de base :
open class Base(){
// Corps de la classe Base
}
// Définition de la classe dérivée :
class Derive(): Base(){
// Corps de la classe Derive
}
8.3.4. Mise en relation d’une classe dérivée avec une classe de base ayant un constructeur AVEC arguments
Notre exemple précédent était très basique car le constructeur de la classe de base ne prennait aucun argument. Cela nous permettait de mettre en avant le principe de mise en relation d’une classe de base avec sa classe dérivée.
Si le constructeur de la classe de base comporte des paramètres, il faut alors impérativement fournir des arguments à l’appel du constructeur de la classe de base dans l’en-tête de la classe dérivée.
// Définition de la classe de base :
open class Base(var attrib01: Int, val attrib02: Int){}
// Définition de la classe dérivée :
class Derive(arg01: Int, arg02: Int): Base(arg01, arg02){}
Il faut bien remarquer que les 2 paramètres du constructeur primaire de la classe Base vont générer 2 attributs (les paramètres sont accompagnés des mots clés var et val). Le constructeur de la classe Derive comporte 2 paramètres qui seront transmis en argument dans l’appel du constructeur Base() de la classe de Base. |
8.3.5. Enrichissement d’une classe dérivée : Ajout d’attributs et de méthodes
Sans ajout d’attributs et/ou de méthodes à une classe dérivée, l’héritage n’a aucun intérêt. Il n’y a pas de difficultés particulières, il suffit de déclarer nos méthodes et attributs dans la classe dérivée exactement comme nous pourrions le faire dans une classe de base.
// Définition de la classe de base :
open class Base(var attrib01: Int, val attrib02: Int){
var attrib04: Int = 4
init{
println("Constructeur primaire de Base")
}
fun methode01(){
println("Méthode01 de Base")
}
}
// Définition de la classe dérivée :
class Derive(arg01: Int, arg02: Int, val attrib03: Int): Base(arg01, arg02){
init{
println("Constructeur primaire de Derive")
println("attrib01 : ${attrib01}\nattrib02 : ${attrib02}\nattrib03 : ${attrib03}")
}
fun methode02(){
println("Méthode02 de Derive")
}
}
Constructeur primaire de Base Constructeur primaire de Derive attrib01 : 1 attrib02 : 2 attrib03 : 3 Attrib04 : 4 Méthode01 de Base Méthode02 de Derive Process finished with exit code 0
Il faut évidement que les identifiants des attributs et méthodes soient distincts. Dans le cas contraire vous vous retrouvez dans une situation de rédéfinition de l’attribut/méthode qui nécessite l’usage des mots clé open et override. |
8.3.6. Redéfinition des attributs et/ou méthodes d’une classe de base
La redéfinition d’un attribut ou d’une méthode est fermée par défaut en Kotlin. Il faudra impérativement pour chaque attribut/méthode redéfini placer en préfixe le mot clé open. |
Redéfinition des attributs
Le redéfinition d’un attribut comporte des limites sur les mots clés var/val et sur le type de l’attribut. On peut redéfinir un val vers var mais la réciproque est fausse ! Le type peut être redéfini si les deux types sont compatibles (ex: de Number vers Int ) |
La redéfinition peut être utilisée pour modifier la valeur d’initialisation :
// Définition de la classe dérivée :
class Derive(arg01: Int, arg02: Int, val attrib03: Int): Base(arg01, arg02){
override var attrib04 = 40 // Redéfinition de la valeur
Mais nous pouvions déjà obtenir le même résultat en utilisant tout simplement le bloc init du constructeur primaire de la classe dérivée.
// Définition de la classe dérivée :
class Derive(arg01: Int, arg02: Int, val attrib03: Int): Base(arg01, arg02){
init{
attrib04 = 40
...
Le principal intérêt de redéfinir un attribut est de personnaliser son getter (accesseur) et/ou son setter (mutateur).
// Définition de la classe dérivée :
class Derive(arg01: Int, arg02: Int, val attrib03: Int): Base(arg01, arg02){
override var attrib04 = 40
get() = field + 10
set(valeur) {field = valeur - 10}
Dans l’exemple ci-dessous on applique un décalage de la valeur de l’attribut redéfini aussi bien pour un accès en lecture (getter) ou en écriture (setter).
Redéfinition des méthodes
Préalable : Le redéfinition d’une méthode doit avoir comporter la même signature de la méthode originale.
open class Base(var attrib01: Int, val attrib02: Int){
open var attrib04: Int = 4
init{
println("Constructeur primaire de Base")
}
open fun methode01(){
println("Méthode01 de Base")
}
}
// Définition de la classe dérivée :
class Derive(arg01: Int, arg02: Int, val attrib03: Int): Base(arg01, arg02){
init{
println("Constructeur primaire de Derive")
println("attrib01 : ${attrib01}\nattrib02 : ${attrib02}\nattrib03 : ${attrib03}")
}
override fun methode01(){
println("Méthode01 redéfinie par Derive")
}
8.3.7. Utilisation du mot clé super
Le mot clé super à l’image du mot clé this permet de lever l’ambiguïté sur des références d’éléments hérités et redéfinis localement dans la classe dérivée.
Accéder à une méthode/attribut redéfini
open class Open(){
open val attrib01: Int = 1
open val attrib02: Int = 10
val attrib03: Int = 100
open fun methode01(){
println("Appel de Open")
}
}
class Derive(): Open(){
override val attrib01 = 2 * super.attrib01 + super.attrib02
override val attrib02: Int = super.attrib02
get() = field + attrib03
override fun methode01(){
super.methode01()
println("Appel de Derive")
}
}
fun main(){
val obj = Derive()
println("obj.attrib01 = ${obj.attrib01}")
obj.methode01()
println("obj.attrib02 = ${obj.attrib02}")
obj.attrib01 = 12 Appel de Open Appel de Derive obj.attrib02 = 110 Process finished with exit code 0
Appel des constructeurs secondaires
L’intérêt des constructeurs secondaires est d’assurer l’héritage avec des classes écrites en Java. On va ici se contenter d’illustrer leur usage sur du code purement en Kotlin. Le principe est identique à celui vu précédemment sur les méthodes et attributs mais appliqué aux constructeurs.
open class Base{
var champ01: Int = 1
var champ02: Int = 2
constructor()
constructor(arg01: Int, arg02: Int): this() {
champ01 = arg01
champ02 = arg02
}
}
class Derive: Base{
constructor(): super()
constructor(arg01: Int, arg02: Int): super(arg01, arg02)
}
8.3.8. Les classes abstraites
Règles & propriétés des classes abstraites
-
Une classe abstraite se déclare avec le mot clé abstract en préfixe du mot clé class
-
Une classe abstraite NE PEUT PAS ETRE INSTANCIEE
-
Une classe abstraite PEUT ETRE HERITEE par une classe dérivée.
-
Il n’est pas nécessaire d’ouvrir une classe abstraite avec open, elle est ouverte par défaut.
-
Une classe abstraite peut avoir des éléments (méthodes/attributs) à la fois non abstraits et abstraits.
-
Les méthodes et attributs d’une classe abstraite NE SONT PAS ABSTRAITS par défaut, il faut le spécifier avec abstract en préfixe des identifiants.
-
Les méthodes et attributs d’une classe abstraite SONT FERMES par défaut, il faut les ouvrir avec open pour rendre la redéfinition possible (ou utiliser abstract mais à ce moment la redéfinition sera obligatoire).
-
Une méthode marquée comme abstraite NE PEUT PAS AVOIR DE CORPS, c’est à la classe fille de réaliser l’implémentation complète de la méthode.
-
Un élément défini abstrait devra OBLIGATOIREMENT REDEFINI par les classes dérivées avec override.
Une méthode ou un attribut ne peuvent être déclarés comme abstract uniquement si elle fait partie d’une classe abstraite. |
Intérêt des classes abstraites
-
Définir l’interface/contrat d’objets qui seront le résultat de l’instanciation de classes héritant de la classe abstraite.
-
Comme son nom l’indique dans une démarche de modélisation objet, la classe de base de plus haut niveau peut collecter des méthodes et attributs communs à des classes dérivée, mais l’instanciation de cette classe abstraite ne correspond pas à un objet "tangible"/"consistant/"existant".
Exemples :
-
Nous pouvons définir une classe abstraite Humain et la faire hériter par une classe Homme et/ou Femme. Une instance de Humain n’a pas de sens alors que nous pouvons instancier une classe Homme ou Femme.
-
De même nous pouvons créer d’autres classes abstraites qui devront être héritées avant de pouvoir instancier des objets faisant sens : Une classe Espece ou Animal devront être héritées de classes comme Chien, Lion, etc, pour instancier des objets animaux.
-
Une classe Figure (figure géométrique) sera abstraite et sera héritée de classes comme : Carre, Rectangle, Cercle, etc.
Exemples d’applications 1 : Héritage et classes abstraites sur des figures géométriques
abstract class Figure{
abstract var couleur: String
abstract val perimetre: Float
abstract val aire: Float
}
abstract class Polygone: Figure(){
abstract val nbCotes: Int
}
abstract class Quadrilatere: Polygone(){
override val nbCotes = 4
}
open class Rectangle(
override var couleur: String,
var longueur: Float,
var largeur: Float
): Quadrilatere(){
final override var perimetre: Float = 0.0F // final permet de mettre fin à l'héritage
get() {
field = 2 * (longueur + largeur)
return field
}
// Blocage de l'accès en écriture
private set // Il faut impérativement repasser l'attribut en final pour rendre privé le setter
final override var aire: Float = 0.0F // final permet de mettre fin à l'héritage
get() {
field = longueur * largeur
return field
}
// Blocage de l'accès en écriture :
private set // Il faut impérativement repasser l'attribut en final pour rendre privé le setter
}
class Carre(couleur: String, longueur: Float): Rectangle(couleur, longueur, longueur)
fun main(){
val carre01 = Carre("blanc", 5.0F)
println("Couleur : ${carre01.couleur}")
println("Perimètre : ${carre01.perimetre}")
println("Aire : ${carre01.aire}")
}
Couleur : blanc Perimètre : 20.0 Aire : 25.0 Process finished with exit code 0
Exemples d’applications 2 : Héritage et classes abstraites sur des personnes
abstract class Personne(
val nom: String,
val prenom: String,
var age: Int){
open fun presenter(){
println("Nom : ${nom}")
println("Prenom : ${prenom}")
println("Age : ${age}")
}
fun vieillir(){
age += 1
}
}
class Employe(nom: String, prenom: String, age: Int, var poste: String): Personne(nom, prenom, age){
var salaireMensuel: Float = 0.0F
override fun presenter(){
super.presenter()
println("Poste : ${poste}")
println("Salaire : ${salaireMensuel}")
}
fun augmenterSalaire(augmentation: Float){
salaireMensuel += augmentation
}
}
fun main(){
val comptable01 = Employe("DOE", "Joe", 28, "Comptable")
comptable01.salaireMensuel = 1_950.00F
comptable01.presenter()
}
Nom : DOE Prenom : Joe Age : 28 Poste : Comptable Salaire : 1950.0 Process finished with exit code 0
8.3.9. Les interfaces
Objectifs et principes des interfaces
Les interfaces sont très proches des classes abstraites au niveau de l’implémentation. Cependant les objectifs ne sont pas similaires.
Comme nous l’avions précisé précédemment, une classe abstraite à pour principal objectif de factoriser du code. Pour ce faire on extrait de classes concrètes et dérivées la partie du code qui est commun.
Lorsque le processus d’héritage est suffisamment poussé nous aboutissons à des classes ne correspondant plus à des objets concrets mais à leurs éléments abstraits et communs. Les classes abstraites contiennent notamment la partie commune de l’implémentation des méthodes. Ces méthodes seront complétées dans les classes dérivée au moyen des mots clés override et super.
Les interfaces n’ont pas pour objectif de factoriser du code, elles sont un moyen permettant de définir un contrat/interface sans se soucier de son implémentation. Alors qu’une classe abstraite sera héritée par une lignée/hiérarchie de classes, une interface pourra être héritée sur différentes hiérarchies de classes. Une interface peut contenir des éléments d’implémentations de méthodes, mais elle permet avant tout de définir les signatures des méthodes.
Règles & propriétés des interfaces
-
Tous les éléments contenus dans une interface sont SYSTEMATIQUEMENT ABSTRAITS.
-
Les attributs ne peuvent être initialisés sur une valeur mais il est possible de définir le getter (accesseur). Mais à ce moment l’attribut perd automatiquement son statut de abstract et devient open
-
Une méthode qui implémente un corpes perd automatiquement son staut de abstract et deveint open
-
Une classe peut hériter de plusieurs interfaces.
-
Une interface peut hériter d’une autre interface.
-
Tous les éléments contenu dans une interface devront être redéfinis par les classes héritant de l’interface sauf si une interface les a déjà redéfini durant le processus de l’héritage.
8.3.10. Définition d’une interface en Kotlin
Nous allons illustrer la définition des interfaces en Kotlin à travers de l’exemple des figures géométriques (exemple utilisé également dans le cadre des classes abstraites).
Pour mettre en avant les interfaces d’un point de vue pédagogique, nous avons pris le parti de n’utiliser que des interfaces et des classes. Mais d’un point de vue développement il aurait été pertinent d’utiliser également des classes abstraites. Un critère pour choisir une implémentation avec une classe abstraite ou une interface est que dès qu’un élément est redéfini, il est alors peut-être plus judicieux de choisir une classe abstraite. Cependant l’un des avantage des interfaces, c’est qu’une interface ou une classe peuvent héritées de plusieurs interfaces ! |
Les langages de programmation n’autorisent généralement pas l’héritage de plusieurs classes. La raison est qu’une méthode pourrait être redéfinie et implémentée différemment par 2 classes qui seraient héritées par une 3ème ! Ce problème est connu sous le nom du "problème du diamant". Les interfaces sont moins sujette à ce problème à partir du fait qu’elles n’implémentes pas les méthodes.
En cas de conflit de noms sur des attributs et/ou méthodes hérité de plusieurs interfaces, on pourra utiliser l’instruction suivante pour spéficifier l’élément en utilisant le mot clé super : super<iDinterface>.elmt
interface Inter01{
val attribut01: Int
fun methode01()
}
interface Inter02{
val attribut02: Int
val attribut03: Int
get() = 2 // get redéfini donc attribut à open au lieu d'abstract.
fun methode02(){} // La méthode à un corps, elle devient open au lieu d'abstract.
}
class Cobaye(): Inter01, Inter02{
override val attribut01 = 1
override val attribut02 = 2
override fun methode01() {}
}
Application des interfaces sur l’exemple des figures :
// Définitions des interfaces :
// ----------------------------
interface Figure{
val couleur: String
get() = "noir"
val perimetre: Float
val surface: Float
fun presenter()
fun tracer()
}
interface Polygone: Figure{
val nbCotes: Int
get() = 3
}
interface PolyGoneRegulier: Polygone{
val longueur: Float
override val perimetre: Float
get() = super.nbCotes * longueur
}
interface Quadrilatere: Polygone{
override val nbCotes: Int
get() = 4
}
interface Parallelogramme: Quadrilatere{
val base: Float
val hauteur: Float
override val surface: Float
get() = base * hauteur
}
// Définition des classes :
// ------------------------
class Rectangle(longueur: Float, largeur: Float): Parallelogramme{
// Redéfinition des attributs hérités :
override val base = longueur
override val hauteur = largeur
override val perimetre: Float
get() = 2 * (base + hauteur)
// Redéfinition des méthodes héritées :
override fun presenter() {
println("Je suis un rectangle")
println("Nombre de côté : ${nbCotes}")
println("Longueur : ${base}")
println("Largeur : ${hauteur}")
println("Périmètre : ${perimetre}")
println("surface : ${surface}")
}
override fun tracer() {
println("Méthode non implémentée")
}
}
class Carre(longueur: Float): PolyGoneRegulier, Parallelogramme{
override val longueur = longueur
override val base = longueur
override val hauteur = longueur
override val perimetre: Float // Attention si non redéfini utilise nbCotes = 3 de Polygone !!!
get() = super<Parallelogramme>.nbCotes * longueur // L'utilisation de super<Parallelogramme> lève l'ambiguïté
override fun presenter(){
println("Je suis un carré")
println("Nombre de côté : ${nbCotes}")
println("Longueur : ${longueur}")
println("Périmètre : ${perimetre}")
println("surface : ${surface}") }
override fun tracer(){
println("Méthode non implémentée")
}
}
fun main(){
val rectangle01 = Rectangle(10.0f, 5.0f)
rectangle01.presenter()
println()
val carre01 = Carre(5.0f)
carre01.presenter()
}
Je suis un rectangle Nombre de côté : 4 Longueur : 10.0 Largeur : 5.0 Périmètre : 30.0 surface : 50.0 Je suis un carré Nombre de côté : 4 Longueur : 5.0 Périmètre : 20.0 surface : 25.0 Process finished with exit code 0
8.4. Les extensions
8.4.1. Objectifs des extensions
Kotlin ré-utilise les bibliothèques et frameworks déjà disponibles en Java. Il n’était pas possible de réécrire toutes ces sources en Kotlin. Afin de permettre d’enrichir ces éléments existants sans pour autant repartir de zéro, Kotlin intègre un nouveau concept, les extensions. Les extensions permettent de rajouter des méthodes et des attributs à des classes existantes.
8.4.2. Principe de l’implémentation des extensions
Le principe est très simple on utilisera la notation pointée pour étendre une classe existante avec nos propres éléments.
val String.demiTaille: Int
get() = length / 2
fun main(){
println("Demi taille du mot \"anticonstitutionnellement\" : ${"anticonstitutionnellement".demiTaille}")
}
Demi taille du mot "anticonstitutionnellement" : 12 Process finished with exit code 0
fun String.debuterMajuscule(): String{
return this[0].toUpperCase().toString() + this.substring(1)
}
fun Int.estPair(): Boolean{
if (this % 2 == 0) return true else return false
}
fun main(){
println("le langage Kotlin.".debuterMajuscule())
println("97 est paire : ${97.estPair()}")
}
Le langage Kotlin. 97 est paire : false Process finished with exit code 0
8.5. Allons plus loin avec les Classes
8.5.1. Redéfinition de la méthode toString() de la classe Any
Comme nous l’avions déjà précisé, tous les objets de Kotlin vont hériter de la classe Any. Any implémente une méthode toString(). Cette méthode est automatiquement appelée lorsque nous désirons convertir un objet en chaîne de caractères. La conversion est implicite lorsque nous passons un objet en argument de la fonction print().
class Cobaye
fun main(){
val obj = Cobaye()
val descriptionObj: String = "Résultat de toString() : " + obj
println(obj)
println(descriptionObj)
}
Cobaye@404b9385 Résultat de toString() : Cobaye@404b9385 Process finished with exit code 0
Nous pouvons naturellement redéfinir la méthode toString() afin de personnaliser la déscription de l’objet.
class Cobaye(val valeur: Int){
override fun toString() = "Je suis un objet de la classe Cobaye.\nJ'ai la valeur : ${valeur}."
}
fun main(){
val obj = Cobaye(38)
println(obj)
}
Je suis un objet de la classe Cobaye. J'ai la valeur : 38. Process finished with exit code 0
8.5.2. Les Data Class (classe de données)
Kotlin propose un type de classe spécialisée pour mémoriser des données. C’est le mot clé préfixe data qui nous permet de définir ce type de classe.
Le compilateur va alors nous faciliter le travail en générant les méthodes suivantes :
-
copy()
-
equals()
-
hashCode()
-
toString()
Pour que Kotlin accepte de générer une classe data, certaines conditions doivent être respectées :
-
Le constructeur primaire doit contenir au moins un paramètre;
-
Les paramètres du constructeur primaire sont tous précédés de val ou var
-
Une classe data peut hériter d’une interface ou d’une classe mais elle ne peut pas être ouverte (open) à l’héritage pour des classes dérivées. De même la classe ne peut être : abstract; inner ou sealed
Exemple : Consignons des articles dans une classe de données
data class Article(val nom: String, var quantite: Int, var prix: Float)
fun main(){
val unArticle = Article("Polo", 5, 35.30f)
println(unArticle) // Utilisation de la méthode toString()
}
article(nom=Polo, quantite=5, prix=35.3) Process finished with exit code 0
Dans notre exemple nous avons généré un objet de la classe de donnée Article. La méthode toString a été automatiquement générée pour afficher les différents champs de notre objet.
-
Les méthodes hashCode() et equals():
hashCode() est une méthode qui génère un numéro d’identification pour chaque objet. On va ainsi pouvoir comparer des objets. La méthode hashCode() réalise la comparaison et retourn un booléen.
data class Article(val nom: String, var quantite: Int, var prix: Float)
fun main(){
val unArticle = Article("Polo", 5, 35.30f)
println(unArticle) // Utilisation de la méthode toString()
println("hashCode unArticle : ${unArticle.hashCode()}")
val unDoublon = Article("Polo", 5, 35.30f)
println("hashCode unDoublon : ${unDoublon.hashCode()}")
println("unArticle identique à unDoublon : ${unArticle.equals(unDoublon)}")
Article(nom=Polo, quantite=5, prix=35.3) hashCode unArticle : -790638800 hashCode unDoublon : -790638800 unArticle identique à unDoublon : true Process finished with exit code 0
-
La méthode copy() : Cette méthode nous permet de générer rapidement des copies et de modifier à la volée certaines données.
data class Article(val nom: String, var quantite: Int, var prix: Float)
fun main(){
val unArticle = Article("Polo", 5, 35.30f)
println(unArticle) // Utilisation de la méthode toString()
println("hashCode unArticle : ${unArticle.hashCode()}")
val nouveauArticle = unArticle.copy(prix = 42.20f)
println(nouveauArticle)
}
Article(nom=Polo, quantite=5, prix=35.3) hashCode unArticle : -790638800 Article(nom=Polo, quantite=5, prix=42.2) Process finished with exit code 0
-
Le "Destructuring Declarations" (déconstruction) :
Les data class permettent de faire du destructuring declarations (déconstruction en français) c’est-à-dire que nous allons pouvoir affecter en une seule ligne d’instruction les données mémorisées dans la data class dans des variables d’accueil.
data class Article(val nom: String, var quantite: Int, var prix: Float)
fun main(){
val unArticle = Article("Polo", 5, 35.30f)
val(designation, nombre, montant) = unArticle
println("designation = $designation")
println("nombre = $nombre")
println("montant = $montant")
}
designation = Polo nombre = 5 montant = 35.3 Process finished with exit code 0
8.5.3. Classes anonymes (expressions objets)
Principe
Le style fonctionnel de Kotlin nous amène à fournir des références d’objets en arguments de fonctions/méthodes. On peut se retrouver au final à définir une classe et à instancier un objet juste pour fournir l’identifiant de l’objet en argument, en dehors de cette application ponctuelle, l’objet et sa classe n’auront plus d’usage. Afin d’alléger nos programmes Kotlin offre la possibilité d’instancier un objet dans l’appel de fonctions/méthodes. Il existe différentes façons de procéder, c’est ce que nous allons voir maintenant.
Utilisation du mot clé object (définition à la volée d’une classe/ "from scratch")
Le mot clé object va nous permettre d’instancier un objet en définissant à la volée la définition de la classe.
// Définition à la volée d'un objet :
val objDemo = object {
val mot1 = "Bonjour"
val mot2 = "à tous !"
override fun toString() = "$mot1 $mot2"
}
fun main(){
// Passage de l'identifiant à la fonction println
println(objDemo)
}
Bonjour à tous ! Process finished with exit code 0
Définition à la volée d’une classe héritant d’une classe et/ou interface
Comme pour la définition d’une classe conventionnelle, nous pouvons avec object faire hériter de notre classe d’une classe et/ou interface. La syntaxe est proche de la syntaxe utilisée de la syntaxe conventionnelle à la différence que object se substitue à class et son identifiant.
// Définition d'une interface :
interface Personne {
val nom: String
val prenom: String
}
// Définition d'une classe abstraite :
abstract class PersonneDefaut(
override val nom: String = "DOE",
override val prenom: String = "John"
): Personne
// Fonction prennant en argument un objet de type Personne
fun direBonjour(argObj: Personne) {
println("Bonjour ${argObj.nom} ${argObj.prenom} !")
}
fun main() {
direBonjour(object: PersonneDefaut(){})
direBonjour(object: Personne{
override val nom: String = "DUPONT"
override val prenom: String = "Jean"})
}
Bonjour DOE John ! Bonjour DUPONT Jean ! Process finished with exit code 0
8.5.4. Les énumérations
Principe des énumérations
Les énumérations permettent de consigner des constantes dans un type de données dédié à cet usage.
Pour illustrer cela, imaginons que nous désirions consigner tous les mouvements possibles d’un personnage dans un jeu. Nous allons d’abord définir le nombre de mouvements, puis les nommer. Enfin, au lieu de mémoriser le nom des mouvements dans une collection (tableau, liste, etc.) on utilisera une énumération.
Les énumérations sont des classes NON INSTANCIABLES qui contiennent les constantes à énumérer. Chaque constante énumérée est elle-même un OBJET de la classe Enum.
Les constantes saisies dans une énumération doivent respecter les contraintes syntaxiques des identifiants de variables, fonctions ou classes/objets (seuls les caractères de type lettres, le tiret du bas et les chiffres mais la constante ne doit pas commencer par un chiffre).
La classe Enum n’est pas héritable par une classe dérivée. Par contre elle peut hériter d’une interface (par contre l’héritage d’une classe n’est pas autorisé).
Voyons comment procéder à la déclaration d’une affectation avant de pousser plus loin les caractéristiques et fonctionnement des énumérations.
Définition & exploitation des énumérations
Reprenons l’exemple des directions possibles dans un jeu, nous décidons de reprendre le nom des points cardinaux (NORD, SUD, EST et OUEST). Voici l’implémentation de l’énumération correspondante :
enum class Direction {
NORD, SUD, EST, OUEST
Dans cet exemple l’énumération est une classe ayant l’identifiant Direction. Comme nous l’avions exposé précédemment, chaque constante est un objet de la classe Enum.
Autre exemple, nous pourrions également consigner les jours de la semaine :
enum class Jours{
LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE
Accès aux objets constants de l’énumération (méthode toString() )
La classe Enum est non instanciable et ne possède pas d’objets compagnons (elle n’a pas d’attributs ou de méthode de classes). Par contre nous pouvons utiliser la notation pointée pour accéder à nos objets constants.
Les objets constants implémentent la méthode toString() afin de retourner un String nous permettant de récupérer le nom de la constante.
fun main(){
println("Direction.EST : ${Direction.EST}")
println("Jours.SAMEDI is String : ${Jours.SAMEDI is Enum<*>}")
}
Direction.EST : EST Jours.SAMEDI is String : true Process finished with exit code 0
Méthode valueOf()
La méthode valueOf() permet d’accéder à une constante comme on le fait avec le notation pointée. La différence réside en 2 points :
-
L’argument fourni à valueOf() est le nom de la constante au format String (constante entre " ")
-
La méthode lève une exception si la constante est absente de l’énumération.
fun main(){
println(Direction.valueOf("SUD"))
println(Direction.valueOf("NORD_EST"))
}
SUD Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Direction.NORD_EST at java.base/java.lang.Enum.valueOf(Enum.java:273) at Direction.valueOf(enumerations.kt) at TestsKt.main(Tests.kt:13) at TestsKt.main(Tests.kt) Process finished with exit code 1
Méthode values()
La méthode values() est la plus intéressante, car elle va nous retourner un tableau (array) de l’ensemble des constantes de l’énumération. On pourra donc appliquer les méthodes et instructions sur ce tableau par exemple pour :
-
Connaitre le nombre de constantes
-
Itérer l’ensemble des constantes.
-
Etc.
fun main(){
println("Direction.values() est-il un Array ? : ${Direction.values() is Array<*>}")
println("Nombre de direction : ${Direction.values().size}")
println("Constantes de l'énumération Direction : ${Direction.values().contentToString()}")
// Affichage des constantes avec forEach() :
Direction.values().forEach { println(it) }
}
Direction.values() est-il un Array ? : true Nombre de direction : 4 Constantes de l'énumération Direction : [NORD, SUD, EST, OUEST] NORD SUD EST OUEST Process finished with exit code 0
Les attributs name & ordinal des objets constants
Les objets constants d’une énumération possède deux attributs :
-
name : Retourne le nom de la constante
-
ordinal : Retourne le rang/position de la constante dans l’énumération
fun main(){
println(Jours.MARDI.name is String)
println(Jours.MARDI.ordinal is Int)
println("${Jours.DIMANCHE} a pour nom : ${Jours.DIMANCHE.name} et est le jour n°${Jours.DIMANCHE.ordinal}")
}
Résultat :
true true DIMANCHE a pour nom : DIMANCHE et le jour n°6 Process finished with exit code 0
Utilisation de la boucle when avec les énumérations
Il peut être intéressant d’exploiter une énumération avec la structure when.
fun presenterJour(jour: Jours) = "Nous sommes ${jour} le jour n°${jour.ordinal + 1}"
fun main(){
when(Jours.MARDI){
Jours.LUNDI -> println(presenterJour(Jours.LUNDI))
Jours.MARDI -> println(presenterJour(Jours.MARDI))
Jours.MERCREDI -> println(presenterJour(Jours.MERCREDI))
Jours.JEUDI -> println(presenterJour(Jours.JEUDI))
Jours.VENDREDI -> println(presenterJour(Jours.VENDREDI))
Jours.SAMEDI -> println(presenterJour(Jours.SAMEDI))
Jours.DIMANCHE -> println(presenterJour(Jours.DIMANCHE))
}
}
Nous sommes MARDI le jour n°2 Process finished with exit code 0
Ajout d’attributs aux objets constants d’une énumération
Les énumérations génèrent des objets, nous allons voir maintenant comment ajouter des attributs à nos constantes.
Le principe est déclarer un constructeur primaire après l’identifiant de l’énumération. Ce constructeur primaire ne s’appliquera pas à l’énumération elle-même (l’énumération n’est pas instanciable et n’a donc pas de constructeur) mais à ces objets constants.
Pour chaque attribut l’instruction : val identification: Type pour définir les attributs.
Exemple, nous désirons définir des couleurs. Pour ce faire nous allons à la fois nommer nos couleurs prédéfinis comme nous avions défini les direction et les jours dans les 2 exemples précédents. Mais ensuite techniquement nous avons également besoin de connaitre le codage RGB (Red, Green, Blue) de nos couleurs.
enum class RGB(val r: Int, val g: Int, val b: Int) {
ROUGE(255, 0, 0),
VERT(0, 255, 0),
BLEU(0, 0, 255),
NOIR(0, 0, 0),
BLANC(255, 255, 255)
}
Maintenant que nous avons défini l’énumération RGB il est possible de l’exploiter, dans l’exemple suivant nous affichons le code RGB du bleu :
fun main(){
println("Le code RGB de la couleur ${RGB.BLEU} est : [${RGB.BLEU.r}, ${RGB.BLEU.g}, ${RGB.BLEU.b}]")
}
Le code RGB de la couleur BLEU est : [0, 0, 255] Process finished with exit code 0
Personnalisation de la classe d’énumération
Nous pouvons implémenter nos méthodes à l’intérieur de l’énumération. Nous pouvons également implémenter des objets compagnons permettant d’accéder au méthodes avec une syntaxe plus courte.
Pour illustrer notre propos nous allons repartir sur une nouvelle version de l’énumération Jours. Nous allons nommer cette nouvelle version JoursSemaine.
enum class JoursSemaine(var numJour: Int, var nomString: String){
// Constantes de l'énumération :
LUNDI(1, "Lundi"),
MARDI(2, "Mardi"),
MERCREDI(3, "Mercredi"),
JEUDI(4, "Jeudi"),
VENDREDI(5, "Vendredi"),
SAMEDI(6, "Samedi"),
DIMANCHE(7, "Dimanche");
// Méthodes supplémentaires :
fun customToString() = "Jour n°${numJour} : ${nomString}"
// Objet compagnon (comportement d'une méthode statique) :
companion object {
fun presenter() {
println(values().contentToString())
}
}
}
fun main(){
println(JoursSemaine.MARDI.customToString())
JoursSemaine.presenter()
}
Jour n°2 : Mardi [LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE] Process finished with exit code 0
Redéfinir une méthode selon l’objet constant grâce à l’abstraction
Les méthodes implémentées vont avoir le même déroulement pour chaque objet constant de l’énumération. L’utilisation d’une méthode abstraite permet de redéfinir la méthode pour chacun des objets.
enum class JoursSemaine(var numJour: Int, var nomString: String){
// Constantes de l'énumération :
LUNDI(1, "Lundi")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, c'est la reprise du boulot..."},
MARDI(2, "Mardi")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, mieux que le lundi."},
MERCREDI(3, "Mercredi")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, encore 3 jours."},
JEUDI(4, "Jeudi")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, bientôt le week-end."},
VENDREDI(5, "Vendredi")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, vivement ce soir !"},
SAMEDI(6, "Samedi")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, c'est le week-end !!!!"},
DIMANCHE(7, "Dimanche")
{override fun customToString() = "Jour n°${numJour} : ${nomString}, ça passe trop vite..."};
// Méthodes supplémentaires :
abstract fun customToString(): String
// Objet compagnon (comportement d'une méthode statique) :
companion object {
fun presenter() {
println(values().contentToString())
}
}
}
fun main(){
println(JoursSemaine.LUNDI.customToString())
println(JoursSemaine.SAMEDI.customToString())
}
Jour n°1 : Lundi, c'est la reprise du boulot... Jour n°6 : Samedi, c'est le week-end !!!! Process finished with exit code 0
Le dernier objet constant de l’énumération doit se terminer par un point-virgule pour pouvoir ajouter à la suite des méthodes. |
Utilisation de l’héritage d’interfaces avec les énumérations
Maintenant que nous avons implémenter une méthode abstraite, nous allons pouvoir conclure en tirant partie des de l’héritage d’une interface. Dans le cadre de notre exemple précédent, cela va rendre le code un peu plus élégant.
interface Jour{
fun customToString(): String
}
enum class JoursSemaine(var numJour: Int, var nomString: String): Jour{
// Constantes de l'énumération :
LUNDI(1, "Lundi")
{override fun customToString() = "${super.customToString()}, c'est la reprise du boulot..."},
MARDI(2, "Mardi")
{override fun customToString() = "${super.customToString()}, mieux que le lundi."},
MERCREDI(3, "Mercredi")
{override fun customToString() = "${super.customToString()}, encore 3 jours."},
JEUDI(4, "Jeudi")
{override fun customToString() = "${super.customToString()}, bientôt le week-end."},
VENDREDI(5, "Vendredi")
{override fun customToString() = "${super.customToString()}, vivement ce soir !"},
SAMEDI(6, "Samedi")
{override fun customToString() = "${super.customToString()}, c'est le week-end !!!!"},
DIMANCHE(7, "Dimanche")
{override fun customToString() = "${super.customToString()}, ça passe trop vite..."};
// Redéfinition de la méthode héritée de l'interface :
override fun customToString() = "Jour n°${numJour} : ${nomString}"
// Objet compagnon (comportement d'une méthode statique) :
companion object {
fun presenter() {
println(values().contentToString())
}
}
}
8.5.6. La surcharge d’opérateurs
Principe
La surcharge des opérateurs consiste à utiliser les symboles des opérateurs conventionnels unaires et binaires pour appliquer des fonctions/méthodes.
La surcharge s’appuie sur les types de/des opérande(s) pour déterminer la fonction à appliquer.
Les fonctions/méthodes devant appliquer une surcharge d’opérateur doivent contenir le mot clé operator au début de leur en-tête.
Surcharge d’opérateurs unaires
Opérateurs |
Méthodes associées |
+a |
a.unaryPlus() |
-a |
a.unaryMinus() |
!a |
a.not() |
Exemple :
Nous définisons une classe PlusOuMoinsCinqOuRien qui prend une valeur et qui retourne la valeur soustrait de 5 avec l’opérateur unaire -x. Rajoute 5 avec l’opérateur +x et retourn 0 avec l’opérateur !x.
class PlusOuMoinsCinqOuRien(var valeur: Int){
operator fun unaryMinus(): Int = valeur - 5
operator fun unaryPlus(): Int = valeur + 5
operator fun not(): Int = 0
}
fun main(){
var x = PlusOuMoinsCinqOuRien(17)
println("x = ${x.valeur}")
println("-x = ${-x}")
println("+x = ${+x}")
println("!x = ${!x}")
}
x = 17 -x = 12 +x = 22 !x = 0 Process finished with exit code 0
8.5.7. Surcharge des opérateurs d’incrémentation/décrémentation
Nous retrouvons le même mode de fonctionnement avec les opérateur ++ et - -
Opérateurs |
Méthodes associées |
a++ |
a.inc() |
a- - |
a.dec() |
Exemple :
Nous définissons une classe Coord qui représente les coordonnées x et y d’un point.
class Coord(var x: Double, var y: Double) {
operator fun inc(): Coord {
x = x + 0.5
y = y + 0.5
return this
}
operator fun dec(): Coord{
x = x - 0.5
y = y - 0.5
return this
}
override fun toString() = "[$x, $y]"
}
fun main(){
var point = Coord(3.0, 7.0)
println(point++)
println(point--)
}
[3.5, 7.5] [3.0, 7.0] Process finished with exit code 0
Surcharge d’opérateurs binaires
Nous ne présenterons pas ici tous les opérateurs binaires, vous pourrez les retrouver sur le site de la documentation de référence de Kotlin (lien disponible plus bas).
Pour information, voilà quelques opérateurs binaires que nous allons ensuite appliquer à notre classe Coord.
Opérateurs |
Méthodes associées |
a |
a.plus(b) |
a - |
a.minus(b) |
a * |
a.times(b) |
class Coord(var x: Double, var y: Double) {
operator fun plus(b: Coord): Coord {
var somme = Coord(x + b.x, y + b.y)
return somme
}
operator fun minus(b: Coord): Coord {
var somme = Coord(x - b.x, y - b.y)
return somme
}
override fun toString() = "[$x, $y]"
}
fun main(){
var pt1 = Coord(3.0, 7.0)
var pt2 = Coord(2.0, 3.0)
println(pt1 + pt2)
println(pt1 - pt2)
}
[5.0, 10.0] [1.0, 4.0] Process finished with exit code 0
Pour aller plus loin, nous vous invitons à consulter la documentation officielle : https://kotlinlang.org/docs/operator-overloading.html
8.5.8. Les classes génériques
Les fonctions et les classes peuvent être formulée génériquement, c’est-à-dire que le type des paramètres n’est pas spécifié.
class Duo <T, U>(val para1: T, val para2 : U)
fun main(){
val objDuo = Duo("One", 1)
println("objDuo.para1 = ${objDuo.para1}\n" +
"objDuo.para2 = ${objDuo.para2}")
}
objDuo.para1 = One objDuo.para2 = 1 Process finished with exit code 0