9 Java Patterns that Kotlin made obsolete

December 4, 2021

After the Kotlin 2021 Premier Online Event taking place just recently and the book "Java to Kotlin: A Refactoring Guidebook" by Duncan McGregor and Nat Pryce coming out, I thought it was a good time to write about Java patterns and how Kotlin can help you implement them or can even make them obsolete!

For all the Java gurus asking themselves: "why even bother with Kotlin when I have Java?" Switching from Java to Kotlin is not just a different syntax and null-safety (which by itself would be the killer argument to switch) but also a different kind of programming. Stay tuned, read along, and find out why Kotlin is not just less, more concise, and idiomatic code, but also more fun to write :)

Note that this article is aimed at beginners with little Kotlin experience. I assume you understand basic Kotlin syntax and basic principles like nullable types. However, even if you write Kotlin daily, there still might be some things you did not yet know. The patterns will start simple but become more complex further in the article so make sure you check out all the sections.


Overloading methods/cascading constructors

Most of you already know this, but it is an especially cumbersome Java pattern that Kotlin solved brilliantly, so I quickly want to gloss over it. While function overloading is still possible in Kotlin, it can be considered a code smell because it's way easier to define functions with parameter default values. So instead of this Java code:

void log(String message) { log(message, LogLevel.INFO); }
void log(String message, LogLevel level) { log(message, level); }
void log(String message, LogLevel level, Object... args) {}

You can write a single line of Kotlin:

fun log(message: String, level: LogLevel = LogLevel.INFO, vararg args: Any) {}

This example is kept simple and short for the sake of brevity but imagine a more sophisticated example with multiple different parameters.

Note that you can also provide arguments in any number and order you like with named arguments. The following example would not be possible in Java, because the method signature would not differ and thus method overloading has reached its limits. But Kotlin hasn't:

fun impossibleForJava(
    who: String = "Sebastian",
    favouriteLang: String = "Kotlin"
) { /* your code goes here... */ }

impossibleForJava("Roman")
impossibleForJava(favouriteLang = "Not Java")
impossibleForJava(who = "Everyone!")

This works with constructors too. In case you need interoperability with Java, you can add the @JvmOverloads annotation to the method signature which instructs the compiler to generate overloads.

Utils

We have all seen them: Utility classes. A private constructor to avoid initialization and static methods are usually what identifies them. In my opinion, they are a prime example of how Java relies too heavily on its object-oriented paradigm and thus requires us to write more code than we need to. They are only classes because you have no way of writing code outside a class scope. Kotlin doesn't have this limitation and lets you define whatever you want in a file as a top-level declaration. This means that you can have global functions that are not bound to any class or object. A great example of how this can simplify your code is reading and writing from/to standard IO. These two code snippets are equivalent in semantics but one is significantly less complicated and shorter:

// Java
final var who = new BufferedReader(
    new InputStreamReader(System.in)
).readLine();
System.out.println("Hello %s!".formatted(who));
// Kotlin
val who = readLine()
println("Hello $who!")

While that by itself is already a great improvement, Kotlin doesn't stop there. It provides us with a neat feature that helps us write functions for specific objects without having to clutter everything into one class. It also allows us to extend existing classes, that we might not even have control over. Introducing: receivers objects…

class Person(
    val name: String,
    val age: Int
)

fun Person.greet() {
    println("Hi, my name is $name")
}

val mike = Person("Mike", 17)
mike.greet()

In the example above, greet is an extension function with the receiver type Person. As you can see, you can call greet on any object of type Person with the only condition that you import the extension function (if necessary). You can also write extension functions on library types or even built-in types like String or Int:

// Java
public static boolean isEven(int i) {
    return i % 2 == 0;
}
NumberUtils.isEven(2);
// Kotlin
fun Int.isEven() = this % 2 == 0
2.isEven()

// or even as an extension property
val Int.isEven: Boolean
    get() = this % 2 == 0
2.isEven

In my opinion, the Kotlin-way looks more object-oriented as you can call the util function like a method whereas in Java you require to put the method in a different class. Naturally, Kotlin provides a lot of useful extension functions for various types in its standard library. Here are some of my favourites:

// collections have lots of extension functions!
val myList = listOf(1, 2, 3)
myList.sum() // 6
myList.filter { it > 1 } // [2, 3] filtering without streaming!
myList.map { it * 2 } // [2, 4, 6] and mapping too!
myList.any { it > 2 } // true
val first = myList.first() // 1

