Über unsMediaKontaktImpressum
Jan Baganz & Dr. Kristine Schaal 05. Mai 2020

Modul- und Sichtbarkeitskonzepte in Kotlin und Java

Darf ich das bitte mal sehen?

Auf den ersten Blick ähnlich, unterscheiden sich die Sichtbarkeitskonzepte von Kotlin und Java doch in wichtigen Punkten. So ist der Default in Kotlin public, in Java dagegen package-private. Packages dienen in Kotlin jedoch nur der Organisation der Klassen, daher sind sie nicht relevant für die Sichtbarkeit. Den Sichtbarkeitsmodifikator protected gibt es in beiden Sprachen, jedoch mit unterschiedlicher Semantik. In Kotlin gibt es zusätzlich den Sichtbarkeitsmodifikator internal. Damit kann man die Sichtbarkeit auf ein Kotlin-Modul einschränken. Aber Vorsicht! Ein Modul in Kotlin ist etwas anderes als ein Modul in Java.

In diesem Artikel erklären wir die Sichtbarkeitsmodifikatoren und Modulkonzepte von Java und Kotlin und vergleichen sie. Wir erläutern, wie sich dabei die Sichtbarkeits- und Modulkonzepte vertragen und gegenseitig auswirken.

Mit Modul- und Sichtbarkeitskonzepten Designprinzipien umsetzen

Die Idee von "Komponentenorientierter Softwareentwicklung" ist seit vielen Jahren ein fester Bestandteil des Software-Architektur-Entwurfs. Verschiedene Definitionen (wie die von [1]) legen den Komponentenbegriff fest. Auch wenn sich die Definitionen im Detail leicht unterscheiden, besagen sie insbesondere:

  • Eine Komponente ist ein Software-Baustein und umfasst als grobgranulare Einheit eine Reihe von Klassen und Packages.
  • Eine Komponente hat explizite Abhängigkeiten zu anderen Komponenten. Diese sind als eingehende und ausgehende Schnittstellen modelliert.
  • Eine Komponente ist selbstständig deploybar.

Mit Komponenten bricht man im Software-Entwurf große Monolithen in kleinere, besser zu handhabende Teile auf. Durch die explizite Modellierung von Abhängigkeiten über die Schnittstellen wird nachvollziehbar, was bei Änderungen passiert und welche Teile von Änderungen überhaupt betroffen sind. Ändert sich eine Komponente, so hat das nur Auswirkungen auf die abhängigen Komponenten. Der Grad der Abhängigkeiten der Komponenten bestimmt die Komplexität der Anwendung maßgeblich.
Im Folgenden referenzieren wir mit "Komponente" den abstrakten Begriff eines Software-Bausteins in Software-Architektur und -Entwurf. Mit "Modul" bezeichnen wir eine konkrete Umsetzung in Kotlin oder Java.

Ein wichtiges Design-Prinzip ist, dass eine Komponente eine öffentliche Schnittstelle bereitstellt, jedoch die Implementierung geheim hält, also vor dem Zugriff von außen schützt. So kann die Implementierung  verändert oder komplett ausgetauscht werden, ohne dass die Benutzer der öffentlichen Schnittstelle betroffen sind. Etliche Beispiele finden sich im Pattern-Katalog von Kirk Knoernschild [2]. Mit dem Pattern Published Interface wird die Außensicht (Schnittstelle) einer Komponente von der Innensicht (Implementierungsgeheimnis) getrennt.

Dieses Design-Prinzip gibt es sowohl im Kleinen, innerhalb einer Klasse bzw. im Zusammenspiel weniger Klassen, als auch im Großen, wenn eine Anwendung in Komponenten aufgeteilt wird. Abb. 1 verdeutlicht dies.

Um Interna innerhalb einer Klasse oder im Zusammenspiel weniger Klassen zu kapseln, benutzt man Sichtbarkeitsmodifikatoren wie beispielsweise private, package-private oder protected. Komponenten und den Zugriffschutz auf ihr Implementierungsgeheimnis modelliert man mit Modulen.

