Leitfaden für Kotlin-Schnittstellen

Leitfaden zu Kotlin-Schnittstellen

1. Überblick

In diesem Tutorial werdenwe’ll discuss how to define and implement interfaces in Kotlin.

Wir werden uns auch ansehen, wie mehrere Schnittstellen von einer Klasse implementiert werden können. Dies kann sicherlich zu Konflikten führen, und wir werden lernen, wie Kotlin sie lösen muss.

2. Schnittstellen in Kotlin

Eine Schnittstelle ist eine Möglichkeit, eine Beschreibung oder einen Vertrag für Klassen in der objektorientierten Programmierung bereitzustellen. Je nach Programmiersprache können sie Eigenschaften und Funktionen abstrakt oder konkret enthalten. Wir werden die Details der Schnittstellen in Kotlin durchgehen.

Interfaces in Kotlin ähneln Interfaces in vielen anderen Sprachen wie Java. Sie haben jedoch eine bestimmte Syntax. Lassen Sie uns sie in den nächsten Unterabschnitten überprüfen.

2.1. Schnittstellen definieren

Beginnen wir mit der Definition unserer ersten Schnittstelle in Kotlin:

interface SimpleInterface

Dies ist die einfachste Schnittstelle, die vollständig leer ist. These are also known as marker interfaces.

Fügen wir unserer Benutzeroberfläche nun einige Funktionen hinzu:

interface SimpleInterface {
    fun firstMethod(): String
    fun secondMethod(): String {
        return("Hello, World!")
    }
}

Wir haben unsere zuvor definierte Schnittstelle um zwei Methoden erweitert:

  • Eine von ihnen namens firstMethod ist eine abstrakte Methode

  • Während der andere mit dem Namen secondMethod eine Standardimplementierung ist.

Lassen Sie uns jetzt einige Eigenschaften zu unserer Benutzeroberfläche hinzufügen:

interface SimpleInterface {
    val firstProp: String
    val secondProp: String
        get() = "Second Property"
    fun firstMethod(): String
    fun secondMethod(): String {
        return("Hello, from: " + secondProp)
    }
}

Hier haben wir unserer Schnittstelle zwei Eigenschaften hinzugefügt:

  • Eine davon heißtfirstProp is vom Typ String und ist abstrakt

  • Das zweite mit dem NamensecondProp ist ebenfalls vom Typ string, definiert jedoch eine Implementierung für seinen Accessor.

Beachten Sie, dassproperties in an interface cannot maintain state. Das Folgende ist also ein illegaler Ausdruck in Kotlin:

interface SimpleInterface {
    val firstProp: String = "First Property" // Illegal declaration
}

2.2. Schnittstellen implementieren

Nachdem wir eine grundlegende Schnittstelle definiert haben, wollen wir sehen, wie wir dies in einer Klasse in Kotlin implementieren können:

class SimpleClass: SimpleInterface {
    override val firstProp: String = "First Property"
    override fun firstMethod(): String {
        return("Hello, from: " + firstProp)
    }
}

Beachten Sie, dassSimpleClass als Implementierung vonSimpleInterface,we only have to provide the implementation for abstract properties and functions definiert wird. Wircan override any previously defined property or function, too.

Überschreiben wir nun alle zuvor definierten Eigenschaften und Funktionen in unserer Klasse:

class SimpleClass: SimpleInterface {
    override val firstProp: String = "First Property"
    override val secondProp: String
        get() = "Second Property, Overridden!"

    override fun firstMethod(): String {
        return("Hello, from: " + firstProp)
    }
    override fun secondMethod(): String {
        return("Hello, from: " + secondProp + firstProp)
    }
}

Hier haben wir die EigenschaftensecondProp und die FunktionsecondFunction überschrieben, die zuvor in der SchnittstelleSimpleInterface definiert wurden.

2.3 Implementing Interfaces through Delegation

Die Delegierung ist ein Entwurfsmuster in der objektorientierten Programmierungto achieve code reusability through composition instead of inheritance. Während dies in vielen Sprachen wie Java implementiert werden kann,Kotlin has native support for implementation through delegation.

Wenn wir mit einer grundlegenden Schnittstelle und Klasse beginnen:

interface MyInterface {
    fun someMethod(): String
}