// and strings have extension functions too!
"1".toInt() // 1, easy conversion, when you know it's a valid int
"not gonna work".toIntOrNull() // null, also easy and you don't have to catch exceptions!

It is considered to be a best practice to keep classes minimal and extend them with extension functions where needed. Note that extension function do not have access to private properties. That is reserved for the class internals only.

Singleton

Sometimes you need objects to be singletons. In Java, util or factory classes are usually singletons. I have covered the "kotlinic" way of providing utils above, and we will look at factories later on. For now, let's check out Java singletons and their Kotlin equivalents.

The standard implementation of a Java singleton looks like this:

class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
    
    public void myInstanceMethod() {
        // whatever...
    }
}

Fairly simple, fairly standard. IntelliJ can even generate most of this code when it recognizes the word "instance" as a property name. While not all too bad, most of this code is boilerplate. And we don't want that in our app. So naturally, Kotlin provides a default solution for this: objects

object Singleton {
    fun myInstanceMethod() {
        // whatever...
    }
}

Kotlin objects can be used as anonymous classes (similar to its Java counterpart) as well as top-level declarations making them perfect for singletons. They are initialized thread-safe upon first access.

Speaking of initialization…

Lazy Initialization

Whilst lazy initialization is not a design pattern in itself, it is often used in combination with singletons and/or the adapter pattern when requiring exactly one instance of an object. To ensure that behaviour in a concurrent Java program, we have to mark our method with the synchronized keyword.

An example in Java would be:

class AnotherService {
    public void getSomeData() {
        // whatever...
    }
}

class MyService {

    private AnotherService service = null;

    private synchronized AnotherService getService() {
        if (service == null) {
            service = new AnotherService();
        }
        return service;
    }
}

I will not go into detail about how synchronized or double-checked locking works (if you are interested, take a look here). It is obvious that this is important for the safety of multithreaded execution, but it can also be confusing, especially to juniors.

Of course, Kotlin provides an easier, default way of doing this for us:

class AnotherService {
    fun getSomeData() {
        // whatever...
    }
}

class MyService {
    private val service: AnotherService by lazy { AnotherService() }
}

And that's it. The lazy property delegate (if you don't know about them, read: Delegated properties) ensures two things for us:

  1. service is lazily instantiated upon its first access
  2. the lambda passed to lazy is only executed once, even in concurrent contexts

This simple delegate makes any lazy initialization thread-safe and easy to read at the same time.

Builder

Builders provide a simpler way to construct complex objects. They often utilize method chaining to reduce the lines of code as much as possible. Take a look at this builder:

public class Person {

    private final String name;
    private final String firstname;
    private final int year;

    public Person(final String name, final String firstname, final int year) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name is null or blank");
        if (firstname == null || firstname.isBlank()) throw new IllegalArgumentException("Firstname is null or blank");
        if (year <= 1900 || year >= 2100) throw new IllegalArgumentException("Invalid year");
        this.name = name;
        this.firstname = firstname;
        this.year = year;
    }

    static class Builder {

        private String name;
        private String firstname;
        private int year = 1940;

        public Builder withName(final String name) {
            this.name = name;
            return this;
        }

        public Builder withFirstname(final String firstname) {
            this.firstname = firstname;
            return this;
        }

        public Builder withYear(final int year){
            this.year = year;
            return this;
        }

        public Person build() {
            return new Person(
                    this.name,
                    this.firstname,
                    this.year
            );
        }
    }

    public static void main(String[] args) {
        final var person = new Builder()
                .withName("Wayne")
                .withFirstname("Bruce")
                .build();
    }
}

Notice that this duplicates the entire class as you need to have every property in your builder as well as in your actual class. However, we solve the cascading constructor issue because we can have default values in the builder, and we provide a simpler way of initialization for users of the class. We can also provide different builders for different use cases. What we cannot do in the builder is the validation of data because that is the responsibility of the class itself.

As we have already seen, Kotlin solves the problem of cascading constructors with default values and named arguments. So the same example would look like this utilizing the powerful data classes that Kotlin provides (note that Java records provide similar features but are not yet as powerful as data classes):

