Einführung in die Kotlin-Sprache

1. Überblick

In diesem Tutorial werfen wir einen Blick auf Kotlin, eine neue Sprache in der JVM-Welt, und einige ihrer grundlegenden Funktionen, einschließlich Klassen, Vererbung, bedingte Anweisungen und Schleifenkonstrukte.

Anschließend werden wir uns einige der wichtigsten Funktionen ansehen, die Kotlin zu einer attraktiven Sprache machen, darunter Sicherheit, Datenklassen, Erweiterungsfunktionen und __String-Vorlagen.

2. Abhängigkeiten von Maven

Um Kotlin in Ihrem Maven-Projekt zu verwenden, müssen Sie die Kotlin-Standardbibliothek zu Ihrer pom.xml hinzufügen:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>1.0.4</version>
</dependency>

Um JUnit-Unterstützung für Kotlin hinzuzufügen, müssen Sie auch die folgenden Abhängigkeiten hinzufügen:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-test-junit</artifactId>
    <version>1.0.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

Sie finden die neuesten Versionen von kotlin-stdlib , kotlin-test-junit und junit auf Maven Central .

Schließlich müssen Sie die Quellverzeichnisse und das Kotlin-Plugin konfigurieren, um einen Maven-Build durchzuführen:

<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
        <plugin>
            <artifactId>kotlin-maven-plugin</artifactId>
            <groupId>org.jetbrains.kotlin</groupId>
            <version>1.0.4</version>
            <executions>
                <execution>
                    <id>compile</id>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
                <execution>
                    <id>test-compile</id>
                    <goals>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Sie können die neueste Version von https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.jetbrains.kotlin%22%20AND%20A%3A%22kotlin-maven-plugin% finden. 22[kotlin-maven-plugin]in Maven Central.

3. Grundlegende Syntax

Schauen wir uns die grundlegenden Bausteine ​​von Kotlin Language an.

Es gibt einige Ähnlichkeiten mit Java (beispielsweise ist das Definieren von Paketen auf dieselbe Weise). Schauen wir uns die Unterschiede an.

3.1. Funktionen definieren

Definieren wir eine Funktion mit zwei Int-Parametern mit dem Rückgabetyp Int :

fun sum(a: Int, b: Int): Int {
    return a + b
}

3.2. Lokale Variablen definieren

Einmalige (schreibgeschützte) lokale Variable zuweisen:

val a: Int = 1
val b = 1
val c: Int
c = 1

Beachten Sie, dass der Typ einer Variablen b von einem Kotlin-Compiler abgeleitet wird. Wir könnten auch veränderliche Variablen definieren:

var x = 5
x += 1

4. Optionale Felder

Kotlin verfügt über eine grundlegende Syntax zum Definieren eines Felds, das nullwertfähig sein kann (optional). Wenn Sie angeben möchten, dass der Feldtyp nullfähig ist, müssen Sie den Typ-Suffix mit einem Fragezeichen verwenden:

val email: String?

Wenn Sie ein Feld mit Nullwert definiert haben, ist es absolut gültig, ihm ein null zuzuweisen:

val email: String? = null

Das bedeutet, dass in einem E-Mail-Feld ein null. stehen könnte. Wenn wir schreiben:

val email: String = "value"

Dann müssen wir dem E-Mail-Feld in derselben Anweisung, in der wir die E-Mail angeben, einen Wert zuweisen. Es darf keinen Nullwert haben. Wir werden in einem späteren Abschnitt auf Kotlin null safety zurückkommen.

5. Klassen

Lassen Sie uns zeigen, wie Sie eine einfache Klasse zum Verwalten einer bestimmten Kategorie eines Produkts erstellen. Unsere ItemManager -Klasse unten hat einen Standardkonstruktor, der die beiden Felder categoryId und dbConnection auffüllt, und ein optionales email -Feld:

class ItemManager(val categoryId: String, val dbConnection: String) {
    var email = ""
   //...
}

