Septembre 2022

# Introduction à Kotlin

## Pourquoi Kotlin

- Google I/O 2017 : langage de premier ordre pour Android

- Intégré par défaut à partir d’Android Studio 3.0

- Langage à typage statique
- S’exécute sur une JVM (et par extension sur ART)
- Beaucoup d’inférence de type, donc moins verbeux que Java
- Multiparadigme (procedural, fonctionnel, orienté objets)

- Pousse aux best practices

Pour l’auto-apprentissage : https://kotlinlang.org/docs/home.html — https://play.kotlinlang.org/

## Règles de base

- Code contenu dans des fichiers `.kt`
- Pas besoin de `;` en fin de ligne

    fun main(args: Array<String>) {
      println("Hello Kotlin")
    }

- Déclarations à la UML
- Conventions de nommage très similaires à Java
  cf. https://kotlinlang.org/docs/coding-conventions.html

## Types

- Pas de types primitifs
- Action sans résultat : singleton `Unit` (≈`void` de Java)
- Rien, fonction qui ne termine pas normalement : `Nothing`
- Nombres : `Double`, `Float`, `Long`, `Int`, `Short`, `Byte`
- Pas de conversions implicite : `toInt()`, `toFloat()`, …
- Littéraux : comme en Java (`0b101010`, `0xB8`, `12`, `3.14f`)
- Possibilité de séparateur `1_000_000`
- Booléens : `Boolean` (`true` et `false`)

- Tableaux : classe `Array` (`get`, `set`, `[]`, `size`) + fonctions

- Texte : `Char`, `String`
- Les chaînes de caractères sont immuables
- `"Hello\tTab"` (escaped string) ou `""" avec saut de lignes """` (raw string)

- Utilisation possible de string templates :

    val name = "Laurent"
    val greetings = "Salut $name !"
    val scream = "EH HO… ${name.toUpperCase()} !"

## Références

- `val` : reférence une valeur constante (immuable)

      val answer : Int = 42
      val age = 33;        // Type inféré
      val what: String     // Type nécessaire si pas d'initialisation
      what = "Whaaat ?"    // Initialisation différée

      age += 2             // Erreur, pas modifiable

- `var` : reférence une variable (peu changer de valeur)

      var x : Int = 2;
      var y = 'y';
      ++x;              // OK, muable

- `const` : constante connue à la compilation

  - soit top level, soit membre d’un `object`
  - initialisée avec une `String` ou un valeur primitive
  - pas de getter personnalisé

      const val PI_APPROX: float = 3.14f;

- Comparaison de références

  - `==` comparaison structurelle (`equals()`)
  - `===` comparaison d’instances (emplacement mémoire)

## Nullable types

- Nullable : `Double?`, `String?`, …
- Pour éviter les `NullPointerException` (NPE)
- `?.` pour un accès sûr
- Elvis operator : `?:`

    val l: Int = if (b != null) b.length else -1
    // équivalent
    val l = b?.length ?: -1

- Forçage : `!!.` lève une NPE si l’objet est `null` (à éviter !)

## Les intervalles (Range)

- Opérateur `..` (issu de la fonction `rangeTo()`)
- Fonctions extensions : `until`, `downTo`, `step`

    1..10              // de 1 inclus jusqu'à 10 inclus
    1 until 10         // de 1 inclus jusqu'à 10 exclus
    4..1               // vide
    4 downto 1         //  4, 3, 2, 1
    10 downto 1 step 2 // 10, 8, 6, 4, 2

- `step` doit être strictement positif

## Le transtypage (cast)

- Savoir si un objet est d’un certain type : `is`

    if (obj is String) {
      print(obj.trim())
    }

    if (obj !is String) { // equivalent à !(obj is String)
      print("Not a String")
    }
    else {
      print(obj.length)
    }

- Cast explicite souvent inutile : le compilateur s’en occupe (smart cast)

    // x est automatiquement casté en String à droite du ||
    if (x !is String || x.length == 0) return

    // x est automatiquement casté en String à droite du && et dans le if
    if (x is String && x.length > 0) {
      print(x.length)
    }

- On peut néamoins caster avec `as` (unsafe cast)
- Lève une exception si pas possible

    val x: String = y as String

- Utiliser `as?` pour safe cast
- Le résultat est un nullable type

    val x: String? = y as? String

## Contrôle du flot d’exécution

- `if`, `while`, `break`, `continue` : comme en Java
- possibilité de labels : `unLabel@` … `break@unLabel`

    loop@ for (i in 1..100) {
      for (j in 1..100) {
        if (…) break@loop
      }
    }

