Prérequis : variables, conditions et boucles. Les solutions sont des programmes console Kotlin/JVM avec la bibliothèque standard, sans dépendance Android. Utilisez Kotlin 1.9 ou ultérieur et un JDK compatible. Copiez un seul programme à la fois dans Main.kt, compilez puis exécutez-le. Chaque programme possède sa propre fonction main. Les montants des exemples sont en FCFA entiers ; les petits jeux de données ne constituent pas une comptabilité de production.
Méthode : répondez à la question de cours, réalisez l’exercice, puis ouvrez le corrigé et vérifiez les cas proposés. Comptez 10 à 25 minutes par exercice et 45 à 60 minutes pour le mini-projet.
20 exercices · 20 questions de cours · 20 corrigés
Fondamentaux
1
val et var
Question de cours. Une référence val rend-elle automatiquement un objet immuable ?
À réaliser. Déclarer le nom NKILItech avec val et un nombre de places avec var ; réserver deux places sur dix.
Voir le corrigé 1Masquer le corrigé 1
Réponse et raisonnement. val interdit la réaffectation de la référence. var autorise une nouvelle valeur. Un objet mutable référencé par val peut encore changer ; ici les chaînes et les nombres ne sont pas modifiés en place.
Programme complet
fun main() {
val centre = "NKILItech"
var places = 10
places -= 2
println("$centre : $places places")
}
Résultat attendu et vérification.
NKILItech : 8 places
2
Calculer un total
Question de cours. Pourquoi représenter ici les montants par des entiers ?
À réaliser. Calculer le prix de trois ateliers à 15 000 FCFA chacun, avec Long.
Voir le corrigé 2Masquer le corrigé 2
Réponse et raisonnement. Le tarif est exprimé en unités monétaires entières : on évite une approximation flottante inutile. Long offre une plage plus grande que Int, mais il possède lui aussi une limite.
Programme complet
fun main() {
val prix: Long = 15_000L
val quantite = 3
println(prix * quantite)
}
Résultat attendu et vérification.
45000
3
Conditions comme expressions
Question de cours. Quelle différence entre une instruction et une expression qui retourne une valeur ?
À réaliser. Attribuer admis pour une note de 12 sur 20, puis tester 9.
Voir le corrigé 3Masquer le corrigé 3
Réponse et raisonnement. if peut produire une valeur affectée à une variable. L’égalité appartient au cas admis avec >= 10 ; l’erreur classique consiste à oublier cette frontière.
Programme complet
fun main() {
for (note in listOf(12, 9, 10)) {
val verdict = if (note >= 10) "admis" else "à renforcer"
println("$note : $verdict")
}
}
Résultat attendu et vérification.
12 : admis
9 : à renforcer
10 : admis
4
Choisir avec when
Question de cours. Pourquoi prévoir un cas else pour une chaîne libre ?
À réaliser. Traduire les codes A, K et une valeur inconnue en noms de formation.
Voir le corrigé 4Masquer le corrigé 4
Réponse et raisonnement. Une chaîne peut prendre bien plus de valeurs que les cas prévus. else garantit une réponse pour chaque entrée lorsque when est utilisé comme expression.
Programme complet
fun formation(code: String): String = when (code) {
"A" -> "Angular"
"K" -> "Kotlin"
else -> "Inconnue"
}
fun main() {
println(formation("A"))
println(formation("K"))
println(formation("X"))
}
Résultat attendu et vérification.
Angular
Kotlin
Inconnue
5
Parcourir un intervalle
Question de cours. Les bornes de 1..5 sont-elles incluses ?
À réaliser. Additionner les numéros de séance de 1 à 5 avec une boucle.
Voir le corrigé 5Masquer le corrigé 5
Réponse et raisonnement. L’intervalle fermé inclut 1 et 5. L’accumulateur démarre à zéro, élément neutre de l’addition. Une initialisation à 1 compterait la première unité deux fois.
Programme complet
fun main() {
var total = 0
for (seance in 1..5) total += seance
println(total)
}
Résultat attendu et vérification.
15
Fonctions et collections
6
Écrire une fonction avec contrat
Question de cours. À quoi sert require ?
À réaliser. Créer total(prix, quantite) qui refuse les nombres négatifs et tester 12 000 × 2.
Voir le corrigé 6Masquer le corrigé 6
Réponse et raisonnement. require vérifie une précondition et lève IllegalArgumentException en cas d’échec. La validation protège la fonction, même si l’appelant ne possède pas de formulaire.
Programme complet
fun total(prix: Long, quantite: Int): Long {
require(prix >= 0 && quantite >= 0) { "Valeurs négatives interdites" }
return prix * quantite
}
fun main() {
println(total(12_000, 2))
println(runCatching { total(-1, 2) }.isFailure)
}
Résultat attendu et vérification.
24000
true
7
Gérer null sans !!
Question de cours. Quelle différence entre ?. et ?: ?
À réaliser. Afficher la longueur d’un nom nullable, puis zéro lorsque le nom manque.
Voir le corrigé 7Masquer le corrigé 7
Réponse et raisonnement. ?. réalise l’accès seulement si la valeur existe ; ?: fournit le repli quand le résultat est null. Un nom vide a déjà une longueur zéro : il n’est pas identique à null.
Programme complet
fun longueur(nom: String?): Int = nom?.length ?: 0
fun main() {
println(longueur("Joel"))
println(longueur(null))
println(longueur(""))
}
Résultat attendu et vérification.
4
0
0
8
Convertir une saisie
Question de cours. Pourquoi toIntOrNull est-il utile avec une entrée utilisateur ?
À réaliser. Accepter une quantité entière entre 1 et 5 ; rejeter abc, 0 et 6.
Voir le corrigé 8Masquer le corrigé 8
Réponse et raisonnement. La conversion renvoie null pour une valeur non entière au lieu de lever une exception. takeIf garde uniquement la valeur qui respecte l’intervalle. La chaîne est nettoyée avec trim.
Programme complet
fun quantite(texte: String): Int? = texte.trim().toIntOrNull()?.takeIf { it in 1..5 }
fun main() {
for (saisie in listOf(" 3 ", "abc", "0", "6")) println(quantite(saisie))
}
Résultat attendu et vérification.
3
null
null
null
9
Collections en lecture seule
Question de cours. List garantit-elle une immutabilité profonde ?
À réaliser. Créer une liste mutable via val, ajouter Kotlin, puis exposer une copie par toList.
Voir le corrigé 9Masquer le corrigé 9
Réponse et raisonnement. val protège la référence, pas les opérations de la collection mutable. List offre une interface de lecture seule ; cela ne garantit pas que tous ses objets internes sont immuables. toList réalise ici une copie de la structure.
Programme complet
fun main() {
val ateliers = mutableListOf("HTML")
ateliers.add("Kotlin")
val copie: List<String> = ateliers.toList()
ateliers.add("Angular")
println(copie)
println(ateliers.size)
}
Résultat attendu et vérification.
[HTML, Kotlin]
3
10
Filtrer et transformer
Question de cours. Quelle différence entre filter et map ?
À réaliser. Sur les notes 8, 12 et 16, garder les admis puis obtenir des libellés.
Voir le corrigé 10Masquer le corrigé 10
Réponse et raisonnement. filter sélectionne les éléments ; map transforme chacun des éléments retenus. La liste initiale n’est pas modifiée par ces deux opérations.
Programme complet
fun main() {
val notes = listOf(8, 12, 16)
val admis = notes.filter { it >= 10 }.map { "$it/20" }
println(admis)
println(notes)
}
Résultat attendu et vérification.
[12/20, 16/20]
[8, 12, 16]
Modélisation et traitement
11
Calculer une moyenne sûre
Question de cours. Pourquoi faut-il traiter une collection vide avant une moyenne ?
À réaliser. Retourner null pour zéro note et une moyenne pour 10, 12, 14.
Voir le corrigé 11Masquer le corrigé 11
Réponse et raisonnement. Une moyenne vide n’a pas de valeur utile pour ce bulletin. Le type Double? exprime explicitement cette absence. Le résultat non vide utilise average pour éviter une division entière.
Programme complet
fun moyenne(notes: List<Int>): Double? = if (notes.isEmpty()) null else notes.average()
fun main() {
println(moyenne(listOf(10, 12, 14)))
println(moyenne(emptyList()))
}
Résultat attendu et vérification.
12.0
null
12
Modéliser avec data class
Question de cours. Que compare == sur une data class ?
À réaliser. Modéliser une inscription par id, nom et formation ; copier la formation sans changer l’original.
Voir le corrigé 12Masquer le corrigé 12
Réponse et raisonnement. Les propriétés du constructeur principal participent à equals généré. copy crée un nouvel objet mais ne copie pas profondément d’éventuels objets mutables imbriqués. Ici toutes les propriétés sont des valeurs simples.
Programme complet
data class Inscription(val id: Int, val nom: String, val formation: String)
fun main() {
val initiale = Inscription(1, "Amina", "HTML")
val nouvelle = initiale.copy(formation = "Kotlin")
println(initiale.formation)
println(nouvelle.formation)
println(initiale == nouvelle)
}
Résultat attendu et vérification.
HTML
Kotlin
false
13
Supprimer les doublons
Question de cours. Quelle propriété distingue Set de List ?
À réaliser. Compter les formations distinctes dans HTML, Kotlin, HTML.
Voir le corrigé 13Masquer le corrigé 13
Réponse et raisonnement. Un ensemble ne conserve qu’une occurrence de chaque valeur selon l’égalité. toSet crée l’ensemble ; on ne dépend ici d’aucun ordre d’itération pour le résultat.
Programme complet
fun main() {
val demandes = listOf("HTML", "Kotlin", "HTML")
val distinctes = demandes.toSet()
println(distinctes.size)
println("HTML" in distinctes)
}
Résultat attendu et vérification.
2
true
14
Compter par catégorie
Question de cours. Quel rôle jouent la clé et la valeur dans une Map ?
À réaliser. Compter trois inscriptions Kotlin et une HTML, puis interroger une catégorie absente.
Voir le corrigé 14Masquer le corrigé 14
Réponse et raisonnement. groupingBy définit la clé ; eachCount compte les occurrences. L’accès par crochets renvoie une valeur nullable si la clé est absente ; ?: 0 représente alors zéro inscription.
Programme complet
fun main() {
val demandes = listOf("Kotlin", "HTML", "Kotlin", "Kotlin")
val effectifs = demandes.groupingBy { it }.eachCount()
println(effectifs["Kotlin"])
println(effectifs["Angular"] ?: 0)
}
Résultat attendu et vérification.
3
0
15
Trier sans modifier la source
Question de cours. Quelle différence entre sortedBy et sortBy ?
À réaliser. Classer trois ateliers par durée croissante, puis par nom en cas d’égalité.
Voir le corrigé 15Masquer le corrigé 15
Réponse et raisonnement. sortedWith renvoie une nouvelle liste. compareBy choisit la première clé ; thenBy départage les durées égales. Les variantes sort modifient une liste mutable en place.
Programme complet
data class Atelier(val nom: String, val heures: Int)
fun main() {
val ateliers = listOf(Atelier("Kotlin", 12), Atelier("HTML", 6), Atelier("Angular", 12))
val tries = ateliers.sortedWith(compareBy<Atelier> { it.heures }.thenBy { it.nom })
println(tries.map { it.nom })
println(ateliers.first().nom)
}
Résultat attendu et vérification.
[HTML, Angular, Kotlin]
Kotlin
Contrats et mini-projet
16
Chercher sans provoquer une exception
Question de cours. Pourquoi firstOrNull convient-il à une recherche incertaine ?
À réaliser. Trouver HTML dans un catalogue puis chercher Rust, absent.
Voir le corrigé 16Masquer le corrigé 16
Réponse et raisonnement. firstOrNull renvoie null si aucun élément ne convient, alors que first lèverait une exception. Le repli est choisi au moment de l’affichage, sans inventer un atelier.
Programme complet
fun main() {
val catalogue = listOf("HTML", "Kotlin")
for (demande in listOf("HTML", "Rust")) {
val trouve = catalogue.firstOrNull { it == demande }
println(trouve ?: "Non trouvé")
}
}
Résultat attendu et vérification.
HTML
Non trouvé
17
Définir une interface
Question de cours. Quel avantage apporte une interface par rapport à une implémentation imposée ?
À réaliser. Définir Notificateur et une version console annonçant une inscription.
Voir le corrigé 17Masquer le corrigé 17
Réponse et raisonnement. L’appelant dépend d’un contrat, ce qui permet de remplacer la sortie console dans un autre contexte ou un test. Aucun courriel réel n’est envoyé par cette implémentation.
Programme complet
interface Notificateur { fun envoyer(message: String) }
class ConsoleNotificateur : Notificateur {
override fun envoyer(message: String) { println(message) }
}
fun confirmer(notificateur: Notificateur) { notificateur.envoyer("Inscription enregistrée") }
fun main() { confirmer(ConsoleNotificateur()) }
Résultat attendu et vérification.
Inscription enregistrée
18
Exprimer un résultat métier
Question de cours. Pourquoi un type scellé aide-t-il à représenter succès et échec ?
À réaliser. Retourner un succès pour trois places et un refus pour zéro, puis afficher chaque résultat.
Voir le corrigé 18Masquer le corrigé 18
Réponse et raisonnement. Les sous-types portent des données adaptées à chaque cas. when traite toutes les variantes connues sans convertir un échec en nombre magique. Le seuil est une règle métier explicite.
Programme complet
sealed interface Reservation {
data class Succes(val places: Int) : Reservation
data class Refus(val raison: String) : Reservation
}
fun reserver(n: Int): Reservation = if (n in 1..5) Reservation.Succes(n) else Reservation.Refus("Quantité invalide")
fun main() {
for (n in listOf(3, 0)) {
val texte = when (val r = reserver(n)) {
is Reservation.Succes -> "${r.places} places réservées"
is Reservation.Refus -> r.raison
}
println(texte)
}
}
Résultat attendu et vérification.
3 places réservées
Quantité invalide
19
Tester les frontières
Question de cours. Pourquoi tester les bornes et pas seulement un cas normal ?
À réaliser. Écrire une fonction d’admission pour une note valide sur 20 ; tester 0, 9, 10, 20 et 21.
Voir le corrigé 19Masquer le corrigé 19
Réponse et raisonnement. Les valeurs autour du seuil détectent une confusion entre > et >=. Les préconditions distinguent une mauvaise note d’une note impossible. check lance une exception si une attente est fausse.
Programme complet
fun admis(note: Int): Boolean {
require(note in 0..20)
return note >= 10
}
fun main() {
check(!admis(0))
check(!admis(9))
check(admis(10))
check(admis(20))
check(runCatching { admis(21) }.isFailure)
println("5 vérifications réussies")
}
Résultat attendu et vérification.
5 vérifications réussies
20
Mini-projet : inscriptions NKILItech
Question de cours. Comment éviter un identifiant dupliqué tout en calculant une recette ?
À réaliser. Créer un registre en mémoire. Ajouter Amina (15 000), Boris (12 000), refuser un doublon, puis calculer la recette et tester les résultats.
Voir le corrigé 20Masquer le corrigé 20
Réponse et raisonnement. Le registre utilise une Map indexée par identifiant. L’insertion est refusée avant modification si la clé existe ; require contrôle le prix. Les assertions vérifient qu’un doublon ne remplace pas une inscription. Il s’agit d’un programme console en mémoire, sans base de données.
Programme complet
data class Inscription(val id: Int, val nom: String, val prix: Long)
class Registre {
private val inscriptions = mutableMapOf<Int, Inscription>()
fun ajouter(i: Inscription): Boolean {
require(i.prix >= 0)
if (i.id in inscriptions) return false
inscriptions[i.id] = i
return true
}
fun recette(): Long = inscriptions.values.sumOf { it.prix }
fun nombre(): Int = inscriptions.size
}
fun main() {
val registre = Registre()
check(registre.ajouter(Inscription(1, "Amina", 15_000)))
check(registre.ajouter(Inscription(2, "Boris", 12_000)))
check(!registre.ajouter(Inscription(1, "Autre", 99_000)))
check(registre.nombre() == 2)
check(registre.recette() == 27_000L)
println("${registre.nombre()} inscriptions : ${registre.recette()} FCFA")
}