Kapitel 1. Einführung

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Die Körnung einer Programmiersprache

Wie das Holz von hat auch eine Programmiersprache eine Maserung. Wenn du mit der Maserung arbeitest, läuft alles glatt. Wenn du gegen die Maserung arbeitest, wird es schwieriger. Wenn du gegen die Maserung einer Programmiersprache arbeitest, musst du mehr Code als nötig schreiben, die Leistung leidet, die Wahrscheinlichkeit, dass du Fehler einführst, steigt, du musst in der Regel bequeme Standardeinstellungen außer Kraft setzen und du musst bei jedem Schritt gegen die Werkzeuge kämpfen.

Gegen den Strom zu schwimmen, bedeutet ständige Anstrengung mit ungewissem Ausgang.

Unter zum Beispiel war es schon immer möglich, Java-Code in einem funktionalen Stil zu schreiben, aber nur wenige Programmierer taten dies vor Java 8 - aus guten Gründen.

Hier ist Kotlin-Code, der die Summe einer Liste von Zahlen berechnet, indem er die Liste mit dem Additionsoperator faltet:

val sum = numbers.fold(0, Int::plus)

Vergleichen wir das mit dem, was in Java 1.0 nötig war, um das Gleiche zu tun.

Der Nebel der Zeit wird über dich hinwegziehen und dich ins Jahr 1995 transportieren...

In Java 1.0 gibt es keine Funktionen erster Klasse, also müssen wir Funktionen als Objekte implementieren und unsere eigenen Schnittstellen für verschiedene Funktionstypen definieren. Die Additionsfunktion benötigt zum Beispiel zwei Argumente, also müssen wir den Typ von Funktionen mit zwei Argumenten definieren:

public interface Function2 {
    Object apply(Object arg1, Object arg2);
}

Dann müssen wir die Funktion fold höherer Ordnung schreiben, die die Iteration und Mutation ausblendet, die von der Klasse Vector benötigt werden. (Die Java-Standardbibliothek von 1995 enthält noch nicht das Collections Framework).

public class Vectors {
    public static Object fold(Vector l, Object initial, Function2 f) {
        Object result = initial;
        for (int i = 0; i < l.size(); i++) {
            result = f.apply(result, l.get(i));
        }
        return result;
    }

    ... and other operations on vectors
}

Wir müssen für jede Funktion, die wir an unsere fold Funktion übergeben wollen, eine eigene Klasse definieren. Der Additionsoperator kann nicht als Wert übergeben werden, und die Sprache hat zu diesem Zeitpunkt keine Methodenreferenzen, Lambdas oder Closures, nicht einmal innere Klassen. Java 1.0 hat auch keine Generics oder Autoboxing - wir müssen die Argumente auf den erwarteten Typ casten und das Boxing zwischen Referenztypen und Primitiven schreiben:

public class AddIntegers implements Function2 {
    public Object apply(Object arg1, Object arg2) {
        int i1 = ((Integer) arg1).intValue();
        int i2 = ((Integer) arg2).intValue();
        return new Integer(i1 + i2);
    }
}

Und schließlich können wir all das verwenden, um die Summe zu berechnen:

int sum = ((Integer) Vectors.fold(counts, new Integer(0), new AddIntegers()))
    .intValue();

Das ist eine Menge Aufwand für einen einzigen Ausdruck in einer Mainstream-Sprache im Jahr 2020.

Aber das ist noch nicht alles: Da Java keine Standardfunktionstypen hat, können wir verschiedene Bibliotheken, die im funktionalen Stil geschrieben wurden, nicht einfach kombinieren. Wir müssen Adapterklassen schreiben, um die Funktionstypen der verschiedenen Bibliotheken miteinander zu verknüpfen. Und da die virtuelle Maschine keinen JIT und einen einfachen Garbage Collector hat, ist unser funktionaler Code leistungsschwächer als die imperative Alternative:

int sum = 0;
for (int i = 0; i < counts.size(); i++) {
    sum += ((Integer)counts.get(i)).intValue();
}

1995 gab es einfach nicht genug Vorteile, um den Aufwand zu rechtfertigen, Java in einem funktionalen Stil zu schreiben. Java-Programmierer fanden es einfacher, imperativen Code zu schreiben, der über Sammlungen iteriert und Zustände verändert.

Das Schreiben von funktionalem Code geht Java 1.0 gegen den Strich.

Die Struktur einer Sprache bildet sich im Laufe der Zeit heraus, wenn ihre Designer und Nutzer ein gemeinsames Verständnis davon entwickeln, wie die Sprachfunktionen zusammenwirken, und ihr Verständnis und ihre Präferenzen in Bibliotheken kodieren, auf denen andere aufbauen. Die Struktur beeinflusst die Art und Weise, wie Programmierer Code in der Sprache schreiben, was wiederum die Entwicklung der Sprache und ihrer Bibliotheken und Programmierwerkzeuge beeinflusst, wodurch sich die Struktur ändert und die Art und Weise, wie Programmierer Code in der Sprache schreiben, in einem ständigen Kreislauf gegenseitiger Rückkopplung und Entwicklung verändert wird.