- `when` : équivalent du `switch` (mais plus sympa)

    when (x) {
      0, 1 -> print("peu")
      in 2..10 -> print("moyen")
      is String -> print("${x.trim()} est une String")
      else -> {
        println("rien de tout ça")
        print("on peu mettre un block")
      }
    }

    println(
      when {
        x.isOdd() -> "impair"
        x.isEven() -> "pair"
        else -> "bizarre"
    })

- `when` et `if` peuvent êrte utilisées comme expressions : elles renvoient comme valeur le resultat de leur dernière instruction exécutée
- `for` : parcours sur tout ce qui fournit un itérateur (`foreach` de C#), i.e. :
  - fournit une méthode `iterator()`
  - cet itérateur a une méthode `next()` et une méthode `hasNext()` qui retourne un `Boolean` (doivent être marquées `operator`)
- Itération classique (intervalle, String, Array, …)

    for (i in 0..9) {
      println(i)
    }
    for (c in "Hello") {
      println(c)
    }

- Itération avec indices

    for (i in array.indices) {
      println(array[i])
    }

    for ((index, value) in array.withIndex()) {
      println("The element at $index is $value")
    }

- Pour les execptions : comme en Java (sauf qu’il n’existe pas de notion de checked exception)
- `try/catch/finally` est un expression

## Les fonctions

- Possibilité de fonction top level (pas forcément membre d’un objet)

    fun sum(a: Int, b: Int): Int {
      return a + b
    }

- Avec inférence du type de retour pour les expression body (pratique pour getter/setter)

    fun sum(a: Int, b: Int) = a + b

- Paramètres avec valeur par défaut (pratique pour les Ctor)

    fun say(text: String = "Something") = println(text)
    say()                     // Affiche Something
    say("Hi guys")            // Affiche Hi guys

- Paramètres nommés

    fun Box.setMargins(left: Int, top: Int, right: Int, bottom: Int) { … }

    myBox.setMargins(10,10,20,20)   // haut ? bas ? gauche ? droite ?
    myBox.setMargins(left = 10, right = 10, top = 20, bottom = 20)

- Paramètres variables (`Type...` de Java)
- Utilisation du spread operator (*) pour paramètres nommés

    fun foo(vararg strings: String) { … }
    foo("s1", "s2")
    foo(strings = *arrayOf("a", "b", "c"))

- On peut déclarer une fonction dans une autre fonction
- Utile pour récursivité terminale

    fun dfs(graph: Graph) {
      val visited = HashSet<Vertex>()

      fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
          dfs(v)
      }

      dfs(graph.vertices[0])
    }

## Classes et objets

- Squelette de classe standard en Java (entre POJO et Bean)

    class Person {
      private String name;
      private int age;

      public Person(String name, int age) {
        this.name = name;
        this.age = age;
      }

      public String getName() { return name; }
      public void setName(String name) { this.name = name; }
      public String getAge() { return age; }
      public void setAge(int age) { this.age = age; }

      public int hashCode() { … }
      public boolean equals() { … }

      @Override
      public toString() {
        return "Person(name=" + name + ", age=" + age + ")";
      }
    }

- L’équivalent en Kotlin

    data class Person(var name: String, var age: Int)

- \o/ sympa… mais contraintes :
  - Ne peut pas être `abstract`, `open`, `sealed`, `inner`
  - Ctor principal doit avoir au moins un paramètre
  - Les paramètres doivent tous être marqués `val` ou `var`
  - `equals()`, `hashCode()`, `toString()` non générée si présentes explicitement
- Instanciation : pas besoin de `new`

    val moi = Person("Laurent Provot", 27)

## Visibilités

Modifieur | Package | Classe
--- | --- | ---
`public` | partout | à tout ceux qui voient la classe
`private` | au sein du fichier contenant la déclaration | au sein de la classe uniquement
`internal` | dans le même module | dans le même module à qui voit la classe
`protected` | — | comme `private` + dans les classes dérivées

- module : ensemble de fichiers Kotlin compilés ensemble (pour Android = Gradle source set)

## Constructeur (Ctor)

- Constructeur principal
- On ne peut pas spécifier de code tout de suite

    class Person constructor(firstName: String) {…}
    class Person(firstName: String) {…}

- Bloc d’initialisation
- On peut utiliser les paramètres du ctor principal dans les blocs d’initialisation et les initialisations d’attributs
- Initialisations effectuées dans l’ordre declaré

    class Person(name: String) {
      val nameUpper = name.toUpperCase();

      init {
        println("My name is: $name");
      }
    }

- Pour initialisation de propriétés

    class Person(val firstName: String, var age: Int) { … }

- Si annotations ou modificateur (visibilité, …)

    class Person private constructor(val firstName: String) { … }
    class Person @Inject constructor(val firstName: String) { … }

- Ctor secondaires

    class Person(val name: String) {
      constructor(name: Streing, parent: Person) : this(name) {
        parent.children.add(this)
      }
    }

