Repost from the Cloudflight Engineering blog
For most projects related to server-side software development, Cloudflight prefers to use Kotlin over Java. This has a multitude of reasons, some of which I’ll describe in this blog post.
When transferring data from one place to another with Java, it’s common to use a plain old java object (POJO). Usually, this is an object with some properties and accessor methods.
Kotlin provides means to reduce the amount of code used in these objects. In this example, we’ll take a POJO, and convert it into a Kotlin class.
Let’s start with a UserDto
, for now, it will only
contain a few fields, and accessor methods for those fields. It could
look as follows:
public class UserDto {
private Integer id;
private String name;
private String email;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
If you are using IntelliJ IDEA and would copy this code into a Kotlin file, we’ll get a notification asking if the Java code should be converted to Kotlin code. After clicking yes, we get the following code.
class UserDto {
var id: Int? = null
var name: String? = null
var email: String? = null
}
The piece of Kotlin code is 25 lines shorter compared to the Java code. All the accessor methods can be omitted by the syntax of Kotlin.
To be able to instantiate a fully initialized copy of the earlier defined Java object we would need to add a constructor. If we would make one for all our arguments, it would look as follows:
public class UserDto {
public UserDto(
: Integer,
id: String,
name: String
email) {
this.id = id;
this.name = name;
this.email = email;
}
...
}
Adding the constructor in Java would add more code. if we add more properties to the class, we would have to update the constructor as well.
Meanwhile, on the Kotlin side, we can update our class as follows to add a constructor.
class UserDto(
var id: Int? = null,
var name: String? = null,
var email: String? = null
)
By replacing the braces with parentheses, we added a constructor to the class, without adding a line of code. Both constructors are called the same way in both Java and Kotlin code.
Commonly, Java classes use some boilerplate methods as well. This
would add more code to our UserDto
class.
public class UserDto {
...
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
= (UserDto) o;
UserDto userDto return id.equals(userDto.id) && name.equals(userDto.name) && email.equals(userDto.email);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
@Override
public String toString() {
return "UserDto{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}
Similar to the constructor, when updating our class, we would need to update these methods as well.
Kotlin again provides a solution for this with only one line change.
Kotlin introduces data classes. Data classes define a handful of extra
methods for us (equals
, hashCode
,
toString
and copy
). If we would use a data
class to give us these methods, our class will look as follows.
data class UserDto(
var id: Int? = null,
var name: String? = null,
var email: String? = null
)
But all of this code can be generated by any modern IDE. Why would I use Kotlin for this?
The resulting Java version has a whopping 63 lines of code, and 63 lines of maintenance. Meanwhile, the Kotlin version has only 5 lines. Leading to a codebase that’s easier to navigate, has fewer surprises and is easier to maintain.
I took a quick look at a relatively small project and found 35 similar classes to the freshly refactored class. I’d estimate that using Kotlin in this project would save 4000 lines of code and just POJO’s.
The following section will refactor a function from a Java-looking function, into a Kotlin function, using features Kotlin provides us to write safe and more expressive code.
First, we define a basic tree. A tree is either a branch with
multiple Tree
items or a leaf with one value in it. Here is
the Java version:
interface Tree<T> {
}
class Branch<T> implements Tree<T> {
public Branch(List<Tree<T>> nodes) {
this.nodes = nodes;
}
List<Tree<T>> nodes;
public List<Tree<T>> getNodes() {
return nodes;
}
}
class Leaf<T> implements Tree<T> {
public Leaf(T value) {
this.value = value;
}
;
T value
public T getValue() {
return value;
}
}
In Kotlin it would look as follows:
interface Tree<T>
class Branch<T>(val nodes: List<Tree<T>>) : Tree<T>
class Leaf<T>(val value: T) : Tree<T>
Next, we write a function to refactor into Kotlin. For this example, I made a function to sum all numbers in the tree. In Java you could make it like this:
class TreeUtil {
static Integer sumTree(Tree<Integer> tree) {
Integer count;
if (tree instanceof Branch) {
var branch = (Branch<Integer>) tree;
= 0;
count for (var node : branch.getNodes()) {
+= sumTree(node);
count }
} else if (tree instanceof Leaf) {
var leaf = (Leaf<Integer>) tree;
= leaf.getValue();
count } else {
throw new IllegalArgumentException("Unknown variant");
}
return count;
}
}
Directly translating this to Kotlin gives me the following:
fun sumTree(tree: Tree<Int>): Int {
var count: Int
if (tree is Branch) {
val branch = tree as Branch
= 0
count for (node in branch.nodes) {
+= sumTree(node)
count }
} else if (tree is Leaf) {
val leaf = tree as Leaf
= leaf.value
count } else {
throw IllegalArgumentException("Unknown variant")
}
return count
}
Now let’s apply some of Kotlin’s features to make this function more readable and expressive.
A common pattern in Java is to define a variable set to null and update it during an if statement. In Kotlin, if statements are expressions, this allows us to return values from the if statement instead of mutating it during the if statement. Allowing us to refactor the method as follows.
fun sumTree(tree: Tree<Int>): Int = if (tree is Branch) {
val branch = tree as Branch
var count = 0
for (node in branch.nodes) {
+= sumTree(node)
count }
count} else if (tree is Leaf) {
val leaf = tree as Leaf
.value
leaf} else {
throw IllegalArgumentException("Unknown variant")
}
Instead of returning a value, we can return the if statement as a whole, making for less noise code.
Type casting an object in Java requires two steps. First, a check needs to be made if the type matches the expected one, then the value needs to be cast into another type. Kotlin introduces smart casting here. After checking the type, the compiler already knows the actual type, so here we could omit the cast. When using smart casting, our code would look like this.
fun sumTree(tree: Tree<Int>): Int = if (tree is Branch) {
var count = 0
for (node in tree.nodes) {
+= sumTree(node)
count }
count} else if (tree is Leaf) {
.value
tree} else {
throw IllegalArgumentException("Unknown variant")
}
After calling the type check, tree
becomes either a
Branch
or a Leaf
implicitly, allowing us to
remove the unsafe typecast and directly access the properties from the
tree
variable.
Kotlin allows us to pattern match for a type, then we could check the type without adding cases to an if statement, refactoring our code to the following.
fun sumTree(tree: Tree<Int>): Int = when (tree) {
is Branch -> {
var count = 0
for (node in tree.nodes) {
+= sumTree(node)
count }
count}
is Leaf -> tree.value
else -> throw IllegalArgumentException("Unknown variant")
}
The when
statement in Kotlin is similar to the
switch
in Java, but it provides some superpowers compared
to switch
, primarily that we can define a condition for
each branch instead of only checking equality.
Kotlin introduces sealed classes, all implementors of a sealed class must be in the same module as the sealed class itself. We can make our tree a sealed class in the following manner:
sealed interface Tree<T>
Now the compiler knows all possible types a tree could be, allowing us to safely remove the else clause from the when statement.
fun sumTree(tree: Tree<Int>): Int = when (tree) {
is Branch -> {
var count = 0
for (node in tree.nodes) {
+= sumTree(node)
count }
count}
is Leaf -> tree.value
}
If we would remove the sealed modifier, the program will not compile
because the pattern match is not exhaustive. Because only two versions
of Tree
are possible, we only need to check for those
types.
To clean up the last part of the Java code, we could rewrite the iteration of all nodes in the branch into something more readable.
fun sumTree(tree: Tree<Int>): Int = when (tree) {
is Branch -> tree.nodes.sumOf { node -> sumTree(node) }
is Leaf -> tree.value
}
The sumOf
method is a standard library method turning
iterable into an int. If we would write it ourselves it could look like
this:
fun <T> Iterable<T>.sumOf(operation: (T) -> Int): Int {
var sum = 0
for (item in this) {
+= operation(item)
sum }
return sum
}
A few things are happening here:
Iterable
is not in our class, we can write functions for
it. This is not possible in Java at all.operation
is a function that expects type
T
and returns a Int
.Using the tools that Kotlin provides, the Java-like function is rewritten into 4 lines of expressive Kotlin code.
Next, some other features of Kotlin.
One of the primary selling points for Kotlin is compile-time null safety. For a variable to be null, it needs to be defined as nullable. Nullable variables need to be handled or checked. This is one of the primary defects in Java systems.
In Kotlin, a parameter can be assigned as nullable as follows:
fun sumTwoNumbers(n1: Int?, n2: Int?): Int {
return n1 + n2 // Will not compile because n1 or n2 could be null
}
This example will not compile. Both n1
and
n2
could be null. To make it compile, we need to add some
checks or have different behavior for null values. Using smart-casting,
we can safely add the values.
fun sumTwoNumbers(n1: Int?, n2: Int?): Int {
if (n1 == null || n2 == null) throw Exception("Invalid value passed")
return n1 + n2
}
Kotlin was designed to be fully cross-compatible with JVM Java. This
allows us to define logic or structures in either Kotlin or Java code,
and call them from both Kotlin and Java code. I’ll take the earlier
created UserDto
as an example.
data class UserDto(var id: Int, var name: String, var email: String)
With Gradle or Maven properly configured, we can call this code the Java side in an idiomatically correct manner. It would look at follows:
void main() {
var user = new UserDto(1, "Cloudflight", "info@cloudflight.io");
System.out.println(user.toString()); // UserDto(id=1, name=Cloudflight, email=info@cloudflight.io)
System.out.println(user.getId()); // 1
}
Java includes a final
keyword, which disallows a
variable from being reassigned. A small example is shown below.
void main() {
var someMutableValue = "The first value";
= "The value is updated"
someMutableValue
final var someImmutableValue = "The first value"
= "Will not compile now"
someImmutableValue }
The final
keyword does add noise to the code. Kotlin has
a much more subtile approach to this.
fun main() {
var someMutableValue = "The first value";
= "The value is updated"
someMutableValue
val someImmutableValue = "The first value"
= "Will not compile now"
someImmutableValue }
Kotlin has two keywords for defining variables, val
and
var
. val
is used for immutable variables,
while var
is for mutable variables.
This pattern can also be found in the Kotlin standard library.
Due to historical reasons, most collections in Java have a method
named add
, which adds an item to the collection. This
includes immutable lists. These usually throw an
UnsupportedOperationException
at runtime when called.
void main() {
var list = new ArrayList();
.add(1);
list.add(2);
list
var immutableList = List.of();
.add(1); // Throws an UnsupportedOperationException at runtime
immutableList}
In Kotlin, collections are immutable by default. To create a mutable
list mutableListOf
should be called, and to create an
immutable list, listOf
is called. An immutable list has no
to add items to a collection, this could prevent runtime defects.
fun main() {
var list = mutableListOf()
.add(1)
list.add(2)
list
var immutableList = listOf()
.add(1) // Will not compile here
immutableList}
Kotlin introduces a new visibility keyword, internal
.
This allows code to be visible in the following cases:
kotlinc
invocation.This helps with modularizing codebases when working with separate modules.
There are a few points commonly made on why switching to Kotlin is a bad idea. This section explains why these points are not as strong as commonly thought of.
According to the StackOverflow developer survey 2022, 9.2% of developers work with Kotlin compared to 33.27% who work with Java. The survey also finds that at least 10% of these developers want to work with Kotlin instead of Java.
Turning a Java developer into a Kotlin developer is easy. Writing Kotlin compared to writing Java is very similar. Both are usually run in the same environment as well. Both for development and execution. Together with the better code safety of Kotlin, a Java developer can quickly write good Kotlin code.
At I/O 2019, Google announced that Kotlin is going to be the preferred programming language for all Android apps. This shows that Google is certain Kotlin will stay and Kotlin stays for a long time to come.
According to Google, 80% of the top 1000 android apps use Kotlin as a programming language.
So, a few of the reasons why Cloudflight (and other companies) prefer to use Kotlin over Java for server-side app development.
© 2024 Nils de Groot