Key Takeaways Kotlin brings compile time null checks, functional aspects and expressive syntax to the JVM platform

Kotlin is interoperable with Java and can be introduced incrementally to an existing Java project

Projects heavy on boilerplate and logic are good candidates adopting Kotlin

Kotlin integrates well with popular frameworks including Spring and HIbernate

Kotlin can significantly reduce the size of a Java code base by eliminating boilerplate

Introducing Kotlin

Kotlin is one of the newer languages on the JVM from JetBrains, the makers of IntelliJ. It is a statically typed language which aims to provide a blend of OO and FP programming styles. Kotlin compiler creates bytecode compatible with the JVM, allowing it to run on the JVM and interoperate with existing libraries. It got its big break when it was endorsed by Google in 2017 for Android development.

JetBrains has a stated goal of making Kotlin a multi-platform language and provide 100% Java interoperability. Kotlin’s recent successes and maturity level put in a good place for it to make inroads into the server side.

The case for Kotlin

A number of languages have attempted to better Java. Kotlin gets a lot of things right, both in terms of the language and the ecosystem. It is a long overdue evolution to a better Java while conserving the JVM and the vast library space. This approach combined with the backing from JetBrains and Google, make it a serious contender. Let’s take a look at some of the features Kotlin brings.

Related Sponsored Content Demystifying Microservices for Jakarta EE & Java EE Developers

Type inference – Type inference is a first class feature. Kotlin infers the type of variable without the need to explicitly specify it. In instances where types are needed for clarity, they can still be specified.

Java 10 has moved in a similar direction by introducing the var keyword. While this looks similar on the surface, its limited in scope to local variables. It cannot be used for fields and method signatures.

Strict null checks – Kotlin treats nullable code flow as a compile time error. It provides additional syntax to handle null checks. Notably it provides protection against NPEs in chained calls.

Interoperability with Java – Kotlin is significantly better than other JVM languages in this area. It interoperates seamlessly with Java. Java classes from frameworks can be imported and used in Kotlin and vice versa. Notably, Kotlin collections can interoperate with Java collections.

Immutability – Kotlin encourages immutable data structures. The commonly used data structures (Set/ List/ Map) are immutable, unless explicitly declared as mutable. Variables are also designated as immutable (val) and mutable (var). These changes add up and the impact on manageability of state is noticeable.

Clean and expressive syntax – Kotlin introduces a number of improvements which make a significant impact on the readability of the code. To mention a few:

Semicolons are optional

Curly braces are optional in instances where they are not useful

Getter/Setters are optional

Everything is an object - primitives are used behind the scenes automatically if needed

Expressions: An expression returns a result when its evaluated.

In Kotlin all functions are expressions, since they return Unit at least. Control flow statements like if, try and when (similar to switch) are also expressions. For example:

String result = null; try { result = callFn(); } catch (Exception ex) { result = “”; } becomes: val result = try { callFn() } catch (ex: Exception) { “” }

- Loops support ranges. For example:

for (i in 1..100) { println(i) }

There are several other improvements which we shall discuss as we go.

Introducing Kotlin to your Java project

Small steps

Given the Java interoperability, it is recommended to add Kotlin to an existing Java project in small steps. Supporting projects for the main product are typically good candidates. Once the team is comfortable, they can evaluate whether they prefer switching completely.

What sort of a project is a good candidate?

All Java projects can benefit from Kotlin. However, projects with the following characteristics can make the decision easier.

Project containing lots of DTO or model/entity object – This is typical for project dealing with a CRUD or data translation. These tend to be cluttered with getters/setters. Kotlin properties can be leveraged here and simplifies the classes significantly.

Projects heavy on utility classes – Utility classes in Java typically exist to compensate for the lack for top level functions in Java. In many instances these contain global stateless functions via public static. These can be factored out into pure functions. Further. Kotlin’s support for FP constructs like Function types and higher order functions can be leveraged to make code more maintainable and testable.

Projects with logic heavy classes – These tend to susceptible to null pointer exceptions (NPE) which are one of the problem areas Kotlin solves well. Developers get support here by letting the language analyze code paths leading to potential NPE(s). Kotlin’s `when` construct (a better `switch`) is useful here to break up nested logic trees into manageable functions. Immutability support for variables and collections helps simplify the logic and avoid hard to find bugs arising from leaky references. While some of the above can be accomplished with Java, Kotlin’s strength is in promoting these paradigms and making in clean and consistent.

