Kotlin - The Good, the Bad and the Ugly

Martin Häusler - Sep 21 '18 - - Dev Community

Hi folks!

This is an article I've wanted to write for quite some time now. I've seen my fair share of Kotlin and also used it in production. Kotlin is a JVM-based language developed by Jetbrains, known for their IDEs and developer tools. Kotlin is very interesting from a language perspective, it has many merits, but also some pitfalls and awkward design choices. Let's get right to it!

The Good

I could fill entire books about the things I like about Kotlin. Here is a run-down of higlights.

Semicolons are optional

Not "optional" as in "well most of the time it works without them, just sometimes it makes a difference" but as in "use them or drop them, the program won't change". Less clutter in code, and my wrist is really happy about this.

NULL is a separate type

String (non-null String) and String? (nullable String) are two distinct types in Kotlin. If you stick to Kotlin methodology, your programs are unable to throw the dreaded NullPointerExceptions at you.

Extension functions

Did you ever want a null-safe variant of myStringVariable.isNullOrEmpty() in Java? Well, Kotlin has you covered! You can write static methods (with certain signatures) and call them as if they were regular member methods. This also marks the death of the dreaded StringUtils class full of static methods. All of these can be extension methods now!

Flow Typing & Type Inference

For all local variables, you just use var and the compiler will figure out the type on its own. Java 10 has that too, but with Kotlin's stronger type system it's even more valuable. Kotlin also does not require a cast operator. If you check that an object is instanceOf some class, then inside that if block, the variable will be treated as such, without you having to downcast it manually. Kotlin calls it "smart casts".

Final made easy

In Java, the final modifier is required to make a local-variable non-assignable. That's a very useful property, because it eliminates a large group of potential errors. However, final String myString is a mouth full. Hardly anybody wants to deal with this. In Kotlin, the character count for final and non-final variables is exactly the same: var myString for mutable and val myString for immutable variables.

Smart standard library

The Kotlin standard library is a treasure trove full of pleasant surprises. Did it ever bother you that there is no elegant way to use a lock in Java with try-with-resources? Well, in Kotlin you have lock.withLock{ doWork() }. It's a small thing, but it really helps. How often did you forget to free a lock in a finally clause? Or unlocked the read lock instead of the write lock? The Kotlin standard library is full of such little helpers.

Here's another one: mutableListOf<T>(T... elments). The Kotlin compiler is smart enough to figure out the T if you provide the elements. So the calls to this method will have one of two shapes:

// empty list, need to specify the type argument explicitly
val myList = mutableListOf<String>()

// pre-filled list, no need for type argument
val yourList = mutableListOf("Hello", "World"!)
Enter fullscreen mode Exit fullscreen mode

Can we stop here for a moment and appreciate how this code reads like prose in both cases? Also, Kotlin distinguishes between mutable and read-only collections, while still remaining compatible with Java. That's quite a feat in and on itself and makes Kotlin code a lot more robust against subtle bugs (hey, who modified this collection!?).

Streams, streamlined

If you want to filter a List in Java, you need to do this:

List<String> newList = oldList.stream()
    .filter( s -> s.startsWith("a") )
    .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

That's quite a lot of text if all we really wanted to do was:

List<String> newList = oldList.filter(s -> s.startsWith("a"));
Enter fullscreen mode Exit fullscreen mode

Then where's the point of using stream()? By surrounding your filters (and map operations and...) with stream() and collect(...), Java streams can be lazy. However, you might argue that there is no point in lazyness if all you want to do is one filter. And you are correct, for a single filter operation, a stream is overkill, both syntactically as well as conceptionally. But in Java, List#filter(...) simply does not exist (you can use a static utility class, but come on, that's not the same thing).

Kotlin solves this problem in a very elegant way:

// single filter, quick and easy
val newList = oldList.filter { it.startsWith("a") }