Dieses Konstrukt ItemManager (…​) erstellt einen Konstruktor und zwei Felder in unserer Klasse: categoryId und dbConnection

Beachten Sie, dass unser Konstruktor das Schlüsselwort val für seine Argumente verwendet. Dies bedeutet, dass die entsprechenden Felder final und unveränderlich sind.

Wenn wir das Schlüsselwort var verwendet hätten (wie bei der Definition des Felds email ), wären diese Felder veränderbar.

Erstellen Sie eine Instanz von ItemManager mit dem Standardkonstruktor:

ItemManager("cat__id", "db://connection")

Wir könnten ItemManager mit benannten Parametern erstellen. Dies ist sehr nützlich, wenn Sie wie in diesem Beispiel eine Funktion haben, die zwei Parameter mit demselben Typ verwendet, z. String , und Sie möchten eine Reihenfolge davon nicht verwechseln. Mit Hilfe von Benennungsparametern können Sie explizit schreiben, welcher Parameter zugewiesen wird. In der Klasse ItemManager gibt es zwei Felder, categoryId und dbConnection , sodass beide mit benannten Parametern referenziert werden können:

ItemManager(categoryId = "catId", dbConnection = "db://Connection")

Dies ist sehr nützlich, wenn mehr Argumente an eine Funktion übergeben werden müssen.

Wenn Sie zusätzliche Konstruktoren benötigen, definieren Sie diese mit dem Schlüsselwort constructor . Definieren wir einen anderen Konstruktor, der auch das Feld email festlegt:

constructor(categoryId: String, dbConnection: String, email: String)
  : this(categoryId, dbConnection) {
    this.email = email
}

Beachten Sie, dass dieser Konstruktor vor dem Festlegen des E-Mail-Felds den oben definierten Standardkonstruktor aufruft. Und da wir categoryId und dbConnection bereits mit dem Schlüsselwort val im Standardkonstruktor als unveränderlich definiert haben, müssen Sie das Schlüsselwort val im zusätzlichen Konstruktor nicht wiederholen.

Nun erstellen wir eine Instanz mit dem zusätzlichen Konstruktor:

ItemManager("cat__id", "db://connection", "[email protected]")

Wenn Sie eine Instanzmethode für ItemManager definieren möchten, verwenden Sie das Schlüsselwort fun :

fun isFromSpecificCategory(catId: String): Boolean {
    return categoryId == catId
}

6. Erbe

Standardmäßig sind Kotlins Klassen für die Erweiterung geschlossen. Dies entspricht einer Klasse, die in Java mit final markiert ist.

Um anzugeben, dass eine Klasse für die Erweiterung geöffnet ist, verwenden Sie bei der Definition der Klasse das Schlüsselwort open .

Definieren wir eine Item -Klasse, die für die Erweiterung geöffnet ist:

open class Item(val id: String, val name: String = "unknown__name") {
    open fun getIdOfItem(): String {
        return id
    }
}

Beachten Sie, dass wir die Methode getIdOfItem () auch als offen bezeichnet haben. Dadurch kann es überschrieben werden.

Lassen Sie uns nun die Item -Klasse erweitern und die getIdOfItem () -Methode überschreiben:

class ItemWithCategory(id: String, name: String, val categoryId: String) : Item(id, name) {
    override fun getIdOfItem(): String {
        return id + name
    }
}

7. Bedingungsanweisungen

In Kotlin entspricht die Bedingungsanweisung if einer Funktion, die einen Wert zurückgibt. Schauen wir uns ein Beispiel an:

fun makeAnalyisOfCategory(catId: String): Unit {
    val result = if (catId == "100") "Yes" else "No"
    println(result)
}

In diesem Beispiel wird Folgendes angezeigt: Wenn catId gleich "100" ist, gibt der bedingte Block "Ja" zurück, andernfalls "Nein".

Sie könnten einen normalen if else -Block erstellen:

val number = 2
if (number < 10) {
    println("number less that 10")
} else if (number > 10) {
    println("number is greater that 10")
}