Zum Beispiel: Mit Java 1.1 wurden anonyme innere Klassen in die Sprache aufgenommen und mit Java 2 wurde das Collections Framework in die Standardbibliothek aufgenommen. Anonyme innere Klassen bedeuten, dass wir nicht für jede Funktion, die wir an unsere fold Funktion übergeben wollen, eine benannte Klasse schreiben müssen, aber der daraus resultierende Code ist schwieriger zu lesen:

int sum = ((Integer) Lists.fold(counts, new Integer(0),
    new Function2() {
        public Object apply(Object arg1, Object arg2) {
            int i1 = ((Integer) arg1).intValue();
            int i2 = ((Integer) arg2).intValue();
            return new Integer(i1 + i2);
        }
    })).intValue();

Funktionale Idiome gehen Java 2 immer noch gegen den Strich.

Im Jahr 2004 ist Java 5 die nächste Version, die die Sprache erheblich verändert. Sie fügt Generika und Autoboxing hinzu, die die Typensicherheit verbessern und die Anzahl der Codebausteine reduzieren:

public interface Function2<A, B, R> {
    R apply(A arg1, B arg2);
}
int sum = Lists.fold(counts, 0,
    new Function2<Integer, Integer, Integer>() {
        @Override
        public Integer apply(Integer arg1, Integer arg2) {
            return arg1 + arg2;
        }
    });

Java Entwickler verwenden oft die Guava-Bibliothek von Google, um einige gängige Funktionen höherer Ordnung über Sammlungen hinzuzufügen (obwohl fold nicht dazu gehört), aber selbst die Autoren von Guava empfehlen, standardmäßig imperativen Code zu schreiben, da dieser eine bessere Leistung hat und in der Regel einfacher zu lesen ist.

Funktionale Programmierung geht Java 5 immer noch weitgehend gegen den Strich, aber wir können den Beginn eines Trends erkennen.

Java 8 fügt der Sprache anonyme Funktionen (auch Lambda-Ausdrücke genannt) und Methodenreferenzen hinzu und erweitert die Standardbibliothek um die Streams-API. Der Compiler und die virtuelle Maschine optimieren Lambdas, um den Performance-Overhead anonymer innerer Klassen zu vermeiden. Die Streams-API umfasst funktionale Idiome und ermöglicht es endlich:

int sum = counts.stream().reduce(0, Integer::sum);

Ganz so einfach ist es jedoch nicht: Wir können den Additionsoperator immer noch nicht als Parameter an die Streams reduce Funktion übergeben, aber wir haben die Standardbibliotheksfunktion Integer::sum , die dasselbe tut. Javas Typsystem schafft immer noch unangenehme Kanten, weil es zwischen Referenz- und primitiven Typen unterscheidet. Der Streams API fehlen einige gängige Funktionen höherer Ordnung, die wir erwarten würden, wenn wir aus einer funktionalen Sprache (oder sogar Ruby) kommen. Geprüfte Ausnahmen passen nicht gut zur Streams-API und zur funktionalen Programmierung im Allgemeinen. Und um unveränderliche Klassen mit Wertesemantik zu erstellen, ist immer noch eine Menge Boilerplate-Code erforderlich. Aber mit Java 8 hat sich Java grundlegend verändert, so dass ein funktionaler Stil, wenn auch nicht ganz mit der Sprache, so doch zumindest nicht gegen sie funktioniert.

Die Versionen nach Java 8 fügen eine Reihe kleinerer Sprach- und Bibliotheksfunktionen hinzu, die mehr funktionale Programmieridiome unterstützen, aber nichts, was unsere Summenberechnung ändert. Und damit sind wir wieder in der Gegenwart.

Im Fall von Java hat sich die Sprache und die Art und Weise, wie sich Programmierer an sie anpassen, durch verschiedene Programmierstile entwickelt.

Eine Geschichte des Java-Programmierstils

Wie die antiken Dichter teilen wir die Entwicklung des Java-Programmierstils in vier verschiedene Zeitalter ein: Primeval, Beans, Enterprise und Modern.

Urzeitlicher Stil

Ursprünglich für den Einsatz in Haushaltsgeräten und interaktivem Fernsehen gedacht, kam Java erst in Schwung, als Netscape Java-Applets in seinen äußerst beliebten Navigator-Browser aufnahm. Sun veröffentlichte das Java Development Kit 1.0, Microsoft integrierte Java in den Internet Explorer, und plötzlich hatte jeder mit einem Webbrowser eine Java-Laufzeitumgebung. Das Interesse an Java als Programmiersprache explodierte.

