Introduction à la langue Kotlin

1. Vue d'ensemble

Dans ce didacticiel, nous allons examiner Kotlin, un nouveau langage dans le monde JVM, et certaines de ses fonctionnalités de base, notamment les classes, l'héritage, les instructions conditionnelles et les constructions en boucle.

Ensuite, nous examinerons certaines des principales fonctionnalités qui font de Kotlin un langage attrayant, y compris la sécurité nulle, les classes de données, les fonctions d'extension et les modèles de chaîne .

2. Dépendances de Maven

Pour utiliser Kotlin dans votre projet Maven, vous devez ajouter la bibliothèque standard Kotlin à votre pom.xml :

 org.jetbrains.kotlin kotlin-stdlib 1.0.4 

Pour ajouter la prise en charge de JUnit pour Kotlin, vous devrez également inclure les dépendances suivantes:

 org.jetbrains.kotlin kotlin-test-junit 1.0.4 test   junit junit 4.12 test 

Vous pouvez trouver les dernières versions de kotlin-stdlib, kotlin-test-junit et junit sur Maven Central.

Enfin, vous devrez configurer les répertoires sources et le plugin Kotlin afin d'effectuer une compilation Maven:

 ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin   kotlin-maven-plugin org.jetbrains.kotlin 1.0.4   compile  compile    test-compile  test-compile      

Vous pouvez trouver la dernière version de kotlin-maven-plugin dans Maven Central.

3. Syntaxe de base

Regardons les éléments de base du langage Kotlin.

Il existe une certaine similitude avec Java (par exemple, la définition des packages se fait de la même manière). Jetons un coup d'œil aux différences.

3.1. Définition des fonctions

Définissons une fonction ayant deux paramètres Int avec le type de retour Int :

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

3.2. Définition des variables locales

Variable locale à attribuer une seule fois (lecture seule):

val a: Int = 1 val b = 1 val c: Int c = 1

Notez que le type d'une variable b est déduit par un compilateur Kotlin. Nous pourrions également définir des variables mutables:

var x = 5 x += 1

4. Champs facultatifs

Kotlin a une syntaxe de base pour définir un champ qui pourrait être nullable (facultatif). Lorsque nous voulons déclarer que ce type de champ est nullable, nous devons utiliser le type suffixé avec un point d'interrogation:

val email: String?

Lorsque vous avez défini un champ Nullable, il est parfaitement valide de lui attribuer une valeur Null :

val email: String? = null

Cela signifie que dans un champ d'e-mail peut être une valeur nulle. Si nous écrivons:

val email: String = "value"

Ensuite, nous devons attribuer une valeur au champ email dans la même déclaration que nous déclarons email. Il ne peut pas avoir de valeur nulle. Nous reviendrons sur la sécurité nulle de Kotlin dans une section ultérieure.

5. Classes

Voyons comment créer une classe simple pour gérer une catégorie spécifique d'un produit. Notre classe ItemManager ci-dessous a un constructeur par défaut qui remplit deux champs - categoryId et dbConnection - et un champ d' e-mail facultatif :