Kotlin hat auch einen sehr nützlichen when -Befehl, der sich wie eine erweiterte switch-Anweisung verhält:

val name = "John"
when (name) {
    "John" -> println("Hi man")
    "Alice" -> println("Hi lady")
}

8. Sammlungen

Es gibt zwei Arten von Sammlungen in Kotlin: veränderlich und unveränderlich.

Wenn wir eine unveränderliche Sammlung erstellen, bedeutet dies, dass nur gelesen werden kann:

val items = listOf(1, 2, 3, 4)

In dieser Liste befindet sich kein Element zum Hinzufügen von Funktionen.

Wenn Sie eine veränderliche Liste erstellen möchten, die geändert werden kann, müssen Sie die Methode mutableListOf () verwenden:

val rwList = mutableListOf(1, 2, 3)
rwList.add(5)

Eine veränderliche Liste hat die Methode add () , sodass wir ein Element daran anhängen können.

Es gibt auch eine gleichwertige Methode für andere Auflistungstypen:

mutableMapOf (), mapOf (), setOf (), mutableSetOf ()

  1. Ausnahmen

Der Mechanismus der Ausnahmebehandlung ist dem in Java sehr ähnlich.

Alle Ausnahmeklassen erweitern Throwable. Die Ausnahme muss eine Nachricht, Stacktrace und eine optionale Ursache haben. Jede Ausnahme in Kotlin ist nicht aktiviert , was bedeutet, dass der Compiler uns nicht zwingt, sie zu fangen.

Um ein Exception-Objekt auszulösen, müssen wir den throw-Ausdruck verwenden:

throw Exception("msg")

Die Behandlung von Ausnahmebedingungen erfolgt mit dem try …​ catch-Block (finally optional):

try {

}
catch (e: SomeException) {

}
finally {

}

10. Lambdas

In Kotlin könnten wir Lambda-Funktionen definieren und als Argumente an andere Funktionen übergeben.

Mal sehen, wie man ein einfaches Lambda definiert:

val sumLambda = { a: Int, b: Int -> a + b }

Wir haben die Funktion sumLambda definiert, die zwei Argumente vom Typ Int als Argument akzeptiert und Int. zurückgibt.

Wir könnten ein Lambda herumfahren:

@Test
fun givenListOfNumber__whenDoingOperationsUsingLambda__shouldReturnProperResult() {
   //given
    val listOfNumbers = listOf(1, 2, 3)

   //when
    val sum = listOfNumbers.reduce { a, b -> a + b }

   //then
    assertEquals(6, sum)
}

11. Schleifen von Konstrukten

In Kotlin könnte das Durchlaufen von Sammlungen durch Verwendung eines Standard- for..in -Konstrukts erfolgen:

val numbers = arrayOf("first", "second", "third", "fourth")
for (n in numbers) {
    println(n)
}

Wenn wir über einen Bereich von ganzen Zahlen iterieren möchten, können wir ein Range-Konstrukt verwenden:

for (i in 2..9 step 2) {
    println(i)
}

Beachten Sie, dass der Bereich im obigen Beispiel auf beiden Seiten inklusive ist. Der step -Parameter ist optional und entspricht einem zweimaligen Inkrementieren des Zählers in jeder Iteration. Die Ausgabe wird folgend sein:

2
4
6
8

Wir könnten eine rangeTo () - Funktion verwenden, die in der Int -Klasse wie folgt definiert ist:

1.rangeTo(10).map{ it **  2 }

Das Ergebnis enthält (beachten Sie, dass rangeTo () auch inklusive ist):

----[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]----

12. Null Sicherheit

Schauen wir uns eine der wichtigsten Funktionen von Kotlin an - die in die Sprache integrierte Nullsicherheit. Um zu zeigen, warum dies nützlich ist, erstellen wir einen einfachen Service, der ein Item -Objekt zurückgibt:

class ItemService {
    fun findItemNameForId(id: String): Item? {
        val itemId = UUID.randomUUID().toString()
        return Item(itemId, "name-$itemId");
    }
}