Die Grundlagen von Java waren zu diesem Zeitpunkt bereits vorhanden:

  • Die virtuelle Maschine Java und ihr Bytecode- und Klassendateiformat

  • Primitive und Referenztypen, Nullreferenzen, Speicherbereinigung

  • Klassen und Schnittstellen, Methoden und Kontrollflussanweisungen

  • Geprüfte Ausnahmen für die Fehlerbehandlung, das abstrakte Windowing Toolkit

  • Klassen für die Vernetzung mit Internet- und Webprotokollen

  • Das Laden und Verknüpfen von Code zur Laufzeit, in einer Sandbox durch einen Sicherheitsmanager

Allerdings war Java noch nicht bereit für die allgemeine Programmierung: Die JVM war langsam und die Standardbibliothek spärlich.

Java sah aus wie eine Kreuzung aus C++ und Smalltalk, und diese beiden Sprachen beeinflussten den damaligen Java-Programmierstil. Die Konventionen "getFoo/setFoo" und "AbstractSingletonProxyFactoryBean", über die sich Programmierer anderer Sprachen lustig machen, waren noch nicht weit verbreitet.

Eine der unbesungenen Innovationen von Java war eine offizielle Kodierungskonvention, die festlegte, wie Programmierer Pakete, Klassen, Methoden und Variablen benennen sollten. C- undC++-Programmierer folgten einer scheinbar unendlichen Vielfalt von Kodierungskonventionen, und Code, der mehrere Bibliotheken kombinierte, sah am Endewie ein rechtes Hundeessen aus, das etwas inkonsistent war. Javas einzige echte Kodierungskonvention bedeutete, dass Java-Programmierer fremde Bibliotheken nahtlos in ihre Programme integrieren konnten, und förderte das Wachstum einer lebendigen Open-Source-Gemeinschaft, die bis heute anhält.

Bohnen-Stil

Nach dem anfänglichen Erfolg von machte sich Sun daran, Java zu einem praktischen Werkzeug für die Erstellung von Anwendungen zu machen. Java 1.1 (1996) fügte Sprachfunktionen hinzu (vor allem innere Klassen), verbesserte die Laufzeit (vor allem Just-in-Time-Kompilierung und Reflection) und erweiterte die Standardbibliothek. Java 1.2 (1998) fügte eine Standard-API für Sammlungen und das plattformübergreifende GUI-Framework Swing hinzu, das dafür sorgte, dass Java-Anwendungen auf jedem Desktop-Betriebssystem gleich gut aussahen und sich auch so anfühlten.

Zu dieser Zeit nahm Sun die Vorherrschaft von Microsoft und Borland bei der Softwareentwicklung in Unternehmen ins Visier. Java hatte das Potenzial, ein starker Konkurrent für Visual Basic und Delphi zu werden. Sun fügte eine Reihe von APIs hinzu, die stark von den Microsoft-APIs inspiriert waren: JDBC für den Datenbankzugriff (entspricht Microsofts ODBC), Swing für die Desktop-GUI-Programmierung (entspricht Microsofts MFC) und das Framework, das den größten Einfluss auf den Java-Programmierstil hatte, JavaBeans.

Die JavaBeans-API war Suns Antwort auf Microsofts ActiveX-Komponentenmodell für die grafische Programmierung per Drag-and-Drop. Windows-Programmierer konnten ActiveX-Komponenten in ihren Visual-Basic-Programmen verwenden oder sie in Office-Dokumente oder Webseiten im Firmenintranet einbetten. So einfach die Verwendung von ActiveX-Komponenten war, so schwierig war es, sie zu schreiben. JavaBeans waren viel einfacher: Du musstest lediglich einige zusätzliche Codierungskonventionen befolgen, damit deine Java-Klasse als "Bean" angesehen wurde, die in einem grafischen Designer instanziiert und konfiguriert werden konnte. Das Versprechen "Write once, run anywhere" bedeutete auch, dass du JavaBean-Komponenten auf jedem Betriebssystem, nicht nur aufWindows, verwenden oder verkaufen konntest.

Damit eine Klasse eine JavaBean ist, muss sie einen Konstruktor haben, der keine Argumente benötigt, serialisierbar sein und eine API deklarieren, die aus öffentlichen Eigenschaften besteht, die gelesen und optional geschrieben werden können, aus Methoden, die aufgerufen werden können, und aus Ereignissen, die von Objekten der Klasse ausgesendet werden. Die Idee war, dass Programmierer Beans in einem grafischen Anwendungsdesigner instanziieren, sie konfigurieren, indem sie ihre Eigenschaften einstellen, und die von Beans ausgesendeten Ereignisse mit den Methoden anderer Beans verbinden. Standardmäßig definierte die Beans-API Eigenschaften durch Methodenpaare, deren Namen mit get und set begannen. Diese Vorgabe konnte zwar überschrieben werden, aber dazu musste der Programmierer weitere Klassen mit Boilerplate-Code schreiben. Programmierer machten sich diese Mühe in der Regel nur, wenn sie bestehende Klassen so umrüsteten, dass sie als JavaBeans fungierten. Bei neuem Code war es viel einfacher, mit der Zeit zu gehen.

