Le Langage logo Kotlin

Sommaire

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.

localisation de Kotlin

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.

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

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.

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

Hello World en Kotlin
fun main(){
    println("Hello World !")
}

Comparaison avec la version Java :

Hello World en 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.
Table 1. Codage des nombres entiers :

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

Table 2. Codage des nombres à virgule flottante :

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.
Exemple : Comparaison du type de 2 variables :
    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.

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.

Ajout de la comparaison des valeurs des 2 variables :
    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 :

Utilisation de MIN_VALUE et MAX_VALUE :
fun main(){
    println(Int.MIN_VALUE)
    println(Int.MAX_VALUE)
    println(Long.MIN_VALUE)
    println(Long.MAX_VALUE)
}
Résultat :
-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 :

Utilisation de SYZE_BYTES :
fun main(){
    println(Int.SIZE_BYTES) // 4
    println(Int.SIZE_BITS)  // 32
}
Résultat :
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 :

Table 3. Codage des booléens et caractère

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.

Syntaxe de la déclaration d’une variable mutable
var idVar: typeIdVar
Exemple d’une déclaration sans affectation
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)
}
Résultat de l’exécution
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

Réaffectation de la variable
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)
}
Résultat de l’exécution
true
true
false

Process finished with exit code 0
Syntaxe de la déclaration d’une variable non mutable
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.

Exemple de déclaration d’une variable non mutable
fun main(){
    val varBinaire: Boolean  // Déclaration d'une référence NON mutable
    varBinaire = true        // Initialisation de la variable
    println(varBinaire)
}
Tentative de modification…​
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...
}
Message d’erreur affiché
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 :

Instruction de déclaration de variables numériques mutables :
    var idVariable: type
Application à tous les types numériques :
    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.
Illustration du caractère "mutable" de la variable :
fun main(){
    var nombreTest: Long
    nombreTest = 165
    nombreTest = 54
    println(nombreTest)
}
Succés, la variable est réaffectée et affichée :
54

Process finished with exit code 0
Instruction de déclaration de variables numériques immuables :
    val idVariable: type
Application à tous les types numériques :
    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.
Illustration du caractère immuable de la variable :
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.

Message d’erreur affiché
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.

Illustration de l’affectation d’une valeur numérique à une variable de type Float
    var unFlottant: Float
    unFlottant = 43.2
Message d’erreur affiché à la compilation
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.

Affectation d’une valeur numérique (constante) marquée comme Float
    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

Exemples :
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 :

Illustration :
    var unInt: Int
    unInt = 13L

Nous nous retrouvons comme prévu avec une erreur :

Erreur affiché :
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 :

Exemple :
    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 :

Exemple :
    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

Exemple :
    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é.

Exemple de déclaration de 2 variables de type Char :
    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 '.

Exemple d’affections sur des variables de type Char :
    var charAnd: Char = '&'
    var charAndUnicode: Char = '\u0026'
    println("charAnd = $charAnd")
    println("charUnicode = $charAndUnicode")
Affichage console du code :
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.

Exemple de déclarations sur des variables de type Char :
    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 " "

Affectation d’une chaîne sans saut de ligne :
    uneLigneDeTexte = "Bonjour à tous."
Affectation d’une chaîne sans avec saut de lignes :
    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.

Exemple :
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.
Exemple d’utilisation des méthodes de transtypage :
 // 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

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

Exemple :
fun main() {
    println("abcd...".toBoolean())
    println("true".toBoolean())
    println("TRUE".toBoolean())
    println("True".toBoolean())
    println("tRuE".toBoolean())
}
Résultat :
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.

Syntaxe de la déclaration d’un Array (tableau)
    var idArray: Array<type>

Le type placé entre les chevrons < > permet d’indiquer au compilateur le type des données à stocker.

Exemples de déclaration de Tableaux (Arrays)
    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.

Syntaxe de l’instruction d’initialisation d’un array :
idRefTableau = Array(taille, lambda)
Une fonction lambda peut être placé juste après la fonction (quand elle placé en dernier paramètre) :
idRefTableau = Array(taille)lambda
Exemple 1:
    // 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())
