Cet atelier de programmation fait partie du cours Kotlin Bootcamp for Programmers. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre. En fonction de vos connaissances, vous pourrez peut-être survoler certaines sections. Ce cours s'adresse aux programmeurs qui connaissent un langage orienté objet et qui souhaitent apprendre Kotlin.
Introduction
Dans cet atelier de programmation, vous allez découvrir les classes, les fonctions et les méthodes génériques, ainsi que leur fonctionnement en Kotlin.
Plutôt que de créer une seule application exemple, les leçons de ce cours sont conçues pour développer vos connaissances, mais sont semi-indépendantes les unes des autres afin que vous puissiez parcourir les sections que vous connaissez. Pour les relier, de nombreux exemples utilisent un thème d'aquarium. Si vous souhaitez en savoir plus sur l'histoire de l'aquarium, consultez le cours Udacity Kotlin Bootcamp for Programmers.
Ce que vous devez déjà savoir
- Syntaxe des fonctions, classes et méthodes Kotlin
- Créer une classe dans IntelliJ IDEA et exécuter un programme
Points abordés
- Utiliser des classes, des méthodes et des fonctions génériques
Objectifs de l'atelier
- Créer une classe générique et ajouter des contraintes
- Créer des types
inetout - Créer des fonctions, des méthodes et des fonctions d'extension génériques
Présentation des génériques
Comme de nombreux langages de programmation, Kotlin possède des types génériques. Un type générique vous permet de rendre une classe générique et donc beaucoup plus flexible.
Imaginez que vous implémentiez une classe MyList contenant une liste d'éléments. Sans les génériques, vous devriez implémenter une nouvelle version de MyList pour chaque type : une pour Double, une pour String et une pour Fish. Avec les génériques, vous pouvez rendre la liste générique afin qu'elle puisse contenir n'importe quel type d'objet. C'est comme si vous faisiez du type un caractère générique qui s'adapterait à de nombreux types.
Pour définir un type générique, placez T entre crochets <T> après le nom de la classe. (Vous pouvez utiliser une autre lettre ou un nom plus long, mais la convention pour un type générique est T.)
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}Vous pouvez référencer T comme s'il s'agissait d'un type normal. Le type de retour pour get() est T, et le paramètre de addItem() est de type T. Bien sûr, les listes génériques sont très utiles. La classe List est donc intégrée à Kotlin.
Étape 1 : Créez une hiérarchie typographique
Dans cette étape, vous allez créer des classes à utiliser dans l'étape suivante. La création de sous-classes a été abordée dans un atelier de programmation précédent, mais voici un bref rappel.
- Pour que l'exemple reste clair, créez un package sous src et appelez-le
generics. - Dans le package generics, créez un fichier
Aquarium.kt. Cela vous permet de redéfinir des éléments en utilisant les mêmes noms sans conflit. Le reste de votre code pour cet atelier de programmation est donc placé dans ce fichier. - Créez une hiérarchie de types d'approvisionnement en eau. Commencez par faire de
WaterSupplyune classeopenafin de pouvoir la sous-classer. - Ajoutez un paramètre booléen
var,needsProcessing. Cela crée automatiquement une propriété mutable, ainsi qu'un getter et un setter. - Créez une sous-classe
TapWaterqui étendWaterSupplyet transmetteztruepourneedsProcessing, car l'eau du robinet contient des additifs qui sont mauvais pour les poissons. - Dans
TapWater, définissez une fonction appeléeaddChemicalCleaners()qui définitneedsProcessingsurfalseaprès le nettoyage de l'eau. La propriéténeedsProcessingpeut être définie à partir deTapWater, car elle estpublicpar défaut et accessible aux sous-classes. Voici le code complet.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}- Créez deux autres sous-classes de
WaterSupply, appeléesFishStoreWateretLakeWater.FishStoreWatern'a pas besoin d'être traité, maisLakeWaterdoit être filtré avec la méthodefilter(). Une fois filtré, il n'est pas nécessaire de le traiter à nouveau. Dansfilter(), définissez doncneedsProcessing = false.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}Si vous avez besoin d'informations supplémentaires, consultez la leçon précédente sur l'héritage en Kotlin.
Étape 2 : Créer une classe générique
Dans cette étape, vous allez modifier la classe Aquarium pour prendre en charge différents types d'approvisionnement en eau.
- Dans Aquarium.kt, définissez une classe
Aquarium, avec<T>entre crochets après le nom de la classe. - Ajoutez une propriété immuable
waterSupplyde typeTàAquarium.
class Aquarium<T>(val waterSupply: T)- Écrivez une fonction appelée
genericsExample(). Comme il ne fait pas partie d'une classe, il peut être placé au niveau supérieur du fichier, comme la fonctionmain()ou les définitions de classe. Dans la fonction, créez unAquariumet transmettez-lui unWaterSupply. Comme le paramètrewaterSupplyest générique, vous devez spécifier le type entre chevrons<>.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}- Dans
genericsExample(), votre code peut accéder àwaterSupplyde l'aquarium. Comme il est de typeTapWater, vous pouvez appeleraddChemicalCleaners()sans aucun transtypage.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Lorsque vous créez l'objet
Aquarium, vous pouvez supprimer les chevrons et ce qui se trouve entre eux, car Kotlin utilise l'inférence de type. Il n'y a donc aucune raison de direTapWaterdeux fois lorsque vous créez l'instance. Le type peut être déduit par l'argument deAquarium. Il créera toujours unAquariumde typeTapWater.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Pour voir ce qui se passe, affichez
needsProcessingavant et après l'appel deaddChemicalCleaners(). Vous trouverez ci-dessous la fonction complète.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}- Ajoutez une fonction
main()pour appelergenericsExample(), puis exécutez votre programme et observez le résultat.
fun main() {
genericsExample()
}⇒ water needs processing: true water needs processing: false
Étape 3 : Soyez plus précis
"Générique" signifie que vous pouvez transmettre presque n'importe quoi, ce qui peut parfois poser problème. Dans cette étape, vous allez rendre la classe Aquarium plus spécifique quant à ce que vous pouvez y mettre.
- Dans
genericsExample(), créez unAquariumen transmettant une chaîne pourwaterSupply, puis imprimez la propriétéwaterSupplyde l'aquarium.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}- Exécutez votre programme et observez le résultat.
⇒ string
Le résultat est la chaîne que vous avez transmise, car Aquarium ne limite pas le type T.. Tout type, y compris String, peut être transmis.
- Dans
genericsExample(), créez un autreAquariumen transmettantnullpour lewaterSupply. SiwaterSupplyest nul, imprimez"waterSupply is null".
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}- Exécutez votre programme et observez le résultat.
⇒ waterSupply is null
Pourquoi pouvez-vous transmettre null lors de la création d'un Aquarium ? Cela est possible, car par défaut, T représente le type Any? pouvant accepter la valeur "null", qui se trouve en haut de la hiérarchie des types. Ce qui suit équivaut à ce que vous avez saisi précédemment.
class Aquarium<T: Any?>(val waterSupply: T)- Pour ne pas autoriser la transmission de
null, définissez explicitementTde typeAnyen supprimant?aprèsAny.
class Aquarium<T: Any>(val waterSupply: T)Dans ce contexte, Any est appelé contrainte générique. Cela signifie que n'importe quel type peut être transmis pour T tant qu'il n'est pas null.
- Ce que vous voulez vraiment, c'est vous assurer que seul un
WaterSupply(ou l'une de ses sous-classes) peut être transmis pourT. RemplacezAnyparWaterSupplypour définir une contrainte générique plus spécifique.
class Aquarium<T: WaterSupply>(val waterSupply: T)Étape 4 : Ajouter d'autres vérifications
Dans cette étape, vous allez découvrir la fonction check() pour vous assurer que votre code se comporte comme prévu. La fonction check() est une fonction de bibliothèque standard en Kotlin. Elle sert d'assertion et génère une IllegalStateException si son argument renvoie la valeur false.
- Ajoutez une méthode
addWater()à la classeAquariumpour ajouter de l'eau, avec uncheck()qui garantit que vous n'avez pas besoin de traiter l'eau au préalable.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}Dans ce cas, si needsProcessing est défini sur "true", check() générera une exception.
- Dans
genericsExample(), ajoutez du code pour créer unAquariumavecLakeWater, puis ajoutez de l'eau.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}- Exécutez votre programme. Vous obtiendrez une exception, car l'eau doit d'abord être filtrée.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)- Ajoutez un appel pour filtrer l'eau avant de l'ajouter à
Aquarium. Désormais, lorsque vous exécutez votre programme, aucune exception n'est générée.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}⇒ adding water from generics.LakeWater@880ec60
Ce qui précède couvre les bases des génériques. Les tâches suivantes couvrent davantage de points, mais le concept important est de savoir comment déclarer et utiliser une classe générique avec une contrainte générique.
Dans cette tâche, vous allez découvrir les types d'entrée et de sortie avec les génériques. Un type in est un type qui ne peut être transmis qu'à une classe, et non renvoyé. Un type out est un type qui ne peut être renvoyé que par une classe.
Si vous examinez la classe Aquarium, vous verrez que le type générique n'est renvoyé que lors de l'obtention de la propriété waterSupply. Il n'existe aucune méthode qui accepte une valeur de type T comme paramètre (sauf pour la définir dans le constructeur). Kotlin vous permet de définir des types out exactement pour ce cas, et il peut déduire des informations supplémentaires sur les endroits où les types peuvent être utilisés en toute sécurité. De même, vous pouvez définir des types in pour les types génériques qui ne sont transmis qu'aux méthodes, et non renvoyés. Cela permet à Kotlin d'effectuer des vérifications supplémentaires pour la sécurité du code.
Les types in et out sont des directives pour le système de types de Kotlin. L'explication de l'ensemble du système de types dépasse le cadre de cet atelier (il est assez complexe). Toutefois, le compilateur signalera les types qui ne sont pas marqués in et out de manière appropriée. Vous devez donc les connaître.
Étape 1 : Définissez un type de sortie
- Dans la classe
Aquarium, remplacezT: WaterSupplypar un typeout.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}- Dans le même fichier, en dehors de la classe, déclarez une fonction
addItemTo()qui attend unAquariumdeWaterSupply.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")- Appelez
addItemTo()depuisgenericsExample()et exécutez votre programme.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}⇒ item added
Kotlin peut s'assurer que addItemTo() n'effectuera aucune opération non sécurisée au niveau du type avec le générique WaterSupply, car il est déclaré en tant que type out.
- Si vous supprimez le mot clé
out, le compilateur génère une erreur lors de l'appel deaddItemTo(), car Kotlin ne peut pas garantir que vous ne faites rien de dangereux avec le type.
Étape 2 : Définissez un type d'entrée
Le type in est semblable au type out, mais pour les types génériques qui ne sont transmis qu'aux fonctions, et non renvoyés. Si vous essayez de renvoyer un type in, une erreur de compilation s'affiche. Dans cet exemple, vous allez définir un type in dans une interface.
- Dans Aquarium.kt, définissez une interface
Cleanerqui utilise unTgénérique contraint àWaterSupply. Étant donné qu'il n'est utilisé que comme argument pourclean(), vous pouvez en faire un paramètrein.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}- Pour utiliser l'interface
Cleaner, créez une classeTapWaterCleanerqui implémenteCleanerpour nettoyerTapWateren ajoutant des produits chimiques.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}- Dans la classe
Aquarium, mettez à jouraddWater()pour qu'il prenne unCleanerde typeTet nettoie l'eau avant de l'ajouter.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}- Mettez à jour l'exemple de code
genericsExample()pour créer unTapWaterCleaner, unAquariumavecTapWater, puis ajoutez de l'eau à l'aide du produit nettoyant. Il utilisera le nettoyant si nécessaire.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}Kotlin utilisera les informations de type in et out pour s'assurer que votre code utilise les génériques de manière sécurisée. Out et in sont faciles à retenir : les types out peuvent être transmis en tant que valeurs de retour, et les types in peuvent être transmis en tant qu'arguments.