Sichtbarkeitsmodifikatoren in Java

Abb. 2 zeigt die Sichtbarkeitsmodifikatoren von Java. In dieser und allen folgenden Abbildungen bedeutet das "Duke"-Logo, dass es sich um Java handelt.

Folgende Sichtbarkeitsmodifikatoren in Java regeln den Zugriff auf Methoden, Variablen oder Klassen.

  1. private: Methoden oder Funktionen sind innerhalb der gleichen Klasse, sowie in inneren Klassen dieser Klasse sichtbar. Eine (nicht-innere) Klasse selbst kann nicht als private deklariert werden. Das ergäbe auch keinen Sinn, denn dann wäre sie überhaupt nicht nutzbar. Innere Klassen können jedoch private sein. Dann sind sie nur für ihre umschließende Klasse sichtbar.  
  2. <package>: Wenn kein Sichtbarkeitsmodifikator angegeben ist, greift als Default die package-Sichtbarkeit: Klassen, Methoden oder Variablen sind innerhalb des gleichen Packages sichtbar.
  3. protected: Sichtbar innerhalb der Klasse und allen ihren Ableitungen. Protected erweitert die package-Sichtbarkeit: Zusätzlich sind Klassen, Methoden oder Variablen also auch im gleichen Package sichtbar.
  4. public: Klassen, Methoden oder Variablen sind überall sichtbar. Mit der Einführung von Modulen, also seit Java 9, sind sie nur noch innerhalb des gleichen Moduls sichtbar.

Die Sichtbarkeitsmodifikatoren in Java sind demnach hierarchisch aufgebaut: Jeder Sichtbarkeitsmodifikator von private bis public erweitert die Sichtbarkeit, beinhaltet also immer auch alle restriktiveren Sichtbarkeitsmodifikatoren. Dass protected auch package-Sichtbarkeit enthält, ist wenig bekannt, nicht sehr intuitiv und wird daher häufig kritisiert.

Die Sichtbarkeitsregeln gelten sowohl zur Compile-Zeit als auch zur Laufzeit (für den Zugriff per Reflection).

Sichtbarkeitsmodifikatoren in Kotlin

Abb. 3 zeigt die Ebenen der Kapselung in Java. In dieser und allen folgenden Abbildungen bedeutet das Kotlin-Logo, dass es sich um Kotlin handelt.

Genau wie in Java regeln die Sichtbarkeitsmodifikatoren in Kotlin den Zugriff auf Methoden, Funktionen, Variablen oder Klassen. Zusätzlich können sie aber auch auf Datei-Ebene eine Bedeutung haben.
In Kotlin sind die Sichtbarkeitsmodifikatoren ebenfalls hierarchisch aufgebaut, d. h. die Menge der Sichtbarkeiten eines Sichtbarkeitsmodifikators erweitert die Sichtbarkeit immer, beinhaltet also immer auch alle restriktiveren Sichtbarkeitsmodifikatoren.

  1. private: Als private gekennzeichnete Methoden oder Variablen sind nur innerhalb ihrer Klasse sichtbar. Auch an Klassen oder Funktionen kann der private-Modifikator stehen. Dann sind sie lediglich innerhalb der gleichen Datei sichtbar.
  2. protected: Sichtbar nur innerhalb der Klasse und allen ihren Ableitungen.
  3. public: Ist kein Sichtbarkeitsmodifikator angegeben, ist public der Default. Klassen, Methoden oder Variablen sind überall sichtbar.

Auch in Kotlin gelten die Sichtbarkeitsregeln sowohl zur Compile-Zeit als auch zur Laufzeit (Zugriff per Reflection).

Vergleich der Sichtbarkeitsmodifikatoren von Java und Kotlin