Affichage de la console :
[9, 9, 9, 9, 9]

Process finished with exit code 0
Exemple 2:
    //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())
Affichage de la console :
[0, 1, 2, 3, 4]

Process finished with exit code 0
Exemple 3 :
    //Déclaration & initialisation  :
    var tableauInt: Array<Int> = Array(5){3 * it}

    // Affichage :
    println(tableauInt.contentToString())
Affichage de la console :
[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().

Exemple :
    //Déclaration & initialisation  :
    var tableauInt = arrayOf("zéro", "un", "deux", "trois", "quatre")

    // Affichage :
    println(tableauInt.contentToString())
Console :
[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).
Génération d’un tableau à partir d’une plage :
    //Déclaration & initialisation  :
    var tableauInt = (5..10).toList().toTypedArray()

    // Affichage :
    println(tableauInt.contentToString())
Console :
[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.

Exemple avec la fonction arrayOf() :
    //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())
Console :
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]

Process finished with exit code 0
Exemple avec le constructeur Array() :
    //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())
Console :
[[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.

Syntaxe d’affectation d’une valeur sur un indice i:
    idArray[i] = nouvelleValeur
Exemple :
    //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())
Console :
[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.

Listes immuables : Initialisations avec la fonction listOf()
    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 :

Listes immmuables vides : Initialisation avec la fonction emptyList()
    val listeImmuableVideString = emptyList<String>()

Nous pouvons également utiliser le constructeur :

Liste immuable : Initialisation avec 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()

*Exemple d’instruction d’affichage :
    println(listeImmuableString)
    println(listeImmuableInt)
    println(listeImmuableVideInt)
    println(listeConstruite)
Affichage dans la console :
[zero, un, deux, trois]
[0, 1, 2, 3]
[]
[0, 1, 2, 3, 4]

Process finished with exit code 0
Listes mutables : Initialisation avec la fonction mutableListOf()
    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().

Listes mutables : Initialisation avec le constructeur MutableList()
    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

Exemples :
    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])
Console :
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.

Ajout et suppresion de 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)
Console
[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.

Exemple de Sets immuables :
    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))
Console :
[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.

Exemple sur les Sets mutables :
    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))
Console :
[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}")
Console :
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*.

Exemple d’une déclaration d’un type Int avec null
fun main(){
    var uneVariable: Int = null
}
Erreur produite :
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.

Exemple d’initialisation d’une variable avec null en utilisant ? et de son utilisation
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().

Syntaxe de l’initialisation d’une variable constante avec const
const val ID_CONSTANTE = valeurInitialisation
Exemple d’utilisation de const
const val MAXIMUM = 37

fun main(){
    println("La constante MAXIMUM : $MAXIMUM")
}
Résultat :
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().

Utilisation de readLine() :
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 :

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

Expression conditionnelle avec bloc d’instructions :
    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.

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

Exemple :
    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 :

Syntaxe de la boucle while :
    while(condition){
        // Instructions à itérer
    }
Exemple d’une boucle de comptage :
    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 :

Syntaxe de la boucle do while :
    do{
        // Instruction à itérer...
    }while (condition)
Exemple d’une boucle de comptage avec do while :
    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.

Syntaxe de la boucle for(…​in…​) :
    for(item in itérable){
        // Instruction à itérer
    }
Exemples sur des collections (List, Map et Set)
    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.

Exemple de boucles for(…​in…​) sur une plage :
    for(i in 0..10){print(i)}

6.5. Boucle forEach

La boucle forEach est particulièrement intéressante avec les collections.

Syntaxe de forEach
    iterable.foreach{lambda}
Exemples sur des collections :
    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) }
Exemple sur une plage :
    (0..10).forEach { println(it) }

6.6. Choix entre for(…​in…​) et forEach

La boucle forEach offre de meilleures performances pour réaliser des itération sur des collections. Par contre pour réaliser des boucles à partir d’une plage c’est la boucle for(…​in…​) qui est plus intéressante en terme de performance.

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