Si vous souhaitez en savoir plus sur les types de problèmes que résolvent les types d'entrée et de sortie, la documentation les aborde en détail.
Dans cette tâche, vous allez découvrir les fonctions génériques et quand les utiliser. En règle générale, il est judicieux de créer une fonction générique chaque fois que la fonction accepte un argument d'une classe qui possède un type générique.
Étape 1 : Créez une fonction générique
- Dans generics/Aquarium.kt, créez une fonction
isWaterClean()qui accepte unAquarium. Vous devez spécifier le type générique du paramètre. Vous pouvez par exemple utiliserWaterSupply.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}Mais cela signifie que Aquarium doit avoir un paramètre de type out pour que cela soit appelé. Parfois, out ou in sont trop restrictifs, car vous devez utiliser un type pour l'entrée et la sortie. Vous pouvez supprimer l'exigence out en rendant la fonction générique.
- Pour rendre la fonction générique, placez des chevrons après le mot clé
funavec un type génériqueTet des contraintes, en l'occurrenceWaterSupply. ModifiezAquariumpour qu'il soit contraint parTau lieu deWaterSupply.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}T est un paramètre de type pour isWaterClean() qui est utilisé pour spécifier le type générique de l'aquarium. Ce modèle est très courant. Il est donc judicieux de prendre le temps de le comprendre.
- Appelez la fonction
isWaterClean()en spécifiant le type entre crochets juste après le nom de la fonction et avant les parenthèses.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}- En raison de l'inférence de type à partir de l'argument
aquarium, le type n'est pas nécessaire. Supprimez-le. Exécutez votre programme et observez le résultat.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}⇒ aquarium water is clean: false
Étape 2 : Créer une méthode générique avec un type réifié
Vous pouvez également utiliser des fonctions génériques pour les méthodes, même dans les classes qui ont leur propre type générique. Dans cette étape, vous allez ajouter une méthode générique à Aquarium qui vérifie s'il s'agit d'un type WaterSupply.
- Dans la classe
Aquarium, déclarez une méthodehasWaterSupplyOfType()qui accepte un paramètre génériqueR(Test déjà utilisé) contraint àWaterSupplyet renvoietruesiwaterSupplyest de typeR. C'est comme la fonction que vous avez déclarée précédemment, mais à l'intérieur de la classeAquarium.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R- Notez que le dernier
Rest souligné en rouge. Pointez dessus pour voir de quelle erreur il s'agit.
- Pour effectuer une vérification
is, vous devez indiquer à Kotlin que le type est reified (réifié), ou réel, et qu'il peut être utilisé dans la fonction. Pour ce faire, placezinlinedevant le mot cléfunetreifieddevant le type génériqueR.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is RUne fois qu'un type est réifié, vous pouvez l'utiliser comme un type normal, car il s'agit d'un type réel après l'intégration. Cela signifie que vous pouvez effectuer des vérifications is à l'aide du type.
Si vous n'utilisez pas reified ici, le type ne sera pas assez "réel" pour que Kotlin autorise les vérifications is. En effet, les types non réifiés ne sont disponibles qu'au moment de la compilation et ne peuvent pas être utilisés au moment de l'exécution par votre programme. Ce point sera abordé plus en détail dans la section suivante.
- Transmettez
TapWatercomme type. Comme pour l'appel de fonctions génériques, appelez des méthodes génériques en utilisant des chevrons avec le type après le nom de la fonction. Exécutez votre programme et observez le résultat.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}⇒ true
Étape 3 : Créer des fonctions d'extension
Vous pouvez également utiliser des types réifiés pour les fonctions régulières et les fonctions d'extension.
- En dehors de la classe
Aquarium, définissez une fonction d'extension surWaterSupplyappeléeisOfType()qui vérifie si leWaterSupplytransmis est d'un type spécifique, par exempleTapWater.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T- Appelez la fonction d'extension comme une méthode.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}⇒ true
Grâce à ces fonctions d'extension, le type de Aquarium (Aquarium, TowerTank ou une autre sous-classe) n'a pas d'importance, tant qu'il s'agit d'un Aquarium. La syntaxe de projection par étoile est un moyen pratique de spécifier différents types de correspondances. De plus, lorsque vous utilisez une projection par étoile, Kotlin s'assure que vous ne faites rien de dangereux.
- Pour utiliser une projection en étoile, placez
<*>aprèsAquarium. DéplacezhasWaterSupplyOfType()pour qu'il devienne une fonction d'extension, car il ne fait pas vraiment partie de l'API Core deAquarium.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R- Remplacez l'appel par
hasWaterSupplyOfType()et exécutez votre programme.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}⇒ true
Dans l'exemple précédent, vous deviez marquer le type générique comme reified et rendre la fonction inline, car Kotlin a besoin de les connaître au moment de l'exécution, et pas seulement au moment de la compilation.
Tous les types génériques ne sont utilisés qu'au moment de la compilation par Kotlin. Cela permet au compilateur de s'assurer que vous faites tout de manière sécurisée. Au moment de l'exécution, tous les types génériques sont effacés. C'est pourquoi le message d'erreur précédent indiquait de vérifier un type effacé.
Il s'avère que le compilateur peut créer un code correct sans conserver les types génériques jusqu'à l'exécution. Mais cela signifie que parfois, vous faites quelque chose, comme des vérifications is sur des types génériques, que le compilateur ne peut pas prendre en charge. C'est pourquoi Kotlin a ajouté des types réifiés ou réels.
Pour en savoir plus sur les types concrétisés et l'effacement de type, consultez la documentation Kotlin.
Cette leçon portait sur les génériques, qui sont importants pour rendre le code plus flexible et plus facile à réutiliser.
- Créez des classes génériques pour rendre le code plus flexible.
- Ajoutez des contraintes génériques pour limiter les types utilisés avec les génériques.
- Utilisez les types
inetoutavec des génériques pour fournir une meilleure vérification des types afin de limiter les types transmis aux classes ou renvoyés par celles-ci. - Créez des fonctions et des méthodes génériques pour travailler avec des types génériques. Exemples :
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... } - Utilisez des fonctions d'extension génériques pour ajouter des fonctionnalités non essentielles à une classe.
- Les types concrets sont parfois nécessaires en raison de l'effacement de type. Contrairement aux types génériques, les types réifiés persistent jusqu'à l'exécution.
- Utilisez la fonction
check()pour vérifier que votre code s'exécute comme prévu. Par exemple :check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentation Kotlin
Si vous souhaitez en savoir plus sur un sujet abordé dans ce cours ou si vous êtes bloqué, https://kotlinlang.org est le meilleur point de départ.
- Génériques
- Contraintes génériques
- Projections d'étoiles
- Types
Inetout - Paramètres concrétisés
- Effacement de type
- Fonction
check()
Tutoriels Kotlin
Le site Web https://try.kotlinlang.org inclut des tutoriels complets appelés Kotlin Koans, un interpréteur Web et un ensemble complet de documentation de référence avec des exemples.
Cours Udacity
Pour consulter le cours Udacity sur ce sujet, reportez-vous à la formation Kotlin pour les programmeurs.
IntelliJ IDEA
La documentation d'IntelliJ IDEA est disponible sur le site Web de JetBrains.
Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :
- Attribuer des devoirs si nécessaire
- Indiquer aux élèves comment rendre leurs devoirs
- Noter les devoirs
Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.
Si vous suivez cet atelier de programmation par vous-même, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.
Répondez à ces questions
Question 1
Quelle est la convention de nommage d'un type générique ?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Question 2
Une restriction sur les types autorisés pour un type générique est appelée :
▢ une restriction générique ;
▢ une contrainte générique ;
▢ Clarification
▢ une limite de type générique
Question 3
"Réifié" signifie :
▢ L'impact réel de l'exécution d'un objet a été calculé.
▢ Un index d'entrées restreint a été défini pour la classe.
▢ Le paramètre de type générique a été transformé en type réel.
▢ Un indicateur d'erreur à distance a été déclenché.
Passez à la leçon suivante :
Pour obtenir un aperçu du cours, y compris des liens vers d'autres ateliers de programmation, consultez Formation Kotlin pour les programmeurs : bienvenue dans le cours.