Skip to main content

My journey with Kotlin - Part 2: Introduction to Kotlin

This is the second post of a 2-part introduction to Kotlin, a programming language for the JVM. If you are a Java developer, you should read this and, like I did a few months ago, consider the option of learning Kotlin.

If you haven't read it, part 1 will tell you the reasons why you would want to go away from Java.

Kotlin

Kotlin is a JVM language initiated and developed by JetBrains, the company behind IntelliJ IDEA. The primary goals of Kotlin are:

  • to make the code more concise and more readable than Java code
  • to make the code safer than pure Java code (i.e. avoid nulls).

Kotlin is a statically typed language and it feels modern. It is also mature: great support is provided in build tools (Maven, Gradle), Spring 5 will provide extended support for Kotlin (to write more idiomatic code), and, last but not least, Android made Kotlin one of its official programming languages.

Kotlin was made for 100% interoperability with Java. You can call Kotlin code from Java code, or call Java code from Kotlin code, without any awkward syntax (unllike sometimes Scala). This also means you can migrate one class at a time. If you use IntelliJ IDEA (and you should), the IDE can convert a file, a package, or a whole codebase automatically. The converted code usually works fine, and you can then adjust it to make it more Kotlin-idiomatic.

Basic syntax

Defining a class and methods

Here is how to define a class with a constructor and 2 methods:

class KafkaDescribeClient(val adminClientFactory: KafkaAdminClientFactory) {
    fun describeCluster(region: String, cluster: String): ClusterDto {
        …
    }
    fun buildUri(securityProtocol: String, instanceName: String, port: String) = "..."
}
  • class is used to define a class
  • fun is used to define a method
  • (val adminClientFactory: KafkaAdminClientFactory) defines a constructor with one parameter, and a field to store this value
  • Parameters and variables are declared with the <name>: <type> syntax. In a similar fashion, the return type of a function is placed after the list of arguments (ClusterDto in the example).
  • When a function is a single statement, you can omit the brackets, the return statement, and optionally the return type. Just use = between the declaration and the definition of the function.

Defining an immutable variable

You can declare a variable without explicitly defining its type (the type inference mechanism will infer the type, in this case String):

val region = "us-east-1"

Or you can explicitly declare the type (again, with <name>: <type> syntax):

val cluster: String = "engine"

When creating an object, the new keyword is omitted:

val adminClient = KafkaDescribeClient(adminClientFactory)

In the 3 cases above, the variables cannot be reassigned. These variables are immutable.

Defining a mutable variable

You could define mutable variables with the var keyword (but you don’t really need that):

var something = 12
something = 42

Static methods

Kotlin doesn't have a static keyword. Instead, you define functions outside of a class:

package com.seigneurin.mapper

fun nodeToDto(node: Node): NodeDto {
    return NodeDto(node.id(), node.host(), node.port(), node.rack())
}

You can then import the function and call it directly, without a class prefix:

import com.seigneurin.mapper.nodeToDto
...
nodeToDto(controller)

Reference equality

=== and !== can be used to test reference equality.

== calls the equals() method under the hood, making it much simpler to compare objects.

From the documentation:

Note that the == operator in Kotlin code is translated into a call to [equals] when objects on both sides of the operator are not null.

Meaning == is safe, and o == null or null == o do not raise NullPointerExceptions.

Strings

For string comparison, use ‘==’