Syntaxe en-tête AVEC retour :
fun idFonction(idArg01: typeArg01, idArg02: typeArg02, ...): typeRetour
Exemple de l’en-tête d’une fonction somme()
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.

Syntaxe en-tête SANS retour :
fun idFonction(idArg01: typeArg01, idArg02: typeArg02, ...)
Exemple de l’en-tête d’une procédure
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 :

Syntaxe de l’en-tête d’une fonction avec valeur
fun idFonction(idArg01: typeArg01 = valDefaut01: , idArg02: typeArg02 = valDefaut02, ...)

On peut naturellement mixer des paramètres avec et sans valeur par défaut.

Exemple de l’en-tête d’une fonction produit prenant 2 arguments :
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

Syntaxe de l’en-tête d’une fonction avec un nombre variable d’arguments :
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 :

Exemple de la fonction somme dont le nombre d’arguments est libre :
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.

Syntaxe :
fun idFonction(idArg01: Type01 = valDefaut01, vararg idVar: typeArguments): typeRetour{
    // Corps de la fonction contenant l'ensemble des instructions.
    return varRetour
Exemple :
fun exemple(mot1: String = "Bonjour", vararg mots: String): String{
    var phrase: String = mot1
    for (mot in mots)
        phrase += " " + mot
    return phrase
}
Appel de la fonction exemple :
fun main(){
    println(exemple("Coucou", "comment", "allez-vous ?"))
}
Affichage dans la console :
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.

Syntaxe :
fun idFoncExpression(arg01: Type01,...): TypeRetour = expression
Exemple :
fun produit(facteurGauche: Double, facteurDroit: Double): Double = facteurGauche * facteurDroit
Appel :
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.

Exemple d’appel d’une fonction :
fun somme(operandeGauche: Double, operandeDroit: Double): Double{
    return operandeGauche + operandeDroit
}

fun main(){
    var resultat = somme(5.0, -2.0)
    println(resultat)
}
Affichage console :
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 !

Exemple d’appel d’une fonction en utilisant le nom des paramètres :
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)
}
Résultat :
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.

Syntaxe d’une fonction d’ordre supérieur prenant en argument une fonction sans argument et sans retour :
fun idFoncOrdreSup(argFonc: () -> Unit): typeRetour{
    // Corps de la fonction...
}
Syntaxe d’une fonction d’ordre supérieur prenant en argument une fonction sans argument mais avec retour :
fun idFoncOrdreSup(argFonc: () -> typeRetour): typeRetour{
    // Corps de la fonction...
}
Syntaxe d’une fonction d’ordre supérieur prenant en argument une fonction avec argument et retour :
fun idFoncOrdreSup(argFonc: (typeRetour) -> typeRetour): typeRetour{
    // Corps de la fonction...
}
Exemple de définition de 3 fonctions d’odre 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("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 ::

Syntaxe de l’appel d’une fonction d’ordre supérieur prenant une fonction régulière en argument :
idFoncSuperieur(::idArgFonc)
Exemple :
// 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)
}
Affichage console :
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 :

Syntaxe d’une fonction lambda :
{arg01: typeArg01, arg02: typeArg02, ...-> expression}
Exemples :
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")
Résultat dans la console :
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.

Application sur l’exemple précédent :
    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.

Exemple d’utilisation du paramètre it :
// 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é.

Syntaxes d’une fonction anonyme :
// 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}
Exemple :
// 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)
}
Console :
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.

Syntaxe de l’en-tête d’une fonction privée :
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 :

Fonction somme() :
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 !

Appel de la fonction somme() avec 2 arguments du type Int
fun main() {
    println(somme(2, 3))
}
Erreur affichée :
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.

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

Exemple de l’utilisation de is :
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)}")
}
Affichage console :
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 :

Amélioration du code, utilisation conjointe du is avec when :
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

Exemples sur l’instruction as :
    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

7.1.9. Les fonctions génériques

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.

