I. Compatibilité▲
I-A. Bytecode▲
Comment s'intègre le code Kotlin avec le code Java ? Peut-on mixer Java et Koltin et si oui, quelles en sont les limites ? Kotlin peut cohabiter sans problème avec Java. C'est-à-dire que vous pouvez progressivement ajouter du code Kotlin dans votre application, sans avoir de problème. Appeler du code Java depuis votre code Kotlin se fait facilement et le contraire tout aussi facilement. Toutefois, certaines fonctionnalités spécifiques à Kotlin ne sont pas accessibles telles quelles depuis une classe Java.
Prenons par exemple les méthodes d'extensions. Une méthode d'extension offre la possibilité de rajouter une méthode sur une API sans modifier cette même API. L'exemple ci-dessous rajoute la méthode toColor() sur le type String. Cette méthode devient alors directement accessible sur les objets de type String en Kotlin.
2.
3.
// ColorExts.ktfun
String
.toColor(): Color = Color.valueOf(this
)
val
color = "#FFFFFF"
.toColor()
Mais qu'en est-il en Java ? Cette méthode d'extension est utilisable en Java, mais elle ne le sera pas directement depuis le type String. Le compilateur va en effet transformer notre méthode d'extension en méthode static
.
2.
3.
// ColorTest.java
Color color = ColorExtsKt.toColor("#FFFFFF"
);
Assert.assertEquals(color, Color.WHITE);
D'autres fonctionnalités, quant à elles, pour des raisons techniques ne vont tout simplement pas être disponibles depuis une classe Java. Ces fonctionnalités sont rares et concernent un usage avancé de Kotlin. L'exemple typique est l'utilisation des méthodes inlinées. Une fonction inlinée est une fonction dont le corps de la méthode va être copié à l'emplacement de l'appel. Cette notion n'existe pas en Java et n'est pas transposable. De par ce fait, vous ne pouvez pas appeler une fonction inlinée en Kotlin depuis votre code Java. Pour vous empêcher de le faire, Kotlin va marquer la fonction comme private final au niveau du code compilé la rendant donc inaccessible pour les développeurs Java.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// ServiceLocator.kt
?object
ServiceLocator {
inline
fun
<reified
T> locate(): T {
return
this
[T::class
.java]
}
}
?// ServiceLocator.class
public
final
class
ServiceLocator {
?
?private
final
<T> T locate() {
;
Intrinsics.reifiedOperationMarker(4
, "T"
);return
(T)get
(Object.class
);
}
}
Sur le langage en lui-même, Kotlin s'exploite parfaitement avec Java, sans interférer. L'intégration de Kotlin dans un projet existant est donc simple : intégrer la chaîne de compilation spécifique au langage. Le plus dur étant fait, vous pouvez alors commencer de nouveaux développements en Kotlin et migrer progressivement les anciens développements vers ce nouveau langage. On préfèrera migrer ici du code mis à jour : migrer du code Java en Kotlin uniquement pour la forme n'apporte pas de valeur ajoutée.
I-B. Frameworks▲
Grâce à la compatibilité Java/Kotlin, vous êtes assurés de pouvoir utiliser Kotlin sans douleur sur votre projet. Toutefois les conventions par défaut de Kotlin ne sont pas les mêmes que celles de Java. Cela peut-il poser des problèmes d'intégration avec différents Frameworks, qui eux, ont été conçus pour Java ?
I-B-1. Spring Framework▲
Spring est parfaitement compatible avec Kotlin à condition de faire quelques adaptations au niveau de vos classes de configuration Spring écrites en Kotlin. En effet, Spring crée un proxy sur certaines classes via CGLib. Pour créer ces proxies, les classes ne doivent pas être marquées comme final
. Or justement, par défaut, les classes sont final
avec Kotlin.
C'est un problème que l'on rencontre également avec Mockito (voir ci-dessous). Il s'agit alors soit de marquer votre classe Kotlin comme open ; soit d'utiliser un plugin développé par Jetbrains pour votre outil de build pour justement marquer, automatiquement, les classes Kotlin comme non finales. À vous de choisir la solution qui vous semble la plus adaptée.
2.
3.
4.
@Configuration
open
class
MyConfiguration {
}
L'autre point d'intention est l'injection des différentes dépendances. La dépendance va être injectée en tant que property d'une classe. Naïvement on pourrait marquer la property comme val. Dans cette configuration, Kotlin vous imposera d'initialiser le champ avec une valeur qui ne pourra être changée. Cette option n'est pas envisageable. En effet, la classe doit être créée puis les champs vont être injectés.
De même, l'utilisation du marqueur var n'est pas suffisante : cela implique toujours de forcer l'initialisation avec une valeur, qui reste pour l'instant inconnue. Cette valeur peut être null. Mais votre type doit alors devenir nullable (exemple : MyRepository? à la place de MyRepository). Cela fonctionne, mais l'utilisation reste fastidieuse : à chaque utilisation de votre dépendance, vous serez obligés de vérifier sa nullité.
Kotlin propose le marqueur lateinit pour indiquer qu'une variable sera déclarée plus tard. À la charge du développeur d'orchestrer correctement l'initialisation de cette variable avant son utilisation. Le code devient un petit peu plus verbeux, mais est complètement fonctionnel avec le Framework Spring.
2.
3.
4.
5.
6.
7.
@Repository
class
MyService {
@Autowired
private
lateinit
var
repository: MyRepository
// ...
}
I-B-2. JPA▲
Comment se comporte une entité JPA avec Kotlin ? Sans problème ! Prenons par exemple l'entité ci-dessous en Java :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
@Entity
class
Example {
@Id
private
String
id;
private
String
otherField;
public
String
getId() {
return
id;
}
public
String
getOtherField() {
return
otherField;
}
}
Cette classe peut être transformée en Kotlin de cette manière :
@Entity
data
class
Example(@Id
var
id: String
, var
otherField: String
)
Si vous essayez d'utiliser le code tel quel, une erreur sera tout de même lancée au moment de la désérialisation de l'objet. Il manque un constructeur par défaut qui est, dans la version Kotlin, manquant.
2.
@Entity
data
class
Example(@Id
var
id: String
= ""
, var
otherField: String
= ""
)
Le constructeur par défaut est nécessaire pour que l'implémentation de JPA instancie la classe (ici Example) puis initialise les différents champs. Pour que cela soit également fonctionnel avec votre code, vous pouvez également utiliser le marqueur lateinit, comme vu précédemment. Mais cela ne se fera pas sans conséquence. Votre classe ne pourra plus être une data class, vos champs étant marqués par lateinit. De plus, il vous faudra spécifier explicitement le constructeur par défaut que seul JPA utilisera.
2.
3.
4.
5.
6.
7.
8.
9.
@Entity
class
Example {
constructor
() { }
@Id
lateinit
var
id: String
lateinit
var
otherField: String
}
Tout de suite, on arrive à un code très proche de la version Java, avec beaucoup de bruit lié à du code technique. De plus, le constructeur par défaut sera sûrement marqué comme non utilisé par votre IDE : il serait tentant de le supprimer. Même si cette alternative fonctionne, elle ne contentera pas tout le monde, et ce à juste titre.
I-B-3. Mockito▲
Mockito est une bibliothèque de mock : vous pouvez configurer le comportement d'une classe ou d'une interface pour un test spécifique. Mais pour cela, Mockito présente quelques contraintes : les classes ne doivent pas être « final ». Cette limitation empêche de mocker directement les classes créées en Kotlin, qui sont par défaut « final ».
2.
3.
4.
5.
6.
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class com.example.account.MockitoTest$RemoteServiceImpl
Mockito cannot mock/spy following:
- final classes
- anonymous classes
- primitive types
Deux solutions s'offrent alors à vous : marquer vos classes open ou alors migrer vers la version 2 de Mockito. La première solution n'est pas idéale, mais elle est simple à mettre en œuvre. Elle oblige en effet à modifier votre code Kotlin et cela uniquement pour les tests, sans aucune valeur directe pour la production. La version 2 de Mockito est capable de mocker des classes, final
mais uniquement via une option qui n'est pas activée par défaut.
II. Features▲
II-A. NullPointer Safety▲
Kotlin propose une déclinaison de tous ses types de base : les types nullables. De base, aucun des types présents dans Kotlin n'accepte la valeur null. Le compilateur sera en erreur par exemple avec le code suivant :
val
name: String
= null
Le langage essaye de minimiser l'utilisation de valeur null ou sinon, de l'expliciter. Ainsi, si null est une valeur possible, vous devez utiliser un type nullable, qui est représenté par le type suivi d'un point d'interrogation :
val
name: String
? = null
Ainsi, à partir du type, vous saurez si la valeur null est acceptée ou non et ainsi éviterez l'erreur à un milliard de dollars : la NullPointerException. On pourrait rapprocher un type nullable au type Optional de Java 8. Pourtant, il n'en est rien : un type nullable n'offre pas les méthodes de transformation d'Optional, mais plutôt une syntaxe qui lui est propre. À l'utilisation, cette syntaxe peut prêter à débat : en effet, pour accéder par exemple au champ d'un objet, le champ peut être accessible en utilisant le symbole '?', transformant ainsi le type de ce champ en nullable. Mais, à moins d'utiliser l'opérateur !!, le type nullable se propage dans un arbre d'objets où la base de cet arbre est nullable : tout le monde n'appréciera pas…
Toujours est-il que grâce à ce type nullable, vous pourrez exprimer vos intentions : votre API accepte-t-elle une valeur null ou non ?
II-B. Alias▲
Dans une application backend, les différentes couches techniques possèdent leurs propres modèles. L'API expose un modèle qui est transformé pour les couches inférieures et ainsi de suite. Un objet métier a ainsi souvent le même nom d'une couche à l'autre, mais depuis des packages différents. La manipulation de ces classes au sein d'un même fichier génère un clash de nommage, les classes ayant le même nom. Même si techniquement cela ne pose pas de problème, la lecture se révèle difficile, car la différenciation se fait alors via le nom des packages des classes.
MyClass apiMyClass =
...
fr.soat.example.persistance.MyClass myClass =
new
fr.soat.example.persistance.MyClass
(
apiMyClass.getName
(
));
Kotlin propose la notion d'alias à l'import. Vous pouvez importer une classe et utiliser un nom différent pour cet import.
2.
3.
4.
import
fr.soat.example.persistance.MyClass as
ModelMyClass
// ...
MyClass apiMyClass = ...
val
myClass: ModelMyClass = ModelMyClass(apiMyClass.getName());
Grâce à cette notion d'alias, la conversion d'un domaine à l'autre reste nécessaire, mais la lecture, elle, se retrouve fortement simplifiée : le bruit généré par l'ajout du package n'a alors plus lieu d'être.
II-C. TypeAlias▲
Sur une API, les types primitifs sont souvent utilisés à tort, pouvant nuire à la compréhension de cette même API.
2.
3.
void
action
(
String id, String session, String action) {
// ...
}
Seul le nom permet de différencier les paramètres de cette API. Pour casser cette ambiguïté, de nouveaux types peuvent être créés : PlayerId, SessionId, ActionName… Les paramètres utilisent alors des types spécifiques explicitant clairement la nature de chaque paramètre.
2.
3.
void
action
(
PlayerId id, SessionId session, ActionName action) {
// ...
}
Si cette gestion de nouveaux types rebute certaines personnes, Kotlin propose la notion d'alias de type qui peut servir de solution intermédiaire.
typealias
PlayerId = String
Cela ajoute uniquement un alias sur le type String. Vous pouvez alors utiliser une String directement comme PlayerId. Nous retrouvons alors la nature des types dans la signature des méthodes, sans la mise en œuvre de nouveaux types spécifiques.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
typealias
PlayerId = String
typealias
SessionId = String
typealias
ActionName = String
fun
action(id: PlayerId, session: SessionId, name: ActionName) {
// ...
}
val
id: PlayerId = "playerId"
val
session: SessionId = "sessionId"
val
action: ActionName = "action"
action(id, session, action)
Mais attention. Aucune vérification de type n'est faite par le compilateur. Vous pouvez utiliser n'importe quelle String pour chacun de ces 'types', qui sont eux-mêmes des String. Cela peut aboutir à la situation inconfortable ci-dessous (mais techniquement valide). Utilisez donc les typealias
avec parcimonie.
2.
3.
4.
val
id: PlayerId = "playerId"
val
session: SessionId = "sessionId"
val
action: ActionName = "action"
action(action, id, id)
III. Architecture▲
DDD définit la notion de Value Object. Ces objets gardent des propriétés, et des objets ayant les mêmes propriétés devraient être égaux entre eux. Prenons l'exemple d'un objet Point : deux points ayant les mêmes coordonnées devraient être égaux. En Java, cela est représenté par une classe où les méthodes equals et hashcode sont redéfinies. De plus, ces Value Object doivent être immuables, ce qui est exprimé en Java grâce à des champs final
:
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
public
class
Point {
private
final
int
x;
private
final
int
y;
public
Point
(
int
x, int
y) {
this
.x =
x;
this
.y =
y;
}
public
int
getX
(
) {
return
x;
}
public
int
getY
(
) {
return
y;
}
@Override
public
boolean
equals
(
Object o) {
if
(
this
==
o) return
true
;
if
(
o ==
null
||
getClass
(
) !=
o.getClass
(
)) return
false
;
Point point =
(
Point) o;
if
(
x !=
point.x) return
false
;
return
y ==
point.y;
}
@Override
public
int
hashCode
(
) {
int
result =
x;
result =
31
*
result +
y;
return
result;
}
}
On se retrouve vite avec du code essentiellement technique n'apportant pas de valeur fonctionnelle. Kotlin propose, pour représenter ces Value Object, la notion de data class : ces class définissent automatiquement les getter et les méthodes hashcode et equals à partir des champs qui constituent cette classe. Une data class définit également une méthode copy qui permet de créer une copie exacte d'un objet, copie que l'on peut directement modifier via cette méthode. Enfin, pour rendre l'objet immuable, les champs doivent seulement être annotés avec le mot clé val
.
2.
3.
data
class
Point(val
x: Int
, val
y: Int
)
val
point = Point(2
, 3
)
val
point2 = point.copy(y = 5
)
IV. Écosystème▲
Kotlin reste un langage relativement nouveau. Pourtant quelques projets existent déjà autour de ce langage. Une adaptation voire une migration vers ce langage est également observable sur différents projets venant de l'écosystème Java. Changement que l'on peut constater notamment à travers la nouvelle version de Spring Framework et les modifications actuellement réalisées sur l'outil de build Gradle.
IV-A. Spring 5▲
C'est au début de l'année 2017 que Pivotal communique sur le support Kotlin par le Framework Spring Framework 5. Spring est un Framework très utilisé sur les applicatifs backend. Le support de Kotlin par ce dernier n'est pas uniquement une garantie que le Framework fonctionne avec Kotlin : cela passe également par l'ajout d'API simplifiant l'utilisation de Spring, avec Kotlin.
Pour récupérer un bean d'un type spécifique dans un contexte Spring, il est systématiquement nécessaire en Java, d'indiquer le type du bean que l'on veut récupérer.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@Service
class
MyService {
@Autowired
ApplicationContext ctx;
// ...
public
void
doSomething
(
) {
MyCustomBean bean =
ctx.getBean
(
MyCustomBean.class
);
}
}
Ce code peut être légèrement simplifié grâce aux méthodes d'extensions ajoutées spécifiquement pour Kotlin, avec l'utilisation de type réifié. Le compilateur peut déduire, à partir du code, le type du bean qui vous intéresse, vous évitant ainsi de devoir le spécifier par vous-même.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@Service
class
MyService {
@Autowired
lateinit var
ctx: ApplicationContext
// ...
public
void
doSomething
(
) {
val bean: MyCustomBean =
ctx.getBean
(
)
}
}
Ce genre de mécanisme se retrouve également sur d'autres composants du Framework, comme sur l'API RestTemplate ou WebClient.
L'autre point intéressant concerne la majorité des mises à jour d'API qui n'interfère pas avec l'API Java : aucun risque de se retrouver avec une API spécifique à Kotlin dans votre code Java. En effet, ces mises à jour d'API sont majoritairement proposées via des méthodes d'extensions. Elles ne sont donc pas directement sur les classes du Framework et ne sont donc pas proposées au développeur Java. Pour pouvoir les exploiter, il vous faudra depuis votre code Kotlin, importer ces méthodes d'extensions, opération assimilable à un import standard en Java.
IV-B. Gradle▲
Gradle se base actuellement sur Groovy pour son DSL (Domain Specific Language). Groovy, par son approche dynamique, permet d'avoir une approche très souple dans l'écriture des scripts, ces Scripts étant relativement simples à écrire grâce à tout ce qu'apporte le langage par défaut. Pourtant sous cette simplicité, il est facile de se perdre dans ce DSL. Certains scripts Gradle sont difficilement compréhensibles. Gradle Inc, la société derrière Gradle, développe actuellement un nouveau plugin pour Gradle. Ce plugin permet de construire des scripts de build non plus en Groovy, mais cette fois en Kotlin. L'objectif étant de le proposer comme alternative à Groovy, voire à terme, de faire en sorte que Kotlin soit le langage principal utilisé pour construire des builds Gradle.
V. Adoption massive ?▲
Kotlin est facilement intégrable grâce à sa compatibilité directe avec Java. La configuration avec les outils actuels est simple. De plus, la valeur ajoutée par Kotlin dans un projet se fait immédiatement ressentir. Malheureusement, à elles seules, ces fonctionnalités ne sont pas suffisantes pour transformer un langage en langage massivement adopté.
Pourtant Kotlin a actuellement le vent en poupe, ce qui pourrait l'aider à se développer. C'est sans compter sur le coup de pouce de Google qui en a fait son nouveau langage de référence sur la plateforme Android pour différents outils et Frameworks. En parallèle, la dernière version de Spring Framework a adapté ses API pour Kotlin. De son côté, Gradle propose une variation de son DSL Groovy en Kotlin. Ainsi de nombreuses sociétés valorisent leurs nouveaux Frameworks, basés sur Kotlin.
Si cette tendance à construire un écosystème autour du langage Kotlin continue, alors oui, Kotlin pourrait devenir le premier langage alternatif sur la JVM, avant de peut-être devenir LE langage qui détrônera Java. Mais ça, seul le futur nous le dira.