Extension Methods in Kotlin

1. Introduction

Kotlin introduces the concept of Extension Methods – which are a handy way of extending existing classes with a new functionality without using inheritance or any forms of the Decorator pattern – after defining an extension. we can essentially use it – as it was the part of the original API.

This can be very useful in making our code easy to read and maintain, as we’re able to add methods that are specific to our needs and have them appear to be part of the original code, even when we don’t have access to the sources.

For example, we might need to perform XML escaping on a String. In standard Java code, we’d need to write a method that can perform this and call it:

String escaped = escapeStringForXml(input);

Whereas written in Kotlin, the snippet could be replaced with:

val escaped = input.escapeForXml()

Not only is this easier to read, but IDEs will be able to offer the method as an autocomplete option the same as if it was a standard method on the String class.

2. Standard Library Extension Methods

The Kotlin Standard Library comes with some extension methods out-of-the-box.

2.1. Context Adjusting Extension Methods

Some generic extensions exist and can be applied to all types in our application. These could be used to ensure that code is run in an appropriate context, and in some cases to ensure that a variable isn’t null.

It turns out that, most likely, we’re leveraging extensions without realizing this.

One of the most popular ones is possibly the let() method, which can be called on any type in Kotlin – let’s pass a function to it that will be executed on the initial value:

val name = "Baeldung"
val uppercase = name
  .let { n -> n.toUpperCase() }

It’s similar to the map() method from Optional or Stream classes – in this case, we pass a function representing an action that converts a given String into its upper-cased representation.

The variable name is known as the receiver of the call because it’s the variable that the extension method is acting upon.

This works great with the safe-call operator:

val name = maybeGetName()
val uppercase = name?.let { n -> n.toUpperCase() }

In this case, the block passed to let() is only evaluated if the variable name was non-null. This means that inside the block, the value n is guaranteed to be non-null. More on this here.

There are other alternatives to let() that can be useful as well though, depending on our needs.

The run() extension works the same as let(), but a receiver is provided as the this value inside the called block:

val name = "Baeldung"
val uppercase = name.run { toUpperCase() }

apply() works the same as run(), but it returns a receiver instead of returning the value from the provided block.

Let’s take advantage of apply() to chain related calls:

val languages = mutableListOf<String>()
languages.apply {
    add("Java")
    add("Kotlin")
    add("Groovy")
    add("Python")
}.apply {
    remove("Python")
}

Notice how our code becomes more concise and expressive not having to explicitly use this or it.

The also() extension works just like let(), but it returns the receiver in the same way that apply() does:

val languages = mutableListOf<String>()
languages.also { list ->
    list.add("Java")
    list.add("Kotlin")
    list.add("Groovy")
}

The takeIf() extension is provided with a predicate acting on the receiver, and if this predicate returns true then it returns the receiver or null otherwise – this works similarly to a combination of a common map() and filter() methods:

val language = getLanguageUsed()
val coolLanguage = language.takeIf { l -> l == "Kotlin" }

The takeUnless() extension is the same as takeIf() but with the reversed predicate logic.

val language = getLanguageUsed()
val oldLanguage = language.takeUnless { l -> l == "Kotlin" }

2.2. Extension Methods for Collections

Kotlin adds a large number of extension methods to the standard Java Collections which can make our code easier to work with.

These methods are located inside Collections.kt,_ Ranges.kt_, and Sequences.kt_, as well as Arrays.kt_ for equivalent methods to apply to Arrays instead. (Remember that, in Kotlin, Arrays can be treated the same as Collections)

There’re far too many of these extension methods to discuss here, so have a browse of these files to see what’s available.

In addition to Collections, Kotlin adds a significant number of extension methods to the String class – defined in Strings.kt_. These allow us to treat Strings as if they were collections of characters.__

All of these extension methods work together to allow us to write significantly cleaner, easier to maintain code regardless of the kind of collection we’re working with.

3. Writing Our Extension Methods

So, what if we need to extend a class with a new functionality – either from the Java or Kotlin Standard Library or from a dependent library that we’re using?

Extension methods are written as any other method, but the receiver class is provided as part of the function name, separated with the period.

For example:

fun String.escapeForXml() : String {
    ....
}