Der Nachteil des Beans-Stils ist, dass er sich stark auf veränderbare Zustände verlässt und dass mehr von diesen Zuständen öffentlich sein müssen als bei normalen Java-Objekten, weil Visual Builder-Tools keine Parameter an den Konstruktor eines Objekts übergeben konnten, sondern stattdessen Eigenschaften setzen mussten. Komponenten der Benutzeroberfläche funktionieren gut als Beans, weil sie sicher mit Standardinhalten und -stilen initialisiert und nach der Erstellung angepasst werden können. Wenn wir Klassen haben, für die es keine vernünftigen Standardwerte gibt, ist die gleiche Behandlung fehleranfällig, weil der Typprüfer uns nicht sagen kann, ob wir alle erforderlichen Werte angegeben haben. Die Beans-Konventionen erschweren das Schreiben von korrektem Code, und Änderungen in den Abhängigkeiten können den Client-Code stillschweigend kaputt machen.

Letztendlich setzte sich die grafische Gestaltung von JavaBeans nicht durch, aber die Programmierkonventionen blieben bestehen. Java-Programmierer folgten den JavaBean-Konventionen, auch wenn sie nicht die Absicht hatten, ihre Klasse als JavaBean zu verwenden. Beans hatten einen enormen, dauerhaften und nicht nur positiven Einfluss auf den Java-Programmierstil.

Unternehmensstil

Java verbreitete sich schließlich in den Unternehmen. Es löste nicht wie erwartet Visual Basic auf dem Unternehmensdesktop ab, sondern verdrängte C++ als bevorzugte serverseitige Sprache. 1998 veröffentlichte Sun die Java 2 Enterprise Edition (damals bekannt als J2EE, heute JakartaEE), eine Reihe von Standard-APIs für die Programmierung serverseitiger, transaktionsverarbeitender Systeme.

Die J2EE-APIs leiden unter der Abstraktionsinversion. Die JavaBeans- und Applets-APIs leiden ebenfalls unter der Abstraktionsinversion - beide verbieten z.B. die Übergabe von Parametern an Konstruktoren -, aber bei J2EE ist das Problem viel gravierender. J2EE-Anwendungen haben keinen einzigen Einstiegspunkt. Sie bestehen aus vielen kleinen Komponenten, deren Lebensdauer von einem Anwendungscontainer verwaltet wird und die über einen JNDI-Namensdienst miteinander verbunden sind. Die Anwendungen benötigen eine Menge Code und veränderliche Zustände, um die Ressourcen, von denen sie abhängen, zu finden. Die Programmiererinnen und Programmierer reagierten darauf, indem sie Dependency Injection (DI)-Frameworks erfanden, die die gesamte Ressourcensuche und -bindung übernahmen und die Lebenszeiten verwalteten. Das erfolgreichste dieser Frameworks ist Spring. Es baut auf den JavaBeans-Kodierungskonventionen auf und nutzt Reflexion, um Anwendungen aus Bean-ähnlichen Objekten zusammenzustellen.

Im Hinblick auf den Programmierstil ermutigen DI-Frameworks die Programmierer, die direkte Verwendung des Schlüsselworts new zu vermeiden und sich stattdessen auf das Framework zu verlassen, um Objekte zu instanziieren. Die Android-APIs weisen ebenfalls eine Abstraktionsinversion auf, und Android-Programmierer wenden sich ebenfalls an DI-Frameworks, um für die APIs zu schreiben. Der Fokus der DI-Frameworks auf Mechanismen statt auf Domänenmodellierung führt zu unternehmensgerechten Klassennamen wie dem berüchtigten AbstractSingletonProxyFactoryBean von Spring.

Positiv zu vermerken ist jedoch, dass in der Enterprise-Ära Java 5 veröffentlicht wurde, mit dem die Sprache um Generics und Autoboxing erweitert wurde - die bis dahin bedeutendste Änderung. In dieser Ära kam es auch zu einer massiven Verbreitung von Open-Source-Bibliotheken in der Java-Gemeinschaft, die durch die Maven-Paketierungskonventionen und das zentrale Paket-Repository vorangetrieben wurde. Die Verfügbarkeit erstklassiger Open-Source-Bibliotheken förderte die Akzeptanz von Java für die Entwicklung geschäftskritischer Anwendungen und führte zu weiteren Open-Source-Bibliotheken, was einen positiven Kreislauf in Gang setzte. Es folgten erstklassige Entwicklungswerkzeuge, darunter die IntelliJ IDE, die wir in diesem Buch verwenden.