Die Unterschiede zwischen den Sichtbarkeitsmodifikatoren in Java und Kotlin sind in Tabelle 1 dargestellt.

Tabelle 1: Vergleich der Sichtbarkeitsmodifikatoren

Java Kotlin
Packages beeinflussen die Sichtbarkeit. Packages haben keine Auswirkung auf die Sichtbarkeit.
Die Default-Sichtbarkeit ist eingeschränkt: Der Default ist package-sichtbar. Die Default-Sichtbarkeit ist offen (public).
Protected bedeutet Sichtbarkeit in Vererbungshierarchie und im package. Protected bedeutet Sichtbarkeit nur innerhalb der Vererbungshierarchie.

Sichtbarkeitsmodifikatoren regeln die Frage, wer auf eine Klasse, Methode, Funktion oder Variable zugreifen darf. Das ist etwas anderes als die Frage, welche Restriktionen für die Vererbung gelten. Die Regeln für die Vererbung stellen wir daher hier nur kurz in Tabelle 2 dar.

Tabelle 2: Vererbung im Vergleich

Java Kotlin
Per Default offen gegenüber Vererbung. Per Default ist keine Vererbung erlaubt.
Explizites Verbot von Vererbung mit dem Keyword final. Explizites Erlauben von Vererbung mit dem Keyword open.
Konvention beim Überschreiben: Annotation @Overrides. Beim Überschreiben muss das Kotlin-Keyword override angegeben werden.

Module in Java

Mit dem "Project Jigsaw" wurde in Java ein Modulsystem eingeführt [3]. Ein Modul gruppiert eine Reihe von Java-Packages sowie ihre Ressourcen. Ein solches Modul wird kompiliert, als JAR-File paketiert und als sogenanntes "Modular JAR" auf den Module-Path gelegt. Innerhalb eines Moduls ändert sich bei Sichtbarkeitsprüfungen nichts gegenüber Java 8 und davor – dort gelten unverändert die oben beschriebenen  Sichtbarkeitsmodifikatoren. Module werden in einem eigenen Modul-Deskriptor definiert. Im Modul-Deskriptor module-info.java definiert man den Modul-Namen, alle Modul-Abhängigkeiten und legt außerdem Sichtbarkeitsregeln auf die Inhalte des Modules fest. Abb. 4 zeigt dies beispielhaft für zwei Module mod.that und mod.this.

Damit mod.this überhaupt Klassen aus mod.that benutzen kann, benötigt es eine sogenannte "Reads"-Abhängigkeit auf mod.that (blauer Pfeil in der Abbildung). Modul-Abhängigkeiten werden also über den Modulnamen statisch modelliert.

Listing 1 zeigt den Modul-Deskriptor von Modul mod.this, in der auch die Abhängigkeit zu mod.that modelliert ist.

Listing 1: module-info.java von mod.this

module mod.this {
    // statische Abhängigkeit auf mod.that
    requires mod.that;
}


Sowohl zur Compile- als auch zur Laufzeit müssen die Beziehungen aufgelöst werden können. Ist mod.that nicht verfügbar, so führt dies zu Compile- bzw. Laufzeit-Fehlern bei mod.this.

Das Modul mod.that definiert über seine "Exports" (roter Pfeil), welche seiner Packages für andere Module überhaupt sichtbar und damit zugreifbar sind. Die Granularität eines solchen Exports ist dabei immer ein Java-Package. In unserem Beispiel darf mod.this nur auf das Package pkg.that von mod.that zugreifen, nicht aber auf dessen internes Package pkg.that.impl. Per Default sind in einem Modul keine Packages exportiert: Was ein Modul also nicht ausdrücklich mit Exports freigibt, ist für andere Module nicht sichtbar.

Listing 2 zeigt den Modul-Deskriptor von Modul mod.that, der die von mod.that exportierten Packages auflistet.

Listing 2: module-info.java von mod.that

