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