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
- 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
etfalse
) 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 :
Références
val
: reférence une valeur constante (immuable)var
: reférence une variable (peu changer de valeur)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é
- soit top level, soit membre d’un
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 :
?:
- Forçage :
!!.
lève une NPE si l’objet estnull
(à éviter !)
Les intervalles (Range)
- Opérateur
..
(issu de la fonctionrangeTo()
) - 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
- Utiliser
as?
pour safe cast - Le résultat est un nullable type
Contrôle du flot d’exécution
if
,while
,break
,continue
: comme en Java- possibilité de labels :
unLabel@
…break@unLabel
when
: équivalent duswitch
(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")
}
}
when
etif
peuvent êrte utilisées comme expressions : elles renvoient comme valeur le resultat de leur dernière instruction exécutéefor
: 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éthodehasNext()
qui retourne unBoolean
(doivent être marquéesoperator
)
- fournit une méthode
- Itération classique (intervalle, String, Array, …)
- 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)
- Avec inférence du type de retour pour les expression body (pratique pour getter/setter)
- 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
- 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
- \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
ouvar
equals()
,hashCode()
,toString()
non générée si présentes explicitement
- Ne peut pas être
- Instanciation : pas besoin de
new
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
- 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
- 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
- Accès :
john.name
(utilisation du getter) - Modification :
john.name = "johnny"
(utilisation du setter) - Syntaxe complète
- Propriété personnalisée : accès à l’attribut avec
field
- Attribut (backing field) fourni seulement si nécessaire
- Changement de visibilité ou ajout d’annotation en conservant l’implémentation par défaut
Héritage
- Hiérarchie basée sur la classe
Any
(≠java.lang.Object
) - Autorisation excplicite de l’héritage avec
open
(l’opposé definal
en Java)
- 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 automatiquementopen
; si non désiré : le marquerfinal
- Redéfinition de propriété : pareil que pour les méthodes
- On peut redéfinir un
val
envar
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
impliqueopen
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
- En plus des propriétés,
toString()
etequals()
/hashCode()
copy()
component1()
, …,componentN()
functions
Méthode
copy
:- 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é
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
- 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 membresstatic
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 typeA
, le second de typeB
et retoune unC
() -> A
: pas de paramètre en entrée(A) -> Unit
: pas de valeur de retourA.(B) -> C
: fonction qui peut être appelée sur un objet de typeA
, prend un paramètre de typeB
et retourne une valeur de typeC
- 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
- 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
- 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)
- obtenus à partir de
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 { ... }
}