Въведение в езика Kotlin

1. Общ преглед

В този урок ще разгледаме Kotlin, нов език в света на JVM, и някои от неговите основни характеристики, включително класове, наследяване, условни инструкции и циклични конструкции.

След това ще разгледаме някои от основните характеристики, които правят Kotlin привлекателен език, включително нулева безопасност, класове данни, функции за разширение и шаблони за низове .

2. Зависимости на Maven

За да използвате Kotlin във вашия проект Maven, трябва да добавите стандартната библиотека Kotlin към вашия pom.xml :

 org.jetbrains.kotlin kotlin-stdlib 1.0.4 

За да добавите поддръжка на JUnit за Kotlin, ще трябва да включите и следните зависимости:

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

Можете да намерите най-новите версии на kotlin-stdlib, kotlin-test-junit и junit на Maven Central.

И накрая, ще трябва да конфигурирате директориите на източника и приставката Kotlin, за да извършите компилация на 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      

Можете да намерите най-новата версия на kotlin-maven-plugin в Maven Central.

3. Основен синтаксис

Нека разгледаме основните градивни елементи на Kotlin Language.

Има известна прилика с Java (например дефинирането на пакети е по същия начин). Нека да разгледаме разликите.

3.1. Дефиниране на функции

Нека дефинираме функция, която има два параметъра Int с тип връщане Int :

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

3.2. Дефиниране на локални променливи

Присвояване еднократно (само за четене) локална променлива:

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

Имайте предвид, че типът на променлива b се извежда от компилатор на Kotlin. Можем също да дефинираме променливи променливи:

var x = 5 x += 1

4. Незадължителни полета

Kotlin има основен синтаксис за дефиниране на поле, което може да бъде занулявано (по избор). Когато искаме да декларираме, че този тип поле е за нулиране, трябва да използваме тип, суфиксиран с въпросителен знак:

val email: String?

Когато сте дефинирали поле за нулиране, е напълно валидно да му присвоите нула :

val email: String? = null

Това означава, че в имейл поле може да има нула. Ако ще напишем:

val email: String = "value"

След това трябва да присвоим стойност на имейл полето в същото изявление, което декларираме имейл. Не може да има нулева стойност. В следващ раздел ще се върнем към нулевата безопасност на Kotlin .

5. Класове

Нека да демонстрираме как да създадем прост клас за управление на определена категория продукт. Нашият клас ItemManager по-долу има конструктор по подразбиране, който попълва две полета - categoryId и dbConnection - и незадължително имейл поле:

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

Тази конструкция ItemManager (...) създава конструктор и две полета в нашия клас: categoryId и dbConnection

Имайте предвид, че нашият конструктор използва ключовата дума val за своите аргументи - това означава, че съответните полета ще бъдат окончателни и неизменни. Ако бяхме използвали ключовата дума var (както направихме, когато дефинирахме имейл полето), тогава тези полета щяха да бъдат променливи.

Нека създадем екземпляр на ItemManager, използвайки конструктора по подразбиране:

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

Можем да изградим ItemManager, като използваме имена на параметри. Много е полезно, когато имате като в този пример функция, която приема два параметъра с един и същ тип, например String , и не искате да бъркате реда им. Използвайки параметри за именуване, можете изрично да напишете кой параметър е присвоен. В клас ItemManager има две полета, categoryId и dbConnection, така че и двете могат да бъдат препращани с помощта на имена на параметри:

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

Много е полезно, когато трябва да предадем повече аргументи на функция.

Ако имате нужда от допълнителни конструктори, бихте ги определили с помощта на ключовата дума конструктор . Нека дефинираме друг конструктор, който също задава имейл полето:

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

Имайте предвид, че този конструктор извиква конструктора по подразбиране, който дефинирахме по-горе, преди да зададем полето за имейл. И тъй като вече дефинирахме categoryId и dbConnection да бъдат неизменни, използвайки ключовата дума val в конструктора по подразбиране, не е необходимо да повтаряме ключовата дума val в допълнителния конструктор.

Сега, нека създадем екземпляр, използвайки допълнителния конструктор:

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

Ако искате да дефинирате метод на екземпляр в ItemManager , бихте го направили, като използвате ключовата дума fun :

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

6. Наследяване

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

Тази статия прави въведение в езика Kotlin и неговите ключови характеристики. Започва с въвеждане на прости понятия като цикли, условни оператори и дефиниране на класове. След това показва някои по-разширени функции като функции за разширение и нулева безопасност.

Внедряването на всички тези примери и кодови фрагменти може да се намери в проекта GitHub.