data class Person(
    val name: String,
    val firstname: String,
    val year: Int = 1940
) {
    init {
        require(name.isNotBlank()) { "Name is blank" }
        require(firstname.isBlank()) { "Firstname is blank" }
        require(year in 1901..2099) { "Invalid year" }
    }
}

val person = Person(name = "Wayne", firstname = "Bruce")

So simple builders are not required in Kotlin as the default classes have you covered. The usage of require is noteworthy here as you might not know about it. The function takes a predicate and in case it's false, throws an IllegalArgumentException with the message returned by the lambda. check works equivalent but throws an IllegalStateException.

But what if you have a Java class, maybe from a third-party library or existing legacy code, that you want to instantiate? One option is to create a builder function that wraps the object creation and provides default parameters (and solves potential nullability issues):

fun personOf(
    name: String,
    firstname: String,
    year: Int = 1940
) = Person(name, firstname, year)

Builders by convention are often named myClassOf(args...) like listOf or mapOf or simply myClass(args...) like coroutineScope from the standard library. Note that in case the Java class does not provide a constructor and only setters, you can utilize the built-in apply function:

fun personOf(
    name: String,
    firstname: String,
    year: Int = 1940
) = Person().apply {
    this.name = name
    this.firstname = firstname
    this.year = year
}

The object this function is called on is used as a receiver for the lambda block and thus exposes the object's properties in the this-context. So Kotlin either eliminates the need for builders or allows us to wrap existing classes safely with little to no code.

Factory

Another creational pattern with a different purpose of hiding the creation or even the internal implementation of an interface. This is a pattern that the Kotlin developers themselves use a lot in their own libraries. You might have used them before, without even realizing it. But as always, let's take a look at a Java example first:

public interface Notification {
    void notify(final String message);
}

public enum NotificationType {
    Email, SMS
}

public class NotificationFactory {

    private NotificationFactory() {}

    private static class EmailNotification implements Notification {
        public void notify(final String message) {}
    }

    private static class SmsNotification implements Notification {
        public void notify(final String message) {}
    }
    
    public static Notification createNotification(final NotificationType type) {
        return switch(type) {
            case Email -> new EmailNotification();
            case SMS -> new SmsNotification();
        };
    }
}

So we have an interface Notification and two different implementations of it depending on the type of notification that we want to send. The user of that interface doesn't care about the concrete implementations, so we want to reduce the complexity and hide them from him. In order to do that we need to put these classes inside the factory class because that is the only way we can have private classes in Java. Note that while we could put package private classes in the same file, they do not hide the implementation completely, since we can define any package anywhere we want. To have access to the classes we provide a factory method which creates the object and returns it. When you call that in your program it looks like this:

final var notification = NotificationFactory.createNotification(NotificationType.SMS);

This looks totally different when creating normal objects with new Notification(). It also clutters the NotificationFactory with many classes if you add more notification types.

Kotlin doesn't replace this pattern, but it makes it more idiomatic, which is one of the key principles of Kotlin:

interface Notification {
    fun notify(message: String)
}

private class SmsNotification: Notification {
    override fun notify(message: String) {}
}

private class EmailNotification: Notification {
    override fun notify(message: String) {}
}

enum class NotificationType {
    Email, SMS
}

fun Notification(type: NotificationType) = when(type){
    NotificationType.Email -> EmailNotification()
    NotificationType.SMS -> SmsNotification()
}

Firstly, you can see that Kotlin allows private top-level class definitions (which Java does not). In Kotlin, you can generally put as many things into a single file as you want and that is also recommended by the Coding Conventions as long as they somewhat belong together. It is kind of similar to Javascript modules where only exported members are available outside the file. Note that in Kotlin no visibility modifier means public, you have to explicitly state that something is private (which is not the case for Javascript modules). You can, however, enable explicit API mode which gives an error if you forget to set the visibility explicitly.

Secondly, you may thought that there was a typo or even a code smell when you read Notification as function name. It's not. The Coding Conventions explicitly state that factory function may be written in pascal case in order to have the same name as the abstract return type (in our case Notification). Why? Because when we "instantiate" our Notification object, we write:

val notification = Notification(NotificationType.Email)

That looks the same as if Notification was a class (in Kotlin we have no new keyword)! The user doesn't care about the implementation, so we can even hide the fact that Notification is an interface and not a class because, at the end, it's all the same to him. The Kotlin coroutines team does the same thing with some of its classes (e.g. Channel).