This will define a new function called escapeForXml as an extension to the String class, allowing us to call it as described above.

Inside this function, we can access the receiver using this, the same as if we had written this inside the String class itself:

fun String.escapeForXml() : String {
  return this
    .replace("&", "&")
    .replace("<", "<")
    .replace(">", ">")
}

3.1. Writing Generic Extension Methods

What if we want to write an extension method that is meant to be applied to multiple types, generically? We could just extend the Any type, – which is the equivalent of the Object class in Java – but there is a better way.

Extension methods can be applied to a generic receiver as well as a concrete one:

fun <T> T.concatAsString(b: T) : String {
    return this.toString() + b.toString()
}

This could be applied to any type that meets the generic requirements, and inside the function this value is typesafe.

For example, using the above example:

5.concatAsString(10) // compiles
"5".concatAsString("10") // compiles
5.concatAsString("10") // doesn't compile

3.2. Writing Infix Extension Methods

Infix methods are useful for writing DSL-style code, as they allow for methods to be called without the period or brackets:

infix fun Number.toPowerOf(exponent: Number): Double {
    return Math.pow(this.toDouble(), exponent.toDouble())
}

We can now call this the same as any other infix method:

3 toPowerOf 2 // 9
9 toPowerOf 0.5 // 3

3.3. Writing Operator Extension Methods

We could also write an operator method as an extension.

Operator methods are ones that allow us to take advantage of the operator shorthand instead of the full method name – e.g., the plus operator method might be called using the + operator:

operator fun List<Int>.times(by: Int): List<Int> {
    return this.map { it * by }
}

Again, this works the same as any other operator method:

listOf(1, 2, 3) * 4 // [4, 8, 12]

4. Calling Kotlin Extension Function from Java

Let’s now see how Java operates with Kotlin extension functions.

In general, every extension method we define in Kotlin is available for us to use in Java. We should remember, though, that the infix method still needs to be called with dot and parentheses. Same with operator extensions — we can’t use only the plus character (+). These facilities are only available in Kotlin.

However, we can’t call some of the standard Kotlin library methods in Java, like let or apply, because they’re marked with @InlineOnly.

4.1. Visibility of the Custom Extension Function in Java

Let’s use one of the previously defined extension functions — String.escapeXml(). Our file containing the extension method is called StringUtil.kt.

Now, when we need to call an extension method from Java, we need to use a class name StringUtilKt. Note that we have to add the Kt suffix:

String xml = "<a>hi</a>";

String escapedXml = StringUtilKt.escapeForXml(xml);

assertEquals("<a>hi</a>", escapedXml);

Please pay attention to the first escapeForXml parameter. This additional argument is an extension function receiver type. Kotlin with top-level extension function is a pure Java class with a static method. That’s why it needs to somehow pass the original String.

And of course, just like in Java, we can use static import:

import static com.baeldung.kotlin.StringUtilKt.*;

4.2. Calling a Built-in Kotlin Extension Method

Kotlin helps us write code easier and faster by providing many built-in extensions functions. For example, there’s the String.capitalize() method, which can be called directly from Java:

String name = "john";

String capitalizedName = StringsKt.capitalize(name);

assertEquals("John", capitalizedName);

However, we can’t call extension methods marked with @InlineOnly from Java, for example:

inline fun <T, R> T.let(block: (T) -> R): R

4.3. Renaming the Generated Java Static Class

We already know that a Kotlin extension function is a static Java method. Let’s rename a generated Java class with an annotation @file:JvmName(name: String).

This has to be added at the top of the file:

@file:JvmName("Strings")
package com.baeldung.kotlin

fun String.escapeForXml() : String {
    return this
      .replace("&", "&")
      .replace("<", "<")
      .replace(">", ">")
}

Now, when we want to call an extension method, we simply need to add the Strings class name:

Strings.escapeForXml(xml);

Also, we can still add a static import:

import static com.baeldung.kotlin.Strings.*;

5. Summary

Extension Methods are useful tools to extend types that already exist in the system – either because they don’t have the functionality we need or simply to make some specific area of code easier to manage.

We’ve seen here some extension methods that are ready to use in the system. Additionally, we explored various possibilities of extension methods. Some examples of this functionality can be found over on GitHub.

Leave a Reply

Your email address will not be published.