Als ich vor ein paar Jahren den Lightning Talk „Wat“ von Gary Bernhardt gesehen habe, war ich erheitert und abgeschreckt davon, wie sich JavaScript an der einen oder anderen Stelle verhält. Mein unerfahreneres Ich dachte, dass so etwas in Java nicht möglich ist. Über die Jahre habe ich dann allerdings an Erfahrung und Einsicht gewonnen. Auch in Java gibt es das ein oder andere überraschende Feature oder Verhalten. Genau solche Stellen wollen wir uns in dieser Kolumne anschauen.
Veränderliche Strings
Ich erinnere mich noch gut daran, dass ich bereits früh gelernt habe, dass ein
String
in Java unveränderlich ist. Ich kann seinen Inhalt also nicht ändern,
sondern muss eine neue Zuweisung tätigen, um den alten Wert einer Variablen zu
ändern.
Auf diese Eigenschaft zielen auch viele Fragen in Java-Quizzen und Bewerbungsgesprächen ab. Und obwohl diese Aussage natürlich stimmt, können wir trotzdem mithilfe von ein wenig Reflection Strings verändern, wie in Listing 1 zu sehen ist.
Wir machen uns hierzu die Eigenheit zunutze, dass Java für Strings als
Optimierung einen Pool nutzt. Manipulieren wir direkt das Feld value
eines im
Pool vorhandenen Strings, ändert sich der Wert an allen Stellen, die auf diesen
String verweisen. Typischerweise sind das mindestens alle Stellen, an denen wir
den String direkt als Literal verwenden. Es gibt allerdings, wie im Code aus
Listing 2 zu sehen, eine Ausnahme.
Obwohl mir meine Entwicklungsumgebung, mittels SonarQube, anzeigt, dass die
Verwendung von new String()
hier redundant sei, macht diese einen deutlichen
Unterschied. Mit dieser Verwendung gibt die Konsole als dritte Ausgabe nämlich
nach wie vor JavaSpektrum
aus. Ohne würde auch die Variable javaSpektrum2
auf den Wert ObjektSpektrum
geändert.
Nützlich ist dieser Trick in der Realität nicht. Ganz im Gegenteil. Wenn überhaupt könnte dies ein Einfallstor für ungewollte Manipulationen sein. Stellen wir uns vor, dass ab jetzt alle unsere HTTP-Calls an den Server eines Angreifers gehen, weil dieser Strings manipuliert hat. Verhindern lässt sich dies zum Beispiel über den Einsatz einer entsprechenden Security Manager Policy, die den Einsatz von Reflection massiv einschränkt.
Ist das noch ein Enum?
Enums sind bereits seit Version 1.5 Bestandteil von Java. Häufig verwenden wir diese wie statische Konstanten mit dem Vorteil, dass diese typsicherer sind und der Compiler uns somit an vielen Stellen hilft.
In der Regel zwar bekannt, aber meiner Erfahrung nach selten genutzt, ist die Möglichkeit, dass ein Enum auch Methoden und damit Fachlogik implementieren kann. Eher unbekannt hingegen ist, dass ein Enum auch ohne eine einzige Konstante definiert werden kann (s. Listing 3).
Auf den ersten Blick erscheint es nutzlos, ein Enum ohne Konstante zu
definieren. Für Hilfsklassen, die nur statische Methoden enthalten, ist diese
Art der Deklaration allerdings die kompakteste Variante. Enums sind nämlich
standardmäßig final
und haben auch implizit einen privaten Konstruktor, der
verhindert, dass eine Instanz erzeugt werden kann.
In der Praxis kommt diese Art der Verwendung trotzdem nicht vor. Vermutlich liegt dies an zwei Gründen. Zum einen lässt sich darüber streiten, ob ein Enum, eine Aufzählung, ohne Konstanten semantisch noch ein Enum ist. Zudem ist der Gewinn zur klassischen Variante der Hilfsklasse nur sehr gering. Die Überraschung über dieses unbekannte Konstrukt allerdings hoch.
Ein großes Nichts
Java hat sich bereits zu Beginn dafür entschieden, neben Objekten für Basistypen wie Zahlen oder Wahrheitswerte primitive Typen anzubieten. Allerdings lassen sich diese nicht an allen Stellen verwenden. Dies zeigt sich vor allem bei der Nutzung von Generics. Deshalb gibt es zu jedem primitiven Typ eine passende Klasse, die sogenannten Wrapper-Klassen. Gäbe es diese nicht, müsste es beispielsweise für jeden primitiven Typ eine spezifische Listenimplementierung geben.
Vielfach unbekannt ist, dass es neben den Wrapper-Klassen für die Zahlenwerte
und Boolean
mit Void
auch eine Wrapper-Klasse für void
gibt. In Listing 4
ist zu sehen, wie die Interfaces Supplier
und Consumer
aussähen, wenn wir
beide als besondere Art von Function
ansehen. Am Beispielcode können wir
allerdings auch bereits sehen, wieso anstatt der Nutzung von Void
in der Regel
doch spezialisierte Klassen zum Einsatz kommen.
Wir müssen für jeden Void
-Parameter ein Argument übergeben und auch für Void
als Rückgabetyp müssen wir innerhalb unserer Methode etwas zurückgeben. Der so
resultierende Code sieht somit komplizierter aus und auch die Nutzung ist nicht
so praktisch wie mit void
.
Ist this möglich
Bei der nächsten Besonderheit geht es um die Nutzung von this
. Mit this
lässt sich der Aufruf von Klassenvariablen und -methoden qualifizieren. Dies
benötigen wir vor allem, wenn beispielsweise ein Parameter einer Methode
denselben Namen wie eine Klassenvariable hat und wir auf die Variable zugreifen
wollen. Auch innerhalb von Konstruktoren benötigen wir this
, um einen anderen
Konstruktor unserer Klasse aufzurufen.
Weniger bekannt ist jedoch die Nutzung von this
als sogenannter
Receiver-Parameter, wie in Listing 5 zu sehen. Ja, richtig gesehen. In der
Methode print
gibt es einen this
benannten Parameter vom Typ unserer Klasse
This
. Trotzdem müssen wir diesen Parameter beim Aufruf innerhalb der
main
-Methode nicht übergeben und auch die Ausgabe auf der Konsole ergibt wie
erwartet This[42] 13
.
Die Möglichkeit, Receiver-Parameter einzusetzen, gibt es bereits seit Java 8 und ist in der Java Language Specification in Kapitel 8.4.1 definiert.
Diese Art der Nutzung von this
ermöglicht es uns, den Parameter mit einer
Annotation zu versehen. Somit könnten wir beispielsweise prüfen, dass gewisse
Methoden nur auf dem Client oder dem Server aufgerufen werden. Der Blog-Post
„Explicit receiver parameters“ von Stephen Colebourne deutet darüber
hinaus noch Möglichkeiten im Rahmen von Value Types an.
Diese Exception muss nicht gefangen werden
Ähnlich wie die Unveränderlichkeit von String
habe ich bereits früh gelernt,
dass Java zwei Arten von Exceptions besitzt: Checked und Runtime Exceptions.
Checked Exceptions erweitern durch Vererbung direkt oder durch eine Subklasse
java.lang.Exception
. Werfen wir eine solche Exception, müssen wir unsere
Methode zwangsläufig mit einer throws
-Klausel versehen. Dadurch wird bei jeder
Nutzung dieser Methode erzwungen, dass die geworfene Exception entweder gefangen
oder weitergeworfen wird.
Runtime Exceptions hingegen erweitern java.lang.RuntimeException
. Im Gegensatz
zu Checked Exceptions muss beim Werfen von diesen keine throws
-Klausel
definiert werden und auch ein Fangen oder Weiterwerfen wird vom Compiler nicht
erzwungen.
Das Konzept von Checked Exceptions existiert allerdings nur innerhalb der
Sprache Java. Im Bytecode und damit in der Java Virtual Machine hingegen gibt es
diese nicht. Sämtliche Prüfungen werden nur vom Compiler während der
Kompilierung geprüft. Mit einem kleinen Trick (s. Listing 6) lässt sich der
Compiler jedoch überlisten. Anschließend können wir in unserem Java-Code eine
Checked Exception werfen, ohne dass wir dazu gezwungen werden, diese zu
deklarieren und zu fangen. Die Ausgabe auf der Konsole (s. Listing 7) zeigt uns
dabei, dass wir wirklich nur eine java.lang.Exception
werfen und diese auch
genauso ankommt.
Mittlerweile hat sich für diesen Trick der Begriff Sneaky Throws etabliert.
Das liegt auch daran, dass es im Project Lombok genau für diesen Trick die
Annotation @SneakyThrows
gibt. Ich persönlich reduziere den
Einsatz dieses Tricks, egal ob mit oder ohne Lombok, auf ein Minimum. Er
produziert Code, der für viele Menschen unerwartet ist, und auch der Nutzen ist
in der Regel begrenzt. Aber gerade bei der Nutzung von Methoden als
java.util.function.Function
kann es Stellen geben, bei denen uns dieser Trick
hilft.
Klein oder groß
Häufig sind wir fasziniert von Kleinstoptimierungen und Performanz. Oft kommt
deswegen die Fragestellung auf, ob nun toLowerCase
oder toUpperCase
bei
einem String
schneller ist.
Um solche Fragestellungen beantworten zu können, bietet sich für Java das Tool JMH an. Mit diesem können wir Benchmarks schreiben. Für die obige Fragestellung habe ich den Benchmark aus Listing 8 geschrieben. Natürlich hängt das Ergebnis eines solchen Benchmarks immer vom konkreten Rechner ab. Und auch der Wert des hier genutzten zufälligen Strings hat einen Einfluss auf das Ergebnis.
Bei mir (s. Listing 9) liegen die Ergebnisse jedoch auch nach mehreren
Ausführungen so nah beieinander, dass sich daraus erst mal nichts Konkretes
ableiten lässt. Spannend ist allerdings, dass SonarQube mich darauf hinweist,
doch bitte bei beiden Methoden die überladene Methode zu nutzen, die als
Argument ein java.util.Locale
entgegennimmt.
Ergänzen wir nun den Benchmark um zwei weitere Benchmarks (s. Listing 10), sieht
das Ergebnis plötzlich anders aus (s. Listing 11). Zwar bleibt auch hier der
Unterschied zwischen toUpper
- und toLowerCase
gering, allerdings zeigt sich,
dass zwischen der Variante ohne explizite Angabe einer Locale und der mit der
Türkischen Locale ein riesiger Unterschied existiert. Dies liegt daran, dass im
Türkischen der Kleinbuchstabe für ein großes I ein kleines I ohne Punkt ist. Für
diese „Sonderregel“ greifen beide Methoden auf eine HashTable
zurück. Ähnliche
Fälle gibt es neben Türkisch auch für Aserbaidschanisch und Litauisch.
Wir sollten uns also bei der Nutzung der beiden Methoden Gedanken machen, ob diese an einer für die Performanz kritischen Stelle genutzt werden, und idealerweise nicht die Variante ohne Locale wählen.
Literale für URLs
An vielen Stellen in Java ist es möglich, eine URL direkt im Code zu verwenden. Listing 12 zeigt ein Beispiel für diese Möglichkeit. Natürlich sind Literale für URLs kein Feature von Java, sondern wir machen uns zwei Eigenschaften von Java in Kombination zunutze.
Die erste Eigenschaft sind Kommentare. Kommentare werden in Java entweder nach
//
als einzeiliger Kommentar oder zwischen /*
und */
für mehrzeilige
Kommentare geschrieben. Kombinieren wir nun einen einzeiligen Kommentar noch mit
einem Label, entstehen diese, auf den ersten Blick „magischen“,
URL-Literale.
Dieser Trick bringt keinen Vorteil, außer Leser zu irritieren. Eingesetzt habe ich diesen deswegen noch nie.
Was für ein Type
Seit Java 10 müssen wir für lokale Variablen innerhalb einer Methode in vielen
Fällen den Typ nicht mehr selbst deklarieren. Durch die Verwendung von
var leitet der Compiler den konkreten Typ selbstständig ab. Listing 13
zeigt die Zuweisung einer anonymen Instanz von java.lang.Object
an eine mit
var
deklarierte lokale Variable.
Auf den ersten Blick könnten wir nun denken, der Compiler würde für die Variable
object
als Typ java.lang.Object
wählen. Das wäre schließlich auch der Typ,
den wir vor Java 10 selbst deklariert hätten. Diese Erwartung ist jedoch falsch.
Der Compiler leitet den speziellsten ihm möglichen Typ ab. Im Falle einer
anonymen Instanz erzeugt er einen Typ speziell für diese eine Instanz und
Stelle. Eine erneute Zuweisung ist also nie wieder möglich, da er genau diesen
Typ nie wieder ableiten wird.
Noch deutlicher wird dies durch den Code aus Listing 14. Wir erweitern unsere
anonyme Instanz um eine Methode print
. Dadurch, dass der Compiler einen
speziellen Typ nur für diese Zuweisung ableitet, sind wir in der Lage, diese
Methode auch aufzurufen. Ohne var
und mit Zuweisung zu java.lang.Object
funktioniert dies nicht.
Dieses Verhalten wirkt auf den ersten Blick vollkommen konstruiert. Allerdings können wir diese Eigenschaft nutzen und einen Typ mit speziellen Methoden ad hoc erzeugen, um diesen in einem Stream zu nutzen. Hier ist dann eine zweite Zuweisung allerdings eher wieder unüblich.
Fazit
Wir haben in diesem Artikel acht Dinge gesehen, die auf den ersten Blick in Java unmöglich oder seltsam erscheinen. Dabei handelt es sich zwar um Besonderheiten, die uns nur selten über den Weg laufen werden und die zum Großteil keinen, oder nur sehr geringen, Nutzen haben. Trotzdem hoffe ich, dass zumindest einige der Besonderheiten dem einen oder anderen Leser noch nicht bekannt war und somit etwas Neues gelernt wurde oder dass zumindest das Lesen kurzweilig war.
Den Quelltext aller Listings gibt es wie gewohnt auf GitHub unter https://github.com/mvitz/javaspektrum-java-wat.