MFormations
Modern Java Engineering

Chapitre 15

15 - Kotlin

15 - Kotlin

Cours 15 : Kotlin

1. Kotlin pour développeurs Java

1.1 Pourquoi Kotlin ?

Kotlin est un langage moderne qui tourne sur la JVM, développé par JetBrains. Il est :

  • Concis : Moins de boilerplate que Java
  • Sûr : Null safety intégré
  • Interopérable : 100% compatible avec Java
  • Moderne : Coroutines, extension functions, sealed classes

1.2 Hello Kotlin

// Kotlin
fun main() {
    println("Hello, World!")
    
    val name: String = "Kotlin"  // Immutable (final)
    var count = 42                // Mutable, type inféré
    count++
    
    // String templates
    println("Hello, $name! Count is $count")
}

1.3 Variables : val vs var

val immutable = "Cannot change"  // Équivalent à final en Java
var mutable = "Can change"       // Équivalent à variable normale

// lateinit (pour l'injection de dépendances)
lateinit var service: OrderService

// Lazy initialization
val config: Config by lazy {
    loadExpensiveConfig()
}

1.4 Types de base

// Types numériques
val int: Int = 42
val long: Long = 42L
val double: Double = 3.14
val float: Float = 3.14f
val byte: Byte = 127

// Chaînes
val str = "Hello"
val multiline = """
    Multiple
    Lines
""".trimIndent()

// Smart casts
fun process(obj: Any) {
    if (obj is String) {
        println(obj.length)  // Smart cast to String
    }
}

1.5 Fonctions

// Fonction simple
fun add(a: Int, b: Int): Int = a + b

// Expression body
fun max(a: Int, b: Int) = if (a > b) a else b

// Default parameters
fun greet(name: String = "World") = "Hello, $name!"

// Named arguments
greet(name = "Kotlin")

// Single-expression function
fun validate(order: Order): Result = when {
    order.total <= 0 -> Result.failure("Invalid total")
    order.customer == null -> Result.failure("No customer")
    else -> Result.success(order)
}

2. Null Safety

2.1 Types nullable et non-nullable

var nonNull: String = "Never null"
// nonNull = null  // COMPILE ERROR!

var nullable: String? = "Can be null"
nullable = null  // OK

// Safe call operator
val length: Int? = nullable?.length

// Elvis operator
val len: Int = nullable?.length ?: 0

// Not-null assertion (use with care!)
val len2: Int = nullable!!.length  // NPE if null

2.2 Idioms null safety

// let with safe call
nullable?.let { value ->
    println("Value is $value")
}

// Multiple safe calls
val city = person?.address?.city ?: "Unknown"

// run with null check
person?.run {
    println("Name: $name")
    println("Age: $age")
}

// also for side effects
person?.also {
    logger.info("Processing: ${it.name}")
}

3. Data Classes & Sealed Classes

3.1 Data classes

data class Order(
    val id: String,
    val customerId: String,
    val amount: BigDecimal,
    val status: OrderStatus = OrderStatus.DRAFT
)

// Avantages par rapport à Java :
// - equals()/hashCode() automatiques
// - toString() automatique
// - copy() pour l'immutabilité
// - componentN() pour le destructuring

// Utilisation
val order = Order("ORD-001", "CUST-001", BigDecimal(100))
val copy = order.copy(status = OrderStatus.SUBMITTED)
val (id, customer, amount, _) = order  // Destructuring

3.2 Sealed classes

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val message: String, val code: Int) : ApiResult<Nothing>()
    object Loading : ApiResult<Nothing>()
}

// When exhaustive (le compilateur vérifie tous les cas)
fun handleResult(result: ApiResult<Order>) = when (result) {
    is ApiResult.Success -> println("Order: ${result.data}")
    is ApiResult.Error -> println("Error: ${result.message}")
    ApiResult.Loading -> println("Loading...")
}

3.3 Sealed interfaces (Kotlin 2.0+)

sealed interface DomainEvent {
    val orderId: String
    val timestamp: Instant
    
    data class Created(override val orderId: String, 
                      val customerId: String) : DomainEvent {
        override val timestamp: Instant = Instant.now()
    }
    
    data class Submitted(override val orderId: String) : DomainEvent {
        override val timestamp: Instant = Instant.now()
    }
}

4. Extension Functions

4.1 Définition

// Extension sur String
fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

// Extension sur Order
fun Order.totalWithTax(taxRate: BigDecimal): BigDecimal {
    return this.amount.multiply(BigDecimal.ONE.add(taxRate))
}

// Utilisation
val email = "user@example.com"
println(email.isValidEmail())  // true

val order = Order("1", "c1", BigDecimal(100))
println(order.totalWithTax(BigDecimal("0.20")))  // 120

4.2 Extension properties

val <T> List<T>.secondOrNull: T?
    get() = if (size >= 2) this[1] else null

val Order.formattedTotal: String
    get() = "$ ${amount.setScale(2)}"

// Utilisation
val list = listOf(1, 2, 3)
println(list.secondOrNull)  // 2

5. Coroutines

5.1 Launch

// Lancement d'une coroutine
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")
    }
    println("Hello,")
}

// Plusieurs coroutines
fun main() = runBlocking {
    repeat(5) { i ->
        launch {
            delay(1000L * i)
            println("Coroutine $i")
        }
    }
}