- Délégation au contructeur principal obligatoire (implicite sinon)
- Tous les blocs d’initialisation sont appelés
- Ctor par défaut si non précisé ou si tous les paramètres ont des valeurs par défaut

## Propriétés & getter / setter

- Attribut déclaré avec `val` = propriété en lecture seule
- Attribut déclaré avec `var` = propriété en lecture / écriture

    class Person {
      var name = "John Doe";
    }
    john = Person();

- Accès : `john.name` (utilisation du getter)
- Modification : `john.name = "johnny"` (utilisation du setter)
- Syntaxe complète

    var <propertyName>[: PropertyType] [= <initializer>]
        [<getter>]
        [<setter>]

- Propriété personnalisée : accès à l’attribut avec `field`

    var speed: Int
        get() = field * 100;
        set(value) {
          if (value >= 0) field = value;
        }

- Attribut (backing field) fourni seulement si nécessaire

    val isEmpty: Boolean
        get() = this.size == 0

- Changement de visibilité ou ajout d’annotation en conservant l’implémentation par défaut

    var devOnly: String
        @NotNull get
        private set

## Héritage

- Hiérarchie basée sur la classe `Any` (≠ `java.lang.Object`)
- Autorisation excplicite de l’héritage avec `open` (l’opposé de `final` en Java)

    open class Base(arg: String)
    class Derived(num: Int) : Base(arg.toString())

- Ctor secondaires : appel obligatoire de `super` ou délégation à un autre ctor

    class Derived : Base {
      constructor(arg: String, num: Int) : super(arg) {…}
      constructor(arg: String) : this(arg, 42)
    }

- Redéfinition de méthode : explicite avec `override`

    open class Base {
      open fun fo() {…}
      fun fc() {…}
    }
    class Derived() : Base() {
      override fun fo() {…}
    }

- Un membre déclaré `override` est automatiquement `open`; si non désiré : le marquer `final`
- Redéfinition de propriété : pareil que pour les méthodes
- On peut redéfinir un `val` en `var`
- `override` peut être utilisé dans le constructeur prinicpal

    open class Base {
      open val x: Int = 0
    }
    class Derived : Base() {
      override var x: Int = 42
    }
    class AnotherDerived(override val x: Int) : Base()

- Comme en Java, le code de la classe dérivée peut appeler des méthodes/propriétés de sa classe de base grâce au mot clé `super`
- Classe et méthode abstraite : déclarées avec le mot clé `abstract`
- `abstract` implique `open`

## Interfaces

- Comme en Java
- Une interface peut contenir des propriétés mais elles seront soit abstraites, soit `val` et sans backing field

    interface Named {
      val name: String
    }

    interface FullNamed : Named {
      val firstName: String
      val lastName: String

      override val name: String get() = "$firstName $lastName"
    }

    data class Person(
      override val firstName: String,
      override val lastName: String
    ) : FullNamed

- Désambiguïsation grâce à `super<…>` si implémentation dans l’interface

    interface A {
      fun foo() { print("A") }
    }

    interface B {
      fun foo() { print("B") }
    }

    class C : A, B {
      override fun foo() {
        super<A>.foo()
        super<B>.foo()
      }
    }

## Classes de données

    data class User(val name: String, val age: Int)

- En plus des propriétés, `toString()` et `equals()` / `hashCode()`
  - `copy()`
  - `component1()`, …, `componentN()` functions

- Méthode `copy` :

      fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

      val john = User(name = "John", age = 42)
      val youngJohn = john.copy(age = 22)

- Méthodes `component` : déstructuration de la classe
- Pour chaque propriété du ctor principal, dans l’ordre de déclaration

- Underscore possible si paramètre non utilisé

      println(john.component1()) // affiche "John"
      val (name, age) = john
      println("$name, $age years old") // affiche "John, 42 years old"

- Sympa pour les retours de fonctions, Map, itérations avec index

## `object` & Companion object

- Généralisation des classes anonymes de Java
- Expression objet = création d’une instance locale avec certains comportements

    button.addMouseListener(object : MouseAdapter() {
      override fun mouseClicked(e: MouseEvent) { … }
      override fun mouseEntered(e: MouseEvent) { … }
    })

- Création d’objet sans supertype non trivial

    fun foo() {
      val local = object {
        var x: Int = 0
        var y: Int = 0
      }
      print(local.x + local.y)
      print(local::class.simpleName)  // affiche null
    }

- `object` comme valeur de retour

    class C {
      // Private function : the return type is the anonymous object type
      private fun foo() = object { val x: String = "x"}

      // Public function : so the return type is Any
      fun publicFoo() = object { val x: String = "x" }

      fun bar() {
        val x1 = foo().x        // OK
        val x2 = publicFoo().x  // ERROR: Unresolved reference 'x'
      }
    }

