Kapitel 4. Optional zu löschbar
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Tony Hoare mag die Erfindung von Null-Referenzen als seinen Milliarden-Dollar-Fehler betrachten ,1 aber wir müssen immer noch die Abwesenheit von Dingen in unseren Softwaresystemen darstellen. Wie können wir Kotlin nutzen, um Null zu nutzen und trotzdem sichere Software zu haben?
Abwesenheit repräsentieren
Die vielleicht attraktivste Eigenschaft von Kotlin für Java-Programmierer ist die Darstellung der Nullbarkeit im Typsystem. Dies ist ein weiterer Bereich, in dem sich die Körner von Java und Kotlin unterscheiden.
Vor Java 8 verließ sich Java auf Konventionen, Dokumentation und Intuition, um zwischen Referenzen zu unterscheiden, die null sein können oder nicht. Wir können ableiten, dass Methoden, die ein Element aus einer Sammlung zurückgeben, in der Lage sein müssen, null
zurückzugeben, aber kann addressLine3
null
sein, oder verwenden wir eine leere Zeichenkette, wenn es keine Informationen gibt?
Im Laufe der Jahre haben sich deine Autoren und ihre Kollegen auf eine Konvention geeinigt, bei der davon ausgegangen wird, dass Java-Referenzen nicht null sind, es sei denn, sie sind anders gekennzeichnet. Wir könnten also ein Feld addressLine3OrNull
oder eine Methode previousAddressOrNull
nennen. Innerhalb einer Codebasis funktioniert das gut genug (auch wenn es ein wenig umständlich ist und ewige Wachsamkeit erfordert, um die Geißel NullPointerException
zu vermeiden).
Einige Codebasen entschieden sich stattdessen für die Verwendung von @Nullable
und @NotNullable
Annotationen, die oft von Tools unterstützt wurden, die die Korrektheit überprüfen. Java 8, das 2014 veröffentlicht wurde, verbesserte die Unterstützung für Annotationen in einem Maße, dass Tools wie das Checker Framework viel mehr als nur die Nullsicherheit statisch überprüfen können. Noch wichtiger ist jedoch, dass Java 8 auch einen Standardtyp Optional
einführte.
Bis hatten sich viele JVM-Entwickler mit Scala beschäftigt. Sie erkannten die Vorteile der Verwendung eines Optional-Typs (in der Scala-Standardbibliothek Option
genannt), wenn die Abwesenheit möglich war, und einfacher Referenzen, wenn dies nicht möglich war. Oracle verwirrte das Wasser, indem es die Entwickler aufforderte, Optional
nicht für Feld- oder Parameterwerte zu verwenden, aber wie bei vielen Funktionen, die in Java 8 eingeführt wurden, war es gut genug und wurde in den Mainstream von Java übernommen.
Je nach Alter wird dein Java-Code einige oder alle dieser Strategien für den Umgang mit Abwesenheit verwenden. Es ist sicherlich möglich, eine Codebasis zu haben, in der NullPointerException
praktisch nie zu sehen ist, aber die Realität ist, dass dies harte Arbeit ist. Java wird durch null belastet und durch seinen halbherzigen Optional
Typ beschämt.
Im Gegensatz dazu setzt Kotlin auf Null. Dadurch, dass die Optionalität Teil des Typsystems und nicht der Standardbibliothek ist, ist der Umgang mit fehlenden Werten in Kotlin-Codebasen erfrischend einheitlich. Es ist nicht alles perfekt: Map<K, V>.get(key)
gibt null
zurück, wenn es keinen Wert für key
gibt, aber List<T>.get(index)
wirft IndexOutOfBoundsException
, wenn es keinen Wert für index
gibt. Ebenso wirft Iterable<T>.first()
NoSuchElementException
, anstatt null
zurückzugeben. Solche Unzulänglichkeiten sind in der Regel auf den Wunsch nach Abwärtskompatibilität mit Java zurückzuführen.
Kotlin hat eigene APIs, die in der Regel gute Beispiele für die sichere Verwendung von Null zur Darstellung von optionalen Eigenschaften, Parametern und Rückgabewerten sind und von denen wir eine Menge lernen können. Wenn du einmal die Erfahrung mit der erstklassigen Nullbarkeit gemacht hast, fühlt sich die Rückkehr zu Sprachen ohne diese Unterstützung unsicher an; du bist dir bewusst, dass du immer nur eine Dereferenzierung von NullPointerException
entfernt bist und dass du dich auf Konventionen verlässt, um den sicheren Weg durch das Minenfeld zu finden.
Funktionale Programmierer von raten dir vielleicht, in Kotlin einen optionalen Typ (auch Maybe genannt) anstelle der Nullbarkeit zu verwenden. Wir raten dir davon ab, auch wenn du damit die gleichen (monadischen - da, wir haben es gesagt) Werkzeuge zur Darstellung von potenzieller Abwesenheit, Fehlern, Asynchronität usw. nutzen kannst. Ein Grund, warum du Optional
in Kotlin nicht verwenden solltest, ist, dass du dadurch den Zugang zu den Sprachfunktionen verlierst, die speziell für die Unterstützung der Nullbarkeit entwickelt wurden; in diesem Bereich unterscheidet sich Kotlin von Scala.
Ein weiterer Grund, keinen Wrapper-Typ zu verwenden, um Optionalität darzustellen, ist subtil, aber wichtig. Im Kotlin-Typsystem ist T
ein Untertyp von T?
. Wenn du einen String
hast, der nicht null sein kann, kannst du ihn immer dort verwenden, wo ein nullable String
benötigt wird. T
ist dagegen kein Untertyp von Optional<T>
. Wenn du einen String
hast und ihn einer optionalen Variablen zuweisen willst, musst du ihn zuerst in einen Optional
einpacken.
Schlimmer noch: Wenn du eine Funktion hast, die ein Optional<String>
zurückgibt, und später eine Möglichkeit entdeckst, immer ein Ergebnis zurückzugeben, wird das Ändern des Rückgabetyps in String
alle deine Clients kaputt machen. Wäre dein Rückgabetyp das löschbare String?
gewesen, hättest du ihn unter Beibehaltung der Kompatibilität in String
umwandeln können. Dasselbe gilt für Eigenschaften von Datenstrukturen: Du kannst leicht von optional zu nicht-optional mit Löschbarkeit wechseln - aber ironischerweise nicht mit Optional
.
Deine Autoren lieben Kotlins Unterstützung für Nullen und haben gelernt, sich darauf zu stützen, um viele Probleme zu lösen. Es dauert eine Weile, bis man sich das Vermeiden von Nullen abgewöhnt hat, aber wenn man es geschafft hat, gibt es buchstäblich eine ganz neue Dimension von Ausdrucksmöglichkeiten, die man erforschen und ausnutzen kann.
Es ist schade, dass Travelator diese Möglichkeit nicht bietet, also schauen wir uns an, wie wir von Java-Code mit Optional
zu Kotlin und nullable migrieren können.
Refactoring von Optional zu Nullable
Die Reisen sind in Leg
s unterteilt, wobei jede Leg
eine ununterbrochene Reise ist. Hier ist eine der Hilfsfunktionen, die wir im Code gefunden haben:
public
class
Legs
{
public
static
Optional
<
Leg
>
findLongestLegOver
(
List
<
Leg
>
legs
,
Duration
duration
)
{
Leg
result
=
null
;
for
(
Leg
leg
:
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
getPlannedDuration
())
)
{
result
=
leg
;
}
}
return
Optional
.
ofNullable
(
result
);
}
private
static
boolean
isLongerThan
(
Leg
leg
,
Duration
duration
)
{
return
leg
.
getPlannedDuration
().
compareTo
(
duration
)
>
0
;
}
}
Die Tests überprüfen, ob der Code wie vorgesehen funktioniert, und ermöglichen es uns, sein Verhalten auf einen Blick zu sehen:
public
class
LongestLegOverTests
{
private
final
List
<
Leg
>
legs
=
List
.
of
(
leg
(
"one hour"
,
Duration
.
ofHours
(
1
)),
leg
(
"one day"
,
Duration
.
ofDays
(
1
)),
leg
(
"two hours"
,
Duration
.
ofHours
(
2
))
);
private
final
Duration
oneDay
=
Duration
.
ofDays
(
1
);
@Test
public
void
is_absent_when_no_legs
()
{
assertEquals
(
Optional
.
empty
(),
findLongestLegOver
(
emptyList
(),
Duration
.
ZERO
)
);
}
@Test
public
void
is_absent_when_no_legs_long_enough
()
{
assertEquals
(
Optional
.
empty
(),
findLongestLegOver
(
legs
,
oneDay
)
);
}
@Test
public
void
is_longest_leg_when_one_match
()
{
assertEquals
(
"one day"
,
findLongestLegOver
(
legs
,
oneDay
.
minusMillis
(
1
))
.
orElseThrow
().
getDescription
()
);
}
@Test
public
void
is_longest_leg_when_more_than_one_match
()
{
assertEquals
(
"one day"
,
findLongestLegOver
(
legs
,
Duration
.
ofMinutes
(
59
))
.
orElseThrow
().
getDescription
()
);
}
...
}
Mal sehen, was wir tun können, um die Dinge in Kotlin besser zu machen. Wenn wir Legs.java
in Kotlin umwandeln, erhalten wir (nach einer kleinen Umformatierung) Folgendes:
object
Legs
{
@JvmStatic
fun
findLongestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Optional
<
Leg
>
{
var
result
:
Leg
?
=
null
for
(
leg
in
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
plannedDuration
))
result
=
leg
}
return
Optional
.
ofNullable
(
result
)
}
private
fun
isLongerThan
(
leg
:
Leg
,
duration
:
Duration
):
Boolean
{
return
leg
.
plannedDuration
.
compareTo
(
duration
)
>
0
}
}
Die Methodenparameter sind so, wie man es erwarten würde, wobei Kotlin List<Leg>
transparent ein java.util.List
akzeptiert. (Wir werden Java- und Kotlin-Sammlungen in Kapitel 6 genauer untersuchen.) Es ist erwähnenswert, dass der Compiler eine Nullprüfung vor dem Funktionskörper einfügt, wenn eine Kotlin-Funktion einen nicht-nullbaren Parameter deklariert (hierlegs
und duration
).
Auf diese Weise wissen wir sofort, wenn Java-Aufrufer eine null
einschleusen. Aufgrund dieser defensiven Prüfungen erkennt Kotlin unerwartete Nullen so nah wie möglich an der Quelle, im Gegensatz zu Java, wo eine Referenz auf null
gesetzt werden kann, die zeitlich und räumlich weit von dem Ort entfernt ist, an dem sie schließlich explodiert.
Um auf das Beispiel zurückzukommen: Die Schleife in Kotlin for
ist der in Java sehr ähnlich, mit der Ausnahme, dass das Schlüsselwort in
anstelle von :
verwendet wird, und gilt für jeden Typ, der Iterable
erweitert.
Der umgewandelte Code von findLongestLegOver
ist nicht sehr idiomatisch für Kotlin. (Seit der Einführung von Streams ist er wohl auch nicht sehr idiomatisch für Java.) Statt einer for
Schleife sollten wir nach etwas suchen, das mehr Absicht verrät, aber lassen wir das erst einmal, denn unsere Hauptaufgabe ist es, von Optional
auf nullable zu migrieren.
Wir werden das veranschaulichen, indem wir unsere Tests einen nach dem anderen umwandeln, so dass wir eine Mischung haben, wie in einerCodebasis, die wir migrieren. Um die Nullability in unseren Clients zu nutzen, müssen sie in Kotlin sein, also konvertieren wir die Tests:
class
LongestLegOverTests
{
...
@Test
fun
is_absent_when_no_legs
()
{
Assertions
.
assertEquals
(
Optional
.
empty
<
Any
>(),
findLongestLegOver
(
emptyList
(),
Duration
.
ZERO
)
)
}
@Test
fun
is_absent_when_no_legs_long_enough
()
{
Assertions
.
assertEquals
(
Optional
.
empty
<
Any
>(),
findLongestLegOver
(
legs
,
oneDay
)
)
}
@Test
fun
is_longest_leg_when_one_match
()
{
Assertions
.
assertEquals
(
"one day"
,
findLongestLegOver
(
legs
,
oneDay
.
minusMillis
(
1
))
.
orElseThrow
().
description
)
}
@Test
fun
is_longest_leg_when_more_than_one_match
()
{
Assertions
.
assertEquals
(
"one day"
,
findLongestLegOver
(
legs
,
Duration
.
ofMinutes
(
59
))
.
orElseThrow
().
description
)
}
...
}
Um nun schrittweise zu migrieren, brauchen wir zwei Versionen von findLongestLegOver
: die bestehende, die Optional<Leg>
zurückgibt, und eine neue, die Leg?
zurückgibt. Das können wir erreichen, indem wir die Eingeweide der aktuellen Implementierung extrahieren. Das ist derzeit:
@JvmStatic
fun
findLongestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Optional
<
Leg
>
{
var
result
:
Leg
?
=
null
for
(
leg
in
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
plannedDuration
))
result
=
leg
}
return
Optional
.
ofNullable
(
result
)
}
Wir "extrahieren die Funktion" an allen Stellen außer der Return-Anweisung dieser findLongestLegOver
. Wir können ihr nicht denselben Namen geben, also verwenden wir longestLegOver
; wir machen sie öffentlich, weil dies unsere neue Schnittstelle ist:
@JvmStatic
fun
findLongestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Optional
<
Leg
>
{
var
result
:
Leg
?
=
longestLegOver
(
legs
,
duration
)
return
Optional
.
ofNullable
(
result
)
}
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
var
result
:
Leg
?
=
null
for
(
leg
in
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
plannedDuration
))
result
=
leg
}
return
result
}
Das Refactoring hat eine rudimentäre result
Variable in findLongestLegOver
hinterlassen. Wir können sie auswählen und "Inline" geben:
@JvmStatic
fun
findLongestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Optional
<
Leg
>
{
return
Optional
.
ofNullable
(
longestLegOver
(
legs
,
duration
))
}
Jetzt haben wir zwei Versionen unserer Schnittstelle, von denen eine in Bezug auf die andere definiert ist. Wir können unsere Java-Clients die Optional
von findLongestLegOver
konsumieren lassen und unsere Kotlin-Clients so konvertieren, dass sie die nullable-returning longestLegOver
aufrufen. Zeigen wir die Konvertierung mit unseren Tests.
Wir nehmen uns zuerst die Abwesenden vor. Sie rufen derzeit assertEquals(Optional.empty<Any>(), findLongestLegOver…)
auf:
@Test
fun
is_absent_when_no_legs
()
{
assertEquals
(
Optional
.
empty
<
Any
>(),
findLongestLegOver
(
emptyList
(),
Duration
.
ZERO
)
)
}
@Test
fun
is_absent_when_no_legs_long_enough
()
{
assertEquals
(
Optional
.
empty
<
Any
>(),
findLongestLegOver
(
legs
,
oneDay
)
)
}
Also ändern wir sie in assertNull(longestLegOver(...)
:
@Test
fun
`
is
absent
when
no
legs
`
()
{
assertNull
(
longestLegOver
(
emptyList
(),
Duration
.
ZERO
))
}
@Test
fun
`
is
absent
when
no
legs
long
enough
`
()
{
assertNull
(
longestLegOver
(
legs
,
oneDay
))
}
Beachte, dass wir die Testnamen so geändert haben, dass sie Bezeichner in Backtick-Anführungszeichen verwenden. IntelliJ macht das für uns, wenn wir Alt-Enter auf function_names with_underscores_in_tests drücken.
Nun zu den Aufrufen, die nicht leer zurückkommen:
@Test
fun
is_longest_leg_when_one_match
()
{
assertEquals
(
"one day"
,
findLongestLegOver
(
legs
,
oneDay
.
minusMillis
(
1
))
.
orElseThrow
().
description
)
}
@Test
fun
is_longest_leg_when_more_than_one_match
()
{
assertEquals
(
"one day"
,
findLongestLegOver
(
legs
,
Duration
.
ofMinutes
(
59
))
.
orElseThrow
().
description
)
}
Das Kotlin-Äquivalent zu Optional.orElseThrow()
(auch bekannt als get()
vor Java 10) istder !!
(bang-bang oder dammit) Operator. Sowohl der Java orElseThrow
als auch der Kotlin !!
geben den Wert zurück oder werfen eine Ausnahme, wenn es keine gibt. Kotlin wirft logischerweise eine NullPointerException
. Java wirft ebenso logischerweise eine NoSuchElementExecption
; sie denken nur anders über Abwesenheit! Vorausgesetzt, wir haben uns nicht auf den Typ der Ausnahme verlassen, können wir findLongestLegOver(...).orElseThrow()
durch longestLegOver(...)!!
ersetzen:
@Test
fun
`
is
longest
leg
when
one
match
`
()
{
assertEquals
(
"one day"
,
longestLegOver
(
legs
,
oneDay
.
minusMillis
(
1
))
!!
.
description
)
}
@Test
fun
`
is
longest
leg
when
more
than
one
match
`
()
{
assertEquals
(
"one day"
,
longestLegOver
(
legs
,
Duration
.
ofMinutes
(
59
))
?.
description
)
}
Wir haben den ersten der Tests ohne Nullrückgabe (is longest leg when one match
) mit dem !!
Operator konvertiert. Wenn er fehlschlagen würde (was er nicht tut, aber wir planen diese Dinge gerne ein), würde er mit einem geworfenen NullPointerException
statt mit einer schönen Diagnose fehlschlagen. Im zweiten Fall haben wir dieses Problem mit dem sicheren Aufrufoperator ?.
gelöst, der die Auswertung nur dann fortsetzt, wenn sein Empfänger nicht null
ist. Das bedeutet, dass die Fehlermeldung folgendermaßen lautet, wenn das Bein null
ist, was viel schöner ist:
Expected :one day Actual :null
Prüfungen sind einer der wenigen Orte, an denen wir !!
in der Praxis verwenden, und selbst hier gibt es meist eine bessere Alternative.
Wir können dieses Refactoring durch unsere Clients durchführen, indem wir sie in Kotlin konvertieren und dann longestLegOver
verwenden. Sobald wir alle konvertiert haben, können wir Optional
löschen und findLongestLegOver
zurückgeben.
Refactoring zu idiomatischem Kotlin
der gesamte Code in diesem Beispiel ist Kotlin und wir haben gesehen, wie man von optional zu nullable migriert. Wir könnten an dieser Stelle aufhören, aber gemäß unserem Grundsatz, die Extrameile beim Refactoring zu gehen, machen wir weiter, um zu sehen, was uns dieser Code noch lehrt.
Hier ist die aktuelle Version von Legs
:
object
Legs
{
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
var
result
:
Leg
?
=
null
for
(
leg
in
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
plannedDuration
))
result
=
leg
}
return
result
}
private
fun
isLongerThan
(
leg
:
Leg
,
duration
:
Duration
):
Boolean
{
return
leg
.
plannedDuration
.
compareTo
(
duration
)
>
0
}
}
Die Funktionen sind in object
enthalten, weil unsere Java-Methoden statisch waren, so dass die Konvertierung einen Platz für sie brauchte. Wie wir in Kapitel 8 sehen werden, braucht Kotlin diese zusätzliche Ebene des Namensraums nicht, so dass wir "Move to top level" auf longestLegOver
wählen können. Zum Zeitpunkt der Erstellung dieses Artikels funktioniert dies nicht sehr gut, weil IntelliJ fehlschlägt, um die FunktionisLongerThan
mit der aufrufenden Funktion zu verbinden, so dass sie in Legs
verbleibt. Dieser Fehler lässt sich jedoch leicht beheben, so dass wir eine Funktion der obersten Ebene und korrigierte Referenzen im bestehenden Code haben:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
var
result
:
Leg
?
=
null
for
(
leg
in
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
plannedDuration
))
result
=
leg
}
return
result
}
private
fun
isLongerThan
(
leg
:
Leg
,
duration
:
Duration
)
=
leg
.
plannedDuration
.
compareTo
(
duration
)
>
0
Du hast vielleicht bemerkt, dass isLongerThan
seine geschweiften Klammern und die Return-Anweisung verloren hat. Wir werden in Kapitel 9 über die Vor- und Nachteile von Funktionen mit einem Ausdruck sprechen.
Wo wir schon mal hier sind, der Ausdruck isLongerThan(leg, ...)
hat etwas Seltsames an sich. Zweifellos wird dich unsere Verliebtheit in Erweiterungsfunktionen langweilen (spätestens am Ende von Kapitel 10), aber solange wir noch dein Wohlwollen haben, drücken wir die Alt-Eingabe für den Parameter leg
und "Convert parameter to receiver", damit wir leg.isLongerThan(...)
schreiben können:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
var
result
:
Leg
?
=
null
for
(
leg
in
legs
)
{
if
(
leg
.
isLongerThan
(
duration
))
if
(
result
==
null
||
leg
.
isLongerThan
(
result
.
plannedDuration
))
result
=
leg
}
return
result
}
private
fun
Leg
.
isLongerThan
(
duration
:
Duration
)
=
plannedDuration
.
compareTo
(
duration
)
>
0
Bisher waren alle unsere Änderungen strukturell, d.h. wir haben geändert, wo der Code definiert ist und wie wir ihn aufrufen. Strukturelle Refactors sind von Natur aus ziemlich (d.h. meistens, nicht vollständig) sicher. Sie können das Verhalten von Code ändern, der auf Polymorphismus (entweder durch Methoden oder Funktionen) oder Reflexion beruht, aber ansonsten, wenn der Code weiterhin kompiliert werden kann, verhält er sich wahrscheinlich.
Jetzt wenden wir uns dem Algorithmus in longestLegOver
zu. Das Refactoring von Algorithmen ist gefährlicher, vor allem von solchen, die auf Mutation beruhen, weil die Tools sie nicht gut unterstützen. Wir haben aber gute Tests, und es ist schwer herauszufinden, was der Algorithmus macht, wenn wir ihn lesen, also schauen wir mal, was wir tun können.
Der einzige Vorschlag, den IntelliJ macht, ist, compareTo
durch >
zu ersetzen. An diesem Punkt hat zumindest Duncan kein Talent mehr zum Refactoring (wenn wir tatsächlich ein Paar wären, hättest du vielleicht einen Vorschlag?) und beschließt, die Funktion von Grund auf neu zu schreiben.
Um die Funktionalität neu zu implementieren, fragen wir uns: "Was will der Code erreichen?" Die Antwort liegt hilfreicherweise im Namen der Funktion: longestLegOver
. Um diese Berechnung zu implementieren, können wir den längsten Schenkel finden und, wenn er länger als die Dauer ist, zurückgeben, andernfalls null
. Nachdem wir legs.
am Anfang der Funktion eingegeben haben, schauen wir uns die Vorschläge an und finden maxByOrNull
.
Unsere längste Strecke wird legs.maxByOrNull(Leg::plannedDuration)
sein. Die API gibt Leg?
zurück (und fügt den Ausdruck orNull
hinzu), um uns daran zu erinnern, dass sie kein Ergebnis liefern kann, wenn legs
leer ist. Wenn wir unseren Algorithmus "Finde die längste Strecke, und wenn sie länger als die Dauer ist, gib sie zurück, andernfalls null" direkt in Code umwandeln, erhalten wir:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
val
longestLeg
:
Leg
?
=
legs
.
maxByOrNull
(
Leg
::
plannedDuration
)
if
(
longestLeg
!=
null
&&
longestLeg
.
plannedDuration
>
duration
)
return
longestLeg
else
return
null
}
Das besteht die Tests, aber die mehrfachen Rückgaben sind hässlich. IntelliJ bietet hilfreicherweise an, die return
aus der if
zu entfernen:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
val
longestLeg
:
Leg
?
=
legs
.
maxByOrNull
(
Leg
::
plannedDuration
)
return
if
(
longestLeg
!=
null
&&
longestLeg
.
plannedDuration
>
duration
)
longestLeg
else
null
}
Kotlins Unterstützung für Nullbarkeit bietet verschiedene Möglichkeiten, dies zu ändern, je nachdem, was du willst.
Wir können den Elvis-Operator ?:
verwenden, der die linke Seite auswertet, es sei denn, es handelt sich um null
. In diesem Fall wird die rechte Seite ausgewertet. So können wir vorzeitig zurückkehren, wenn wir keine längste Strecke haben:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
val
longestLeg
=
legs
.
maxByOrNull
(
Leg
::
plannedDuration
)
?:
return
null
return
if
(
longestLeg
.
plannedDuration
>
duration
)
longestLeg
else
null
}
Wir könnten mit einem einzigen ?.let
Ausdruck gehen. Der ?.
wertet null
aus, wenn er mit einem null
gefüttert wird; andernfalls leitet er die längste Strecke für uns in den let
Block:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
=
legs
.
maxByOrNull
(
Leg
::
plannedDuration
)
?.
let
{
longestLeg
->
if
(
longestLeg
.
plannedDuration
>
duration
)
longestLeg
else
null
}
Innerhalb der let
kann longestLeg
also nicht null
sein. Das ist kurz und bündig, aber auf den ersten Blick schwer zu verstehen. Die Optionen mit when
zu buchstabieren, ist klarer:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
{
val
longestLeg
=
legs
.
maxByOrNull
(
Leg
::
plannedDuration
)
return
when
{
longestLeg
==
null
->
null
longestLeg
.
plannedDuration
>
duration
->
longestLeg
else
->
null
}
}
Um das Ganze weiter zu vereinfachen, brauchen wir einen Trick, den Duncan (der das hier schreibt) bisher noch nicht verinnerlicht hat:takeIf
gibt seinen Empfänger zurück, wenn ein Prädikat true
ist; andernfalls gibt er null
zurück. Das ist genau die Logik unseres vorherigen let
-Blocks. Wir können also schreiben:
fun
longestLegOver
(
legs
:
List
<
Leg
>,
duration
:
Duration
):
Leg
?
=
legs
.
maxByOrNull
(
Leg
::
plannedDuration
)
?.
takeIf
{
longestLeg
->
longestLeg
.
plannedDuration
>
duration
}
Je nachdem, wie viel Erfahrung unser Team mit Kotlin hat, könnte das zu subtil sein. Nat meint, dass es in Ordnung ist, aber wir werden uns auf die Seite der Eindeutigkeit schlagen, also bleibt die Version von when
bestehen, zumindest bis zum nächsten Refactoring hier.
Zum Schluss wandeln wir den Parameter legs
in den Empfänger in einer Erweiterungsfunktion um. So können wir die Funktion in etwas weniger Fragwürdiges umbenennen:
fun
List
<
Leg
>.
longestOver
(
duration
:
Duration
):
Leg
?
{
val
longestLeg
=
maxByOrNull
(
Leg
::
plannedDuration
)
return
when
{
longestLeg
==
null
->
null
longestLeg
.
plannedDuration
>
duration
->
longestLeg
else
->
null
}
}
Bevor wir dieses Kapitel beenden, nimm dir die Zeit, diese Version mit dem Original zu vergleichen. Gibt es irgendwelche Vorteile der alten Version?
public
class
Legs
{
public
static
Optional
<
Leg
>
findLongestLegOver
(
List
<
Leg
>
legs
,
Duration
duration
)
{
Leg
result
=
null
;
for
(
Leg
leg
:
legs
)
{
if
(
isLongerThan
(
leg
,
duration
))
if
(
result
==
null
||
isLongerThan
(
leg
,
result
.
getPlannedDuration
())
)
{
result
=
leg
;
}
}
return
Optional
.
ofNullable
(
result
);
}
private
static
boolean
isLongerThan
(
Leg
leg
,
Duration
duration
)
{
return
leg
.
getPlannedDuration
().
compareTo
(
duration
)
>
0
;
}
}
Normalerweise würden wir sagen "es kommt darauf an", aber in diesem Fall sind wir der Meinung, dass die neue Version in fast jeder Hinsicht besser ist. Sie ist kürzer und einfacher; es ist leichter zu erkennen, wie sie funktioniert; und in den meisten Fällen führt sie zu weniger Aufrufen von getPlannedDuration()
, was eine relativ teure Operation ist. Was wäre, wenn wir den gleichen Ansatz in Java gewählt hätten? Eine direkte Übersetzung ist:
public
class
Legs
{
public
static
Optional
<
Leg
>
findLongestLegOver
(
List
<
Leg
>
legs
,
Duration
duration
)
{
var
longestLeg
=
legs
.
stream
()
.
max
(
Comparator
.
comparing
(
Leg:
:
getPlannedDuration
));
if
(
longestLeg
.
isEmpty
())
{
return
Optional
.
empty
();
}
else
if
(
isLongerThan
(
longestLeg
.
get
(),
duration
))
{
return
longestLeg
;
}
else
{
return
Optional
.
empty
();
}
}
private
static
boolean
isLongerThan
(
Leg
leg
,
Duration
duration
)
{
return
leg
.
getPlannedDuration
().
compareTo
(
duration
)
>
0
;
}
}
Eigentlich ist das nicht schlecht, aber im Vergleich zur Kotlin-Version kannst du sehen, wie Optional
so ziemlich jeder Zeile der Methode Rauschen hinzufügt. Deshalb ist eine Version, die Optional.filter
verwendet, wahrscheinlich vorzuziehen, auch wenn sie unter denselben Verständnisproblemen leidet wie die Kotlin-Version takeIf
. Das heißt, Duncan kann nicht sagen, dass sie funktioniert, ohne die Tests auszuführen, aber Nat bevorzugt sie.
public
static
Optional
<
Leg
>
findLongestLegOver
(
List
<
Leg
>
legs
,
Duration
duration
)
{
return
legs
.
stream
()
.
max
(
Comparator
.
comparing
(
Leg:
:
getPlannedDuration
))
.
filter
(
leg
->
isLongerThan
(
leg
,
duration
));
}
Weiterziehen
Die Abwesenheit oder Anwesenheit von Informationen ist in unserem Code unausweichlich. Indem Kotlin sie in den Status der ersten Klasse erhebt, stellt es sicher, dass wir die Abwesenheit berücksichtigen, wenn wir es müssen, und nicht davon überwältigt werden, wenn wir es nicht müssen. Im Vergleich dazu fühlt sich Javas Optional
Typ unbeholfen an. Glücklicherweise können wir leicht von Optional
zu nullable migrieren und beides gleichzeitig unterstützen, wenn wir noch nicht bereit sind, unseren gesamten Code auf Kotlin umzustellen.
In Kapitel 10, Funktionen zu Erweiterungsfunktionen, werden wir sehen, wie nullable Typen mit anderen Kotlin-Sprachmerkmalen - den sicheren Aufruf- und Elvis-Operatoren und Erweiterungsfunktionen - kombiniert werden, um ein Design zu bilden, das sich von dem in Java unterscheidet.
Im nächsten Kapitel schauen wir uns eine typische Java-Klasse an und übersetzen sie in eine typische Kotlin-Klasse. Die Übersetzung von Java nach Kotlin ist mehr als nur syntaktisch: Die beiden Sprachen unterscheiden sich in ihrer Akzeptanz von veränderbaren Zuständen.
1 "Null-Referenzen: The Billion Dollar Mistake" auf YouTube.
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.