// streams for multiple filters (same syntax!)
val newList = oldList.asSequence().filter { it.startsWith("a") }.toList()
Enter fullscreen mode Exit fullscreen mode

So, Kotlin gives you both stream-based lazy chains (called Sequences) as well as one-shot operations. The best thing about it is that the syntax for filter on a list is exactly the same as for a filter on a sequence. If you work with existing code and realize that you need another filter, simply insert asSequence() before and toList() after to make it lazy, and leave the existing filter in place.

Long live the Iterator

Iterators are a very basic and powerful concept. They exist in Java since version 1.0 (which says a lot). However, Iterators are treated in a rather stepmotherly fashion in Java. You can't do a for loop over an iterator in the same way as you can over a Collection. Collection#addAll accepts only other Collections as argument, not Iterables. Hell, you can't even do iterator.stream(). I never understood why.

Kotlin fixes all of the aforementioned problems. Dealing with iterators in Kotlin is just as pleasant as dealing with collections, without losing the benefits of iterators:

for(element in iterator) { ... } /* works! */

iterator.asSequence().filter { ... } /* works! */

myList.addAll(iterable) /* works! */

Enter fullscreen mode Exit fullscreen mode

Java Interop

... is really good. As in "Really REALLY good". You can mix Kotlin and Java sources (and binaries!) in the same project without issues. There are some pitfalls though; we will cover more on that later.

Many Platforms, one Language

Kotlin translates to JVM byte code, as well as JavaScript. Native implementations are also in the works. I think it goes without saying that this is a huge deal.

The Bad & The Ugly

For all its merits, Kotlin also have some downsides. They are not really breaking, but noteworthy nontheless.

No static modifier

In Kotlin, there is no static keyword. This causes some annoyance in case you just want to define that one utility method. What you have to do instead is:

class MyClass {
   companion object {
       @JvmStatic
       fun myStaticMethod(/*...*/) { /*...*/ }
   }
}

Enter fullscreen mode Exit fullscreen mode

This is ugly. In Kotlin's defense, there's const for constants (which is roughly the same as public static final members in Java), and you can declare top-level functions outside any class (which effectively makes them static). But if you really want a static method, the way outlined above is the only option (as far as I know).

The companion object was originally introduced to ease the construction of singletons. A companion object is a singleton instance of the class it resides in. I feel that this was a bad decision. Firstly because static methods are useful and should not require that much syntax, and secondly because the Singleton pattern isn't so verbose as to warrant this shorthand, and also isn't that useful to deserve such a prominent langauge feature.

The open keyword

All classes in Kotlin by default are final. Let that sink in for a bit.

class MyClass {

}

class SubClass : MyClass { /* NOPE: compile error! MyClass is final! */

}
Enter fullscreen mode Exit fullscreen mode

This is only the tip of the iceberg. Imagine frameworks such as Spring or Hibernate, which generate bytecode at runtime that extends your class. Nope, can't do that - the class is final. Within the bink of an eye, all of your Kotlin classes have suddenly become incompatible with pretty much any commonly used framework. There are two ways to circumvent the issue:

  • Either declare your class as open class MyClass or
  • Add a compiler plug-in to the Kotlin compiler.

Neither option is very pretty. The reasoning of the Kotlin language developers is that extensibility should be an opt-in thing. Only if your class is intended to be extended, it should be able to be extended. In my opinion, this reasoning is flawed: defensive programming best-practices will tell you that you should prepare your code to work in the most unexpected of environments. If you write a class that potentially breaks when it is subclassed, you wrote a bad class. Kotlin gives the utterly wrong incentive here, by simply sealing off everything.

Constructor Galore

Kotlin as a langauge has a really odd fetish for constructors. Have a look:

// primary constructor
class PersonA(val firstName: String, val lastName: String) { }

// regular constructor
class PersonB {

    val firstName: String
    val lastName: String

    constructor(firstName: String, lastName: String){
        this.firstName = firstName
        this.lastName = lastName
    }

}
Enter fullscreen mode Exit fullscreen mode

