• 12 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 13/11/2023

Enrichissez vos classes

Dans ce chapitre, nous irons encore plus loin dans l’étude du fonctionnement de Kotlin et de son approche sur les classes.

Une histoire d’héritage

Comme vous devez le savoir, en Java, vous pouvez hériter (  extends  ) de n’importe quelle classe et redéfinir (  override  ) ses méthodes à moins que celle-ci soit explicitement "protégée" par le mot-clé  final  : on dit qu’une classe Java, ainsi que l’ensemble de ses méthodes, sont "ouvertes" (ou "open") par défaut.

Cela peut s’avérer très pratique, mais aussi une source de problèmes importante. En effet, si par défaut, en Java, une classe peut avoir des sous-classes (via l'héritage), vous prenez le risque qu’un développeur redéfinisse (  override  ) une des méthodes de la classe parente d’une manière inattendue (ou du moins d’une manière que vous n’avez pas prévue !).

Ainsi, cela peut entraîner des changements de comportement entre la classe parent et les possibles classes enfants, ce qui ne respecte pas vraiment les principes de la programmation orientée objet.

D’ailleurs, Joshua Bloch, l’auteur de "Effective Java" (un des livres les plus respectés et recommandés dans l’univers Java), indiquait :

Design and document for inheritance or else prohibit it.

En français, cela donne quelque chose comme : "Concevez et documentez (ndlr : vos classes) pour l’héritage ou interdisez-le." En clair, nous devrions à chaque fois interdire l’héritage des classes non pensées pour être héritées, notamment grâce au mot-clé  final , afin de les rendre "fermées" à l’héritage.

D’accord, mais pourquoi tu nous racontes tout cela ?

Eh bien, en Kotlin, toutes les classes et leurs méthodes sont "fermées" par défaut. Vous ne pourrez donc pas hériter d’une classe ou redéfinir ses méthodes sans l’autorisation explicite d’un développeur. :)

                                         

Ainsi, le développeur devra indiquer dans son code explicitement, grâce au mot-clé  open  , si une classe ou une méthode peut être "ouverte" à l’héritage

Par exemple, imaginons que nous ayons créé la classe  Button.java  (modélisant un simple bouton) :

public class Button{
    
    public void show(){
        // ... very complex processing to show button
    }
 
    public void hide(){
        // ... very complex processing to hide button
    }
}

... et qu’un second développeur (que nous appellerons "Toto") crée la classe  CircularButton.java  (représentant un bouton rond), héritant de la classe  Button.java  :

public class CircularButton extends Button{
    
    @Override
    public void show(){
        super.show();
        // ... adding some effects
    }
 
    @Override
    public void hide(){
        // ... adding some effects
    }
}

Notre classe Button contient deux méthodes,  show  et  hide , créées dans le but d’afficher et de masquer notre bouton. Jusqu’ici, tout va bien. :)

Puis, Toto arrive, et se dit qu’il aimerait bien créer un bouton rond. Naturellement, il crée une classe  CircularButton  héritant de la classe  Button  , afin de récupérer un comportement de bouton déjà créé.

Cependant, Toto souhaite modifier le comportement des méthodes  show  et  hide  : il décide donc de les redéfinir afin, notamment, d’ajouter des animations.

Toto, étant très tête-en-l’air, oublie d’appeler la méthode originale (  super.hide()  ) lors de la redéfinition de la méthode  hide  : le comportement de cette méthode se voit alors complètement changé ! La méthode  show()  voit aussi son comportement changé, même si ce n’est qu’un simple ajout d’animation.

Comme vous n’avez pas forcément de visibilité sur le code de Toto, vous ne savez pas que ce dernier a modifié le comportement des méthodes  show  et  hide  en y ajoutant des animations.

Bien plus tard, vous décidez de faire évoluer les méthodes  hide()  et  show()  en ajoutant… des animations ! Toto verra donc son bouton rond exécuter des animations EN PLUS de celles qu’il aura déjà créées précédemment… Bref, vous comprenez la panique ! ;)

Tout aurait été plus simple si les méthodes  show  et  hide  avaient été déclarées en  final  dès le début : Toto n’aurait jamais pu les redéfinir (et aurait dû créer ses propres méthodes).

C’est pour cette raison précise que les classes et leurs méthodes sont maintenant "fermées" en Kotlin. Voici l’équivalent en Kotlin, de manière plus "sécurisée" :

Commençons par la classe  Button :

open class Button {
    fun show(){ }
    fun hide(){ }
}