module mod.that {
    // Export von package pkg.that
    exports pkg.that;
}

Zusammengefasst werden folgende Checks in dieser Reihenfolge ausgeführt:

  1. Zugriff auf ein anderes Modul (im Beispiel: mod.this auf mod.that): Readability, d. h. besteht eine reads-Beziehung auf das benötigte Modul?
  2. Zugriff auf ein anderes Modul (im Beispiel: mod.this auf mod.that): Accessibility, d. h. ist das Package, auf das ich (mod.this) zugreifen will, exportiert?
  3. Check der Sichtbarkeitsmodifikatoren public, protected, <package>, private.

Beim Zugriff über Modulgrenzen hinweg muss also zunächst eine statische Abhängigkeit per requires modelliert sein. Danach wird geprüft, ob ein Package per exports auch exportiert ist. Schlussendlich greifen danach als drittes die Checks auf die altbekannten Sichtbarkeitsmodifikatoren.

Diese Checks werden nicht nur zur Compile-Zeit, sondern auch zur Laufzeit ausgeführt, also insbesondere auch beim Zugriff per Reflection. Den Zugriff ausschließlich zur Laufzeit kann man mit dem neuen Keyword opens erlauben. Genaueres dazu kann man in [3] nachlesen.

Alle Regeln zum Zugriffsschutz greifen auch im JDK selbst. Mit der Einführung des Modulsystems wurde das JDK komplett modularisiert, so dass nun verschiedene JDK-Packages nicht mehr zugreifbar sind.

Module in Kotlin

In Kotlin beschränkt das keyword internal die Sichtbarkeit auf das Modul. Es kann an Klassen, Funktionen, Methoden oder Variablen stehen. Alles, was mit internal gekennzeichnet ist, ist nur innerhalb eines Moduls sichtbar. Listings 3, 4 und 5 zeigen Beispiele.

Listing 3: internal-Klasse

internal class Foo {…}

Listing 4: internal-Variablen

class Foo {
    internal val a = 42
    internal fun bar(){…}
}

Listing 5: internal-Funktion

internal fun bazz() {…}

Die Einschränkung der Sichtbarkeit auf ein Modul kann nicht über Vererbung oder per Extension Function umgangen werden.

Wenn eine Klasse oder Funktion internal ist, muss auch ihre Ableitung oder Extension Function internal (oder private) sein. Listing 6 enthält ein Beispiel für eine Extension Function

Listing 6: Die Extension Function einer internal class muss ebenfalls mindestens internal sein

internal class Foo {…}
internal fun Foo.extend() {…}

Wir haben nun gesehen, wie man die Sichtbarkeit auf ein Kotlin-Modul einschränkt. Das Modul selber wird in Kotlin als Compile-Unit gebildet. Was ein Kotlin-Modul ist (und damit die "Grenze" für den Modifikator internal) wird zur Compile-Zeit über die zu diesem Zeitpunkt angegebene Struktur definiert. Das erläutern wir an einem Beispiel.

In Abb. 5 sind mehrere Kotlin-Klassen zu sehen, verteilt auf zwei Verzeichnisse (dira und dirb) und zwei Packages (pkga und pkgb). Alle liegen im gemeinsamen Verzeichnis src. Die Klasse B1 in dirb/pkgb ist internal.

Je nachdem, wie man diese Klassen nun compiliert, kann man verschiedene Module erzeugen.

Variante 1: Wir  kompilieren ein einziges Modul mit diesem Befehl:
kotlinc src -d kmod.jar.

Wichtig ist hierbei, dass wir auf der Ebene des Verzeichnisses src kompilieren.

Alle Klassen liegen nun in einem einzigen Modul, dem Modul kmod. Alle Klassen, insbesondere A1 und A2, dürfen auf die internal-Klasse B1 zugreifen, da sie im gleichen Modul liegen. Abb. 6 verdeutlicht das.