Factory, Builder or DSL?

The syntax and features of Kotlin allow us to write, or rather define, so-called domain specific languages. DSLs are languages created to solve a specific set of problems. Two examples from JetBrains themselves are kotlinx.html (which is available for JVM as well as JS by the way) and the Kotlin Gradle DSL. The former abstracts our beloved (or sometimes unbeloved) HTML tag-syntax to plain Kotlin code:

val name = "HTML DSL"
head {
    title {
        +name
    }
}
body {
    h1 {
        classes = setOf("main-title")
        +"Hello $name!"
    }
    a("#") {
        role = "button"
        +"Click me"
    }
}

Which would result in the following HTML:

<head>
<title>HTML DSL</title>
</head>
<body>
<h1 class="main-title">Hello HTML DSL!</h1>
<a href="#" role="button">Click me</a>
</body>

This is neat because we can create templates by just writing plain Kotlin code. Here an example taken from their docs:

fun UL.dropdown(block : LI.() -> Unit) {
    li("dropdown") {
        block()
    }
}

// ...

ul {
    dropdown {
        li { +"First item" }
        li { +"Second item" }
    }
}

The same is true for the Gradle DSL, which lets us define our Gradle projects in Kotlin instead of Groovy (which, let's be honest, no one really understands anyway).

But why?

Why should you care? Even though you might not develop a new DSL by yourself (although it's fun) you can use parts of it to provide a more humanly-readable API. Even if it's just a simple builder or method call, it can improve the developer experience significantly. An example of this is the built-in to function which is defined on any type (or Any, the Object equivalent of Kotlin) and wraps the left- and right-hand side of it into a Pair of values:

val pair = "first" to "second" // = Pair("first", "second")

This is useful if we want to build a map:

val myMap = mapOf(
    "A" to "B",
    "C" to "D"
)

This is significantly more humanly-readable than its Java-equivalent:

final var myMap = Map.ofEntries(
    Map.entry("A", "B"),
    Map.entry("C", "D")
);

But how?

The nice thing about this is that none of this requires any additional library, tool, parser or quirks except the built-in Kotlin features. I will now show you how you can create your own DSL-like parts.

They key to all these are extension functions, which we have seen above. They provide the foundation for all DSL-like structures as they provide a context for the other objects. Let's look how the to function might be implemented (disregarding generics):

infix fun Any.to(that: Any) = Pair(this, that)
val pair = "key" to "value"

As you can see, we take the receiver (accessible with this) and a parameter that which we then have access to in the function block. We also add the infix modifier to the function which allows us to write "key" to "value" instead of having to write "key".to("value").

Extension functions are limited by their scope just like any other function, which means that if we define an extension function within a class, it will only be visible within that scope. With this we can structure our DSL and provide context to our functions.

One more thing...

You now know the basics to write your own simple DSLs. Except there is one more tiny thing that you might have spotted and wondered what it was about. Did you catch it? What's that plus in front of these strings doing there in the HTML example?

ul {
    dropdown {
        li { +"First item" }
        li { +"Second item" }
    }
}

Kotlin allows us to define operator functions which overload operators such as +,- or even []. It enables us for example to access a list item by writing myList[1] or to add an element with myList += 2.

So the +"First item" is the unaryPlus operator function in the LI class scope:

class LI {
    var innerText = ""
    
    operator fun String.unaryPlus() {
        innerText += this
    }
}

These operator functions are also useful outside DSLs. Especially the index access and comparison operators.

Strategy

Neither Java nor Kotlin provides multiple inheritance as C++ does. If developers want to "inherit" features of different classes whilst also avoiding code duplication, they often make use of the strategy pattern. Let's start with my favourite example: the ducks!

abstract class Duck {
    abstract void fly();
    abstract void swim();
    abstract void walk();
}

class MallardDuck extends Duck{
    void fly() { System.out.println("I fly"); }
    void swim() { System.out.println("I swim"); }
    void walk() { System.out.println("I walk"); }
}

class DonaldDuck extends Duck{
    void fly() { System.out.println("I can't fly");  }
    void swim() { System.out.println("I swim"); }
    void walk() { System.out.println("I walk"); }
}

As you can see, we have two types of ducks. One that can fly and one that can't. So we implement the Duck class twice. However, we have a problem when we add the RubberDuck because it can't fly or walk. All it can do is swim (or more like float but that somewhat counts). We don't want to implement the entire behaviour again because it has shared behaviour with the Donald ducks and that would mean duplicating the entire fly method. Also notice that all methods here are similar so we could improve that as well. Let's use the strategy pattern (reduced to the fly method for simplicity):

interface Flying { void fly(); }

class FlyBehaviour implements Flying {
    public void fly() { System.out.println("I fly"); }
}

class NoFlyBehaviour implements Flying {
    public void fly() { System.out.println("I can't fly"); }
}

abstract class Duck implements Flying {}

class MallardDuck extends Duck{
    final Flying flying = new FlyBehaviour();
    public void fly() { flying.fly(); }
}

class DonaldDuck extends Duck{
    final Flying flying = new NoFlyBehaviour();
    public void fly() { flying.fly();  }
}

class RubberDuck extends Duck{
    final Flying flying = new NoFlyBehaviour();
    public void fly() { flying.fly();  }
}

Oof. This code just got long (imagine we added Swimming and Walking too).

So we extracted all the shared code into separate classes called "Behaviours". Our duck instances then delegate all behaviour to these classes. Like this we could also easily parameterize the behaviour by passing arguments to them. Note that in this case we insist that all ducks (all instances of Duck) implement all behaviours. However, we still have to implement the method in the duck classes and then delegate to the behaviour. It would be nice if we could avoid that useless line of code. And it turns out, with Kotlin we can:

interface Flying { fun fly() }

class FlyBehaviour : Flying {
    override fun fly() { println("I fly") }
}

class NoFlyBehaviour : Flying {
    override fun fly() { println("I can't fly") }
}

abstract class Duck : Flying

class MallardDuck : Duck(), Flying by FlyBehaviour()
class DonaldDuck : Duck(), Flying by NoFlyBehaviour()
class RubberDuck : Duck(), Flying by NoFlyBehaviour()

val donald = DonaldDuck()
donald.fly() // I can't fly

We delegate the interface implementation to an object which in this case we instantiate upon creation. This can also be a parameter we receive in the constructor:

class GenericDuck(flying: Flying): Duck(), Flying by flying

This reduces code duplication, avoids boilerplate code and strongly encourages composition over inheritance.

When designing modern software, we often want to avoid polluting the domain models with code that belongs into an infrastructure module. An example of this would be onion architecture which defines its models in the core with no dependency on any other module. Database access is then implemented in a separate infrastructure module. The benefit is that our domain logic does not depend on any framework or technology besides the language it's written in, and we can thus exchange any library easily.

However, many frameworks, like Spring, discourage such behaviour because it's so easy to create entities by simply adding an annotation to our classes or properties. We can circumvent that by either defining entities in an xml file or by creating separate entity classes and then mapping them onto the domain object. Both of these things can get cumbersome and are thus not ideal solutions.

With Kotlin, we can separate the declaration and implementation more easily as it allows us to define properties on interfaces. We can then implement the interface as an entity in an entirely different module and use all the annotations or infrastructure code we want:

// domain module
    interface Person {
    val name: String
    val firstname: String
    val year: Int
}

// infrastructure module
@Entity
class PersonEntity(
    @Column
    override val name: String,
    @Column
    override val firstname: String,
    @Column
    override val year: Int
) : Person

With this approach we avoid having to create two separate classes and reduce the domain models to interfaces, just like we would with services.

We should limit the number of entity properties to as few as possible. Only add the absolutely necessary properties to the entity and implement all further logic as extension functions or properties in the domain module:

val Person.age: Int
    get() = LocalDate.now().year - year

This ensures simplicity and a clean domain model while also staying extensible within the domain. It takes a little time to get used to this idea, but I have had success with this pattern in past projects.

Conclusion

These were those nine patterns that Kotlin approaches differently. I hope you could take something with you, even if you are already a Kotlin expert.

Kotlin requires but also encourages us to design our software differently. When starting out, this means that we have to learn things anew. Known patterns and principles that might have been best practice in other languages, aren't in Kotlin. The way of doing things is different. This demands us to have a more open mindset and makes us question existing patterns and methods. This is a good thing as it pushes ourselves and our code to the next level.

About the author: Sebastian Brunner

Software engineer with lots of enthusiasm for Kotlin and a passion for machine learning and AI.

Comments
Join us