5.2 Async/Await

fun main() = runBlocking {
    val deferred1 = async { fetchOrder("ORD-001") }
    val deferred2 = async { fetchCustomer("CUST-001") }
    
    val order = deferred1.await()
    val customer = deferred2.await()
    
    println("Order: $order, Customer: $customer")
}

suspend fun fetchOrder(id: String): Order {
    delay(1000)
    return Order(id, "CUST-001", BigDecimal(100))
}

5.3 Flow

// Cold stream asynchrone
fun getOrders(): Flow<Order> = flow {
    val orders = listOf(
        Order("1", "c1", BigDecimal(100)),
        Order("2", "c2", BigDecimal(200))
    )
    for (order in orders) {
        delay(500)
        emit(order)
    }
}

fun main() = runBlocking {
    getOrders()
        .map { it.copy(amount = it.amount * BigDecimal("1.20")) }
        .filter { it.amount > BigDecimal(150) }
        .catch { e -> println("Error: $e") }
        .collect { order ->
            println("Received: $order")
        }
}

5.4 Structured Concurrency

fun main() = runBlocking {
    coroutineScope {
        launch { 
            delay(1000)
            println("Task 1") 
        }
        launch { 
            delay(500)
            println("Task 2")
        }
    }
    println("Both tasks completed")
    
    // supervisorScope pour ignorer les erreurs d'une coroutine
    supervisorScope {
        val job = launch {
            throw RuntimeException("Failed")
        }
        job.join()
        launch {
            println("This still runs")
        }
    }
}

6. Spring Boot + Kotlin

6.1 Configuration

@SpringBootApplication
class OrderServiceApplication

fun main(args: Array<String>) {
    runApplication<OrderServiceApplication>(*args)
}

6.2 Controller

@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
    
    @GetMapping
    suspend fun getAll(@RequestParam page: Int = 0,
                       @RequestParam size: Int = 20): List<Order> {
        return orderService.findAll(page, size)
    }
    
    @GetMapping("/{id}")
    fun getById(@PathVariable id: String): ResponseEntity<Order> {
        return orderService.findById(id)
            ?.let { ResponseEntity.ok(it) }
            ?: ResponseEntity.notFound().build()
    }
    
    @PostMapping
    fun create(@RequestBody @Valid order: OrderRequest): Order {
        return orderService.create(order)
    }
}

6.3 Service

@Service
class OrderService(private val repository: OrderRepository) {
    
    fun findById(id: String): Order? = repository.findById(id)
    
    fun findAll(page: Int, size: Int): List<Order> {
        return repository.findAll(PageRequest.of(page, size)).content
    }
    
    @Transactional
    fun create(request: OrderRequest): Order {
        val order = Order(
            customerId = request.customerId,
            amount = request.total,
            status = OrderStatus.DRAFT
        )
        return repository.save(order)
    }
}

6.4 Repository avec Spring Data

interface OrderRepository : JpaRepository<Order, String> {
    
    fun findByCustomerId(customerId: String): List<Order>
    
    @Query("SELECT o FROM Order o WHERE o.amount > :minAmount")
    fun findExpensiveOrders(@Param("minAmount") minAmount: BigDecimal): List<Order>
    
    fun findByStatusOrderByCreatedAtDesc(status: OrderStatus, pageable: Pageable): Page<Order>
}

7. Ktor

7.1 Application

fun main() {
    embeddedServer(Netty, port = 8080) {
        configureRouting()
        configureSerialization()
        configureStatusPages()
    }.start(wait = true)
}

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello, Ktor!", ContentType.Text.Plain)
        }
        
        get("/api/orders") {
            val orders = orderService.findAll()
            call.respond(orders)
        }
        
        post("/api/orders") {
            val order = call.receive<Order>()
            val created = orderService.create(order)
            call.respond(HttpStatusCode.Created, created)
        }
    }
}

7.2 Plugins

fun Application.configureSerialization() {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            ignoreUnknownKeys = true
        })
    }
}

fun Application.configureStatusPages() {
    install(StatusPages) {
        exception<OrderNotFoundException> { call, cause ->
            call.respond(HttpStatusCode.NotFound, mapOf(
                "error" to cause.message
            ))
        }
    }
}

8. Kotlin Multiplatform

8.1 Structure

// commonMain
expect fun platformName(): String

class Greeting {
    fun greet(): String = "Hello from ${platformName()}"
}

// androidMain
actual fun platformName(): String = "Android"

// iosMain
actual fun platformName(): String = "iOS"

9. Interop Java/Kotlin

9.1 Appel Kotlin depuis Java

// Kotlin
class OrderService {
    fun processOrder(@NotNull order: Order): Result {
        // ...
    }
}
// Java
OrderService service = new OrderService();
Order order = new Order("1", "c1", BigDecimal.TEN);
Result result = service.processOrder(order);

Points clés

  • val/var : Immuabilité vs mutabilité
  • Null safety : ?. ?: !! .let
  • Data classes : equals, hashCode, toString, copy automatiques
  • Sealed classes : Unions typées avec when exhaustif
  • Extensions : Ajouter des méthodes sans héritage
  • Coroutines : launch, async, Flow pour l'asynchrone
  • Spring Boot : Constructors binding, suspend functions
  • Ktor : Framework HTTP léger
  • Interop : 100% compatible Java