Erweiterungsmethoden in Kotlin

1. Einleitung

Kotlin führt das Konzept der Erweiterungsmethoden ein - eine praktische Möglichkeit, vorhandene Klassen mit einer neuen Funktionalität zu erweitern, ohne Vererbung oder andere Formen des Decorator-Musters zu verwenden - nachdem eine Erweiterung definiert wurde. Wir können es im Wesentlichen verwenden - da es der Teil der ursprünglichen API war.

Dies kann sehr nützlich sein, um das Lesen und Verwalten unseres Codes zu vereinfachen, da wir Methoden hinzufügen können, die auf unsere Bedürfnisse zugeschnitten sind und als Teil des ursprünglichen Codes erscheinen, auch wenn wir keinen Zugriff darauf haben die Quellen.

Beispielsweise müssen wir möglicherweise XML-Escapezeichen für einen String ausführen . In Standard-Java-Code müssten wir eine Methode schreiben, die dies ausführen und aufrufen kann:

String escaped = escapeStringForXml(input);

Während in Kotlin geschrieben, könnte das Snippet ersetzt werden durch:

val escaped = input.escapeForXml()

Dies ist nicht nur einfacher zu lesen, sondern IDEs können die Methode auch als Option für die automatische Vervollständigung anbieten, als wäre sie eine Standardmethode für die String- Klasse.

2. Standardmethoden zur Bibliothekserweiterung

Die Kotlin Standard Library enthält einige sofort einsatzbereite Erweiterungsmethoden.

2.1. Kontext Anpassen von Erweiterungsmethoden

Es gibt einige generische Erweiterungen, die auf alle Typen in unserer Anwendung angewendet werden können . Diese können verwendet werden, um sicherzustellen, dass Code in einem geeigneten Kontext ausgeführt wird, und in einigen Fällen, um sicherzustellen, dass eine Variable nicht null ist.

Es stellt sich heraus, dass wir höchstwahrscheinlich Erweiterungen nutzen, ohne dies zu bemerken.

Eine der beliebtesten ist möglicherweise die let () -Methode, die für jeden Typ in Kotlin aufgerufen werden kann. Übergeben wir eine Funktion, die für den Anfangswert ausgeführt wird:

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

Es ähnelt der map () -Methode aus optionalen oder Stream- Klassen. In diesem Fall übergeben wir eine Funktion, die eine Aktion darstellt, die einen bestimmten String in seine Darstellung in Großbuchstaben konvertiert .

Der variable Name wird als der Empfänger des Anrufs bekannt , weil es die Variable ist , dass die Erweiterungsmethode auf fungiert.

Dies funktioniert hervorragend mit dem Safe-Call-Operator:

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

In diesem Fall hat der Block () lassen sich nur dann , wenn die Variable ausgewertet Name nicht-null war . Dies bedeutet, dass innerhalb des Blocks der Wert n garantiert nicht Null ist. Mehr dazu hier.

Es gibt andere Alternativen zu let () , die je nach unseren Anforderungen ebenfalls nützlich sein können.

Die run () -Erweiterung funktioniert genauso wie let () , jedoch wird ein Empfänger als dieser Wert innerhalb des aufgerufenen Blocks bereitgestellt :

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

apply () funktioniert genauso wie run () , gibt jedoch einen Empfänger zurück, anstatt den Wert aus dem bereitgestellten Block zurückzugeben .

Lassen Sie uns apply () für kettenbezogene Aufrufe nutzen:

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

Beachten Sie, wie unser Code wird knapper und ausdrucksvoll mit nicht explizit verwenden , dies oder es .

Die Erweiterung also () funktioniert genauso wie let () , gibt den Empfänger jedoch auf dieselbe Weise zurück wie apply () :

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

Die Erweiterung takeIf () wird mit einem Prädikat versehen, das auf den Empfänger wirkt. Wenn dieses Prädikat true zurückgibt , gibt es den Empfänger oder andernfalls null zurück. Dies funktioniert ähnlich wie eine Kombination aus einer gemeinsamen map () - und einer filter () -Methode:

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

Die takeUnless () -Erweiterung ist dieselbe wie takeIf (), jedoch mit der umgekehrten Prädikatenlogik.

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

2.2. Erweiterungsmethoden für Sammlungen

Kotlin fügt den Standard-Java-Sammlungen eine große Anzahl von Erweiterungsmethoden hinzu, mit denen unser Code einfacher zu bearbeiten ist .

Diese Methoden befinden sich in _Collections.kt, _Ranges.kt und _Sequences.kt sowie in _Arrays.kt, damit äquivalente Methoden stattdessen auf Arrays angewendet werden können. (Denken Sie daran, dass Arrays in Kotlin genauso behandelt werden können wie Sammlungen. )

Es gibt viel zu viele dieser Erweiterungsmethoden, um sie hier zu diskutieren. Durchsuchen Sie diese Dateien, um zu sehen, was verfügbar ist.

Zusätzlich zu Sammlungen fügt Kotlin der in _Strings.kt definierten String- Klasse eine erhebliche Anzahl von Erweiterungsmethoden hinzu . Diese ermöglichen es uns, Strings so zu behandeln, als wären sie Sammlungen von Charakteren .

Alle diese Erweiterungsmethoden arbeiten zusammen, damit wir unabhängig von der Art der Sammlung, mit der wir arbeiten, deutlich saubereren und einfacher zu pflegenden Code schreiben können.

3. Schreiben unserer Erweiterungsmethoden

Was ist, wenn wir eine Klasse mit einer neuen Funktionalität erweitern müssen - entweder aus der Java- oder Kotlin-Standardbibliothek oder aus einer abhängigen Bibliothek, die wir verwenden?

Erweiterungsmethoden werden wie jede andere Methode geschrieben , aber die Empfängerklasse wird als Teil des Funktionsnamens bereitgestellt, getrennt durch den Punkt.

Zum Beispiel:

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("<", "", ">") }

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.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.times(by: Int): List { 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 = "hi"; String escapedXml = StringUtilKt.escapeForXml(xml); assertEquals("hi", 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.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("<", "", ">") }

Wenn wir jetzt eine Erweiterungsmethode aufrufen möchten, müssen wir einfach den Namen der Strings- Klasse hinzufügen :

Strings.escapeForXml(xml);

Wir können auch noch einen statischen Import hinzufügen:

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

5. Zusammenfassung

Erweiterungsmethoden sind nützliche Tools zum Erweitern von Typen, die bereits im System vorhanden sind - entweder weil sie nicht über die von uns benötigten Funktionen verfügen oder um einfach die Verwaltung eines bestimmten Codebereichs zu vereinfachen.

Wir haben hier einige Erweiterungsmethoden gesehen, die im System verwendet werden können. Zusätzlich haben wir verschiedene Möglichkeiten von Erweiterungsmethoden untersucht. Einige Beispiele für diese Funktionalität finden Sie auf GitHub.