Das Wichtigste, was Sie beachten sollten, ist der Typ dieser Methode. Es ist ein Objekt, dem das Fragezeichen folgt. Es ist ein Konstrukt aus der Kotlin-Sprache, was bedeutet, dass Item von dieser Methode zurückgegeben werden könnte.

Wir müssen diesen Fall zur Kompilierzeit behandeln und entscheiden, was wir mit diesem Objekt tun möchten (es entspricht mehr oder weniger dem Typ Java 8 Optional <T> ).

Wenn die Methodensignatur einen Typ ohne Fragezeichen aufweist:

fun findItemNameForId(id: String): Item

Dann muss der aufrufende Code keinen Nullfall behandeln, da dies vom Compiler und der Sprache Kotlin garantiert wird. Das zurückgegebene Objekt kann nicht Null sein.

Andernfalls ** wird ein nullfähiges Objekt, das an eine Methode übergeben wird, und dieser Fall wird nicht behandelt, nicht kompiliert.

Schreiben wir einen Testfall für Kotlin-Typensicherheit:

val id = "item__id"
val itemService = ItemService()

val result = itemService.findItemNameForId(id)

assertNotNull(result?.let { it -> it.id })
assertNotNull(result!!.id)

Wir sehen hier, dass nach der Ausführung der Methode findItemNameForId () der zurückgegebene Typ Kotlin Nullable ist. Um auf ein Feld dieses Objekts ( id ) zuzugreifen, müssen wir diesen Fall zur Kompilierzeit behandeln. Die Methode let () wird nur ausgeführt, wenn ein Ergebnis nicht nullwertfähig ist. Auf das Id__-Feld kann innerhalb einer Lambda-Funktion zugegriffen werden, da es nullsicher ist.

Eine andere Möglichkeit, auf dieses nullfähige Objektfeld zuzugreifen, ist die Verwendung des Kotlin-Operators !! . . Es entspricht:

if (result == null){
    throwNpe();
}
return result;

Kotlin prüft, ob das Objekt ein null ist. Wenn dies der Fall ist, wird eine NullPointerException ausgelöst. Andernfalls wird ein ordnungsgemäßes Objekt zurückgegeben.

Die Funktion throwNpe () ist eine interne Kotlin-Funktion.

13. Datenklassen

Ein sehr schönes Sprachkonstrukt, das in Kotlin gefunden werden kann, sind Datenklassen (es entspricht "Fallklasse" aus der Sprache Scala). Der Zweck solcher Klassen besteht darin, nur Daten zu speichern. In unserem Beispiel hatten wir eine Item -Klasse, die nur die Daten enthält:

data class Item(val id: String, val name: String)

Der Compiler erstellt für uns Methoden hashCode () , equals () und toString () . Es empfiehlt sich, Datenklassen mithilfe eines val -Schlüsselworts unveränderlich zu machen. Datenklassen können Standardfeldwerte haben:

data class Item(val id: String, val name: String = "unknown__name")

Wir sehen, dass das Feld name den Standardwert "unknown name" hat. .__

14. Erweiterungsfunktionen

Angenommen, wir haben eine Klasse, die Teil der Bibliothek eines Drittanbieters ist, wir möchten sie jedoch mit einer zusätzlichen Methode erweitern. Kotlin ermöglicht dies durch die Verwendung von Erweiterungsfunktionen.

Betrachten wir ein Beispiel, in dem wir eine Liste von Elementen haben und ein zufälliges Element aus dieser Liste nehmen möchten. Wir möchten eine neue Funktion random () zur List -Klasse von Drittanbietern hinzufügen.

So sieht es in Kotlin aus:

fun <T> List<T>.random(): T? {
    if (this.isEmpty()) return null
    return get(ThreadLocalRandom.current().nextInt(count()))
}

Das Wichtigste, das Sie hier beachten sollten, ist eine Signatur der Methode.

Der Methode ist ein Name der Klasse vorangestellt, der diese zusätzliche Methode hinzugefügt wird.