Nous avons utilisé le mot-clé  open  devant le mot-clé  class  afin d’indiquer explicitement que cette classe pourra être utilisée comme classe parente (via l’héritage). Cependant, nous n’autorisons pas les méthodes  show  et  hide  à être redéfinies, afin d’éviter que d’autres développeurs (comme Toto ;) ) modifient sans le vouloir leur comportement.

Ainsi, Toto recréa en Kotlin la classe  CircularButton :

Comme vous pouvez le voir, en Kotlin, nous utilisons les deux points  :  en remplacement du mot-clé  extends  en Java, suivi du nom de la classe parente (  Button  ). Petite subtilité tout de même, nous devons indiquer à Kotlin quel constructeur utiliser. Nous reviendrons sur ce point d’ici quelques minutes. ;)

Cependant, lorsque Toto souhaite maintenant redéfinir les méthodes  show  et  hide  , une belle erreur s’affiche en indiquant que cela est impossible, car ces méthodes sont considérées comme étant  final  ! Eh oui, comme nous l’avons vu plus haut, par défaut, toutes les classes et méthodes sont "fermées" en Kotlin, à moins que nous les "ouvrions" explicitement grâce au mot-clé  open  .

D’ailleurs, voici un petit récapitulatif des modificateurs d’accès disponibles en Kotlin (à ne pas confondre avec les modificateurs de visibilité vus dans un précédent chapitre !) :

  • final  : Classe/Méthode/Propriété ne pouvant pas être redéfinie. C’est l’état par défaut de tous les éléments en Kotlin.

  • open  : Classe/Méthode/Propriété pouvant être redéfinie. Ce modificateur d’accès doit être indiqué explicitement.

  • abstract  : Classe/Méthode/Propriété devant être redéfinie. Ce modificateur d’accès peut être utilisé uniquement dans des classes abstraites.

Et bien entendu, le mot-clé  override  sera utilisé pour redéfinir un élément d’une classe parente (ou d’une interface).

Plutôt efficace, non ? :)

Des constructeurs multiples

J’ai une petite question : est-ce possible de créer plusieurs constructeurs pour une même classe en Kotlin ?

Bien sûr ! Reprenons notre exemple précédent, et étoffons-le un peu…

Bloc d'initialisation en Kotlin
Bloc d'initialisation en Kotlin

Tout d’abord, nous avons ajouté une propriété  color  de type  String  à notre classe  Button  directement dans son constructeur primaire. Bon, ça, vous devez maîtriser ! ;)

Cependant, nous avons ajouté également un mystérieux bloc  init  ...

Euuuuuuh ! À quoi peut bien servir ce bloc ? :waw:

En fait, vous avez déjà remarqué que la syntaxe pour créer le constructeur primaire (ou principal) d’une classe était très épurée ! Peut-être trop pour certains cas… notamment les cas où nous souhaitons réaliser une action spéciale après qu’un objet soit construit. Eh bien, c’est le rôle du bloc  init  .;) Celui-ci sera exécuté à chaque fois que la classe sera instanciée (qu’un objet de cette classe sera créé, si vous préférez).

Nous avons également modifié la classe  CircularButton  afin de lui rajouter un constructeur contenant un paramètre qui sera directement utilisé par le constructeur primaire de la classe parente  Button . Simple, efficace. :D

Effectivement ! Et si maintenant, je souhaite créer un second constructeur, vide cette fois-ci, à ma classe  Button  , comment cela se passe-t-il ?

Très simple ! Pour cela, nous utiliserons le mot-clé  constructor  :

Constructeur secondaire en Kotlin
Constructeur secondaire en Kotlin

Ce mot-clé vous permettra de créer un constructeur dit "secondaire". En pratique, cette syntaxe sera surtout utilisée dans certains cas spécifiques, notamment en termes d'interopérabilité entre une classe parente écrite en Java possédant plusieurs constructeurs, et une classe enfant écrite en Kotlin. Voici un exemple en Android, avec la classe parente  View  :

import andoid.content.Content
import andoid.util.AttributeSet
import andoid.view.View

class CustomView : View {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

Nous utilisons dans cet exemple des constructeurs secondaires en Kotlin pour "matcher" avec les différents constructeurs de la classe  View  .

Pour terminer, comme nous l’avons vu dans un précédent chapitre, il serait beaucoup plus "propre" dans notre cas actuel de ne PAS utiliser de "second constructeur", mais plutôt la notion de "paramètre par défaut" et de "paramètre nommé" :

Constructeur avec paramètres par défaut
Constructeur avec paramètres par défaut

En plus, cette approche est beaucoup plus évolutive ! ;)