- Déclaration d’objet = singletons
- Initialisation thread-safe

    object DatabaseManager {
      fun connect(conn: Connector) { … }
      fun fetchData() : Data { … }
    }

- Ne peut pas être du côté droit d’une affectation
- Ne peut pas être déclaré local
- Déclaration dans une classe possible avec le mot clé `companion`

    class MyClass {
      companion object Factory {
        fun create(): MyClass = MyClass()
      }
    }

    val instance = MyClass.create()

- Utilisation à la `static` de Java
- Mais attention `Factory` implique une instance d’objet
- Utilisation de `@JvmStatic` si on veut que cela génère des membres `static` dans le bytecode Java
- Les expressions objet sont exécutées immédiatement, les déclarations objet sont initialisées de manière paresseuse et les `companion object` au moment du chargement de leur classe d’attachement

## Retour sur les fonctions

- Les fonctions sont des variables comme les autres
- Elles ont un type :
  - `(A, B) -> C` : une fonction qui prend 2 paramètres le premier de type `A`, le second de type `B` et retoune un `C`
  - `() -> A` : pas de paramètre en entrée
  - `(A) -> Unit` : pas de valeur de retour
  - `A.(B) -> C` : fonction qui peut être appelée sur un objet de type `A`, prend un paramètre de type `B` et retourne une valeur de type `C`
- Création concise grâce aux lambda

    val square = { num: Int -> num * num }; // (Int) -> Int
    val more : (String, Int) -> String = { str, num -> str + num }
    val noReturn : Int -> Unit = { num -> println(num) }

- Pour les lambdas qui ne prennent qu’un paramètre : `it`

    val noReturn : Int -> Unit = { println(it) }
    val concatInt : String.(Int) -> String = { this + it }

- Utilisation

    fun <T, R> Collection<T>.fold(
        initial: R,
        combine: (acc: R, nextElement: T) -> R
    ): R {
        var accumulator: R = initial
        for (element: T in this) {
            accumulator = combine(accumulator, element)
        }
        return accumulator
    }

    val items = listOf(1, 2, 3, 4, 5)
    items.(0, {acc, i -> acc + i})   // 15
    items.("Elts :", {res, i -> res + " " + i})  // "Elts : 1 2 3 4 5"
    items.(1, Int::times)   // 120

- Une fonction de type `A.(B) -> C` peut être utilisée en lieu et place de `(A, B) -> C` et vice versa
- Lorsque le dernier paramètre d’une fonction est une fonction, si on passe un lambda on peut la sortir des parenthèses

    items.fold(0) {sum, i -> sum + i}

- Fonctions d’extension
- Possible d’ajouter des fonctions à une classe après coup
- Toutes les instances peuvent en profiter

    fun String.reverse() = StringBuilder(this).reverse.toString()
    "That's cool !".reverse()   // "! looc s'tahT"

- Fonction infixes : `infix`
  - Fonction membre ou fonction d’extension
  - Un seul paramètre
  - Pas de `vararg` ou de paramètre par défaut

    infix fun String.open(rights: Acces): File { … }

    "/home/provot/lecture" open Access.WRITE
    // équivalent à
    "/home/provot/lecture".open(Access.WRITE)

## Surcharge d’opérateur

- Il est possible de surcharger les opérateurs
- On redéfinit des méthodes spécifiques de la classe
- Elles sont marquées `operator`
- +, -, *, /, %, ..
  - `a + b` équivalent à `a.plus(b)`
  - `a..b` équivalent à `a.rangeTo(b)`
- `in`, `!in`
  - `a.contains(b)`
- Accès indexé `[]`
  - `a[i]` équivalent à `a.get(i)`
  - `a[i] = b` équivalent à `a.set(i, b)`
- Appel de méthode
  - `a(i, j)` équivalent à `a.invoke(i, j)`
- a == b
  - `a?.equals(b) ?: (b === null)`
- a > b, a < b, a >= b, a <= b
  - obtenus à partir de `a.compareTo(b)`

## Documentation

- KDoc + génération avec Dokka
- Comme la javadoc
- Tags : `@param`, `@return`, `@constructor`, `@receiver`, `@property`, `@throws`, `@exception`, `@sample`, `@see`, `@author`, `@since` et `@suppress`

    /**
     * A group of *members*.
     *
     * This class has no useful logic; it's just a documentation example.
     *
     * @param T the type of a member in this group.
     * @property name the name of this group.
     * @constructor Creates an empty group.
     */
    class Group<T>(val name: String) {
        /**
         * Adds a [member] to this group.
         * @return the new size of the group.
         */
        fun add(member: T): Int { ... }
    }