if (port == "9092") {

Kotlin also has built-in string interpolation using the $variable syntax (or ${...} for more complex expressions):

private fun buildUri(securityProtocol: String, instanceName: String, port: String) =
        "$securityProtocol://$instanceName$domain:$port"

Multi-line strings can be created with """ (and there is also an option to trim the margin):

        System.err.println("""
========== Data checking ==========
Purpose: checks data quality of messages in a topic.
- Kafka brokers      = ${config.kafkaBrokers}
- Kafka group ID     = ${config.applicationId}
...
      """)

Data classes

Kotlin has a special type of classes to replace POJOs: data classes.

You can declare such classes with the data class keywords and, most of the times, your class won't need a body:

data class AclParameters(
        val operation: AclOperation,
        val user: String,
        val hostname: String?,
        val groupId: String?)

The equals(), hashCode() and toString() methods will be automatically generated by the compiler.

Again, to create an instance of a class, Kotlin doesn't have a new keyword:

val aclParams = AclParameters(AclOperation.READ, "u1", "h1", “g1")

Notice that it is a good practice to make your classes immutable by using val for your fields. Instead of modifying an instance, you would then make a copy and change some fields:

val aclParamsCopy = aclParams.copy(groupId = groupName)

If you use Jackson to map JSON values to object, Jackson has a module for this.

Data classes can also be destructured to get the values of the individual fields of the class:

val (operation, user, hostname, groupId) = aclParams

Or you could partially destructure the class if you don't need all the values:

val (operation, _, hostname, _) = aclParams

Expressions

In Kotlin, complex expression can be evaluated and the result be assigned to a variable:

val res = if (operation == CREATE) {
    createAcl(aclParameters)
} else if (operation == ALTER) {
    alterAcl(aclParameters)
} else if (operation == DELETE) {
    deleteAcl(aclParameters)
} else {
    throw RuntimeException("Operation not supported: ${operation}")
}

This is a good example of how you can use immutable variables instead of mutable ones.

Streams

If you have read part 1 of this post, you probably emember this piece of Java code:

List<AclBinding> userAclBindings = aclBindings.stream()
        .filter(aclBinding -> aclBinding.entry().principal().equals(filterUser))
        .collect(Collectors.toList());

You can achieve the same in Kotlin in a more readable way:

val userAclBindings = aclBindings.filter { it.entry().principal() == filterUser }

A few things to note:

  • The type of the returned value (userAclBindings) is inferred by the compiler.
  • filter() is directly called on the collection (no call to stream()).
  • The result is a collection (no need to use a collector).
  • it automatically refers to the argument of the lambda expression.

Here is another example of Java code:

private List<String> buildKafkaUris(List<Instance> instances, String securityProtocol, String port) {
    return instances.stream()
            .map(i -> getTagOrFail(i, TAG_KEY_NAME))
            .map(name -> buildUri(securityProtocol, name, port))
            .collect(Collectors.toList());
}

The same in Kotlin:

private fun buildKafkaUris(instances: List<Instance>, securityProtocol: String, port: String): List<String> {
    return instances
            .map { getTag(it, TAG_KEY_NAME) }
            .map { buildUri(securityProtocol, it, port) }
}

(We also have null-safe code, here - see below.)

Null safety

Kotlin tries to eliminate NullPointerExceptions through a simple syntax and extra checks provided by the compiler.

Variables that allow null values have their types suffixed with ?:

var s: String? = null

If you try to use the value of a nullable variable without testing for nulls, the compiler will raise an error:

return s.length
Error:(37, 17) Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

A simple if statement will solve the error:

if (s !=null) {
    return s.length
}

Notice that, once you have tested a value for nulls, its type automatically becomes a non-nullable type (String instead of String?) in the rest of the function.

The compiler will not allow you to return a potentially null value if the return type was not declared as nullable:

private fun getTag(instance: Instance, tagKey: String): String {
    val tag = instance.tags
            ...
            .firstOrNull()
    return tag // this can be null
}
Error:(40, 16) Kotlin: Type mismatch: inferred type is String? but String was expected

The following is safe:

private fun getTag(instance: Instance, tagKey: String): String {
    val tag = instance.tags
            ...
            .firstOrNull()
    if (tag == null) {
        throw  RuntimeException("Tag [${tagKey}] not found")
    }
    return tag // this cannot be null
}

(Or just call first() in this case.)

Elvis operator

Kotlin has an Elvis operator to get a value or, if it is null, a default value:

val hostname = aclParameters.hostname ?: “*”

Safe calls

If a value is nullable, you can make a safe call, i.e. call the method/field is the object is defined, or return null otherwise:

val length: Int? = s?.length

Because null can be returned, the type of the returned value is a nullable type.

Functions

Kotlin has first class support for functions:

protected fun <R> withAdminClient(func: (AdminClient) -> R): R {
    val adminClient = adminClientFactory.createAdminClient()
    return adminClient.use { func(adminClient) }
}

Here, func is a variable of type (AdminClient) -> R, i.e. a function that takes one argument of type AdminClient and returns a value of type R.

Then, the function is called directly by passing parameters: func(adminClient).

Type aliases

Aliases can be defined on any type:

typealias Clusters = List<Cluster>

This can be handy when you want to avoid repeating complex types, e.g.:

typealias KakfaAdminFunc<R> = (AdminClient) -> R

protected fun <R> withAdminClient(func: KakfaAdminFunc<R>): R {
    ...

Notice that type aliases are local to your compilation unit.

Extension functions

Kotlin also allows you to extend existing classes with new methods. These extension function are resolved statically / locally and make it as if you were calling a real method of a class.

You define the function by prefixing the name of the function with the name of the class to extend:

fun AmazonEC2.findInstances(request: DescribeInstancesRequest): ArrayList<Instance> {
    ...
    return allInstances
}

You then call the function like any other method:

val ec2: AmazonEC2 = ...
ec2.findInstances(request)

Conclusion

This post gives you an overview of the syntax of Kotlin. Make sure to read the reference of the language to get more details.

So far, I have had a very positive experience with Kotlin:

  • The language is easy to use and intuitive.
  • It has greater expressivity than Java.
  • It is simpler than Scala.

I really encourage you to try it out. And if you need help convincing your team to migrate from Java to Kotlin, I will be happy to assist!

Post by Alexis Seigneurin
Feb 12, 2018 8:09:00 AM

Comments

©Copyright 2024 Ippon USA. All Rights Reserved.   |   Terms and Conditions   |   Privacy Policy   |   Website by Skol Marketing