Moderner Stil

Java 8 brachte die nächste große Veränderung in der Sprache - Lambdas - und bedeutende Ergänzungen in der Standardbibliothek, um sie zu nutzen. Die Streams-API förderte einen funktionalen Programmierstil, bei dem die Verarbeitung durch die Umwandlung von Strömen unveränderlicher Werte erfolgt, anstatt den Zustand veränderlicher Objekte zu verändern. Eine neue Datums-/Zeit-API ignorierte die JavaBeans-Kodierungskonventionen für Eigenschaftszugriffsmethoden und folgte den Kodierungskonventionen des Urzeitalters.

Das Wachstum der Cloud-Plattformen bedeutete, dass Programmierer ihre Server nicht mehr in JavaEE-Anwendungscontainern bereitstellen mussten. Leichtgewichtige Webanwendungs-Frameworks ermöglichten es Programmierern, eine main Funktion zu schreiben, um ihre Anwendungen zu komponieren. Viele serverseitige Programmierer haben aufgehört, DI-Frameworks zu verwenden - Funktion undObjektkomposition waren gut genug -, sodass DI-Frameworks stark vereinfachte APIs veröffentlichten, um relevant zu bleiben. Da es kein DI-Framework und keinen veränderlichen Zustand gibt, ist es weniger notwendig, dieJavaBean-Kodierungskonventionen zu befolgen. Innerhalb einer einzelnen Codebase ist es kein Problem, Felder mit unveränderlichen Werten freizugeben, da die IDE ein Feld im Handumdrehen hinter Accessoren kapseln kann, wenn diese benötigt werden.

Mit Java 9 wurden Module eingeführt, aber bisher haben sie außerhalb des JDK selbst keine große Verbreitung gefunden. Das Aufregendste an den letzten Java-Versionen war die Modularisierung des JDK und die Auslagerung selten genutzter Module wie CORBA aus dem JDK in optionale Erweiterungen.

Die Zukunft

Die Zukunft von Java verspricht weitere Funktionen, die die Anwendung von Modern Style erleichtern: Datensätze, Mustervergleiche, benutzerdefinierte Wertetypen und schließlich die Vereinheitlichung von Primitiv- und Referenztypen in einem einheitlichen Typensystem.

Dies ist jedoch ein schwieriges Unterfangen, das viele Jahre in Anspruch nehmen wird. Java begann mit einigen tief sitzenden Inkonsistenzen und Kanten, die schwer in saubere Abstraktionen zu vereinheitlichen und gleichzeitig abwärtskompatibel zu bleiben sind. Kotlin hat den Vorteil, dass wir 25 Jahre zurückblicken können und einen Neuanfang wagen können.

Die Körnung von Kotlin

Kotlin ist eine junge Sprache, aber sie hat eindeutig eine andere Maserung als Java.

Als wir dies schrieben, wurden im Abschnitt "Warum Kotlin" auf der Kotlin-Homepage vier Designziele genannt: prägnant, sicher, interoperabel und werkzeugfreundlich. Die Designer der Sprache und ihrer Standardbibliothek haben auch implizite Präferenzen festgelegt, die zu diesen Designzielen beitragen. Zu diesen Präferenzen gehören:

Kotlin zieht die Transformation unveränderlicher Daten der Mutation des Zustands vor.

Data Klassen machen es einfach, neue Typen mit Wertesemantik zu definieren. Die Standardbibliothek macht es einfacher und übersichtlicher, Sammlungen unveränderlicher Daten zu transformieren, als Daten an Ort und Stelle zu iterieren und zu mutieren.

Kotlin zieht es vor, dass das Verhalten explizit ist.

So gibt es zum Beispiel keine implizite Koerziation zwischen Typen, auch nicht von einem kleineren zu einem größeren Bereich. Java wandelt die Werte von int implizit in die Werte von long um, weil es keinen Präzisionsverlust gibt. In Kotlin musst du Int.toLong() explizit aufrufen. Die Vorliebe für Explizitheit ist besonders ausgeprägt, wenn es um den Kontrollfluss geht. Obwohl du arithmetische und Vergleichsoperatoren für deine eigenen Typen überladen kannst, kannst du die logischen Verknüpfungsoperatoren (&& und ||) nicht überladen, weil du dadurch einen anderen Kontrollfluss definieren könntest.

Kotlin bevorzugt statische gegenüber dynamischen Bindungen.

Kotlin fördert einen typsicheren, kompositorischen Programmierstil. Erweiterungsfunktionen werden statisch gebunden. Standardmäßig sind Klassen nicht erweiterbar und Methoden nicht polymorph. Du musst dich explizit für Polymorphie und Vererbung entscheiden. Wenn du Reflection verwenden willst, musst du eine plattformspezifische Bibliotheksabhängigkeit hinzufügen. Kotlin wurde von Anfang an für die Verwendung mit einer sprachsensiblen IDE entwickelt, die den Code statisch analysiert, um den Programmierer zu führen, die Navigation zu automatisieren und die Programmumwandlung zu automatisieren.