Let's take a pause here to look at a typical Java logic snippet and it’s Kotlin counterpart:

public class Sample { public String logic(String paramA, String paramB) { String result = null; try { if (paramA.length() > 10) { throw new InvalidArgumentException(new String[]{"Unknown"}); } else if ("AB".equals(paramA) && paramB == null) { result = subLogicA(paramA + "A", "DEFAULT"); } else if ("XX".equals(paramA) && "YY".equals(paramB)) { result = subLogicA(paramA + "X", paramB + "Y"); } else if (paramB != null) { result = subLogicA(paramA, paramB); } else { result = subLogicA(paramA, "DEFAULT"); } } catch (Exception ex) { result = ex.getMessage(); } return result; } private String subLogicA(String paramA, String paramB) { return paramA + "|" + paramB; } }

Kotlin counterpart:

fun logic(paramA: String, paramB: String?): String { return try { when { (paramA.length > 10) -> throw InvalidArgumentException(arrayOf("Unknown")) (paramA == "AB" && paramB == null) -> subLogicA(paramA + "A") (paramA == "XX" && paramB == "YY") -> subLogicA(paramA + "X", paramB + "X") else -> if (paramB != null) subLogicA(paramA, paramB) else subLogicA(paramA) } } catch (ex: Exception) { ex.message ?: "UNKNOWN" } } private fun subLogicA(paramA: String, paramB: String = "DEFAULT"): String { return "$paramA|$paramB" }

While these snippets are functionally equivalent, there are some distinct differences.

The logic() function does not need to be in a class. Kotlin has top level functions. This opens up a big space and encourages us to think if something really needs to be an object. Stand alone, pure functions are easier to test. This gives the team options for adopting cleaner functional approaches.

Kotlin introduces `when`, a powerful construct for organizing conditional flow. It's a lot more capable that `if` or `switch` statements. Arbitrary logic can be cleanly organized using `when`.

Notice that in the Kotlin version we never declared a return variable. This is possible since Kotlin allows us to use `when` and `try` as expressions.

In the subLogicA function we were able to assign a default value to paramB in the function declaration.