Des classes originales !

Des classes pour nos données

En Java (et dans tout langage de programmation orientée objet), il est très habituel de créer des classes représentant nos modèles de données, comme la classe  User  représentant un utilisateur de notre programme.

En Kotlin, cela pourrait donner quelque chose comme ça :

class User(var email: String, var password: String, var isConnected: Boolean)

Jusque-là, pas de souci ! Cependant, très souvent, nous effectuons des actions répétitives sur ce genre de classe, comme :

  • Afficher son contenu sous forme de texte via la méthode  toString()  (dans une console de log, par exemple).

  • Comparer deux objets de même type (via la méthode  equals()  de sa superclasse).

  • Dupliquer un objet de manière distincte en un second objet (via la méthode  copy()  ou  clone()  de sa superclasse).

En Java, cela nous obligeait à redéfinir les méthodes  toString()  ,  hashCode()  , equals()  et  copy()  dans le corps de notre classe (certains IDE s’occupaient même de générer ces méthodes pour nous). Mais bon, ça, c’était avant ! :D

En Kotlin, nous allons pouvoir définir une classe comme étant destinée à contenir des modèles de données grâce au mot-clé  data  :

data class User(var email: String, var password: String, var isConnected: Boolean)

Grâce à ce simple mot-clé, le compilateur Kotlin implémentera pour nous les principales méthodes utilisées pour "comparer" et "décrire" un objet contenant des données comme les méthodes  toString()  ,  hashCode()  , equals()  ou encore  copy()  :

val user = User("toto@gmail.com", "azerty", true)
val secondUser = User("tata@gmail.com", "youhou", false)

// Description 
println(user.toString())

// Comparison
if (secondUser == user) println("Users are equal!")
else println("Users are not equal...")

// Clone
val userCloned = user.copy()
if (userCloned == user) println("Users are equal!")
else println("Users are not equal...")
Résultat de l'utilisation d'une classe
Résultat de l'utilisation d'une classe "data" en Kotlin

Plutôt pratique, non ?>_<

Un compagnon de route

Référence : Le seigneur des anneaux
Référence : Le Seigneur des anneaux

Comme nous l’avons vu dans le premier chapitre de cette partie, le mot-clé  static  n’existe plus. En effet, nous ne créons plus de méthodes ou de propriétés statiques publiques, mais plutôt des propriétés/fonctions de premier niveau (ou "top-level") créées à l’intérieur d’un fichier plutôt que d’une classe, comme nous le faisions précédemment en Java.

Cependant, celles-ci auront plus un rôle "global" pour notre programme. Imaginons que nous souhaitions créer l’équivalent "à l’intérieur" d’une classe afin d’accéder à l’ensemble de ses membres (propriétés, fonctions, constructeurs), qu’ils soient publics ou même privés : la notion de "top-level" ne pourra pas fonctionner dans ce cas-là !

Eh bien, c’est pour cette raison que Kotlin a créé le principe "d’objet compagnon" (ou "companion object") ! :)

Ce dernier va nous permettre de créer des propriétés ou des méthodes à l’intérieur d’une classe, accessibles même si aucune instance de cette classe n'existe (en d’autres mots, même si aucun objet n’est créé).

Prenons l’exemple ci-dessous :

data class User(var email: String, var password: String, var isConnecetd: Boolean) {
    
    companion object {
        fun newInstanceAfterSignUp(email: String, password: String) = User(email, password, isConnected: true)    
    }
}
// First syntax 
val user = User.newInstanceAfterSignUp("toto@gmail.com", "azerty")

// Second syntax 
val secondUser = User.Companion.newInstanceAfterSignUp("tata@gmail.com", "youhou")

Nous avons légèrement modifié notre classe  User  afin d’y ajouter un objet compagnon. Pour cela, nous utilisons dans le corps de notre classe les mots-clés  companion object  : à l’intérieur de cet objet, nous avons créé la méthode  newInstanceAfterSignUp  dont l’objectif sera de créer un objet de type  User  .

En somme, nous venons de créer une méthode de Factory. ;)

Puis, afin d’appeler cette nouvelle méthode, nous le ferons de la même manière que nous le faisions en Java sur des méthodes statiques publiques. :) Les deux syntaxes utilisées dans l’exemple sont équivalentes.