Table 4. Tableau des modificateurs de visibilités :

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

Syntaxe d’une classe vide
---
class IdClasse
---
Classe vide :
Figure 1. Classe vide :
Syntaxe pour la déclaration et/ou initialisation d’un objet :
    // 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.

Example 1. Caractéristiques (attributs) d’un objet cercle :

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

Classe Cercle :
Figure 2. Classe Cercle :

Etape 1 : Création de la classe et de l’en-tête du construction principal

Etape 1 : Création de la classe Cercle avec déclaration des attributs à partir de l’en-tête du constructeur 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);

Test de la classe Cercle :
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 :

*Reformulation de l’en-tête à 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

Classe Cercle :
Figure 3. Classe Cercle :

En préalable, rajouter l’import de la constante PI et la méthode pow en rajoutant les instructions suivantes :

Import de PI et pow :
import kotlin.math.pow
import kotlin.math.PI

Voici ce que cela donne pour notre classe.

Etape 2 : Ajout des attributs, méthodes et du corps du constructeur principal
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 :

Fonction main() :
    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}")
Résultat dans la console :
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.

Etape 3 : Finalisation de la classe Cercle :
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.

Code de démontration :
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}")
}
Résultat :
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 :

Classe avec 1 constructeur secondaire :
Figure 4. Classe avec 1 constructeur secondaire :
Constructeur primaire non défini :
class Exemple01{
    init {
        println("Constructeur primaire")
    }
    constructor(){
        println("Constructeur secondaire")
    }
}

fun main(){
    val obj01 = Exemple01()
}
Affichage de la console : L’affichage de "Constructeur primaire" témoigne de l’éxecution du corps du constructeur primaire
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.

Classe avec 1 constructeur secondaire :
Figure 5. Classe avec 1 constructeur secondaire :
Constructeur primaire défini : L’ajout de la paire de parenthèse marque la définition du constructeur primaire.
class Exemple02(){
    init {
        println("Constructeur primaire")
    }
    constructor(bourage: Int): this(){
        println("Constructeur secondaire")
    }
}

fun main(){
    val obj01 = Exemple02(0)
}
Affichage de la console:
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.

Classe avec 2 constructeurs secondaires :
Figure 6. Classe avec 2 constructeurs secondaires :
Classe complète :
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")
    }
}
Résultat dans la console :
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.

Classe avec éléments statiques :
Figure 7. Classe avec éléments statiques :
Exemple d’implémentation d’attributs et de 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}")
}
Résultat :
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.

Syntaxe pour ouvrir une classe à l’héritage :
open class IdClasse(paramètres & attributs du constructeur...) {
    // Corps de la classe
}
Table 5. Liste des modificateurs d’accès des classes :

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 :

Héritage d’une classe dérivée à partir de sa classe de base :
Figure 8. Héritage d’une classe dérivée à partir de sa classe de base :
Instructions élémentaires pour l’héritage entre une classe dérivée et sa classe de base :
// 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.

Héritage d’une classe dérivée à partir de sa classe de base :
Figure 9. Héritage d’une classe dérivée à partir de sa classe de base :
Instructions de passage des arguments de la classe dérivée vers sa classe de base :
// 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.

Héritage d’une classe dérivée à partir de sa classe de base :
Figure 10. Héritage d’une classe dérivée à partir de sa classe de base :
Exemples illustrant les instructions pour ajouter des méthodes et attributs à la classe dérivée :
// 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")
    }
}
Résultat dans la console :
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 :

Héritage d’une classe dérivée à partir de sa classe de base :
Figure 11. Héritage d’une classe dérivée à partir de sa classe de base :
Utilisation de override pour modifier la valeur d’initialisation d’un attribut :
// 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.