class MyClass() : MyInterface {
    override fun someMethod(): String {
        return("Hello, World!")
    }
}

Bisher nichts Neues. Jetzt können wir eine andere Klasse definieren, dieMyInterface durch Delegation implementiert:

class MyDerivedClass(myInterface: MyInterface) : MyInterface by myInterface

MyDerivedClass erwartet einen Delegaten als Argument, das die SchnittstelleMyInterface tatsächlich implementiert.

Mal sehen, wie wir eine Funktion der Schnittstelle durch Delegieren aufrufen können:

val myClass = MyClass()
MyDerivedClass(myClass).someMethod()

Hier haben wirMyClass instanziiert und diese als Delegat verwendet, um Funktionen der Schnittstelle aufMyDerivedClass, aufzurufen, die diese Funktionen tatsächlich nie direkt implementiert haben.

3. Mehrfachvererbung

Mehrfachvererbung ist ein Schlüsselbegriff im objektorientierten Programmierparadigma. This allows for a class to inherit characteristics from more than one parent object, like an interface, for example.

Dies bietet zwar mehr Flexibilität bei der Objektmodellierung, ist jedoch mit einer Reihe von Komplexitäten verbunden. Eines davon ist das „Diamantproblem“.

Java 8 has its own mechanisms for addressing the diamond problem, wie jede andere Sprache, die Mehrfachvererbung zulässt.

Mal sehen, wie Kotlin es über Schnittstellen angeht.

3.1. Mehrere Schnittstellen erben

Zunächst definieren wir zwei einfache Schnittstellen:

interface FirstInterface {
    fun someMethod(): String
    fun anotherMethod(): String {
        return("Hello, from anotherMethod in FirstInterface")
    }
}

interface SecondInterface {
    fun someMethod(): String {
        return("Hello, from someMethod in SecondInterface")
    }
    fun anotherMethod(): String {
        return("Hello, from anotherMethod in SecondInterface")
    }
}

Beachten Sie, dass beide Schnittstellen Methoden mit demselben Vertrag haben.

Definieren wir nun eine Klasse, die von diesen beiden Schnittstellen erbt:

class SomeClass: FirstInterface, SecondInterface {
    override fun someMethod(): String {
        return("Hello, from someMethod in SomeClass")
    }
    override fun anotherMethod(): String {
        return("Hello, from anotherMethod in SomeClass")
    }
}

Wie wir sehen können, implementiertSomeClass sowohlFirstInterface als auchSecondInterface. Während dies syntaktisch recht einfach ist, gibt es hier ein bisschen Semantik, die Beachtung erfordert. Wir werden darauf im nächsten Unterabschnitt näher eingehen.

3.2. Konflikte lösen

Bei der Implementierung mehrerer Schnittstellen kann eine Klasse eine Funktion erben, die eine Standardimplementierung für denselben Vertrag in mehreren Schnittstellen hat. Dies wirft das Problem des Aufrufs für diese Funktion von einer Instanz der implementierenden Klasse aus auf.

To resolve this conflict, Kotlin requires the subclass to provide an overridden implementation for such functions to make the resolution explicit.

Zum Beispiel implementiertSomeClass obenanotherMethod. Andernfalls würde Kotlin nicht wissen, ob er die Standardimplementierung vonFirst oderSecondInterface’svonanotherMethod aufrufen soll. SomeClassmust implement anotherMethod for this reason.

someMethod ist jedoch etwas anders, da es tatsächlich keinen Konflikt gibt. FirstInterface bietet keine Standardimplementierung fürsomeMethod. Das heißt,SomeClass muss es noch implementieren, weilKotlin forces us to implement all inherited functions, no matter if they are defined once or multiple times in parent interfaces.

3.3. Lösen des Diamantproblems

Ein „Diamantproblem“ tritt auf, wenn zwei untergeordnete Objekte eines Basisobjekts ein bestimmtes Verhalten beschreiben, das vom Basisobjekt definiert wird. Nun muss ein Objekt, das von diesen beiden untergeordneten Objekten erbt, auflösen, welches geerbte Verhalten es abonniert.

Kotlins Lösung für dieses Problem besteht in den Regeln, die im vorherigen Unterabschnitt für die Mehrfachvererbung definiert wurden. Definieren wir einige Schnittstellen und eine implementierende Klasse, um das Diamantproblem darzustellen:

interface BaseInterface {
    fun someMethod(): String
}

interface FirstChildInterface: BaseInterface {
    override fun someMethod(): String {
        return("Hello, from someMethod in FirstChildInterface")
    }
}

interface SecondChildInterface: BaseInterface {
    override fun someMethod(): String {
        return("Hello, from someMethod in SecondChildInterface")
    }
}

class ChildClass: FirstChildInterface, SecondChildInterface {
    override fun someMethod(): String {
        return super.someMethod()
    }
}

Hier haben wirBaseInterface definiert, die eine abstrakte Funktion namenssomeMethod deklariert haben. Sowohl die SchnittstellenFirstChildInterface als auchSecondChildInterface erben vonBaseInterface und implementieren die FunktionsomeMethod.

Wenn wir nunChildClass implementieren, die vonFirstChildInterface undSecondChildInterface erben, müssen wir die FunktionsomeMethod überschreiben. even though we must override the method, we can still simply call super as we do here with SecondChildInterface.

4. Schnittstellen im Vergleich zu abstrakten Klassen in Kotlin

Abstrakte Klassen in Kotlin sindclasses which cannot be instantiated. Dies kann eine oder mehrere Eigenschaften und Funktionen enthalten. Diese Eigenschaften und Funktionen können abstrakt oder konkret sein. Jede Klasse, die von einer abstrakten Klasse erbt, muss alle geerbten abstrakten Eigenschaften und Funktionen implementieren, es sei denn, diese Klasse selbst ist ebenfalls als abstrakt deklariert.

4.1. Unterschiede zwischen Schnittstelle und abstrakter Klasse

Warten! Klingt das nicht genau so wie eine Schnittstelle?

Tatsächlich unterscheidet sich eine abstrakte Klasse von Anfang an nicht sehr von der Schnittstelle. Es gibt jedoch subtile Unterschiede, die die Wahl bestimmen, die wir treffen:

  • Eine Klasse in Kotlin kann beliebig viele Interfaces implementieren, sie kann sich jedoch nur von einer abstrakten Klasse aus erstrecken

  • Eigenschaften in der Schnittstelle können den Status nicht beibehalten, während dies in einer abstrakten Klasse der Fall ist

4.2. Wann sollten wir was verwenden?

Eine Schnittstelle ist nur eine Blaupause zum Definieren von Klassen. Optional können sie auch einige Standardimplementierungen haben. Andererseits ist eine abstrakte Klasse eine unvollständige Implementierung, die von den Erweiterungsklassen vervollständigt wird.

In der Regelinterfaces should be used to define the contract, wodurch die versprochenen Fähigkeiten hervorgerufen werden. Eine implementierende Klasse ist dafür verantwortlich, diese Versprechen zu erfüllen. Einabstract class, however, should be used to share partial characteristics with extending classes. Eine weiterführende Klasse kann es weiterführen, um es zu vervollständigen.

5. Vergleich mit Java-Schnittstellen

Mit den Änderungen an der Java-Schnittstelle in Java 8 werdenthey have come very close to Kotlin interfaces. Einer unserer vorherigen Artikel erfasstthe new features introduced in Java 8 including changes to the interface.

Es gibt jetzt hauptsächlich syntaktische Unterschiede zwischen Java- und Kotlin-Schnittstellen. Ein Unterschied, der auffällt, hängt mit dem Schlüsselwort „Override“ zusammen. In Kotlin müssen abstrakte Eigenschaften oder Funktionen, die von einer Schnittstelle geerbt wurden, mit dem Schlüsselwort „override“ qualifiziert werden. In Java gibt es keine solchen expliziten Anforderungen.

6. Fazit

In diesem Tutorial haben wir Kotlin-Schnittstellen besprochen, wie sie definiert und implementiert werden. Dann sprachen wir über das Erben von mehreren Schnittstellen und den Konflikt, den sie möglicherweise verursachen. Wir haben uns angesehen, wie Kotlin mit solchen Konflikten umgeht.

Schließlich haben wir Schnittstellen im Vergleich zu abstrakten Klassen in Kotlin diskutiert. Wir haben auch kurz darüber gesprochen, wie sich die Kotlin-Oberfläche mit der Java-Oberfläche vergleicht.

Wie immer ist der Code für die Beispieleover on GitHub verfügbar.