Voici un exemple nécessitant l’utilisation d’une propriété de premier niveau (ou "top-level property") :

private const val DEFAULT_SCORE = 100 // Top-Level Property

data class User(var email: String, var password: String, var isConnecetd: Boolean, var defaultScore: Int = DEFAULT_SCORE)

plutôt que l’utilisation de l’objet compagnon :

data class User(var email: String, var password: String, var isConnecetd: Boolean, var defaultScore: Int = DEFAULT_SCORE) {
    companion object {
        private const val DEFAULT_SCORE = 100
    }
}

Les deux solutions fonctionnent, mais la première est un peu plus Kotlin-Friendly que la seconde ! ;)

Pour terminer, sachez également qu’un objet compagnon peut avoir un nom et même implémenter des interfaces :

interface ModelFactory<T> { fun newInstanceFromJson(json: String) : T }

data class User(var email: String, var password: String, var isConnecetd: Boolean) {
    
    companion object Factory: ModelFactory<User> {
        override fun newInstanceFromJson(json: String): User = ...
    }
}

Par exemple, ici, nous avons créé une interface générique appelée  ModelFactory  (plus d’informations sur les interfaces en Kotlin à ce lien) qui sera implémentée par l’objet compagnon (portant désormais le nom de  Factory ) de notre classe  User  .

Cette interface contient symboliquement, et pour l’exemple, une méthode appelée  newInstanceFromJson  dont l’objectif sera de retourner un objet  User  construit à partir d’un JSON.

Il n'y a pas à dire, Kotlin, c’est vraiment pratique ! 🙌

Un Singleton par-ci, un Singleton par-là !

Il est très probable qu’en tant que développeur Java, vous ayez rencontré au moins une fois la notion de Singleton permettant de limiter l’instanciation d’une classe à un seul et même objet.

On le rencontre très souvent en combinaison avec le patron de conception DAO (design pattern) permettant de centraliser l’accès à certaines données, comme une base de données. :)

C’est vrai, ça ! Mais pourquoi tu nous parles de ça ?

Eh bien, en Kotlin, la création d’un Singleton va s’avérer, comment dire… très simple ! ;)

Différence d'implémentation d'un
Différence d'implémentation d'un "Singleton" en Kotlin & en Java

Dans cet exemple, nous avons imaginé un DAO contenant les requêtes que nous pourrions faire sur une base de données pour la table  User  . Ce DAO sera également un Singleton.

Remarquez la différence entre les deux déclarations Java et Kotlin ! Comme à son habitude, Kotlin nous permet d’écrire moins de code, pour un résultat encore plus lisible. :) Afin de créer un Singleton en Kotlin, il vous suffira d’utiliser le mot-clé  object  devant le nom de l’objet en question. Pas besoin d’utiliser le mot-clé  class  !

Puis, afin d’appeler les différentes fonctions publiques se trouvant à l’intérieur de notre Singleton, il vous suffit de renseigner dans votre code le nom de votre Singleton suivi d’un point  .  et du nom de la méthode, un peu comme vous le faisiez avec les méthodes publiques statiques en Java.

Practice Makes Perfect!

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

En résumé

  • En Kotlin, toutes les classes et leurs méthodes sont "fermées" à l'héritage par défaut (équivalent du mot-clé  final  en Java). Pour les ouvrir à l’héritage, il faut utiliser le mot-clé  open  .

  • L’héritage en Kotlin s’effectue grâce aux deux points  :  suivis du nom de la classe parente et de son constructeur entre parenthèses ()  ; par exemple :   class CircleButton : Button()  .

  • Il est possible d’ajouter un second constructeur (appelé également "constructeur secondaire") en Kotlin grâce au mot-clé  constructor  , ainsi qu’un bloc d’initialisation  init  qui sera appelé une fois que l’objet sera construit.

  • Le mot-clé  data  devant le nom d’une classe l’indique comme pouvant contenir des données (modèles) : le compilateur Kotlin implémentera automatiquement à notre place les principales méthodes utilisées pour  comparer  et  décrire  un objet, comme les méthodes  toString()  ,  hashCode()  , equals() ou encore  copy()  .

  • Un objet compagnon (ou "companion object") permet de créer des propriétés ou des méthodes à l’intérieur d’une classe, accessibles même si aucune instance de cette classe n'existe (équivalent du mot-clé  static  ).

  • Le mot-clé  object  suivi d’un nom permettra de créer automatiquement une classe de type Singleton.

Pour aller plus loin

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