Kotlin mag keine Sonderfälle.

Im Vergleich zu Java gibt es in Kotlin weniger Sonderfälle, die auf unvorhersehbare Weise interagieren. Es gibt keine Unterscheidung zwischen Primitiv- und Referenztypen. Es gibt keinen void Typ für Funktionen, die einen Wert zurückgeben, aber nicht zurückkehren; Funktionen in Kotlin geben entweder einen Wert zurück oder kehren überhaupt nicht zurück. Mit Erweiterungsfunktionen kannst du neue Operationen zu bestehenden Typen hinzufügen, die am Aufrufpunkt gleich aussehen. Du kannst neue Kontrollstrukturen als Inline-Funktionen schreiben, und die Anweisungen break, continue und return verhalten sich genauso wie in eingebauten Kontrollstrukturen.

Kotlin bricht seine eigenen Regeln, um die Migration zu erleichtern.

Die Sprache Kotlin verfügt über Funktionen, die es ermöglichen, dass idiomatischer Java- und Kotlin-Code in derselben Codebasis koexistieren kann. Einige dieser Funktionen heben die Garantien der Typprüfung auf und sollten nur verwendet werden, um mit Legacy-Java zu interagieren. lateinit öffnet beispielsweise eine Lücke im Typsystem, so dass Java-Abhängigkeitsinjektions-Frameworks, die Objekte durch Reflexion initialisieren, Werte durch die Kapselungsgrenzen injizieren können, die normalerweise vom Compiler erzwungen werden. Wenn du eine Eigenschaft als lateinit var deklarierst, musst du sicherstellen, dass der Code die Eigenschaft initialisiert, bevor er sie liest. Der Compiler wird deine Fehler nicht bemerken.

Wenn wir, Nat und Duncan, auf den ersten Code zurückblicken, den wir in Kotlin geschrieben haben, sieht er oft wie Java in Kotlin-Syntax aus. Wir kamen zu Kotlin, nachdem wir jahrelang viel in Java geschrieben hatten, und hatten eingefahrene Gewohnheiten, die sich darauf auswirkten, wie wir Kotlin-Code schrieben. Wir schrieben unnötige Boilerplate, nutzten die Standardbibliothek nicht richtig und vermieden die Verwendung von null, weil wir noch nicht daran gewöhnt waren, dass der Typprüfer null safety erzwingt. Die Scala-Programmierer in unserem Team gingen zu weit in die andere Richtung - ihr Code sah aus, als würde Kotlin versuchen, Scala zu sein und sich als Haskell ausgeben. Keiner von uns hatte bisher den Sweet Spot gefunden, der sich aus der Arbeit mit Kotlin ergibt.

Der Weg zu idiomatischem Kotlin wird durch den Java-Code erschwert, den wir auf dem Weg dorthin weiter bearbeiten müssen. In der Praxis reicht es nicht aus, einfach nur Kotlin zu lernen. Wir müssen mit den unterschiedlichen Körnern von Java und Kotlin arbeiten und uns auf beide einlassen, während wir schrittweise von der einen zur anderen Sprache übergehen.

Refactoring zu Kotlin

Als mit der Umstellung auf Kotlin begann, waren wir für die Wartung und Verbesserung geschäftskritischer Systeme verantwortlich. Wir konnten uns nie nur auf die Konvertierung unserer Java-Codebasis nach Kotlin konzentrieren. Wir mussten den Code immer gleichzeitig mit der Anpassung des Systems an neue Geschäftsanforderungen nach Kotlin migrieren und dabei eine gemischte Java/Kotlin-Codebasis beibehalten. Wir haben das Risiko gemanagt, indem wir mit kleinen Änderungen gearbeitet haben, die leicht zu verstehen und billig zu verwerfen waren, wenn wir herausfanden, dass sie etwas kaputt gemacht haben. Unser Prozess bestand zunächst darin, Java-Code nach Kotlin zu konvertieren, so dass wir ein Java-ähnliches Design in Kotlin-Syntax erhielten. Dann haben wir schrittweise Kotlin-Spracheigenschaften angewandt, um den Code immer leichter zu verstehen, typsicherer und prägnanter zu machen und eine kompositorischere Struktur zu erhalten, die leichter zu ändern ist, ohne dass es zu unangenehmen Überraschungen kommt.

Kleine, sichere, reversible Änderungen, die das Design verbesserten: Wir haben von idiomatischem Java zu idiomatischem Kotlin refaktorisiert.