Variante 2: Wir kompilieren (aus den gleichen Klassen, ohne irgendetwas an diesen Klassen zu ändern) zwei verschiedene Module. Das geht mit diesen Befehlen:
kotlinc src/dira -d kmoda.jar
kotlinc src/dirb -d kmodb.jar

Diesmal kompilieren wir das Verzeichnis src/dira zum Modul kmoda, bzw. das Verzeichnis src/dirb zum Modul kmodb, erhalten also zwei Module. A1 und A2 liegen im Modul kmoda, B1, B2 und B3 dagegen in kmodb. A1 und A2 dürfen nun nicht mehr auf die internal-Klasse B1 zugreifen! Abb. 7 verdeutlicht das.

In der täglichen Arbeit spielt das in der Regel keine Rolle. Entwicklungs- und Build-Werkzeuge wie IntelliJ und Maven oder Gradle organisieren den Code genauso, wie man das erwarten würde. Ein Kotlin-Modul ist ein IntelliJ-Modul ist ein Maven/Gradle-Modul.

Ein Kotlin-Modul bietet nur statischen Zugriffschutz, also zur Compile-Zeit. Zur Laufzeit, bei Zugriff per Reflection, haben Module in Kotlin keine Auswirkung. Zur Laufzeit ist der Zugriff also über Modulgrenzen hinweg möglich. Das verdeutlicht Abb. 8: Per Reflection können Klassen aus kmoda immer noch auf die internal-Klasse B1 aus kmodb zugreifen.

Vergleich der Modulsysteme von Java und Kotlin

Die Modulsysteme in Java und Kotlin sind sehr unterschiedlich, ihre Konzepte sind kaum zu vergleichen.
Der Zugriffsschutz im Java-Modulsystem ist sehr restriktiv. Per Default ist nichts über Modulgrenzen hinweg sichtbar. Was zugreifbar sein soll, muss explizit exportiert werden. In Kotlin ist dagegen alles zugreifbar und die Einschränkungen müssen explizit definiert werden.

Die Module in Java wirken sich zur Compile- und zur Laufzeit aus, in Kotlin nur zur Compile-Zeit.
Auch die Granularität, auf der der Zugriffsschutz greift, ist völlig anders. In Java sind es Packages, die exportiert werden. In Kotlin dagegen kann auf jeder Ebene bis hinunter zu einer Variablen alles als Modul-intern markiert werden. Das heißt jedoch auch, dass man jede Klasse einzeln schützen muss und keine grobgranulareren Einheiten zur Verfügung hat.

In Java ist das Modulsystem ein echtes "Add on" zu den vorhandenen Sichtbarkeitsmodifikatoren. Module werden zusätzlich definiert, die Sichtbarkeitsmodifikatoren greifen weiter innerhalb eines Moduls, bzw. als zusätzliche Prüfungen, wenn ein Package exportiert ist. In Kotlin dagegen ist das ein Entweder-oder. Internal ist nicht mit anderen Sichtbarkeitsmodifikatoren kombinierbar (konkret nicht mit protected. Die anderen ergäben ohnehin keinen Sinn).

Mit den Moduldeskriptoren als eigenes Konstrukt in Java kann man Module definieren, ohne Klassen zu ändern. Wenn man in Kotlin Module definieren will, muss man dagegen Klassen ändern, nämlich an den richtigen Stellen das internal-Keyword hinzufügen.  

Mit den Moduldeskriptoren sind in Java die Module explizit definiert. In Kotlin dagegen legt man erst mit der Compile-Unit implizit fest, was die Modulgrenzen sind, also welchen Scope der internal-Modifikator hat. Tabelle 3 fasst die Unterschiede zusammen.

Tabelle 3: Modulsystem von Java und Kotlin im Vergleich