Utilisation du bloc init 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){
    init{
        attrib04 = 40
        ...

Le principal intérêt de redéfinir un attribut est de personnaliser son getter (accesseur) et/ou son setter (mutateur).

Redéfinition des getter et setter d’un attribut :
// 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.

Héritage d’une classe dérivée à partir de sa classe de base :
Figure 12. Héritage d’une classe dérivée à partir de sa classe de base :
Application sur l’exemple :
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
Héritage d’une classe dérivée à partir de sa classe de base :
Figure 13. Héritage d’une classe dérivée à partir de sa classe de base :
Exemple d’utilisation de super avec des attributs et méthodes redéfinis :
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}")
Résultat :
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.

Héritage d’une classe dérivée à partir de sa classe de base :
Figure 14. Héritage d’une classe dérivée à partir de sa classe de base :
Exemple :
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

Exemple de classes abstraites :
Figure 15. Exemple de classes abstraites :
Héritage sur plusieurs niveaux :
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}")
}
Résultat :
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

Exemple de classes abstraites :
Figure 16. Exemple de classes abstraites :
Héritage sur plusieurs niveaux :
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()
}
Résultat :
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

Représentation d’une interface en UML
Figure 17. Représentation d’une interface en UML
Exemple :
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 :

Représentation d’une interface en UML :
Figure 18. Représentation d’une interface en UML :
Exemple :
// 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()
}
Résultat :
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.

Exemple d’implémentation d’une extension d’attribut à la classe String
val String.demiTaille: Int
    get() = length / 2

fun main(){
    println("Demi taille du mot  \"anticonstitutionnellement\" : ${"anticonstitutionnellement".demiTaille}")
}
Résultat :
Demi taille du mot  "anticonstitutionnellement" : 12

Process finished with exit code 0
Exemple d’implémentation de l’extension de 2 méthodes aux classes String et Bool
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()}")
}
Résultat :
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().

Exemple :
class Cobaye

fun main(){
    val obj = Cobaye()
    val descriptionObj: String = "Résultat de toString() : " + obj
    println(obj)
    println(descriptionObj)
}
Résultat :
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.

Exemple :
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)
}
Résultat :
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

Exemple d’une classe de données (data class) :
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()
}
Résultat produit :
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.

Exemple des méthodes hashCode() et equals() :
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)}")
Résultat produit :
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.

Exemple de copy() :
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)
}
Résultat :
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.

Exemple de copy()
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")
}
Résultat :
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.

Utilisation du mot clé object pour définir à la volée une classe durant l’instance de son objet :
// 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)
}
Résultat :
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.

Exemple :
// 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"})
}
Résultat :
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ération contenant les points cardinaux :
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ération contenant 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.

Accès à une constante d’une énumération :
fun main(){
    println("Direction.EST : ${Direction.EST}")
    println("Jours.SAMEDI is String : ${Jours.SAMEDI is Enum<*>}")
}
Résultat :
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.

Méthode valueOf() de la classe Enum :
fun main(){
    println(Direction.valueOf("SUD"))
    println(Direction.valueOf("NORD_EST"))
}
Résultat : "SUD" est bien affiché, mais "NORD_EST" lève l’exception
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.

Exploitation de la méthode values() :
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) }
}
Résultat :
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

Utilisation des attributs name et ordinal sur l’énumération Jours :
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.

Utilisation de when sur l’énumération Jours :
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))
    }
}
Résultat :
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ération sur le codage RGB avec codage décimal des 3 composantes :
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 :

Exploitation de l’énumération RGB :
fun main(){
   println("Le code RGB de la couleur ${RGB.BLEU} est : [${RGB.BLEU.r}, ${RGB.BLEU.g}, ${RGB.BLEU.b}]")
}
Résultat :
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.

Intégration d’une méthode et d’un objet compagnon dans une énumération
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()
}
Résultat :
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.

Exemple :
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())
}
Résultat :
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.

Implémentation d’une interface à une énumération :
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.5. Les classes scellées

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.

Exemple :
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}")
}
Résultat :
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.

Exemple :
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--)
}
Résultat :
[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)

Addition et soustraction de 2 points :
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)

}
Résultats :
[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é.

Exemple :
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}")
}
Résultat :
objDuo.para1 = One
objDuo.para2 = 1

Process finished with exit code 0