Das Refactoring zwischen verschiedenen Sprachen ist in der Regel schwieriger als das Refactoring innerhalb einer einzelnen Sprache, weil Refactoring-Tools nicht gut über die Sprachgrenzen hinweg funktionieren, wenn sie überhaupt funktionieren. Die Portierung von Logik von einer Sprache in eine andere muss manuell erfolgen, was länger dauert und mehr Risiken birgt. Sobald mehrere Sprachen im Einsatz sind, erschwert die Sprachgrenze das Refactoring, weil die IDE beim Refactoring von Code in einer Sprache abhängigen Code, der in anderen Sprachen geschrieben wurde, nicht aktualisiert, damit er kompatibel ist.

Was die Kombination von Java und Kotlin so einzigartig macht, ist die (relativ) nahtlose Grenze zwischen den beiden Sprachen. Dank des Designs der Kotlin-Sprache, der Art und Weise, wie sie auf die JVM-Plattform abgebildet wird, und der Investitionen von JetBrains in Entwicklerwerkzeuge ist das Refactoring von Java zu Kotlin und das Refactoring einer kombinierten Java/Kotlin-Codebasis fast so einfach wie das Refactoring in einer einzelnen Codebasis.

Wir haben die Erfahrung gemacht, dass wir Java zu Kotlin refaktorisieren können, ohne die Produktivität zu beeinträchtigen, und dass die Produktivität steigt, wenn wir mehr von der Codebasis auf Kotlin umstellen.

Refactoring-Prinzipien

Die Praxis des Refactorings hat einen langen Weg hinter sich, seit sie in Martin Fowlers Buch Refactoring ( Addison-Wesley , 1999 ) erstmals vorgestellt wurde : Improving the Design of Existing Code (Addison-Wesley), das 1999 veröffentlicht wurde. In diesem Buch mussten selbst einfache Refactorings wie das Umbenennen von Bezeichnern manuell durchgeführt werden, aber es wird darauf hingewiesen, dass einige moderne Entwicklungsumgebungen allmählich automatisierte Unterstützung bieten, um diese Plackerei zu reduzieren. Heutzutage erwarten wir von unseren Tools, dass sie selbst komplizierte Szenarien wie das Extrahieren einer Schnittstelle oder das Ändern von Funktionssignaturen automatisieren.

Diese einzelnen Refactorings stehen jedoch selten für sich allein. Da die Refactorings der Bausteine nun automatisch durchgeführt werden können, haben wir die Zeit und Energie, sie zu kombinieren, um größere Änderungen an unserer Codebasis vorzunehmen. Wenn die IDE keine eindeutigen Benutzeroberflächenaktionen für eine große Transformation hat, die wir durchführen wollen, müssen wir sie als eine Abfolge von granulareren Refactorings durchführen. Wir nutzen das automatische Refactoring der IDE, wann immer es möglich ist, und greifen auf die Textbearbeitung zurück, wenn die IDE eine von uns benötigte Transformation nicht automatisiert.

Es ist mühsam und fehleranfällig, Refactoring durch Editieren von Text durchzuführen. Um das Risiko und unsere Langeweile zu verringern, minimieren wir die Menge an Text, die wir bearbeiten müssen. Wenn wir Text bearbeiten müssen, ziehen wir es vor, dass die Bearbeitung nur einen einzigen Ausdruck betrifft. Also verwenden wir automatische Refactorings, um den Code so umzuwandeln, dass dies möglich ist, bearbeiten einen Ausdruck und verwenden dann automatische Refactorings, um den endgültigen Zustand wiederherzustellen, den wir anstreben.

Wenn wir zum ersten Mal ein großes Refactoring beschreiben, gehen wir es Schritt für Schritt durch und zeigen, wie sich der Code bei jedem Schritt ändert. Das nimmt ziemlich viel Platz auf der Seite ein und erfordert ein wenig Lesezeit, um zu folgen. In der Praxis sind diese großen Refactorings jedoch schnell anzuwenden. Sie dauern in der Regel ein paar Sekunden, höchstens ein paar Minuten.

Wir gehen davon aus, dass die hier veröffentlichten Refactorings mit der Verbesserung der Tools recht schnell veraltet sind. Die einzelnen IDE-Schritte können umbenannt werden, und einige Kombinationen können als eigenständige Refactorings implementiert werden. Experimentiere in deinem Kontext, um Wege zu finden, deinen Code schrittweise und sicher umzuwandeln, die besser sind als die hier vorgestellten, und teile sie dann mit der Welt.

Wir gehen von einer guten Testabdeckung aus