Both examples above do the exactly same thing. You may argue that the variant with the "primary constructor" is much shorter, thus better. I disagree, for the following reasons:

  • The primary constructor must always list (and assign) all fields. This is super annoying when you require an additional transient field for local caching purposes.
  • The class declaration line gets exceedingly long.
  • If you extend from a base class which has a primary constructor, you...
    • ... must declare a primary constructor yourself
    • ... must call the primary constructor of the base class
    • ... must do all of this in the class declaration

In summary, once you are locked into the primary constructor workflow, you can't break free anymore. Your class declarations quickly grow to enormous sizes. Consider this:

class Student(val firstName: String, val lastName: String, val studentId: String) : Person(firstName,lastName) {

}

Enter fullscreen mode Exit fullscreen mode

Of course, you get a lot of mileage out of such a line. It declares a subclass, declares the fields, declares the constructor, states the base class and finally states how inherited fields should be filled. That's a very high information density right there. I still don't like it. It's too much information packed together, and can be really difficult to parse for a human. I do not recommend the usage of primary constructors (except for data classes where you have no choice), but sometimes you are forced to use them because of the classes in the standard library.

Data Classes - a brilliant concept with half baked execution

Data classes in and on themselves are a brilliant idea. Have a look:

data class Person(val firstName: String, val lastName: String)
Enter fullscreen mode Exit fullscreen mode

You specify the fields of a class and their types, and you get:

  • the specified fields
  • getter/setters
  • a constructor which does exactly what you would expect
  • hashCode
  • equals
  • toString
  • clone
  • various utilities

... all for free, without writing them. That means your hashCode() will never go out of sync with your equals(). Your toString() will never forget to print a field you just recently added to the class. All of this is possible because there is no text representation for these things; the compiler just generates the bytecode directly. It's a really cool concept and vastly superior to generating all of the aforementioned things via an IDE (because the generated code can get out of sync easily).

The problem is that it wasn't thought through until the end. The main issue is that you cannot use inheritance when writing data classes. Data classes cannot extend any base class, and are final themselves (nope, you can't use open here either). The reasoning behind the decision was that things might break (in particular in the clone utilities) when you try to introduce inheritance. In my opinion, this is total rubbish. UML and Ecore have shown how to correctly have data classes with multiple inheritance and cloning utilities.

The lack of inheritance options drastically limits the applicability of data classes. Even simple Data Transfer Objects (DTOs) or JPA entities (prime examples where data classes would be ever so useful!) often require inheritance.

Abundance of Keywords

Kotlin has a very large set of Keywords and Operators. Kotlin (like C#) has the notion of "Soft Keywords", meaning that a certain word is only considered a keyword in certain contexts, but can be used as an identifier (e.g. a variable name) in all other places in the code. Some of them are quite... arcane and awfully specific, such as crossinline. Regardless, this makes the language quite difficult to learn, and even more difficult to master, as you have to know all of these keywords. In regular usage, you don't need more than you would need in Java. But if you want to read and understand the library functions, you will encounter them quite often. I personally think that a lot of these keywords would have been better expressed by annotations (e.g. @Actual instead of the modifier actual).

Verdict

I absolutely recommend any Java programmer to at least have a closer look at Kotlin. In very many areas it is a far superior language than Java. It carries over all of the Java infrastructure and libraries, and adds a lot of smart design choices on top. It has a better type system for enhanced safety and compile-time error checking. The fact alone that it is null-safe by default should be enough to justify the use of Kotlin over Java. However, it isn't perfect. Kotlin makes it easier to write code that is short and concise, but it can also be used to produce a mess of seemingly random character combinations that nobody can decipher anymore after two days. However, in spite of its shortcomings, weaknesses and the occasional strange design choice, it is an excellent language.

Do you have any experiences to share with this language? Do you use it in production, and are you happy with it? Leave a comment below.

. . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player