private fun subLogicA(paramA: String, paramB: String = "DEFAULT"): String {

Now we have the ability to invoke either function signature:

subLogicA(paramA, paramB)

or

subLogicA(paramA) # In this case the paramB used the default value in the function declaration

The logic is now easier to follow and the line count is reduced by ~35%

Adding Kotlin to your Java build

Maven and Gradle support Kotlin via plugins. The Kotlin code is compiled to Java classes and included in the build process. Newer build tools like Kobalt also look promising. Kobalt is inspired by Maven/Gradle but written purely in Kotlin

To get started, add the Kotlin plugin dependencies to your Maven or Gradle build file.

If you are using Spring and JPA, you should add the kotlin-spring and kotlin-jpa compiler plugins too. The project should compile and build without any noticeable differences.

The plugin is required to generate JavaDoc for Kotlin codebase.

IDE plugins are available for IntelliJ and Eclipse studio but as might be expected, Kotlin’s development and build tooling benefits greatly from the IntelliJ association. The IDE has first class support for Kotlin, starting from the community edition. One of the notable features is the support for automatically converting existing Java code to Kotlin. The conversion is accurate and a good learning tool for writing idiomatic Kotlin.

Integration with popular frameworks

Since we are introducing Kotlin to an existing project, framework compatibility is a concern. Kotlin fits seamlessly into the Java ecosystem, since it compiles down to Java bytecode. Several popular frameworks have announced Kotlin support – including Spring, Vert.x, Spark and others. Let's take a look at what it's like to use Kotlin with Spring and Hibernate.

Spring

Spring has been one of the early supporters of Kotlin, first adding support in 2016. Spring 5 leverages Kotlin for proving cleaner DSLs. You can expect existing Java Spring code to continue working without any changes.

Spring annotations in Kotlin

Spring annotations and AOP work as out of the box. You can annotate Kotlin classes just the same as you would annotate Java. Consider the service declaration snippet below.

@Service @CacheConfig(cacheNames = [TOKEN_CACHE_NAME], cacheResolver = "envCacheResolver") open class TokenCache @Autowired constructor(private val repo: TokenRepository) {

These are standard Spring annotations

@Service: org.springframework.stereotype.Service

@CacheConfig: org.springframework.cache

Notice the constructor is a part of the class declaration.

@Autowired constructor(private val tokenRepo: TokenRepository)

Kotlin refers to this as the primary constructor and it can be a part of the class declaration. In this instance tokenRepo is a property which is declared inline.

Compile time constants can be used in annotations and these generally help with avoiding typos.

Handling final classes

Kotlin classes are final by default. It advocates the approach of allowing inheritance as a conscious design choice. This doesn’t work will with Spring AOP but is not hard to compensate for. We need to mark the relevant classes as open – Kotlin’s keyword for non-final.

IntelliJ gives you a friendly warning.

You can get around this by using the ‘all open’ maven plugin. This plugin makes the classes with specific annotations open. The simpler option is to mark the class as ‘open’.

Auto wiring and null checks

Kotlin enforces null checking strictly. It requires all properties marked as not nullable to be initialized. These can be initialized at the declaration site or in the constructor. This runs contrary to the Dependency Injection – which populates properties at runtime.

The lateinit modifier allows you to specify that the property will be initialized before use. In the following snippet, Kotlin trusts that the config object will be initialized before first use.

@Component class MyService { @Autowired lateinit var config: SessionConfig }

While lateinit is useful for auto wiring, I would recommend using it sparingly. On the flip side, it turns off the compile time null checks on the property. You still get a runtime error if its null at first use but you lose a lot of the compile time null checking.

Constructor injection can be used as an alternative. This works well with Spring DI and removes a lot of clutter. Example:

@Component class MyService constructor(val config: SessionConfig)

This is a good example of Kotlin coaxing you to follow best practices.

Hibernate

Hibernate works well with Kotlin out of the box, and no major changes are required. A typical entity class would look like:

@Entity @Table(name = "device_model") class Device { @Id @Column(name = "deviceId") var deviceId: String? = null @Column(unique = true) @Type(type = "encryptedString") var modelNumber = "AC-100" override fun toString(): String = "Device(id=$id, channelId=$modelNumber)" override fun equals(other: Any?) = other is Device && other.deviceId?.length == this.deviceId?.length && other.modelNumber == this.modelNumber override fun hashCode(): Int { var result = deviceId?.hashCode() ?: 0 result = 31 * result + modelNumber.hashCode() return result } }

In the above snippet we leveraged several Kotlin features.

Properties

By using the properties syntax we do not have to define getters and setters explicitly.

This cuts down on clutter and allows us to focus on the data model.

Type inference

In instances where we can provide an initial value, we can skip the type specification since that can be inferred. For example:

var modelNumber = "AC-100"

modelNumber property is inferred to be of type String.

Expressions

If we take a closer look at the toString() method, there are a few difference from Java

override fun toString(): String = "Device(id=$id, channelId=$modelNumber)"

The return statement is missing. We are using Kotlin’s expressions here. For function returning a single expression, we can skip the curly braces and assign via ‘=’.

String templates

"Device(id=$id, channelId=$modelNumber)"

Here, we can use templating more naturally. Kotlin allows embedding ${expression} in any String. This eliminated the need for awkward concatenation or external helpers like String.format

Equality testing

In the equals method you might have noticed this expression.

other.deviceId?.length == this.deviceId?.length

Its comparing two Strings with an == sign. This has been a long standing gotcha in Java, which treats String as a special case for equality testing. Kotlin finally fixes it by using == consistently for structural equality (equals() in Java). Referential equality is checked with ===

Data class

Kotlin also offers a special type of class known as Data class. These are especially suited to scenarios where the primary purpose of the class is to hold data. Data classes automatically generate equals(), hashCode() and toString() methods, further reducing boilerplate.

A Data class would change our last example to this:

@Entity @Table(name = "device_model") data class Device2( @Id @Column(name = "deviceId") var deviceId: String? = null, @Column(unique = true) @Type(type = "encryptedString") var modelNumber: String = "AC-100" )

Both the attributes are passed in as constructor parameters. The equals, hashCode and toString are provided by the data class.

However, Data classes do not provide a default constructor. This is a problem for Hibernate, which uses default constructors to create the entity objects. We can leverage the kotlin-jpa plugin here, which generates an additional zero-argument constructor for JPA entity classes.

One of the things which set Kotlin apart in the JVM language space is that it's not just about engineering elegance but deals with problems in real world.

Practical benefits of adopting Kotlin

Reduction in Null Pointer Exceptions

Addressing NPEs in Java is one of the major objectives for Kotlin. Explicit null checking is the most visible change when Kotlin is introduced to a project.

Kotlin tackles null safety by introducing some new operators. The ? operator Kotlin offers null safe calls. For example:

val model: Model? = car?.model

The model attribute will be read only if the car object is not null. If car is null model evaluates to null. Note that the type of model is Model? - indicating that the result can be null. At this point flow analysis kicks in and we get a compile time check for NPE in any code consuming the model variable.

This can be used in chained calls too

val year = car?.model?.year

The following is the equivalent Java code:

Integer year = null; if (car != null && car.model != null) { year = car.model.year; }

On a large codebase, a lot of these null checks would be missed. It saves considerable development time to have these checks done automatically with compile time safety.

In cases where the expression evaluates to null the Elvis operator ( ?: ) can be used to provide a default value

val year = car?.model?.year ?: 1990

In the above snippet if year ultimately is null, the value 1990 is used instead. The ?: operator takes the value on the right is the expression on the left is null.

Functional programming options

Kotlin build on top of Java 8 capabilities and provides first class functions. First class functions can be stored in variables / data structures and passed around. For example, in Java we can return functions:

@FunctionalInterface interface CalcStrategy { Double calc(Double principal); } class StrategyFactory { public static CalcStrategy getStrategy(Double taxRate) { return (principal) -> (taxRate / 100) * principal; } }

Kotlin makes this a lot more natural, allowing us to clearly express intent:

// Function as a type typealias CalcStrategy = (principal: Double) -> Double fun getStrategy(taxRate: Double): CalcStrategy = { principal -> (taxRate / 100) * principal } Things change when we move to deeper usage of functions. The following snippet in Kotlin defines a function generating another function: val fn1 = { principal: Double -> { taxRate: Double -> (taxRate / 100) * principal } }

We easily can invoke fn1 and the resulting function:

fn1(1000.0) (2.5)

Output

25.0

Although the above is achievable in Java, it’s not straight forward and involves boilerplate code.

Having these capabilities available encourages the team(s) to experiment with FP concepts. This leads to a better fit for purpose code ultimately resulting in more stable products.

Note that the lambda syntax is subtly different in Kotlin and Java. This can be a source of annoyance in the early days.

Java

( Integer first, Integer second ) -> first * second

Equivalent Kotlin

{ first: Int, second: Int -> first * second }

Over time it becomes apparent that the altered syntax is needed for the use cases Kotlin is supporting.

Reduce project footprint

One of the most understated advantages of Kotlin is that it can reduce the file count in you project. A Kotlin file can contain multiple/mix of class declarations, functions and other constructs like enum classes. This opens up a lot of possibilities not available in Java. On the flip side it presents a new choice – what's the right way to organize classes and functions?

In his book Clean Code, Robert C Martin introduces the newspaper metaphor. Good code should read like a newspaper - high level constructs near the top with detail increasing as you move down the file. The file should tell a cohesive story. Code layout in Kotlin can take a cue from this metaphor.

The recommendation is – to group similar things together – within the larger context

While Kotlin won’t stop you from abandoning structure, doing so can make it difficult to navigate code at a later time. Organize things by their ‘relation and order of usage’. For example:

enum class Topic { AUTHORIZE_REQUEST, CANCEL_REQUEST, DEREG_REQUEST, CACHE_ENTRY_EXPIRED } enum class AuthTopicAttribute {APP_ID, DEVICE_ID} enum class ExpiryTopicAttribute {APP_ID, REQ_ID} typealias onPublish = (data: Map<String, String?>) -> Unit interface IPubSub { fun publish(topic: Topic, data: Map<String, String?>) fun addSubscriber(topic: Topic, onPublish: onPublish): Long fun unSubscribe(topic: Topic, subscriberId: Long) } class RedisPubSub constructor(internal val redis: RedissonClient): IPubSub { ...}

In practice this reduces mental overhead significantly by reducing the number of files you have to jump to form the complete picture.

A common case is the Spring JPA repositories, which clutter up the package. These can be re-organized in the same file:

@Repository @Transactional interface DeviceRepository : CrudRepository<DeviceModel, String> { fun findFirstByDeviceId(deviceId: String): DeviceModel? } @Repository @Transactional interface MachineRepository : CrudRepository<MachineModel, String> { fun findFirstByMachinePK(pk: MachinePKModel): MachineModel? } @Repository @Transactional interface UserRepository : CrudRepository<UserModel, String> { fun findFirstByUserPK(pk: UserPKModel): UserModel? }

The end result of the above is that the number of Lines Of Code (LOC) in shrinks significantly. This has a direct impact on speed of delivery and maintainability.

We measured the number of files and lines of code in a Java project which was ported to Kotlin. This is a typical REST service containing data model, some logic and caching. In the Kotlin version, the LOC shrunk by ~50%. Developers spent significantly less time navigating between files and writing boilerplate code.

Enable clear, expressive code

Writing clean code is a wide topic and it depends on a combination of language, design and technique. However, Kotlin sets the team up for success by providing a good toolset. Below are some examples.

Type inference

Type inference ultimately reduces noise in the code. This helps the developers focus on the objective of the code.

It is a commonly voiced concern that type inference might make it harder to track the object we are dealing with. From practical experience, this concern is valid only for a small number of scenarios, typically less that 5%. In a vast majority of the scenarios the type is obvious.

Example:

LocalDate date = LocalDate.now(); String text = "Banner";

Becomes

val date = LocalDate.now() val text = "Banner"

Kotlin is also fine with the type being specified.

val date: LocalDate = LocalDate.now() val text: String = "Banner"

It is worth noting that Kotlin offers a comprehensive solution. For example, we can define a function type in Kotlin as:

val sq = { num: Int -> num * num }

Java 10 on the other hand, infers type by looking at the type of the expression on the right. This introduces some limitations. If we tried to do the above operation in Java, we get an error:

Typealias

This is a handy feature in Kotlin which lets us assign an alias to an existing type. It does not introduce a new type but allows us to refer to an existing type with an alternate name. For example:

typealias SerialNumber = String

SerialNumber is now an alias for the String type and can be used interchangeably with the String type. For example:

val serial: SerialNumber = "FC-100-AC"

is the equivalent of

val serial: String = "FC-100-AC"

A lot of times typealias can act as an ‘explaining variable’, to introduce clarity. Consider the following declaration:

val myMap: Map<String, String> = HashMap()

We know that the myMap holds Strings but we have no information on what those Strings represent. We could clarify this code by introducing typealiases for the String type:

typealias ProductId = String typealias SerialNumber = String

Now the map declaration above can be changed to:

val myMap: Map<ProductId, SerialNumber> = HashMap()

The above two definitions of myMap are equivalent but in the latter we can easily identify the contents of the map.

Kotlin compiler replace the typealias with the underlying type. Hence, the runtime behavior of myMap is unaffected, for example:

myMap.put(“MyKey”, “MyValue”)

The cumulative effect of such calcifications is a reduction in number of subtle bugs. On large distributed teams, bugs often a result of failure to communicate intent.

Early adoption

Getting early traction is usually the hardest part of introducing change. Start by identifying suitable projects for experimentation. Typically, there are early adopters who will be willing experiment and write the initial Kotlin code. Over the coming weeks the larger team will have an opportunity to look at this code. The early human response is to avoid the new and unfamiliar. Give it some time for the changes to get vetted. Help the evaluation by making reading resources and tech talks available. At the end of the first few weeks, a larger group of people can decide on the level of adoption.

The learning curve is small for developers familiar with Java. In my experience most Java developers are productive with Kotlin within 1 week. Junior developers were able to pick up and run with it without special training. Previous exposure to a different language or familiarity with FP concepts further reduces adoption time.

Things to come

Co-routines have been available in Kotlin since version 1.1. Conceptually they are similar to async/await in JavaScript. They reduce complication in async programming by allowing us to suspend flow without blocking the thread.

They have marked as experimental, until now. Co-routines will graduate from experimental status in 1.3 release. This opens up more exciting opportunities.

Kotlin roadmap is guided via the Kotlin Evolution and Enhancement Process (KEEP). Keep an eye on this for discussions and upcoming features.

About the Author

Baljeet Sandhu is an technical lead with a depth of experience delivering software across domains ranging from manufacturing to finance. He is interested in clean code, security and scalable distributed systems. Baljeet currently works for HYPR, building decentralized authentication solutions to eliminate fraud, enhance user experience and enable true password-less security.