class ItemManager(val categoryId: String, val dbConnection: String) { var email = "" // ... }

Cette construction ItemManager (…) crée un constructeur et deux champs dans notre classe: categoryId et dbConnection

Notez que notre constructeur utilise le mot-clé val pour ses arguments - cela signifie que les champs correspondants seront définitifs et immuables. Si nous avions utilisé le mot-clé var (comme nous l'avons fait lors de la définition du champ email ), alors ces champs seraient mutables.

Créons une instance de ItemManager en utilisant le constructeur par défaut:

ItemManager("cat_id", "db://connection")

Nous pourrions construire ItemManager en utilisant des paramètres nommés. C'est très utile lorsque vous avez comme dans cet exemple de fonction qui prend deux paramètres avec le même type par exemple String , et que vous ne voulez pas en confondre un ordre. En utilisant les paramètres de dénomination, vous pouvez écrire explicitement quel paramètre est affecté. Dans la classe ItemManager, il y a deux champs, categoryId et dbConnection afin que les deux puissent être référencés à l'aide de paramètres nommés:

ItemManager(categoryId = "catId", dbConnection = "db://Connection")

C'est très utile lorsque nous devons passer plus d'arguments à une fonction.

Si vous avez besoin de constructeurs supplémentaires, vous devez les définir à l'aide du mot clé constructor . Définissons un autre constructeur qui définit également le champ email :

constructor(categoryId: String, dbConnection: String, email: String) : this(categoryId, dbConnection) { this.email = email }

Notez que ce constructeur invoque le constructeur par défaut que nous avons défini ci-dessus avant de définir le champ email. Et comme nous avons déjà défini categoryId et dbConnection pour être immuables à l'aide du mot-clé val dans le constructeur par défaut, nous n'avons pas besoin de répéter le mot-clé val dans le constructeur supplémentaire.

Maintenant, créons une instance en utilisant le constructeur supplémentaire:

ItemManager("cat_id", "db://connection", "[email protected]")

Si vous souhaitez définir une méthode d'instance sur ItemManager , vous devez le faire en utilisant le mot-clé fun :

fun isFromSpecificCategory(catId: String): Boolean { return categoryId == catId }

6. Héritage

By default, Kotlin's classes are closed for extension — the equivalent of a class marked final in Java.

In order to specify that a class is open for extension, you would use the open keyword when defining the class.

Let's define an Item class that is open for extension:

open class Item(val id: String, val name: String = "unknown_name") { open fun getIdOfItem(): String { return id } }

Note that we also denoted the getIdOfItem() method as open. This allows it to be overridden.

Now, let's extend the Item class and override the getIdOfItem() method:

class ItemWithCategory(id: String, name: String, val categoryId: String) : Item(id, name) { override fun getIdOfItem(): String { return id + name } }

7. Conditional Statements

In Kotlin, conditional statement if is an equivalent of a function that returns some value. Let's look at an example:

fun makeAnalyisOfCategory(catId: String): Unit { val result = if (catId == "100") "Yes" else "No" println(result) }

In this example, we see that if catId is equal to “100” conditional block returns “Yes” else it returns “No”. Returned value gets assigned to result.

You could create a normal ifelse block:

val number = 2 if (number  10) { println("number is greater that 10") }

Kotlin has also a very useful when command that acts like an advanced switch statement:

val name = "John" when (name) { "John" -> println("Hi man") "Alice" -> println("Hi lady") } 

8. Collections

There are two types of collections in Kotlin: mutable and immutable. When we create immutable collection it means that is read only:

val items = listOf(1, 2, 3, 4)

There is no add function element on that list.

When we want to create a mutable list that could be altered, we need to use mutableListOf() method:

val rwList = mutableListOf(1, 2, 3) rwList.add(5)

A mutable list has add() method so we could append an element to it. There are also equivalent method to other types of collections: mutableMapOf(), mapOf(), setOf(), mutableSetOf()

9. Exceptions

Mechanism of exception handling is very similar to the one in Java.

All exception classes extend Throwable. The exception must have a message, stacktrace, and an optional cause. Every exception in Kotlin is unchecked, meaning that compiler does not force us to catch them.

To throw an exception object, we need to use the throw-expression:

throw Exception("msg")

Handling of exception is done by using try…catch block(finally optional):

try { } catch (e: SomeException) { } finally { }

10. Lambdas

In Kotlin, we could define lambda functions and pass them as arguments to other functions.

Let's see how to define a simple lambda:

val sumLambda = { a: Int, b: Int -> a + b }

We defined sumLambda function that takes two arguments of type Int as an argument and returns Int.

We could pass a lambda around:

@Test fun givenListOfNumber_whenDoingOperationsUsingLambda_shouldReturnProperResult() { // given val listOfNumbers = listOf(1, 2, 3) // when val sum = listOfNumbers.reduce { a, b -> a + b } // then assertEquals(6, sum) }

11. Looping Constructs

In Kotlin, looping through collections could be done by using a standard for..in construct:

val numbers = arrayOf("first", "second", "third", "fourth")
for (n in numbers) { println(n) }

If we want to iterate over a range of integers we could use a range construct:

for (i in 2..9 step 2) { println(i) }

Note that the range in the example above is inclusive on both sides. The step parameter is optional and it is an equivalent to incrementing the counter twice in each iteration. The output will be following:

2 4 6 8

We could use a rangeTo() function that is defined on Int class in the following way:

1.rangeTo(10).map{ it * 2 }

The result will contain (note that rangeTo() is also inclusive):

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

12. Null Safety

Let's look at one of the key features of Kotlin – null safety, that is built into the language. To illustrate why this is useful, we will create simple service that returns an Item object:

class ItemService { fun findItemNameForId(id: String): Item? { val itemId = UUID.randomUUID().toString() return Item(itemId, "name-$itemId"); } }

The important thing to notice is returned type of that method. It is an object followed by the question mark. It is a construct from Kotlin language, meaning that Item returned from that method could be null. We need to handle that case at compile time, deciding what we want to do with that object (it is more or less equivalent to Java 8 Optional type).

If the method signature has type without question mark:

fun findItemNameForId(id: String): Item

then calling code will not need to handle a null case because it is guaranteed by the compiler and Kotlin language, that returned object can not be null.

Otherwise, if there is a nullable object passed to a method, and that case is not handled, it will not compile.

Let's write a test case for Kotlin type-safety:

val id = "item_id" val itemService = ItemService() val result = itemService.findItemNameForId(id) assertNotNull(result?.let { it -> it.id }) assertNotNull(result!!.id) 

We are seeing here that after executing method findItemNameForId(), the returned type is of Kotlin Nullable. To access a field of that object (id), we need to handle that case at compile time. Method let() will execute only if a result is non-nullable. Id field can be accessed inside of a lambda function because it is null safe.

Another way to access that nullable object field is to use Kotlin operator !!. It is equivalent to:

if (result == null){ throwNpe(); } return result;

Kotlin will check if that object is a null if so, it will throw a NullPointerException, otherwise it will return a proper object. Function throwNpe() is a Kotlin internal function.

13. Data Classes

A very nice language construct that could be found in Kotlin is data classes (it is equivalent to “case class” from Scala language). The purpose of such classes is to only hold data. In our example we had an Item class that only holds the data:

data class Item(val id: String, val name: String)

The compiler will create for us methods hashCode(), equals(), and toString(). It is good practice to make data classes immutable, by using a val keyword. Data classes could have default field values:

data class Item(val id: String, val name: String = "unknown_name")

We see that name field has a default value “unknown_name”.

14. Extension Functions

Suppose that we have a class that is a part of 3rd party library, but we want to extend it with an additional method. Kotlin allows us to do this by using extension functions.

Let's consider an example in which we have a list of elements and we want to take a random element from that list. We want to add a new function random() to 3rd party List class.

Here's how it looks like in Kotlin:

fun  List.random(): T? { if (this.isEmpty()) return null return get(ThreadLocalRandom.current().nextInt(count())) }

The most important thing to notice here is a signature of the method. The method is prefixed with a name of the class that we are adding this extra method to.

Inside the extension method, we operate on a scope of a list, therefore using this gave use access to list instance methods like isEmpty() or count(). Then we are able to call random() method on any list that is in that scope:

fun  getRandomElementOfList(list: List): T? { return list.random() }

We created a method that takes a list and then executes custom extension function random() that was previously defined. Let's write a test case for our new function:

val elements = listOf("a", "b", "c") val result = ListExtension().getRandomElementOfList(elements) assertTrue(elements.contains(result)) 

The possibility of defining functions that “extends” 3rd party classes is a very powerful feature and can make our code more concise and readable.

15. String Templates

A very nice feature of Kotlin language is a possibility to use templates for Strings. It is very useful because we do not need to concatenate Strings manually:

val firstName = "Tom" val secondName = "Mary" val concatOfNames = "$firstName + $secondName" val sum = "four: ${2 + 2}" 

We can also evaluate an expression inside the ${} block:

val itemManager = ItemManager("cat_id", "db://connection") val result = "function result: ${itemManager.isFromSpecificCategory("1")}"

16. Kotlin/Java Interoperability

Kotlin – Java interoperability is seamlessly easy. Let's suppose that we have a Java class with a method that operates on String:

class StringUtils{ public static String toUpperCase(String name) { return name.toUpperCase(); } }

Now we want to execute that code from our Kotlin class. We only need to import that class and we could execute java method from Kotlin without any problems:

val name = "tom" val res = StringUtils.toUpperCase(name) assertEquals(res, "TOM")

As we see, we used java method from Kotlin code.

Calling Kotlin code from a Java is also very easy. Let's define simple Kotlin function:

class MathematicsOperations { fun addTwoNumbers(a: Int, b: Int): Int { return a + b } }

Executing addTwoNumbers() from Java code is very easy:

int res = new MathematicsOperations().addTwoNumbers(2, 4); assertEquals(6, res);

We see that call to Kotlin code was transparent to us.

When we define a method in java that return type is a void, in Kotlin returned value will be of a Unit type.

There are some special identifiers in Java language ( is, object, in, ..) that when used them in Kotlin code needs to be escaped. For example, we could define a method that has a name object() but we need to remember to escape that name as this is a special identifier in java:

fun `object`(): String { return "this is object" }

Then we could execute that method:

`object`()

17. Conclusion

Cet article fait une introduction au langage Kotlin et à ses principales fonctionnalités. Il commence par introduire des concepts simples comme les boucles, les instructions conditionnelles et la définition des classes. Puis montre quelques fonctionnalités plus avancées telles que les fonctions d'extension et la sécurité nulle.

L'implémentation de tous ces exemples et extraits de code se trouve dans le projet GitHub.