Développement avec Kotlin: Exercices Pratiques
Configuration de l’environnement
Pour ces exercices, vous pouvez utiliser :
- Kotlin Playground
- IntelliJ IDEA
- Terminal avec SDK :
sdk install kotlinCompilation et Exécution en ligne de commande
Le plus simple pour commencer est d’utiliser Kotlin Playground
Ensuite, pour compiler et exécuter un fichier Kotlin :
Fichier unique
# Compiler un fichier Kotlin
kotlinc MonFichier.kt -d MonFichier.jar
# Exécuter le fichier compilé
java -jar MonFichier.jarCompilation avec dépendances
# Compiler avec inclusion des dépendances runtime
kotlinc -cp "lib/*" MonFichier.kt -include-runtime -d MonFichier.jar
# Compiler plusieurs fichiers
kotlinc Fichier1.kt Fichier2.kt -include-runtime -d MonProjet.jarFitTracker App - Exercices Pratiques
1. Introduction aux bases
En Kotlin, la déclaration des variables se distingue de Java par plusieurs aspects clés. Kotlin utilise les mots-clés val pour les variables immuables (similaire à final en Java) et var pour les variables mutables. Le type est déclaré après le nom de la variable, et grâce à l’inférence de type, il peut souvent être omis. Les constantes sont déclarées avec const val au niveau du fichier ou d’un objet companion. Cette approche rend le code plus concis et sûr, tout en maintenant une excellente lisibilité.
Créez un nouveau fichier BasicExercise.kt et :
Déclarez les variables suivantes pour un exercice :
- Nom (non modifiable)
- Durée en minutes (modifiable)
- Calories brûlées (modifiable)
Définissez une constante pour la durée minimale d’entraînement (15 minutes)
Utilisez l’inférence de type pour simplifier vos déclarations
2. Classes
Les classes en Kotlin offrent une approche moderne et concise de la programmation orientée objet. Contrairement à Java, Kotlin propose des data classes qui automatisent la génération des méthodes utilitaires (equals, hashCode, toString, copy), des classes avec constructeur primaire intégré, et une syntaxe simplifiée pour les propriétés. Les classes Kotlin sont final par défaut pour favoriser l’encapsulation, et la visibilité par défaut est public. L’héritage nécessite le mot-clé open, encourageant ainsi une conception réfléchie de la hiérarchie des classes. Les propriétés peuvent être déclarées directement dans le constructeur primaire, réduisant considérablement le boilerplate code par rapport à Java.
Dans un fichier Model.kt, créez les classes suivantes :
- Une data class
BasicExerciseavec :- Type (enum class)
- Nom
- Durée
- Calories
- Date
- Une classe
Workoutcontenant :- Liste d’exercices
- Méthodes de calcul (durée et calories totales)
- Une classe
Useravec :- Profil utilisateur
- Historique des workouts
3. Control Flow
Kotlin offre des structures de contrôle modernes et expressives qui améliorent la lisibilité et la sécurité du code. Le when remplace avantageusement le switch de Java avec une syntaxe plus flexible et la possibilité de retourner des valeurs. Les boucles en Kotlin combinent la simplicité des itérateurs avec la puissance des expressions lambda. Les collections peuvent être parcourues de multiples façons, du for traditionnel aux fonctions d’ordre supérieur comme forEach. L’API des séquences permet une gestion efficace des itérations, particulièrement pour les grandes collections. Cette approche fonctionnelle du contrôle de flux rend le code plus concis et moins sujet aux erreurs.
Dans WorkoutLogic.kt, implémentez :
Compiler en utilisant Model.kt pour les classes de données.
Une fonction
calculateIntensityqui retourne :- “Facile” si < 100 calories
- “Moyen” si entre 100 et 200 calories
- “Difficile” si > 200 calories
Une fonction
categorizeExerciseutilisantwhenpour classifier :- Cardio (course, vélo, natation)
- Force (squat, pompes, tractions)
- Flexibilité (yoga, étirements)
Une fonction calculant les calories totales d’une liste d’entraînements.
Parcours de collections Dans
WorkoutLogic.kt, implémentez différentes façons de parcourir une liste d’exercices :
- Utilisation d’un
fortraditionnel - Utilisation de
forEach - Utilisation de
forEachIndexed - Parcours avec gestion d’index manuels
- Itérateur personnalisé Créez un itérateur spécialisé dans
WorkoutIterator.ktqui :
- Implémente l’interface
Iterator - Filtre les exercices selon leur intensité
- Permet de parcourir les exercices par type
- Utilise les séquences Kotlin
4. Functions
Les fonctions en Kotlin offrent une flexibilité et une expressivité supérieures à Java. Les extension functions permettent d’ajouter des méthodes à des classes existantes sans les modifier, les paramètres nommés et par défaut réduisent la nécessité de surcharges, et les fonctions d’ordre supérieur permettent de manipuler les fonctions comme des valeurs de première classe. Cette approche fonctionnelle, combinée avec la concision de la syntaxe et l’inférence de type, rend le code plus lisible et plus maintenable tout en réduisant significativement le boilerplate code.
Dans un fichier Extensions.kt, créez des fonctions pour implémenter :
Une extension function sur
List<Exercise>calculant la moyenne des caloriesUne fonction
createExerciseavec des paramètres par défaut pour :- Type
- Durée
- Intensité
Une fonction d’ordre supérieur filtrant les exercices par type. Le critère de filtrage sera une lambda.
5. Collections
Les collections en Kotlin offrent une API riche et intuitive, fortement inspirée de la programmation fonctionnelle. Contrairement à Java, Kotlin fait une distinction claire entre les collections mutables et immutables, et propose des opérations de transformation puissantes comme map, filter, groupBy et maxBy. Ces opérations peuvent être chaînées de manière fluide et sont optimisées pour la performance. L’API des collections Kotlin simplifie considérablement le traitement des données tout en réduisant les risques d’erreurs grâce à son typage fort et ses opérations null-safe.
Dans un fichier DataManipulation.kt, créez des fonctions pour : Travaillez avec une liste d’exercices pour :
Filtrer les exercices > 20 minutes avec map/filter
Grouper les exercices par type avec groupBy
Trouver l’exercice le plus intense avec maxBy
6. Scope Functions
Les scope functions (let, run, with, apply, et also) sont des fonctions spécifiques à Kotlin qui permettent d’exécuter un bloc de code dans le contexte d’un objet. Chaque scope function a un cas d’utilisation particulier : with pour grouper des opérations sur un objet, let pour le traitement des nullables, apply pour la configuration d’objets, run pour la combinaison des deux précédents, et also pour les effets secondaires. Ces fonctions améliorent la lisibilité du code en évitant la répétition et en clarifiant l’intention du développeur, tout en maintenant un scope bien défini pour les opérations.
Dans un fichier ScopeFunctions.kt, créez des scope functions pour :
Configurer un nouveau workout avec
withTraiter un exercice optionnel avec
letInitialiser un profil utilisateur avec
apply
7. Null Safety
La gestion des valeurs nullables est l’une des fonctionnalités phares de Kotlin, conçue pour éliminer le risque de NullPointerException. Contrairement à Java, Kotlin différencie explicitement les types nullables (marqués par ?) des types non-nullables. Le langage fournit des opérateurs sécurisés comme ?. (safe call), ?: (Elvis), et !! (non-null assertion) pour manipuler les valeurs potentiellement nulles. Cette approche force les développeurs à gérer explicitement les cas de nullité dès la compilation, rendant le code plus sûr et plus prévisible.
Dans un fichier NullSafety.kt, créez des fonctions pour :
Compléter la classe
Exerciseavec un coach optionnelUne fonction utilisant l’opérateur Elvis pour la durée
Une fonction safe pour accéder aux détails d’exercice
8. Coroutines
Les coroutines sont la solution de Kotlin pour la programmation asynchrone, offrant une alternative élégante aux callbacks et aux promesses. Elles permettent d’écrire du code asynchrone de manière séquentielle, ce qui le rend plus lisible et plus facile à maintenir. Les coroutines utilisent des suspending functions qui peuvent être pausées et reprises sans bloquer le thread principal, offrant ainsi une excellente performance et une meilleure utilisation des ressources. Avec les scopes et les dispatchers, Kotlin fournit un contrôle précis sur l’exécution des tâches asynchrones, tout en simplifiant la gestion des erreurs et l’annulation des opérations en cours.
Dans un fichier AsyncTasks.kt, créez des fonctions pour :
Une fonction de chargement asynchrone d’historique. Cette fonction simule un chargement réseau avec un délai de 2 secondes. Executez la, puis ecrivez une boucle qui affiche “Chargement…” toutes les 500ms jusqu’à ce que le chargement soit terminé.
Un compteur de temps pour exercice en cours. Cette fonction simule un exercice en cours avec un délai de 1 seconde. Executez la, puis ecrivez une boucle qui affiche le temps écoulé toutes les 30 jusqu’à ce que l’exercice soit terminé.
Un gestionnaire d’exercices simultanés. Cette fonction simule plusieurs exercices en parallèle en simulant leur durée. Appeler cette fonction dans le programme principal pour afficher les exercices en cours et afficher le nombre de calories brûlées à la fin pour chaque exercice.
9. Héritage et Interfaces
L’héritage en Kotlin est plus restrictif qu’en Java pour favoriser une meilleure conception. Les classes sont final par défaut et nécessitent le mot-clé open pour être héritées. Les interfaces peuvent contenir des implémentations par défaut, et Kotlin supporte la délégation de classe comme alternative à l’héritage. Les propriétés et méthodes doivent également être explicitement marquées comme open pour être surchargées, encourageant ainsi une conception plus réfléchie de la hiérarchie des classes. Les membres non abstracts d’une classe abstraite sont final par défaut, et les classes internes sont static par défaut.
La délégation de classe est une alternative à l’héritage qui permet à une classe d’utiliser les membres d’une autre classe sans hériter de celle-ci. Cela permet de réutiliser du code sans créer de dépendances inutiles entre les classes. Kotlin fournit une syntaxe concise pour la délégation de classe, ce qui rend le code plus lisible et plus maintenable.
Dans un fichier ExerciseHierarchy.kt, implémentez (dans un sous-package pour éviter les confusions avec les classes précédentes) :
- Interface de base
- Créez une interface
Exerciseavec :- Une propriété
nameen lecture seule - Une propriété
durationen lecture seule - Une méthode
calculateCalories()
- Une propriété
- Classe abstraite
- Créez une classe abstraite
AbstractExercisequi :- Implémente l’interface
Exercise - Déclare toutes les propriétés et méthodes comme abstraites
- Ajoute une méthode
describe()ouverte à la surcharge
- Implémente l’interface
- Exercices spécifiques
- Créez deux classes concrètes :
CardioExerciseavec :- Distance parcourue (km)
- Intensité (1-10)
- Calcul des calories :
duration * intensity * 3.5
StrengthExerciseavec :- Nombre de répétitions
- Poids (kg)
- Calcul des calories :
duration * weight * 2.1
- Système de suivi
- Créez une interface
ExerciseTrackeravec :startExercise()pauseExercise()stopExercise()getCurrentStatus()
- Implémentation basique
- Créez
BasicExerciseTrackerqui :- Implémente
ExerciseTracker - Gère un statut interne (“Not started”, “Started”, “Paused”, “Stopped”)
- Implémente toutes les méthodes de l’interface
- Implémente
- Délégation multiple
- Créez une classe
SmartTrackerqui :- Combine un exercice et un tracker
- Utilise la délégation pour implémenter les deux interfaces
- Permet de suivre n’importe quel type d’exercice
- Programme principal
- Créez une fonction
mainqui :- Instancie différents types d’exercices
- Crée un tracker basique
- Crée des smart trackers pour chaque exercice
- Démontre le cycle de vie complet des exercices
10. Sealed Class, Gestion des Erreurs
Les sealed classes en Kotlin représentent une hiérarchie de classes restreinte où toutes les sous-classes doivent être déclarées dans le même fichier que la classe scellée. C’est une forme de restriction plus souple que enum mais plus stricte qu’une classe abstraite classique. Cette fonctionnalité est particulièrement utile pour représenter un ensemble limité d’états ou de types, comme dans le cas de la gestion des résultats d’opérations.
Kotlin améliore la gestion des erreurs par rapport à Java avec le type Result, les expressions try et une meilleure gestion des exceptions. Le type Result encapsule un succès ou un échec, permettant une gestion plus fonctionnelle des erreurs. Les expressions try peuvent retourner une valeur, et les exceptions vérifiées ne sont pas obligatoires. Cette approche rend le code plus sûr et plus expressif tout en réduisant le boilerplate code.
Le try en Kotlin est une expression, ce qui signifie qu’il peut retourner une valeur. Cela permet de gérer les erreurs de manière plus concise et expressive. Les exceptions vérifiées ne sont pas obligatoires en Kotlin, ce qui simplifie la gestion des erreurs et améliore la lisibilité du code. Les coroutines offrent également une gestion des erreurs plus efficace, avec des fonctions de sécurité pour les appels asynchrones`
Dans un fichier ErrorHandling.kt, implémentez et lisez en détail :
package fr.univtln.bruno.exercices.basic
import kotlinx.coroutines.runBlocking
// Sealed class to represent the result of an operation, which can be either a success or an error
sealed class ExerciseResult<out T> {
// Success case holding the data
data class Success<T>(val data: T) : ExerciseResult<T>()
// Error case holding the exception
data class Error(val exception: Exception) : ExerciseResult<Nothing>()
}
// Function to validate an exercise, returning an ExerciseResult
fun validateExercise(exercise: Exercise): ExerciseResult<Exercise> = when {
exercise.duration < 0 -> ExerciseResult.Error(IllegalArgumentException("Duration cannot be negative"))
exercise.name.isBlank() -> ExerciseResult.Error(IllegalArgumentException("Name cannot be blank"))
else -> ExerciseResult.Success(exercise)
}
// Extension function for handling success cases
fun <T> ExerciseResult<T>.onSuccess(action: (T) -> Unit): ExerciseResult<T> {
if (this is ExerciseResult.Success) action(data)
return this
}
// Extension function for handling error cases
fun <T> ExerciseResult<T>.onError(action: (Exception) -> Unit): ExerciseResult<T> {
if (this is ExerciseResult.Error) action(exception)
return this
}
// Define a Client interface for fetching exercises
interface Client {
suspend fun getExercise(id: Int): Exercise
}
// Implement the Client interface with a simulated API call
class ApiClient : Client {
override suspend fun getExercise(id: Int): Exercise {
// Simulate an API call
return CardioExercise("Running", 30, 10.0, 5)
}
}
// Function to safely make an API call, returning an ExerciseResult
suspend fun safeApiCall(call: suspend () -> Exercise): ExerciseResult<Exercise> = try {
ExerciseResult.Success(call())
} catch (e: Exception) {
ExerciseResult.Error(e)
}
// Function to get an exercise safely using a client, with error handling
suspend fun getExerciseSafely(client: Client, id: Int): ExerciseResult<Exercise> = safeApiCall {
client.getExercise(id)
}
// Main function to demonstrate error handling
fun main(): Unit = runBlocking {
val client = ApiClient()
val exercise = CardioExercise("Running", 30, 10.0, 5)
// Validate the exercise and handle the result
validateExercise(exercise)
.onSuccess { println("Exercise is valid: $it") }
.onError { println("Validation failed: ${it.message}") }
// Fetch an exercise safely and handle the result
getExerciseSafely(client, 1)
.onSuccess { println("Retrieved exercise: $it") }
.onError { println("API call failed: ${it.message}") }
}11. Génériques
Les génériques en Kotlin améliorent le système de type en permettant d’écrire du code qui fonctionne avec différents types tout en maintenant la sécurité du typage. Kotlin ajoute des fonctionnalités avancées comme la variance déclarée (out/in), les contraintes de type (where), et les projections de type (*). Ces concepts permettent une meilleure flexibilité et sécurité dans la conception des APIs.
La covariance (out) permet à un type générique de préserver la relation d’héritage du type qu’il contient. En d’autres termes, si B hérite de A, alors Container<B> peut être utilisé là où Container<A> est attendu.
La covariance (in) permet à un type générique de consommer des valeurs de type générique. En d’autres termes, si B hérite de A, alors Container<A> peut être utilisé là où Container<B> est attendu.
Dans un fichier Generics.kt, implémentez :
import fr.univtln.bruno.exercices.basic.inheritance.Exercise
import fr.univtln.bruno.exercices.basic.inheritance.CardioExercise
import fr.univtln.bruno.exercices.basic.inheritance.StrengthExercise
// 1. Container générique avec covariance
// T est covariant, on peut donc utiliser Container<StrengthExercise> là où Container<Exercise> est attendu
// Par exemple, si Container<StrengthExercise> hérite de Container<Exercise>, on peut utiliser Container<StrengthExercise> là où Container<Exercise> est attendu
// Cela permet de récupérer des objets de type StrengthExercise dans un container de type Exercise
interface Container<out T> {
fun get(): T
}
// 2. Logger générique avec contravariance
// T est contravariant, on peut donc utiliser Logger<Exercise> là où Logger<StrengthExercise> est attendu
// Par exemple, si Logger<Exercise> hérite de Logger<StrengthExercise>, on peut utiliser Logger<Exercise> là où Logger<StrengthExercise> est attendu
// Cela permet de logger des objets de type Exercise dans un logger de type StrengthExercise
interface Logger<in T> {
fun log(item: T)
}
// 3. Classe de gestion d'exercices générique
// T est un type générique qui hérite de Exercise
// On peut donc utiliser ExerciseManager<CardioExercise> ou ExerciseManager<StrengthExercise>
// Cela permet de gérer des exercices de type CardioExercise ou StrengthExercise
class ExerciseManager<T : Exercise> {
private val exercises = mutableListOf<T>()
fun add(exercise: T) {
exercises.add(exercise)
}
fun getByType(predicate: (T) -> Boolean): List<T> =
exercises.filter(predicate)
}
// 4. Classe avec contraintes multiples
// T est un type générique qui hérite de Exercise et Comparable<T>
// On peut donc utiliser WorkoutAnalyzer<CardioExercise> ou WorkoutAnalyzer<StrengthExercise>
// Cela permet d'analyser des exercices de type CardioExercise ou StrengthExercise
// La méthode findMostIntense renvoie l'exercice le plus intense en fonction de la comparaison
class WorkoutAnalyzer<T> where T : Exercise, T : Comparable<T> {
fun findMostIntense(exercises: List<T>): T? =
exercises.maxOrNull()
}
// 5. Extension function générique avec reified type
// Cette fonction d'extension permet de filtrer une liste d'exercices par type
// Par exemple, si on a une liste d'exercices de différents types, on peut filtrer les exercices de type CardioExercise
// inline permet d'utiliser le type reified T pour filtrer les éléments de la liste
// reified permet de récupérer le type de T à l'exécution
inline fun <reified T : Exercise> List<Exercise>.filterByType(): List<T> =
filterIsInstance<T>()
// Programme de démonstration
fun main() {
// Création d'instances
val cardioManager = ExerciseManager<CardioExercise>()
val strengthManager = ExerciseManager<StrengthExercise>()
// Ajout d'exercices
cardioManager.add(CardioExercise("Running", 30, 5.0, 8))
strengthManager.add(StrengthExercise("Squats", 20, 12, 60))
// Utilisation des filtres génériques
// it est un paramètre implicite qui représente chaque élément de la liste
val longCardio = cardioManager.getByType { it.duration > 20 }
println("Long cardio exercises: $longCardio")
// Utilisation de l'extension function avec type reified
val exercises = listOf(
CardioExercise("Running", 30, 5.0, 8),
StrengthExercise("Squats", 20, 12, 60)
)
val cardioOnly = exercises.filterByType<CardioExercise>()
println("Cardio exercises: $cardioOnly")
}Koans Kotlin
Les Koans Kotlin sont des exercices interactifs conçus pour apprendre les idiomes Kotlin progressivement. Disponibles sur Kotlin Koans Online, ils couvrent les concepts essentiels du langage.
Un vrai projet Kotlin
IntelliJ IDEA
- Créer un projet Kotlin
- File → New → Project
- Sélectionner “Kotlin”
- Choisir “JVM” et “Gradle”
- Exécution
- Cliquer sur le bouton ▶️ à côté de
main() - Ou utiliser
Shift + F10
- Cliquer sur le bouton ▶️ à côté de
Dans VS Code
- Installer les extensions :
- “Kotlin” de mathiasfrohlich
- “Kotlin Language” de fwcd
- “Code Runner” de Jun Han
- Créer un fichier
.ktet ajouter une fonctionmain:
fun main() {
println("Hello Kotlin!")
}- Cliquer sur ▶️ en haut à droite ou utiliser
Ctrl+Alt+N
Structure de projet
monprojet/
├── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── MonFichier.kt
│ └── test/
│ └── kotlin/
│ └── MonFichierTest.kt
└── build/
└── libs/
└── monprojet.jar
Rest Client App - Exercices Pratiques
Créer un projet Kotlin pour une application de gestion de clients REST dans IntelliJ IDEA.
Nous allons utiliser Ktor pour effectuer des requêtes HTTP vers une API REST. Ktor est un framework client et serveur pour créer des applications web et mobiles.
ajouter les dépendances suivantes dans le fichier build.gradle.kts :
plugins {
kotlin("jvm") version "1.9.0"
kotlin("plugin.serialization") version "1.9.0"
}
dependencies {
implementation("io.ktor:ktor-client-core:2.3.7")
implementation("io.ktor:ktor-client-cio:2.3.7")
implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}Créer les classes qui représentent les entités dans l’application clientes. La bibliothèque Ktor utilise kotlinx.serialization pour la sérialisation/désérialisation des objets JSON.
package com.example.api
import kotlinx.serialization.Serializable
@Serializable
data class Exercise(
val id: Int,
val name: String,
val duration: Int,
val calories: Int
)Créer une classe cliente pour effectuer des requêtes HTTP vers l’API REST.
package com.example.api
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
class ExerciseApiClient(private val baseUrl: String = "http://localhost:8080") {
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json()
}
}
suspend fun getExercises(): List<Exercise> {
return client.get("$baseUrl/exercises").body()
}
suspend fun getExercise(id: Int): Exercise {
return client.get("$baseUrl/exercises/$id").body()
}
suspend fun createExercise(request: CreateExerciseRequest): Exercise {
return client.post("$baseUrl/exercises") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
suspend fun updateExercise(id: Int, request: CreateExerciseRequest): Exercise {
return client.put("$baseUrl/exercises/$id") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
suspend fun deleteExercise(id: Int): Boolean {
val response = client.delete("$baseUrl/exercises/$id")
return response.status == HttpStatusCode.NoContent
}
fun close() {
client.close()
}
}package com.example
import com.example.api.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val client = ExerciseApiClient()
try {
// Créer un exercice
val newExercise = client.createExercise(
CreateExerciseRequest(
name = "Course",
duration = 30,
calories = 300
)
)
println("Exercice créé: $newExercise")
// Récupérer tous les exercices
val exercises = client.getExercises()
println("Liste des exercices: $exercises")
// Récupérer un exercice spécifique
val exercise = client.getExercise(newExercise.id)
println("Détail de l'exercice: $exercise")
// Mettre à jour un exercice
val updatedExercise = client.updateExercise(
newExercise.id,
CreateExerciseRequest(
name = "Course rapide",
duration = 45,
calories = 450
)
)
println("Exercice mis à jour: $updatedExercise")
// Supprimer un exercice
val deleted = client.deleteExercise(newExercise.id)
println("Exercice supprimé: $deleted")
} catch (e: Exception) {
println("Erreur: ${e.message}")
} finally {
client.close()
}
}
Comment Kotlin compile-t-il ?
Structure de compilation
.ktFonction
main()Fichier avec plusieurs
main()Compilation des fichiers
Le compilateur : 1. Crée un fichier
.classpour chaque classe 2. Crée un fichier*Kt.classpour les fonctions top-level 3. Ajoute les métadonnées Kotlin.Configuration du point d’entrée
Dans les cas simples, le point d’entrée est la fonction
main(). Dans les cas plus complexes, il sera défini par Gradle ou Maven.