Innerhalb der Erweiterungsmethode arbeiten wir mit einem Gültigkeitsbereich einer Liste. Daher haben wir mit this Zugriff auf Listeninstanzmethoden wie isEmpty () oder __count () ist in diesem Bereich:

fun <T> getRandomElementOfList(list: List<T>): T? {
    return list.random()
}

Wir haben eine Methode erstellt, die eine Liste übernimmt und dann die zuvor definierte benutzerdefinierte Erweiterungsfunktion random () ausführt. Schreiben wir einen Testfall für unsere neue Funktion:

val elements = listOf("a", "b", "c")

val result = ListExtension().getRandomElementOfList(elements)

assertTrue(elements.contains(result))

Die Möglichkeit, Funktionen zu definieren, die Drittanbieter-Klassen "erweitern", ist eine sehr leistungsfähige Funktion und kann unseren Code übersichtlicher und lesbarer machen.

15. String-Vorlagen

Eine sehr nette Funktion der Kotlin-Sprache ist die Möglichkeit, Vorlagen für __String s zu verwenden. Dies ist sehr nützlich, da String __s nicht manuell verkettet werden muss:

val firstName = "Tom"
val secondName = "Mary"
val concatOfNames = "$firstName + $secondName"
val sum = "four: ${2 + 2}"

Wir können auch einen Ausdruck im $ \ {} -Block auswerten:

val itemManager = ItemManager("cat__id", "db://connection")
val result = "function result: ${itemManager.isFromSpecificCategory("1")}"
  1. Kotlin/Java-Interoperabilität

Kotlin - Java-Interoperabilität ist nahtlos einfach. Nehmen wir an, wir haben eine Java-Klasse mit einer Methode, die auf String ausgeführt wird:

class StringUtils{
    public static String toUpperCase(String name) {
        return name.toUpperCase();
    }
}

Nun wollen wir diesen Code aus unserer Kotlin-Klasse ausführen. Wir müssen nur diese Klasse importieren und wir können die Java-Methode von Kotlin ohne Probleme ausführen:

val name = "tom"

val res = StringUtils.toUpperCase(name)

assertEquals(res, "TOM")

Wie wir sehen, haben wir die Java-Methode aus Kotlin-Code verwendet.

Das Aufrufen von Kotlin-Code von Java aus ist ebenfalls sehr einfach. Definieren wir eine einfache Kotlin-Funktion:

class MathematicsOperations {
    fun addTwoNumbers(a: Int, b: Int): Int {
        return a + b
    }
}

Das Ausführen von addTwoNumbers () aus Java-Code ist sehr einfach:

int res = new MathematicsOperations().addTwoNumbers(2, 4);

assertEquals(6, res);

Wir sehen, dass der Aufruf von Kotlin für uns transparent war.

Wenn wir in java eine Methode definieren, ist der Rückgabetyp ein void , der in Kotlin zurückgegebene Wert ist vom Typ Unit .

Es gibt einige spezielle Bezeichner in der Java-Sprache ( is , object , in , ..), die bei Verwendung in Kotlin-Code mit Escapezeichen versehen werden müssen. Zum Beispiel könnten wir eine Methode definieren, die einen Namen object () hat, aber wir müssen daran denken, diesen Namen zu umgehen, da dies eine spezielle Kennung in Java ist:

fun `object`(): String {
    return "this is object"
}

Dann könnten wir diese Methode ausführen:

`object`()

17. Fazit

Dieser Artikel enthält eine Einführung in die Kotlin-Sprache und ihre wichtigsten Funktionen. Am Anfang werden einfache Konzepte wie Schleifen, bedingte Anweisungen und Definieren von Klassen eingeführt. Zeigt dann einige erweiterte Funktionen wie Erweiterungsfunktionen und Nullsicherheit.

Die Implementierung all dieser Beispiele und Code-Snippets finden Sie unter https://github.com/eugenp/tutorials/tree/master/core-kotlin (GitHub-Projekt). Dies ist ein Maven-Projekt, daher sollte es einfach sein Importieren und ausführen, wie es ist.