Wie Martin Fowler in Refactoring sagt : Improving the Design of Existing Code: "Eine gute Testabdeckung stellt sicher, dass die Codetransformationen, mit denen wir nur das Design verbessern wollen, nicht versehentlich das Verhalten unseres Systems verändert haben. In diesem Buch gehen wir davon aus, dass du über eine gute Testabdeckung verfügst. Wir gehen nicht darauf ein, wie du automatisierte Tests schreibst. Andere Autoren haben diese Themen ausführlicher behandelt, als wir es in diesem Buch tun konnten: Test-Driven Development By Example von Kent Beck (Addison-Wesley) und Growing Object-Oriented Software Guided By Tests von Steve Freeman und Nat Pryce (Addison-Wesley). Wir zeigen jedoch, wie wir Kotlin-Funktionen zur Verbesserung unserer Tests einsetzen können.

Während wir durch mehrstufige Codeumwandlungen gehen, werden wir nicht immer sagen, wann wir die Tests ausführen. Nimm an, dass wir unsere Tests nach jeder Änderung, die wir zeigen, dass sie kompiliert, ausführen, egal wie klein sie ist.

Wenn dein System nicht bereits über eine gute Testabdeckung verfügt, kann es schwierig (und teuer) sein, den Code nachträglich mit Tests zu versehen, weil die Logik, die du testen willst, mit anderen Aspekten des Systems verwoben ist. Du befindest dich in einer Henne-Ei-Situation: Du musst refaktorisieren, um Tests hinzufügen zu können, damit du sicher refaktorisieren kannst. Auch hier haben sich andere Autoren ausführlicher mit diesen Themen befasst, als wir es konnten, zum Beispiel: Working Effectively with Legacy Code von Michael Feathers (Pearson).

Wir haben weitere Bücher zu diesen Themen in der Bibliografie aufgelistet.

Wir engagieren uns für Git Bisect

Nur, denn wir geben weder explizit an, wann wir unsere Tests durchführen, noch, wann wir unsere Änderungen festschreiben. Nimm an, dass wir unsere Änderungen festschreiben, sobald sie einen Mehrwert für den Code darstellen, egal wie klein sie sind.

Wir wissen, dass unsere Testsuite nicht perfekt ist. Wenn wir versehentlich etwas kaputt machen, das von unseren Tests nicht erfasst wird, wollen wir den Commit finden, der den Fehler eingeführt hat, und ihn so schnell wie möglich beheben.

Der Befehl git bisect automatisiert diese Suche. Wir schreiben einen neuen Test, der den Fehler demonstriert, und git bisect führt eine binäre Suche in der Historie durch, um den ersten Commit zu finden, der den Test fehlschlägt.

Wenn die Commits in unserer Historie sehr umfangreich sind und ein Sammelsurium an unzusammenhängenden Änderungen enthalten, kann git bisect nicht so viel helfen, wie es könnte. Es kann nicht erkennen, welche der Quellcodeänderungen innerhalb eines Commits den Fehler verursacht hat. Wenn die Commits Refactoring und Verhaltensänderungen mischen, ist es wahrscheinlich, dass die Rückgängigmachung eines fehlerhaften Refactoring-Schrittes andere Verhaltensweisen imSystem zerstört.

Deshalb committen wir kleine, fokussierte Änderungen, die Refactorings voneinander und von Änderungen am Verhalten trennen, damit man leicht nachvollziehen kann, was sich geändert hat, und um fehlerhafte Änderungen zu beheben. Aus demselben Grund quetschen wir Commits sehr selten.

Hinweis

Wir bevorzugen es, Änderungen direkt in den Hauptzweig zu übertragen - "stammbasierte Entwicklung" -, aber es ist genauso vorteilhaft, Code in einer Abfolge kleiner, unabhängiger Übertragungen zu ändern, wenn man in Zweigen arbeitet und weniger häufig zusammenführt.

Woran arbeiten wir?

In den folgenden Kapiteln nehmen wir Beispiele aus der Codebasis von Travelator, einer fiktiven Anwendung für die Planung und Buchung von internationalen Landreisen. Unsere (immer noch fiktiven) Nutzer planen Routen per Schiff, Bahn und Straße, suchen nach Unterkünften und Sehenswürdigkeiten, vergleichen ihre Optionen nach Preis, Zeit und Spektakel und buchen schließlich ihre Reisen - alles über Web- und Mobilfrontends, die Backend-Dienste über HTTP aufrufen.

Jedes Kapitel enthält ein informatives Beispiel aus einem anderen Teil des Travelator-Systems, aber alle haben gemeinsame Konzepte: Geld, Währungsumrechnung, Reisen, Reiserouten, Buchungen und so weiter.

Unser Ziel ist es, dass dieses Buch dir wie unsere Travelator-Anwendung hilft, deine Reise von Java zu Kotlin zu planen.

Lass uns loslegen!

Genug geplaudert. Wahrscheinlich juckt es dich in den Fingern, dein Java in Kotlin umzuwandeln. Im nächsten Kapitel fangen wir damit an, die Kotlin-Unterstützung in die Build-Datei unseres Projekts zu integrieren.

Get Von Java zu Kotlin now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.