Java Kotlin
Per Default closed. Was nicht explizit exportiert ist, ist nicht zugreifbar. Per Default open. Was nicht explizit internal ist, ist zugreifbar.
Zugriffsschutz zur Compile- und Laufzeit. Zugriffsschutz nur zur Compile-Zeit.
Zugriffsschutz ausschließlich auf Package-Ebene. Zugriffsschutz auf allen Ebenen: Funktionen, Methoden, Klassen. Package ist nicht relevant.
Modulsystem als Add-on zu den Sichtbarkeitsmodifikatoren. Entweder-Oder: Keine Kombination anderer Sichtbarkeitsmodifikatoren mit internal.
Definition über Moduldeskriptor, keine Änderung an Klassen. Modifikator im Code.
Modulbildung über explizite Beschreibung. Modulbildung implizit über gemeinsam kompilierte Klassen.

Java-Module testen

Der mit dem Java-Modulsystem eingeführte Zugriffsschutz hat Auswirkungen auf das Testen. Denn dabei benötigt man einerseits den Zugriff auf den zu testenden Code (den "Testling"), andererseits soll der Testcode getrennt von operativem Code abgelegt und verwaltet werden.

Wir unterscheiden zwischen Blackbox- und Whitebox-Tests. Blackbox-Tests testen Funktionalität ohne Kenntnis der Implementierung, also über öffentliche Schnittstellen. Whitebox-Tests dagegen testen Implementierungsdetails.

Blackbox-Tests kann man einfach in ein separates Java-Modul legen. Das Test-Modul mod.test benötigt eine reads-Beziehung auf das zu testende Modul und hat damit ausreichenden Zugriff auf die öffentliche Schnittstelle (s. Abb. 9).

Schwieriger ist die Umsetzung von Whitebox-Tests mit Java-Modulen: Weil der Whitebox-Testcode Zugriff auf das (nicht-exportierte) Implementierungsgeheimnis benötigt, muss ihm dieser Zugriff ermöglicht werden. Mit den Mitteln, die wir bisher vorgestellt haben, gibt es nur zwei Möglichkeiten.

  • Entweder man legt die Testklassen zu dem zu testenden Code dazu. Dann hat man jedoch zusätzlichen Aufwand, um die Testklassen nicht mit dem operativen Code zusammen zu deployen.
  • Oder man legt die Testklassen wie im Blackbox-Test in ein eigenes Modul und exportiert die zu testenden Packages. Damit konterkariert man jedoch den gewünschten Zugriffsschutz. Man könnte einen gerichteten Export nutzen, der das zu testende Implementierungsdetail mit seinem/n Package/s gezielt für das Test-Modul öffnet. Damit handelt man sich aber eine unschöne statische Abhängigkeit von operativem Code zu Testcode ein.

Mit Jigsaw gibt es glücklicherweise eine elegantere Lösung: Man kann die Testklassen separat ablegen und sie für den Compile der Tests sowie für die Testdurchführung in den Testling "hineinpatchen". Mit so einem Patch kann externer (Test-)Code also hinsichtlich des Zugriffsschutzes so tun, als wäre er Teil eines Moduls. Damit hat der Whitebox-Testcode vollen Zugriff auf den Testling. Genau das nutzen auch Build-Werkzeuge. Das Surefire-Plugin von Maven beispielsweise arbeitet mit dieser Patch-Option.

Kotlin-Module testen

Testcode für Blackbox-Tests kann man in Kotlin genau wie in Java in ein separates Modul legen, da die Tests nur die öffentliche Schnittstelle benötigen.

Für Whitebox-Tests macht man sich in Kotlin die Modulbildung über Compile-Units zunutze. Abb. 10 zeigt die Aufteilung von operativem und Testcode nach der Maven-Konvention in die Verzeichnisse src/main/kotlin bzw. src/test/kotlin.

Für Tests kompiliert man alle Klassen aus src/main und src/test in ein Modul, indem man das Verzeichnis src kompiliert.

kotlinc src -d kmod.jar

Das Ergebnis zeigt Abb. 11.

