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 12]
4. 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.