Ein Modul, das nur den operativen Code enthält, stellt man her, indem man das Verzeichnis src/main kompiliert, das Modul enthält nun nur den Code aus src/main.
kotlinc src/main -d kmod.jar

Das Ergebnis zeigt Abb. 12.

Auch hier nehmen einem die gängigen Werkzeuge wie IntelliJ mit Maven oder Gradle die Arbeit ab und nutzen genau diese Möglichkeit.

Zugriffschutz in Kotlin-Modulen beim Aufruf aus Java

Einer der großen Vorzüge von Kotlin ist, dass man Kotlin-Code problemlos mit Java-Code kombinieren kann. Auch dabei spielen Sichtbarkeitsmodifikatoren und Zugriffsschutz eine wichtige Rolle.

Betrachten wir zwei Kotlin-Module, kmoda und kmodb, näher. In kmodb gibt es die Klasse Foo mit der internal-Funktion bar. Diese Funktion kann also nicht aus kmoda aufgerufen werden. Abb. 13 zeigt diese Situation.

Wir kombinieren nun den Kotlin-Code mit Java-Code, indem die Funktion bazz nicht mehr direkt die Funktion bar aufruft, sondern zunächst eine Java-Klasse JavaWrapper, die im gleichen Modul liegt. Die Methode hack aus JavaWrapper ruft nun die internal-Funktion bar im anderen Modul auf. Und das ist tatsächlich erlaubt! Abbildung 14 zeigt dies.

Der in Abb. 14 dargestellte Aufruf der Kotlin-Funktion bar in der Methode hack benutzt einen veränderten Namen: Der Funktionsname hat das Suffix $kotlinInternal bekommen. Um dem Kotlin-Interpreter mitzuteilen, dass eine Funktion internal ist, benutzt Kotlin das Suffix $kotlinInternal am Funktionsnamen. Daher muss der Java-Code die Funktion unter dem Namen inklusive dem Suffix aufrufen, also als bar$kotlinInternal.

Die Erklärung für dieses Verhalten ist auf Bytecode-Ebene zu finden. Kotlin kompiliert zum gleichen JVM- Bytecode wie Java. Die JVM kennt zur Laufzeit jedoch keine Kotlin-Module und insbesondere den Modifikator internal nicht. Daher ist der Zugriffschutz zur Laufzeit nicht mehr vorhanden und man kann ihn über den Umweg über Java aushebeln.

Zugriffschutz in Java-Modulen beim Aufruf aus Kotlin

Wir schauen nun die umgekehrte Richtung an: Kann man mit dem Umweg über Kotlin auch den Zugriffsschutz von Java-Modulen aushebeln?

Die Java-Module mod.this und mod.that, sowie den Kotlin-Wrapper in mod.this zeigt Abb. 15. Das Modul mod.that exportiert keines seiner Packages. Abb. 15 zeigt das Ergebnis: Dieser Weg ist nicht möglich!

Die Erklärung gibt auch hier der Bytecode. Java-Module sind im Bytecode verankert, der Zugriffsschutz wird zur Laufzeit überprüft. Daher kann er nicht ausgehebelt werden.

Man kann Java-Module in Kotlin benutzen, wenn man diesen strengen Zugriffsschutz in seinem Kotlin-Code nutzen möchte. Dazu schreibt man einfach die Modul-Deskriptoren für Java-Module in der oben eingeführten Syntax. Sie funktionieren dann unverändert. Da der Export auf Package-Ebene gemacht wird, hat dann aber plötzlich das Package eine Bedeutung für den Zugriffsschutz in Kotlin!

Zum Abschluss fassen wir hier zusammen, wer bei den Sichtbarkeitsprüfungen "gewinnt". Das leitet sich weitgehend daraus ab, dass Kotlin-Code zu Java-Bytecode kompiliert. Die Sichtbarkeitsmodifikatoren public, protected und private bleiben im Byte-Code erhalten, sie werden also auch von Java-Code respektiert. Dagegen ist internal in Java und im Java-Byte-Code nicht bekannt, internal kompiliert zu public. Das erklärt, dass internal über Java-Code gebrochen werden kann. Umgekehrt kann Kotlin keine Zugriffsbeschränkungen von Java brechen.

Fazit

Man muss sich als Java-Programmierer, der noch nicht mit Kotlin vertraut ist, daran gewöhnen, dass public in Kotlin der Default ist. Es erfordert Disziplin, immer wieder daran zu denken, den Zugriffsschutz explizit einzuschränken. Die gebräuchlichen IDEs weisen aber mit einer Warnung darauf hin.

Die Modulsysteme in Kotlin und Java haben unterschiedliche Ziele und daher auch unterschiedliche Konzepte. Die Java-Module zielen auf die Umsetzung eines strikten Komponentenschnitts im Sinne der Trennung einer öffentlichen Schnittstelle und einer gekapselten Implementierung. Wie man die Module schneidet ist also eine bewusste Architekturentscheidung. Mark Reinhold, Chef-Architekt für Java bei Oracle, bezieht sich in seinem Blogeintrag "Jigsaw Complete" [4] explizit auf das Buch von Kirk Knoernschild [2] und nennt die Patterns eine Grundlage für das Design des Java-Modulsystems. Wie gut die Modularity Patterns sich mit dem Java-Modulsystem umsetzen lassen untersucht unser Artikel [5]. Die Architekten des Modulkonzeptes in Java hatten die Sprach- und Framework-Entwicklung im Fokus, das Modulkonzept ist strikt umgesetzt und statisch (beispielsweise werden Module immer explizit über ihren Namen referenziert, bereits zur Compile-Zeit wird geprüft, ob sie vorhanden sind).

Kotlin-Module sind dagegen sehr viel leichtgewichtiger. Mit ihnen kann man Interna kapseln. Die Entscheidung, was man kapseln möchte, trifft man nicht auf der Architekturebene, sondern auf der Implementierungsebene. Da die Module erst über Compile-Units definiert werden, sind sie zwar flexibel zusammensetzbar, doch mit dieser Flexibilität geht auch Fehleranfälligkeit einher. Kotlin-Module setzen somit ein Design-Konzept der Komponentenmodellierung von statischen Software-Bausteinen nur bedingt um.

Beide Modulsysteme stehen jedoch nicht in Konkurrenz zueinander. Sie sind in Kotlin sogar kombinierbar.

Quellen
  1. C. Szyperski: Component Software: Beyond Object-Oriented Programming. Addison Wesley. ISBN-13: 978-0321753021
  2. K. Knoernschild: Java Application Architecture: Modularity Patterns With Examples Using Osgi, ISBN-13: 978-8131775219
  3. Informatik Aktuell: R. Grammes, M. Lehmann, Dr. K. Schaal: Java 9 bringt das neue Modulsystem Jigsaw
  4. M. Reinhold: Project Jigsaw: Complete!
  5. Informatik Aktuell: M. Lehmann, Dr. K. Schaal: Modularity Patterns mit dem Java-Modulsystem Jigsaw

Autoren

Dr. Kristine Schaal

Dr. Kristine Schaal arbeitet seit fast 20 Jahren in der Softwareentwicklung und ist in Projekten der Individualentwicklung für Kunden verschiedener Branchen unterwegs, technisch überwiegend im Java-Umfeld.
>> Weiterlesen

Jan Baganz

Jan Baganz ist Softwareentwickler bei der Accso - Accelerated Solutions GmbH. Er entwickelt seit vielen Jahren Anwendungen in Java und Kotlin. Aktuell unterstützt er Kunden in der Entwicklung und dem Betrieb cloud-nativer…
>> Weiterlesen
Das könnte Sie auch interessieren

Kommentare (0)

Neuen Kommentar schreiben