Effective Java - Joshua Bloch - E-Book

Effective Java E-Book

Joshua Bloch

0,0

Beschreibung

Seit der Vorauflage von "Effective Java", die kurz nach dem Release von Java 6 erschienen ist, hat sich Java dramatisch verändert. Dieser preisgekrönte Klassiker wurde nun gründlich aktualisiert, um die neuesten Sprach- und Bibliotheksfunktionen vorzustellen. Erneut zeigt Java-Kenner Joshua Bloch anhand von Best Practices, wie Java moderne Programmierparadigmen unterstützt. Wie in früheren Ausgaben besteht jedes Kapitel von "Effective Java" aus mehreren Themen, die jeweils in Form eines kurzen, eigenständigen Essays präsentiert werden. Dieses enthält jeweils spezifische Ratschläge, Einblicke in die Feinheiten der Java-Plattform und Codebeispiele. Umfassende Beschreibungen und Erklärungen für jedes Thema beleuchten, was zu tun ist, was nicht zu tun ist und warum es zu tun ist. Die dritte Auflage behandelt Sprach- und Bibliotheksfunktionen, die in Java 7, 8 und 9 hinzugefügt wurden, einschließlich der funktionalen Programmierkonstrukte. Neue Themen sind unter anderem: - Functional Interfaces, Lambda-Ausdrücke, Methodenreferenzen und Streams - Default- und statische Methoden in Interfaces - Type Inference, einschließlich des Diamond-Operators für generische Typen - Die Annotation @SafeVarargs - Das Try-with-Resources-Statement - Neue Bibliotheksfunktionen wie das Optional Interface, java.time und die Convenience-Factory-Methoden für Collections

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern

Seitenzahl: 674

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Joshua Bloch ist Professor an der Carnegie Mellon University. Er war früher Chief Java Architect bei Google, ein Distinguished Engineer bei Sun Microsystems sowie Senior System Designer bei Transarc. Er leitete das Design und die Implementierung zahlreicher Java-Plattformfunktionen, einschließlich der JDK-5.0-Spracherweiterungen und des Java-Collections-Frameworks. Er hat einen Ph.D. in Informatik von der Carnegie Mellon University und einen B.S. in Informatik von der Columbia University.

Zu diesem Buch – sowie zu vielen weiteren dpunkt.büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei dpunkt.plus+:

www.dpunkt.plus

Joshua Bloch

Effective Java

Best Practices für die Java-Plattform

Joshua Bloch

Lektorat: Melanie Feldmann

Übersetzung: Dirk Louis

Copy-Editing: Claudia Lötschert, www.richtiger-text.de

Satz: Gerhard Alfes, mediaService, Siegen, www.mediaservice.tv

Herstellung: Stefanie Weidner

Umschlaggestaltung: Helmut Kraus, www.exclam.de

Bibliografische Information der Deutschen Nationalbibliothek

Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.

ISBN:

Buch 978-3-86490-578-0

PDF      978-3-96088-638-9

ePub    978-3-96088-639-6

mobi    978-3-96088-640-2

Übersetzung der englischsprachigen 3. Originalausgabe 2018

Translation Copyright für die deutschsprachige Ausgabe © dpunkt.verlag GmbH

Wieblinger Weg 17

69123 Heidelberg

978-0134685991

Authorized translation from the English language edition, entitled EFFECTIVE JAVA, 3rd Edition by JOSHUA BLOCH, published by Pearson Education, Inc, publishing as Addison-Wesley Professional, Copyright © 2018 Pearson Education, Inc

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

German language edition published by DPUNKT.VERLAG GMBH, Copyright © 2018

Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.

Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.

Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.

5 4 3 2 1 0

Inhaltsverzeichnis

Vorbemerkung

Vorwort

Danksagung

1Einleitung

2Objekte erzeugen und auflösen

2.1Thema 1: Statische Factory-Methoden als Alternative zu Konstruktoren

2.2Thema 2: Erwägen Sie bei zu vielen Konstruktorparametern den Einsatz eines Builders

2.3Thema 3: Erzwingen Sie die Singleton-Eigenschaft mit einem private-Konstruktor oder einem Aufzählungstyp

2.4Thema 4: Erzwingen Sie die Nicht-Instanziierbarkeit mit einem private-Konstruktor

2.5Thema 5: Arbeiten Sie mit Dependency Injection statt Ressourcen direkt einzubinden

2.6Thema 6: Vermeiden Sie die Erzeugung unnötiger Objekte

2.7Thema 7: Löschen Sie veraltete Objektreferenzen

2.8Thema 8: Vermeiden Sie Finalizer und Cleaner

2.9Thema 9: Verwenden Sie try-with-resources anstelle von try-finally

3Methoden, die allen Objekten gemeinsam sind

3.1Thema 10: Halten Sie beim Überschreiben von equals den allgemeinen Vertrag ein

3.2Thema 11: Überschreiben Sie, wenn Sie equals überschreiben, immer auch hashCode

3.3Thema 12: Überschreiben Sie immer toString

3.4Thema 13: Vorsicht beim Überschreiben von clone

3.5Thema 14: Denken Sie darüber nach, Comparable zu implementieren

4Klassen und Schnittstellen

4.1Thema 15: Minimieren Sie den Zugriff auf Klassen und Member

4.2Thema 16: Verwenden Sie in öffentlichen Klassen Accessor-Methoden und keine öffentlichen Felder

4.3Thema 17: Bevorzugen Sie unveränderliche Klassen

4.4Thema 18: Ziehen Sie Komposition der Vererbung vor

4.5Thema 19: Entwerfen und dokumentieren Sie für Vererbung oder verbieten Sie sie

4.6Thema 20: Geben Sie Schnittstellen den Vorzug vor abstrakten Klassen

4.7Thema 21: Entwerfen Sie Ihre Schnittstellen für die Nachwelt

4.8Thema 22: Verwenden Sie Schnittstellen nur zum Definieren von Typen

4.9Thema 23: Arbeiten Sie mit Klassenhierarchien statt mit Tag-Klassen

4.10Thema 24: Ziehen Sie statische Member-Klassen den nicht-statischen vor

4.11Thema 25: Beschränken Sie Quelltextdateien auf eine einzige Toplevel-Klasse

5Java Generics

5.1Thema 26: Hände weg von Rohtypen

5.2Thema 27: Eliminieren Sie unchecked-Warnungen

5.3Thema 28: Verwenden Sie Listen statt Arrays

5.4Thema 29: Bevorzugen Sie generische Typen

5.5Thema 30: Bevorzugen Sie generische Methoden

5.6Thema 31: Eingeschränkte Wildcard-Typen machen Ihre APIs flexibler

5.7Thema 32: Vorsicht beim Kombinieren von Java Generics mit varargs-Methoden

5.8Thema 33: Nutzen Sie typsichere heterogene Container

6Aufzählungen und Annotationen

6.1Thema 34: Verwenden Sie Aufzählungen statt int-Konstanten

6.2Thema 35: Verwenden Sie Instanzfelder statt Ordinalzahlen

6.3Thema 36: Verwenden Sie EnumSet statt Bitfelder

6.4Thema 37: Verwenden Sie EnumMap statt Ordinalzahlindizierung

6.5Thema 38: Emulieren Sie erweiterbare Enums mit Schnittstellen

6.6Thema 39: Ziehen Sie die Annotationen den Namensmustern vor

6.7Thema 40: Verwenden Sie konsequent die Annotation Override

6.8Thema 41: Definieren Sie Typen mit Markierungsschnittstellen

7Lambdas und Streams

7.1Thema 42: Lambdas sind oft besser als anonyme Klassen

7.2Thema 43: Denken Sie an Methodenreferenzen als Alternative zu Lambdas

7.3Thema 44: Verwenden Sie nach Möglichkeit die funktionalen Schnittstellen aus dem Standard

7.4Thema 45: Setzen Sie Streams mit Bedacht ein

7.5Thema 46: Bevorzugen Sie in Streams Funktionen ohne Nebeneffekte

7.6Thema 47: Verwenden Sie als Rückgabewert eher Collection als Stream

7.7Thema 48: Seien Sie vorsichtig, wenn Sie Streams parallelisieren

8Methoden

8.1Thema 49: Prüfen Sie Parameter auf Gültigkeit

8.2Thema 50: Erstellen Sie bei Bedarf defensive Kopien

8.3Thema 51: Entwerfen Sie Methodensignaturen sorgfältig

8.4Thema 52: Verwenden Sie Überladung mit Bedacht

8.5Thema 53: Verwenden Sie varargs mit Bedacht

8.6Thema 54: Geben Sie nicht null, sondern leere Sammlungen oder Arrays zurück

8.7Thema 55: Verwenden Sie den Rückgabetyp Optional mit Bedacht

8.8Thema 56: Schreiben Sie Doc-Kommentare für alle offengelegten API-Elemente

9Allgemeine Programmierung

9.1Thema 57: Minimieren Sie den Gültigkeitsbereich lokaler Variablen

9.2Thema 58: Ziehen Sie for-each-Schleifen den traditionellen for-Schleifen vor

9.3Thema 59: Machen Sie sich mit den Bibliotheken vertraut und nutzen Sie sie

9.4Thema 60: Vermeiden Sie float und double, wenn genaue Antworten benötigt werden

9.5Thema 61: Ziehen Sie die elementaren Datentypen den Wrapper-Typen vor

9.6Thema 62: Vermeiden Sie Strings, wenn andere Typen besser geeignet sind

9.7Thema 63: Denken Sie an die Leistungseinbußen bei der String-Verkettung

9.8Thema 64: Referenzieren Sie Objekte über ihre Schnittstellen

9.9Thema 65: Ziehen Sie Schnittstellen der Java Reflection vor

9.10Thema 66: Vorsicht bei der Arbeit mit nativen Methoden

9.11Thema 67: Optimieren Sie mit Bedacht

9.12Thema 68: Halten Sie sich an die allgemein anerkannten Namenskonventionen

10Ausnahmen

10.1Thema 69: Verwenden Sie Ausnahmen nur für Ausnahmebedingungen

10.2Thema 70: Verwenden Sie geprüfte Ausnahmen für behebbare Situationen und Laufzeitausnahmen für Programmierfehler

10.3Thema 71: Vermeiden Sie den unnötigen Einsatz von geprüften Ausnahmen

10.4Thema 72: Ziehen Sie Standardausnahmen vor

10.5Thema 73: Werfen Sie Ausnahmen passend zur Abstraktion

10.6Thema 74: Dokumentieren Sie alle Ausnahmen, die jede Methode auslöst

10.7Thema 75: Geben Sie in Detailnachrichten Fehlerinformationen an

10.8Thema 76: Streben Sie nach Fehleratomizität

10.9Thema 77: Ignorieren Sie Ausnahmen nicht

11Nebenläufigkeit

11.1Thema 78: Synchronisieren Sie den Zugriff auf gemeinsam genutzte, veränderliche Daten

11.2Thema 79: Vermeiden Sie übermäßige Synchronisation

11.3Thema 80: Ziehen Sie Exekutoren, Aufgaben und Streams den Threads vor

11.4Thema 81: Ziehen Sie die Nebenläufigkeitsdienste den Methoden wait und notify vor

11.5Thema 82: Dokumentieren Sie die Thread-Sicherheit

11.6Thema 83: Verwenden Sie die späte Initialisierung mit Bedacht

11.7Thema 84: Verlassen Sie sich nicht auf den Thread-Planer

12Serialisierung

12.1Thema 85: Verwenden Sie statt der Java-Serialisierung besser deren Alternativen

12.2Thema 86: Implementieren Sie Serializable mit großer Vorsicht

12.3Thema 87: Verwenden Sie möglichst eine eigene serialisierte Form

12.4Thema 88: Implementieren Sie readObject defensiv

12.5Thema 89: Ziehen Sie zur Instanzenkontrolle die Aufzählungstypen der Methode readResolve vor

12.6Thema 90: Verwenden Sie möglichst Serialisierungs-Proxys anstelle von serialisierten Instanzen

Index

Literatur

Vorbemerkung

Wenn ein Kollege Ihnen sagen würde: »Ehefrau von mir heute Abend bereitet ungewöhnlich Essen in einem Heim. Du willst mitmachen?«, würden Ihnen wahrscheinlich drei Dinge in den Sinn kommen: Erstens, dass Sie zum Essen eingeladen wurden, zweitens, dass Deutsch nicht die Muttersprache Ihres Kollegen ist, und drittens, dass diese Einladung ziemlich verwirrend ist.

Wenn Sie selbst schon einmal eine Fremdsprache gelernt und dann mutig Ihre Sprachkenntnisse außerhalb des Unterrichts angewendet haben, wissen Sie, dass Sie drei Dinge beherrschen müssen: die Struktur der Sprache (Grammatik), die Benennung der Dinge, über die Sie sprechen möchten (Wortschatz), und die landesübliche Art und Weise, alltägliche Dinge auszudrücken (idiomatische Verwendung). Allzu oft werden nur die ersten beiden Punkte im Unterricht behandelt, sodass es nicht verwundert, dass Muttersprachler ständig ihr Lachen unterdrücken, wenn Sie versuchen, sich verständlich zu machen.

Ähnlich verhält es sich mit einer Programmiersprache. Sie müssen wissen, welches Paradigma der Sprache zugrunde liegt: Ist sie algorithmisch, funktional, objektorientiert? Sie müssen das Vokabular kennen: Welche Datenstrukturen, Operationen und Mechanismen stehen Ihnen in den Standardbibliotheken zur Verfügung? Und Sie müssen sich mit den sprachspezifischen Möglichkeiten zur Strukturierung Ihres Codes auskennen. Bücher über Programmiersprachen befassen sich häufig nur mit den ersten beiden Aspekten, und wenn sie auf die Nutzung eingehen, dann nur punktuell. Vielleicht liegt das daran, dass man die ersten zwei Aspekte leichter beschreiben kann. Grammatik und Vokabular sind ausschließlich Eigenschaften der Sprache, während die Nutzung charakteristisch für eine Community ist, die mit der Sprache arbeitet.

Die Programmiersprache Java ist zum Beispiel objektorientiert, mit Einfachvererbung und imperativem (anweisungsorientiertem) Programmierstil innerhalb der Methoden. Ihre Bibliotheken bieten Unterstützung für grafische Darstellung, Vernetzung, verteiltes Rechnen und Sicherheit. Aber wie nutzt man die Sprache am besten in der Praxis?

Und es gibt noch einen anderen Punkt. Im Gegensatz zur gesprochenen Sprache und den meisten Büchern und Zeitschriften sind Programme im Laufe der Zeit immer wieder Änderungen unterworfen. Normalerweise reicht es nicht, Code zu produzieren, der effizient läuft und von anderen Programmierern leicht verstanden wird. Der Code muss auch so organisiert sein, dass er leicht zu modifizieren ist. Es gibt wahrscheinlich zehn Varianten, Code für eine Aufgabe T zu schreiben. Von diesen zehn Varianten sind sieben unhandlich, ineffizient oder verwirrend. Doch welche der drei restlichen Varianten entspricht am ehesten dem Code, der im nächsten Software-Release zur Lösung der Aufgabe T’ benötigt wird?

Es gibt zahlreiche Bücher, mit denen Sie die Grammatik der Programmiersprache Java erlernen können, einschließlich The JavaTMProgramming Language von Arnold, Gosling und Holmes, oder The JavaTMLanguage Specification von Gosling, Joy, meiner Wenigkeit und Bracha. Ebenso gibt es Dutzende von Büchern über die Bibliotheken und APIs, die Ihnen beim Programmieren in Java zur Verfügung stehen.

Dieses Buch deckt jedoch den dritten Aspekt ab: die normale und effektive Nutzung von Java. Josua Bloch hat Jahre bei Sun Microsystems damit verbracht, die Programmiersprache Java zu erweitern, zu implementieren und einzusetzen, und dabei viele Code-Beispiele anderer Programmierer gelesen, einschließlich meiner eigenen. Sein Buch ist eine übersichtliche Zusammenstellung von Tipps, die Ihnen helfen, Ihren Code so zu strukturieren, dass er gut funktioniert, andere ihn leicht verstehen, zukünftige Änderungen und Verbesserungen weniger Kopfschmerzen bereiten und vielleicht sogar, dass am Ende schöne, elegante und ansprechende Programme dabei herauskommen.

Guy L. Steele Jr.

Burlington, Massachusetts

April 2001

Vorwort

1997, als Java noch in den Anfängen steckte, beschrieb James Gosling, der Vater von Java, die Sprache als eine Arbeitssprache, die ziemlich einfach sei [Gosling97]. Etwa zur gleichen Zeit, beschrieb Bjarne Stroustrup (der Vater von C++) C++ als eine Multi-Paradigmen-Sprache, die »sich bewusst von Sprachen unterscheidet, die vom Entwurf her nur einen einzigen Weg, Programme zu schreiben, unterstützen« [Stroustrup95]. Stroustrup warnte:

Die relative Einfachheit von Java ist – wie bei den meisten neuen Sprachen – teils eine Illusion, teils auf ihre Unvollständigkeit zurückzuführen. Mit der Zeit wird Java deutlich an Umfang und Komplexität zunehmen. Ihr Umfang wird sich verdoppeln oder verdreifachen und die Zahl der implementierungsabhängigen Erweiterungen oder Bibliotheken wird wachsen.

[Stroustrup]

Heute, zwanzig Jahre später, kann man sagen, dass Gosling und Stroustrup beide recht hatten. Java ist inzwischen umfangreich und komplex, mit einer Vielzahl an Abstraktionen zum Beispiel für parallele Ausführung, Iteration und Repräsentation von Datum und Uhrzeit.

Ich mag Java immer noch, obwohl sich meine Begeisterung mit zunehmendem Umfang der Plattform etwas gelegt hat. Doch je umfangreicher und komplexer Java wird, desto notwendiger ist ein Handbuch der aktuellen Best Practices. Mit dieser dritten Ausgabe von Effective Java habe ich mein Bestes getan, Ihnen ein solches Handbuch an die Hand zu geben. Ich hoffe, dass diese Ausgabe den Ansprüchen der Leser weiterhin gerecht wird und dabei dem Geist der ersten beiden Ausgaben treu bleibt.

Klein ist schön, aber einfach ist nicht leicht.

San Jose, Kalifornien

November 2017

PS: Ich möchte nicht versäumen eine branchenweite Best Practice zu erwähnen, der ich in letzter Zeit ziemlich viel Zeit gewidmet habe. Seit den Anfängen unseres Berufsstands in den 1950ern haben wir unsere APIs anderen zur kostenlosen Reimplementierung zur Verfügung gestellt. Diese Praxis hatte entscheidenden Anteil an der rasanten Entwicklung der Computertechnologie. Ich bin bestrebt, dass dies so bleibt [CompSci17], und möchte Sie ermutigen, sich mir anzuschließen. Es ist für die Weiterentwicklung unseres Berufsstands immens wichtig, dass wir uns das Recht vorbehalten, die APIs anderer kostenlos zu reimplementieren.

Danksagung

Ich danke den Lesern der ersten beiden Ausgaben, dass sie dieses Buch mit so viel Freude und Begeisterung aufgenommen und seine Vorschläge beherzigt haben. Danke auch, dass ihr mich habt wissen lassen, was für einen positiven Einfluss das Buch auf euch und eure Arbeit hatte. Ich danke den vielen Professoren, die das Buch ihren Kursen zugrunde legten, und den vielen Programmiererteams, die es als Referenz nutzten.

Ich danke dem ganzen Team von Addison-Wesley und Pearson, das es auch bei hohem Zeitdruck nie an Freundlichkeit, Professionalität, Geduld und Nachsicht mangeln ließ. Die ganze Zeit über konnte meinen Lektor Greg Doench nichts aus der Ruhe bringen: ein hervorragender Lektor und ein wahrer Gentleman. Ich fürchte, dieses Projekt hat ihn ein paar graue Haare beschert, und möchte mich dafür an dieser Stelle entschuldigen. Mit meiner Projektmanagerin Julie Nahil und meiner Projektlektorin Dana Wilson hätte ich es nicht besser treffen können: Sie waren fleißig, prompt, organisiert und freundlich. Meine Korrektorin Kim Wimpsett war akribisch und stilsicher.

Und auch dieses Mal hatte ich das beste Korrektorenteam an meiner Seite, das man sich nur vorstellen kann. Ihnen allen danke ich von ganzem Herzen. Zu dem Kernteam, das fast alle Kapitel gelesen hat, gehören Cindy Bloch, Brian Kernighan, Kevin Bourrillion, Joe Bowbeer, William Chargin, Joe Darcy, Brian Goetz, Tim Halloran, Stuart Marks, Tim Peierls, und Yoshiki Shibata. Weitere Korrektoren waren Marcus Biel, Dan Bloch, Beth Bottos, Martin Buchholz, Michael Diamond, Charlie Garrod, Tom Hawtin, Doug Lea, Aleksey Shipilëv, Lou Wasserman und Peter Weinberger. Ihre zahlreichen Vorschläge haben dieses Buch erheblich verbessert und mich vor vielen Peinlichkeiten bewahrt.

Mein besonderer Dank gilt William Chargin, Doug Lea und Tim Peierls, die viele der Gedanken aus diesem Buch prüften und nicht nur viel Zeit investierten, sondern mich auch an ihrem Wissen partizipieren ließen.

Und schließlich möchte ich meiner Frau Cindy Bloch danken, dass sie mich ermutigt hat, dieses Buch zu schreiben. Sie hat jedes Thema in der Rohfassung gelesen, den Index erstellt, mir bei allem geholfen, was bei einem großen Projekt so anfällt, und meine Launen beim Schreiben ertragen.

1Einleitung

Dieses Buch soll Ihnen helfen, die Programmiersprache Java und ihre grundlegenden Bibliotheken wie java.lang, java.util und java.io sowie deren Unterpakete wie java.util.concurrent und java.util.function effektiv zu nutzen. Gelegentlich wird, wo nötig, auch auf andere Bibliotheken eingegangen.

Dieses Buch besteht aus neunzig Themen, die jeweils eine Regel vermitteln. Die Regeln beschreiben Praktiken, die die besten und erfahrensten Programmierer als nützlich erachten. Die Themen sind in den folgenden elf Kapiteln lose zusammengefasst, von denen jedes einen umfangreichen Aspekt des Software-designs abdeckt. Das Buch ist nicht dafür konzipiert, um von Anfang bis Ende gelesen zu werden: Jedes Thema steht mehr oder weniger für sich allein. Die Themen sind mit vielen Querverweisen versehen, die Ihnen helfen, Ihrem eigenen Weg durch das Buch zu folgen.

Seit der letzten Ausgabe dieses Buchs wurden viele neue Features in die Plattform integriert. Die meisten Themen in diesem Buch gehen in irgendeiner Weise auf diese Features ein. Die folgende Tabelle zeigt Ihnen, wo Sie die wichtigsten Features ausführlicher besprochen werden:

Feature

Thema

Version

Lambdas

Themen 42–44

Java 8

Streams

Themen 45–48

Java 8

Optionale

Thema 55

Java 8

Standardmethoden in Schnittstellen

Thema 21

Java 8

try-with-resources

Thema 9

Java 7

@SafeVarargs

Thema 32

Java 7

Module

Thema 15

Java 9

Die meisten Themen werden anhand von Programmbeispielen veranschaulicht. Ein Charakteristikum dieses Buchs ist dabei, dass seine Code-Beispiele viele Designmuster und Idiome illustrieren. Wo es angebracht ist, finden Sie zudem Querverweise auf das dem Thema entsprechende Standardwerk [Gamma95].

Viele Themen enthalten auch ein oder mehrere Negativbeispiele aus der Programmierpraxis. Solche Beispiele, die manchmal auch als Anti-Pattern bezeichnet werden, sind eindeutig mit einem Kommentar wie // So nicht! versehen. In allen Fällen wird erklärt, warum das Beispiel schlecht ist, und ein alternativer Ansatz aufgezeigt.

Dieses Buch ist nicht für Anfänger gedacht: Es geht davon aus, dass Sie sich bereits mit Java auskennen. Wenn das nicht der Fall ist, sollten Sie eine der vielen sehr guten Einführungen, wie Java Precisely von Peter Sestoft [Sestoft16], in Betracht ziehen. Effective Java ist so konzipiert, dass jeder mit ausreichenden Sprachkenntnissen etwas damit anfangen kann, dennoch kann es auch fortgeschrittenen Programmierern eine Hilfe sein und Denkanstöße liefern.

Den meisten Regeln in diesem Buch liegen einige wenige Grundprinzipien zugrunde. Das Hauptaugenmerk liegt auf Klarheit und Einfachheit. Der Benutzer einer Komponente sollte nie von ihrem Verhalten überrascht werden. Die Komponenten selbst sollten so klein wie möglich, aber nicht kleiner sein. In diesem Buch bezieht sich der Begriff Komponente auf jedes wiederverwendbare Softwareelement, von einer einzelnen Methode bis hin zu einem komplexen Framework, das aus mehreren Paketen besteht. Code sollte wiederverwendet und nicht kopiert werden. Die Abhängigkeiten zwischen den Komponenten sollten auf ein Minimum beschränkt sein. Fehler sollten nach ihrem Auftreten so schnell wie möglich erkannt werden, im Idealfall zur Kompilierzeit.

Die Regeln in diesem Buch treffen zwar nicht in hundert Prozent der Fälle zu, erweisen sich aber in den allermeisten Fällen als beste Programmierpraxis. Dennoch sollten Sie diese Regeln nicht sklavisch befolgen; wenn es einen guten Grund gibt, dürfen Sie sie auch gelegentlich verletzen. Beim Programmieren lernen sollten Sie, wie in den meisten anderen Disziplinen, zuerst die Regeln lernen und dann, wann man sie verletzen darf.

Meistens geht es in diesem Buch nicht um Performance, sondern darum, Programme zu schreiben, die klar, korrekt, stabil, flexibel sowie benutzer- und wartungsfreundlich sind. Wenn Sie das hinbekommen, ist es in der Regel relativ einfach, die gewünschte Leistung zu erhalten (Thema 67). Einige Themen befassen sich mit Performance-Problemen und liefern zum Teil sogar Performance-Zahlen. Diese Zahlen, die mit »Auf meinem Rechner« eingeleitet werden, sind bestenfalls als approximative Werte zu lesen.

Für Interessierte: Mein Rechner ist ein schon älterer, selbst zusammengebastelter 3,5 GHz Quad-Core Intel Core i7-4770K mit 16 Gigabyte DDR3-1866 CL9 RAM, auf dem die Azul Zulu-Version 9.0.0.0.15 des OpenJDK unter Microsoft Windows 7 Professional SP1 (64-bit) installiert ist.

Bei der Diskussion von Features der Programmiersprache Java und ihrer Bibliotheken ist es manchmal notwendig, auf bestimmte Versionen zu verweisen. Der Einfachheit halber werden in diesem Buch Kurzbezeichnungen anstelle der offiziellen Versionsnamen verwendet. Diese Tabelle zeigt eine Zuordnung der Versionsnamen zu den Kurzbezeichnungen:

Offizieller Versionsname

Kurzbezeichnung

JDK 1.0.x

Java 1.0

JDK 1.1.x

Java 1.1

Java 2 Platform, Standard Edition, v1.2

Java 2

Java 2 Platform, Standard Edition, v1.3

Java 3

Java 2 Platform, Standard Edition, v1.4

Java 4

Java 2 Platform, Standard Edition, v5.0

Java 5

Java Platform, Standard Edition 6

Java 6

Java Platform, Standard Edition 7

Java 7

Java Platform, Standard Edition 8

Java 8

Java Platform, Standard Edition 9

Java 9

Die Beispiele sind einigermaßen vollständig, aber die Lesbarkeit hat Vorrang gegenüber der Vollständigkeit. Sie verwenden Klassen aus den Paketen java.util und java.io. Um Beispiele zu kompilieren, müssen Sie eventuell eine oder mehrere Importdeklarationen oder ähnlichen Boilerplate-Code hinzufügen. Auf der Website des Buchs, http://joshbloch.com/effectivejava, finden Sie zu jedem Beispiel eine erweiterte Version, die Sie kompilieren und ausführen können.

Dieses Buch verwendet zum größten Teil Fachbegriffe, wie sie in der Java Language Specification, Java SE 8 Edition [JLS] definiert sind. Einige Begriffe verdienen besondere Erwähnung. Die Sprache unterstützt vier Arten von Typen: die Schnittstellen (einschließlich Annotationen), Klassen (einschließlich Enums), Arrays und elementare Typen (Primitives). Die ersten drei werden als Referenztypen bezeichnet. Klasseninstanzen und Arrays sind Objekte, elementare Werte nicht. Die Member einer Klasse bestehen aus ihren Feldern, Methoden, Member-Klassen und Member-Schnittstellen. Die Signatur einer Methode besteht aus ihrem Namen und den Typen ihrer Formalparameter; die Signatur enthält nicht den Rückgabetyp der Methode.

Dieses Buch verwendet einige Begriffe, die von der Java Language Specification abweichen, zum Beispiel verwendet dieses Buch Vererbung als Synonym für Ableitung. Anstatt bei Schnittstellen von Vererbung zu sprechen, heißt es in diesem Buch, dass eine Klasse eine Schnittstelle implementiert oder dass eine Schnittstelle eine andere erweitert. Um die Zugriffsebene zu beschreiben, die gilt, wenn keine angegeben ist, verwendet dieses Buch traditionelle package private anstelle des technisch korrekten Begriffs package access [JLS, 6.6.1].

Außerdem verwendet dieses Buch einige Fachbegriffe, die nicht in der Java Language Specification definiert sind. Der Begriff exportierte API, oder einfach API, bezieht sich auf die Klassen, Schnittstellen, Konstruktoren, Member und serialisierte Formen, über die ein Programmierer auf eine Klasse, eine Schnittstelle oder ein Paket zugreift. Der Begriff API, kurz für Application Programming Interface, wird ansonsten dem gern genutzten Begriff Schnittstelle vorgezogen, um Verwechslungen mit dem gleichnamigen Sprachkonstrukt zu vermeiden. Ein Programmierer, der ein Programm schreibt, das eine API verwendet, wird als Benutzer der API bezeichnet. Eine Klasse, deren Implementierung eine API verwendet, ist ein Client der API.

Klassen, Schnittstellen, Konstruktoren, Member und serialisierte Formen werden zusammen als API-Elemente bezeichnet. Eine exportierte API besteht aus den API-Elementen, auf die außerhalb des Pakets, in dem die API definiert ist, zugegriffen werden kann. Dies sind die API-Elemente, die jeder Client verwenden kann und die zu unterstützen sich der Autor der API verpflichtet. Nicht zufällig sind sie auch die Elemente, für die Javadoc standardmäßig eine Dokumentation generiert. Die exportierte API eines Pakets besteht aus den öffentlichen und geschützten Membern und Konstruktoren jeder öffentlichen Klasse oder Schnittstelle des Pakets.

In Java 9 wurde die Plattform um ein Modulsystem erweitert. Wenn eine Bibliothek das Modulsystem nutzt, vereint ihre exportierte API die exportierten APIs aller Pakete, die durch die Moduldeklaration der Bibliothek exportiert werden.

2Objekte erzeugen und auflösen

In diesem Kapitel geht es um das Erzeugen und Auflösen von Objekten: wann und wie Sie Objekte am besten erzeugen, wann und wie Sie die Objekterzeugung vermeiden, wie Sie eine zeitnahe Objektauflösung sicherstellen können und wie Sie Aufräumaktionen managen, die der Objektauflösung vorausgehen müssen.

2.1Thema 1: Statische Factory-Methoden als Alternative zu Konstruktoren

Damit Clients Instanzen einer Klasse erzeugen können, bieten Letztere im Allgemeinen öffentliche Konstruktoren an. Daneben gibt es aber noch eine weitere Technik, die zum Standard-Repertoire jedes Programmierers gehören sollte: Die Klasse stellt als Teil ihrer öffentlichen Schnittstelle eine statische Factory-Methode bereit. Eine solche statische Factory-Methode ist nichts anderes als einfach eine statische Methode, die eine Instanz der Klasse zurückgibt. Das folgende Beispiel stammt aus der Klasse Boolean (der Wrapper-Klasse für boolean). Die Methode übernimmt einen Wert des elementaren Typs boolean und übersetzt ihn in eine Boolean-Objektreferenz:

public static Boolean valueOf(boolean b) {

return b ? Boolean.TRUE : Boolean.FALSE;

}

Verwechseln Sie die hier beschriebenen statischen Factory-Methoden nicht mit dem Factory-Method-Muster aus Design Patterns [Gamma95]. Die hier beschriebenen statischen Factory-Methoden haben kein direktes Äquivalent in Design Patterns.

Klassen können ihren Clients statische Factory-Methoden als Ersatz oder als Ergänzung zu öffentlichen Konstruktoren anbieten. Ersteres hat sowohl Vor- als auch Nachteile.

Ein Vorteil der statischen Factory-Methoden gegenüber den Konstruktoren ist, dass sie einen Namen haben. Wenn die Parameter eines Konstruktors das zurückgelieferte Objekt nur schlecht beschreiben, ist eine statische Factory-Methode mit einem gut gewählten Namen leichter einzusetzen, und der resultierende Code wird besser lesbar. So wäre es zum Beispiel sinnvoller, den Konstruktor BigInteger(int, int, Random), der einen BigInteger zurückliefert, der wahrscheinlich eine Primzahl darstellt, durch eine statische Factory-Methode BigInteger.probablePrime zu ersetzen. (In Java 4 wurde diese Methode dann hinzugefügt.)

Klassen können nur einen einzigen Konstruktor mit einer gegebenen Signatur haben. Viele Programmierer umgehen diese Einschränkung, indem sie zwei Konstruktoren definieren, die sich lediglich in der Reihenfolge ihrer Parametertypen unterscheiden. Doch dies ist keine gute Idee. Die Benutzer einer solchen API werden sich nie sicher merken können, welcher Konstruktor welcher ist, und irgendwann versehentlich den falschen aufrufen. Programmierer, die Code lesen, in denen solche Konstruktoren verwendet werden, können ohne Zuhilfenahme der Klassendokumentation nicht verstehen, was der Code macht.

Da statische Factory-Methoden frei zu vergebende Namen tragen, unterliegen sie nicht den oben diskutierten Beschränkungen. In Situationen, in denen für eine Klasse mehrere Konstruktoren mit derselben Signatur benötigt werden, ersetzen Sie daher die Konstruktoren durch statische Factory-Methoden und geben diesen sorgfältig gewählte Namen, um die Unterschiede zwischen den Methoden herauszuarbeiten.

Ein zweiter Vorteil der statischen Factory-Methoden besteht darin, dass sie – anders als Konstruktoren – nicht zwangsweise bei jedem Aufruf ein Objekt zurückliefern müssen. Dies ermöglicht es unveränderlichen Klassen (Thema 17) mit vorkonstruierten Instanzen zu arbeiten oder einmal erzeugte Instanzen abzuspeichern und danach wiederholt zurückzugeben und so die unnötige Erzeugung identischer Objekte zu vermeiden. Die Methode Boolean.valueOf(boolean) veranschaulicht diese Technik: Sie erzeugt nie ein Objekt. Diese Technik ähnelt dem Flyweight-Muster [Gamma95], das die Performance in Fällen, in denen häufig äquivalente Objekte angefordert werden, erheblich steigern kann – umso mehr, wenn diese Objekte auch noch aufwendig zu erzeugen sind.

Ein dritter Vorteil statischer Factory-Methoden ist, dass sie – anders als Konstruktoren – Objekte jedes beliebigen abgeleiteten Typs ihres Rückgabetyps zurückliefern können. Dies lässt dem Programmierer viel Freiheit bei der Wahl des Klassentyps des zurückgelieferten Objekts.

Eine Möglichkeit ist zum Beispiel, dass eine API Objekte zurückliefert, ohne dass deren Klassen öffentlich gemacht werden müssen. Implementierende Klassen auf diese Weise zu verbergen, führt zu äußerst kompakten APIs. Eine Technik, die in schnittstellenbasierten Frameworks (Thema 20) eingesetzt wird, wo Schnittstellen als natürliche Rückgabetypen für statische Factory-Methoden dienen.

Vor Java 8 konnten Schnittstellen keine statischen Methoden enthalten. Die statischen Factory-Methoden zu einer Schnittstelle namens Type wurden daher per Konvention in eine nicht-instanziierbare Begleitklasse (Thema 4) namens Types ausgelagert. Das Java-Collections-Framework enthält fünfundvierzig solcher Convenience-Implementierungen seiner Schnittstellen, für unveränderliche Collections, synchronisierte Collections und so weiter. Nahezu alle dieser Implementierungen werden über statische Factory-Methoden in eine nicht-instanziierbare Klasse (java.util.Collections) exportiert. Die Klassen der zurückgelieferten Objekte sind sämtlich nicht-öffentlich.

Die Collections-Framework-API ist auf diese Weise viel kleiner als sie sein würde, wenn sie fünfundvierzig separate öffentliche Klassen exportieren würde, eine Klasse für jede Convenience-Implementierung. Dabei wurde nicht nur der schiere Umfang der API reduziert, sondern zugleich auch ihr »konzeptuelles Gewicht«, sprich die Zahl und Komplexität der Konzepte, die Programmierer beherrschen müssen, um die API korrekt nutzen zu können. Der Programmierer weiß, dass das zurückgelieferte Objekt exakt die von seiner Schnittstelle spezifizierte API besitzt, und braucht daher keine zusätzliche Dokumentation zur implementierenden Klasse. Darüber hinaus fördern solche statische Factory-Methoden einen guten Stil, da der Client die zurückgelieferten Objekte als Objekt der Schnittstelle und nicht als Objekt der Implementierungsklasse referenziert (Thema 64).

In Java 8 wurde die Beschränkung, dass Schnittstellen keine statischen Methoden enthalten dürfen, aufgehoben, weswegen es in der Regel keinen Grund gibt, den Schnittstellen nicht-instanziierbare Klassen an die Seite zu stellen. Viele öffentliche statische Member, die früher in einer solchen Klasse untergekommen wären, sollten jetzt direkt in die Schnittstelle integriert werden. Nichtsdestotrotz kann es aber weiterhin nötig sein, den größten Teil des Implementierungscodes hinter diesen statischen Methoden in eine separate package private Klasse zu packen, da Java 8 keine privaten statischen Methoden erlaubt. Java 9 erlaubt mittlerweile auch private statische Methoden, während statische Felder und Member-Klassen weiterhin public sein müssen.

Ein vierter Vorteil statischer Factory-Methoden besteht darin, dass der Klassentyp des zurückgelieferten Objekts in Abhängigkeit von den an die Parameter übergebenen Argumenten variieren kann. Jeder vom deklarierten Rückgabetyp abgeleitete Typ ist dabei erlaubt. Und auch das zurückgelieferte Objekt kann von Version zu Version verschieden sein.

Die Klasse EnumSet (Thema 36) besitzt keinen öffentlichen Konstruktor, nur statische Factory-Methoden. In der OpenJDK-Implementierung liefern diese eine Instanz zurück, die je nach Größe des zugrunde liegenden Aufzählungstyps einer von zwei Subklassen angehört: Wenn der Aufzählungstyp vierundsechzig oder weniger Elemente enthält, was für die meisten Aufzählungstypen zutrifft, liefert die statische Factory-Methode eine RegularEnumSet-Instanz zurück, die auf einem einzelnen long-Wert basiert. Enthält der Aufzählungstyp dagegen mehr als vierundsechzig Elemente, liefert die statische Factory-Methode eine JumboEnumSet-Instanz zurück, die intern auf einem long-Array basiert.

Die Clients merken nichts davon, dass es zwei Implementierungsklassen gibt. Würde RegularEnumSet irgendwann einmal für kleine Aufzählungstypen keine Performance-Vorteile mehr bringen, könnte die Klasse aus zukünftigen Versionen ohne Probleme gestrichen werden. Umgekehrt könnte jederzeit eine dritte oder vierte Implementierung von EnumSet hinzugefügt werden, sollte dies der Performance dienlich sein. Die Clients wissen nicht, noch kümmert es sie, welchem Klassentyp das von der Factory-Methode zurückgelieferte Objekt tatsächlich angehört; für sie ist allein wichtig, dass es sich um eine Subklasse von EnumSet handelt.

Ein fünfter Vorteil der statischen Factory-Methoden ist, dass der Klassentyp des zurückgelieferten Objekts noch gar nicht existieren muss, wenn die Klasse mit der Methode geschrieben wird. Solche flexiblen statischen Factory-Methoden bilden die Basis von Service-Provider-Frameworks wie der Java-Database-Connectivity-API (JDBC). Ein Service-Provider-Framework ist ein System, in dem Provider einen Dienst implementieren, und das System stellt den Clients die Implementierungen zur Verfügung – wodurch Clients und Implementierung entkoppelt werden.

In einem Service-Provider-Framework gibt es drei essenzielle Komponenten: eine Serviceschnittstelle, die eine Implementierung repräsentiert, eine Provider-Registrierungs-API, die die Provider zur Registrierung der Implementierungen nutzen; und eine Service-Access-API, mit deren Hilfe die Clients vom Dienst Objekte anfordern. Die Service-Access-API kann so konzipiert sein, dass die Clients Kriterien für die Auswahl der Implementierung vorgeben können. Werden keine Kriterien spezifiziert, liefert die API eine Instanz einer Standardimplementierung oder erlaubt dem Client, die verfügbaren Implementierungen durchzugehen. Die Service-Access-API ist die flexible statische Factory, der die Basis des Service-Provider-Frameworks bildet.

Optional kann ein Service-Provider-Framework als vierte Komponente eine Service-Provider-Schnittstelle haben, die ein Factory-Objekt beschreibt, dass Instanzen der Serviceschnittstelle erzeugt. Gibt es keine Service-Provider-Schnittstelle, müssen die Implementierungen per Reflection instanziiert werden (Thema 65). Im Falle der JDBC übernimmt Connection den Part der Serviceschnittstelle, DriverManager.registerDriver ist die Provider-Registrierungs-API, DriverManager.getConnection die Service-Access-API und Driver die Service-Provider-Schnittstelle.

Von dem Service-Provider-Framework-Muster gibt es viele Varianten. Beispielsweise kann die Service-Access-API den Clients eine umfangreichere Service-schnittstelle zur Verfügung stellen, als es die Schnittstelle der Provider macht. Dies ist das Bridge-Muster [Gamma95]. Dependency-Injection-Frameworks (Thema 5) können als leistungsfähige Service-Provider angesehen werden. Seit Java 6 gehört zur Plattform bereits ein allgemeines Service-Provider-Framework (java.util.ServiceLoader). Es ist also nicht nötig, dass Sie ein eigenes Framework schreiben (Thema 59), und im Allgemeinen sollten Sie dies auch nicht tun. Da es JDBC bereits vor Java 6 gab, basiert es nicht auf ServiceLoader.

Der größte Nachteil von Klassen, die nur statische Factory-Methoden zur Verfügung stellen, ist, dass ohnepublic- oderprotected-Konstruktoren keine Subklassen von ihnen abgeleitet werden können. Weswegen zum Beispiel von keiner der Convenience-Implementierungsklassen des Collections-Framework Subklassen abgeleitet werden können. Man kann diesen Nachteil allerdings auch als einen verdeckten Vorteil ansehen, denn es ermutigt die Programmierer dazu, Komposition statt Vererbung zu nutzen (Thema 18). Und für die Implementierung unveränderlicher Typen (Thema 17) ist es sowieso eine Grundbedingung.

Ein zweiter Nachteil der statischen Factory-Methoden ist, dass sie nicht so leicht gefunden werden. In der API-Dokumentation sind sie nicht so prominent hervorgehoben wie die Konstruktoren. Deswegen ist es manchmal gar nicht so leicht herauszufinden, wie eine Klasse, die anstelle von Konstruktoren nur statische Factory-Methoden zur Verfügung stellt, instanziiert werden kann. Vielleicht wird das Javadoc-Tool die statischen Factory-Methoden irgendwann deutlicher hervorheben. Bis dahin können Sie das Problem mildern, indem Sie einerseits selbst in der Klassen- oder Schnittstellen-Dokumentation auf die statische Factory-Methode hinweisen und sich andererseits an die diesbezüglichen gängigen Namenskonventionen halten. Hier einige typische Beispiele:

Zusammenfassend lässt sich festhalten, dass sowohl statische Factory-Methoden als auch öffentliche Konstruktoren ihre Berechtigung haben und es sich auszahlt, wenn man sich über die Vor- und Nachteile beider Varianten im Klaren ist. Meistens sind allerdings statische Factory-Methoden vorzuziehen. Deswegen sollte man nicht automatisch öffentliche Konstruktoren anbieten, ohne zuvor über statische Factory-Methoden als Alternative nachgedacht zu haben.

2.2Thema 2: Erwägen Sie bei zu vielen Konstruktorparametern den Einsatz eines Builders

Statische Factory-Methoden und Konstruktoren haben ein gemeinsames Manko: Beide lassen sich nur schlecht skalieren, wenn es viele optionale Parameter gibt. Betrachten wir den Fall einer Klasse zur Repräsentation von Nährwertangaben, wie man sie auf Lebensmittelverpackungen findet. Einige dieser Nährwertangaben sind obligatorisch, zum Beispiel Portionsgröße, Portion pro Packung und Kalorien pro Portion, während mehr als 20 Angaben optional sind, wie Gesamtfett, gesättigte Fette, Transfette, Cholesterin, Natrium und so weiter. Die meisten Produkte geben nur für einige dieser optionalen Angabefelder Werte an.

Welche Art von Konstruktor oder statischer Factory-Methode eignet sich am besten für eine solche Klasse? Traditionell arbeiten Programmierer in solchen Fällen mit dem Teleskopkonstruktor-Muster, das heißt, sie erstellen einen Konstruktor mit den obligatorischen Parametern, dann einen weiteren für den ersten optionalen Parameter, einen dritten mit zwei optionalen Parametern und so weiter, bis der letzte Konstruktor schließlich alle optionalen Parameter auflistet. In der Praxis sieht das folgendermaßen aus. Wir beschränken uns aber hier der Kürze halber auf vier optionale:

Wenn Sie eine Instanz erzeugen wollen, wählen Sie den Konstruktor mit der kürzesten Parameterliste, die alle von Ihnen benötigten Parameter enthält:

new NutritionFacts(240, 8, 100, 0, 35, 27);

Häufig müssen bei einem solchen Konstruktoraufruf auch Werte für Parameter übergeben werden, die der Programmierer eigentlich gar nicht setzen möchte: so wie im obigen Beispiel, wo wir für fat einen Wert von 0 übergeben haben. Bei nur sechs Parametern scheint das nicht so gravierend zu sein, doch mit zunehmender Parameteranzahl kann man leicht den Überblick verlieren.

Kurzum, Teleskopkonstruktoren funktionieren, doch je mehr Parameter es gibt, umso schwieriger wird es, fehlerfreien oder gar verständlichen Client-Code zu schreiben. Client-Programmierer werden rätseln, was sich hinter all diesen Werten verbirgt, und sind gezwungen, die Parameter sorgfältig abzuzählen, um sie korrekt den Werten zuordnen zu können. Lange Auflistungen von Parametern des gleichen Typs können zudem zu schwer feststellbaren Fehlern führen. Der Client braucht nur zufällig zwei Parameter zu vertauschen – ein Fehler, der dem Compiler nicht auffallen wird –, und das Programm wird sich zur Laufzeit seltsam verhalten (Thema 51).

Es gibt eine Alternative, wenn Sie es mit vielen optionalen Parametern in einem Konstruktor zu tun haben: das JavaBeans-Muster. Hierbei rufen Sie zur Objekterzeugung einen parameterlosen Konstruktor auf, um anschließend mittels Set-Methoden alle obligatorischen und alle benötigten optionalen Parameter zu setzen:

Dieses Muster weist keinen der Nachteile des Teleskopkonstruktors auf. Die Instanziierung ist einfach, wenn auch ein bißchen textlastig, und der endgültige Code leicht zu lesen.

JavaBeans bergen jedoch ganz eigene gravierende Nachteile. Da die Konstruktion über mehrere Aufrufe verteilt erfolgt, kann sich eine JavaBean im Laufe ihrer Konstruktion in einem inkonsistenten Zustand befinden. Die Klasse kann dies nicht verhindern, zumindest nicht durch Gültigkeitsprüfung der Konstruktorparameter. Zugriffe auf Objekte, die sich in einem inkonsistenten Zustand befinden, können zu Fehlern führen, die wegen der räumlichen Entfernung von der Fehlerquelle beim Debuggen nur schwer zu finden sind. Ein weiterer damit zusammenhängender Nachteil ist, dass es in JavaBeans nicht möglich ist, eine Klasse unveränderlich zu machen (Thema 17), und Programmierer zusätzlichen Aufwand betreiben müssen, um Thread-Sicherheit zu gewährleisten.

Es besteht zwar die Möglichkeit, diese Nachteile abzumildern, indem die Objekte nach Abschluss ihrer Konstruktion manuell eingefroren und erst nach dem Einfrieren zur Verwendung freigegeben werden. Aber in der Praxis hat sich diese Vorgehensweise als unhandlich erwiesen und nicht bewährt. Hinzu kommt, dass hierdurch zur Laufzeit Fehler auftreten können, da der Compiler nicht sicherstellen kann, dass der Programmierer die Einfriermethode auf ein Objekt aufruft, bevor er es benutzt.

Zum Glück gibt es eine dritte Alternative, die die Sicherheit eines Teleskopkonstruktors mit der besseren Lesbarkeit der JavaBeans vereint – das Builder-Muster [Gamma 95]. Anstatt das Objekt direkt zu erstellen, ruft der Client einen Konstruktor oder eine statische Factory-Methode mit allen erforderlichen Parametern auf und erhält ein Builder-Objekt. Anschließend ruft der Client Setterähnliche Methoden des Builder-Objekts auf, um die gewünschten optionalen Parameter zu setzen. Zum Schluss ruft der Client eine parameterlose build-Methode auf, um das Objekt zu erzeugen, das typischerweise unveränderlich ist. In der Praxis sieht das folgendermaßen aus:

// Builder-Muster

public class NutritionFacts {

private final int servingSize;

private final int servings;

private final int calories;

private final int fat;

private final int sodium;

private final int carbohydrate;

Die Klasse NutritionFacts ist unveränderlich, und alle Standardwerte der Parameter befinden sich an einem Ort. Die Set-Methoden des Builders liefern den Builder selbst zurück, sodass die Aufrufe verkettet werden können. Das Ergebnis ist eine sprechende Schnittstelle. Der Client-Code lautet:

Der Client-Code ist einfach aufzusetzen und, was noch wichtiger ist, einfach zu lesen. Das Builder-Muster simuliert benannte optionale Parameter, wie man sie aus Python und Scala kennt.

Der Kürze halber wurde auf Gültigkeitstests verzichtet. Um ungültige Parameter möglichst früh zu erkennen, prüfen Sie die Parameter im Konstruktor und in den Methoden des Builders auf Gültigkeit. Prüfen Sie die Invarianten mit mehreren Parametern im Konstruktor, der von der build-Methode aufgerufen wird. Um diese Invarianten vor Angriffen zu schützen, führen Sie die Prüfungen auf Objektfelder aus, nachdem die Parameter aus dem Builder kopiert sind (Thema 50). Wenn eine Prüfung fehlschlägt, werfen Sie eine IllegalArgumentException (Thema 72), die Sie darüber informiert, welche Parameter ungültig sind (Thema 75).

Das Builder-Muster eignet sich besonders gut für Klassenhierarchien. Verwenden Sie eine parallele Hierarchie von Buildern, die jeweils in der entsprechenden Klasse eingebettet sind. Abstrakte Klassen haben abstrakte Builder, konkrete Klassen haben konkrete Builder. Betrachten wir beispielsweise eine abstrakte Klasse als Wurzel einer Hierarchie verschiedener Arten Pizza:

Beachten Sie, dass Pizza.Builder ein generischer Typ mit einem rekursiven Typ-parameter (Thema 30) ist. Zusammen mit der abstrakten self-Methode sorgt dies dafür, dass die Methodenverkettung auch in den Subklassen funktioniert, ohne dass Typenumwandlungen vorgenommen werden müssen. Das Problem, dass Java keinen self-Typ aufweist, lässt sich mit diesem sogenannten simulierten self-Typ gut umgehen.

Der folgende Code beschreibt zwei konkrete Subklassen von Pizza. Eine der Subklassen repräsentiert eine Pizza im New-York-Stil und die andere eine Pizza Calzone. Erstere verfügt über einen erforderlichen Größenparameter namens size, während die zweite Pizza die Wahl bietet, ob die Füllung innen oder außen sein soll:

Hier fällt auf, dass die Deklaration der build-Methode im Builder der jeweiligen Subklasse die korrekte Subklasse zurückliefert: Die build-Methode von NyPizza.Builder liefert NyPizza zurück, während die build-Methode in Calzone.BuilderCalzonezurückliefert. Diese Technik, bei der eine Methode der Subklasse laut Deklaration einen Subtyp des in der Superklasse deklarierten Rückgabetyps zurückliefert, wird als kovarianter Rückgabetyp bezeichnet. So können Clients diese Builder nutzen, ohne Typumwandlungen vornehmen zu müssen.

Der Client-Code für diese hierarchischen Builder entspricht im Wesentlichen dem Code des einfachen NutritionFacts-Builders. Der nachfolgende Beispielcode geht der Kürze halber von statischen Importen der enum-Konstanten aus:

Builder haben gegenüber Konstruktoren insofern einen kleinen Vorteil, als sie mehrere varargs-Parameter haben können, da jeder Parameter in seiner eigenen Methode angegeben wird. Alternativ können Builder die Parameter, die in mehreren Aufrufen einer Methode übergeben werden, in einem einzigen Feld zusammenfassen, wie die Methode addTopping von weiter oben demonstriert.

Das Builder-Muster ist ziemlich flexibel. Ein einzelner Builder kann wiederholt zum Erstellen von mehreren Objekten verwendet werden, er kann zwischen den Aufrufen der build-Methode umkonfiguriert werden, um variierende Objekte zu erzeugen, und er kann einzelne Felder bei der Objekterzeugung sogar automatisch füllen – wie eine Seriennummer, die sich bei jedem neu erzeugten Objekt erhöht.

Es gibt aber auch Nachteile. Um ein Objekt erzeugen zu können, müssen Sie zuerst seinen Builder erzeugen. Die Kosten für die Erzeugung eines Builders werden sich in der Praxis zwar selten bemerkbar machen, könnten aber in Performance-kritischen Situationen ein Problem sein. Auch ist das Builder-Muster wesentlich codelastiger als das Teleskopkonstruktor-Muster. Es sollte also nur eingesetzt werden, wenn die Anzahl der Parameter, sagen wir vier und mehr, dies rechtfertigt. Sie sollten jedoch auch berücksichtigen, dass Sie vielleicht irgendwann Parameter ergänzen wollen. Wenn Sie in einem solchen Fall zuerst auf Konstruktoren oder statische Factorys zurückgreifen und erst später, nachdem die Weiterentwicklung der Klasse die Anzahl der Parameter hat ausufern lassen, zu einem Builder wechseln, werden die veralteten Konstruktoren oder statischen Factorys als Fremdkörper direkt ins Auge fallen. Deshalb ist es oft besser, gleich einen Builder zu verwenden.

Zusammenfassend lässt sich sagen, dass das Builder-Muster eine gute Wahl ist, wenn Sie Klassen entwerfen, deren Konstruktoren oder statische Factory-Methoden mehr als eine Handvoll Parameter aufweisen, vor allem wenn viele dieser Parameter optional oder vom gleichen Typ sind. Client-Code ist mit Buildern viel einfacher zu lesen und schreiben als mit Teleskopkonstruktoren; außerdem sind Builder viel sicherer als JavaBeans.

2.3Thema 3: Erzwingen Sie die Singleton-Eigenschaft mit einem private-Konstruktor oder einem Aufzählungstyp

Als Singleton bezeichnet man eine Klasse, die genau einmal instanziiert wird [Gamma 95]. Singletons werden meist dazu genutzt, ein zustandsloses Objekt wie eine Funktion (Thema 24) zu repräsentieren oder eine Systemkomponente, die von Natur aus einmalig ist. Eine Klasse zu einem Singleton zu machen, kann das Testen ihrer Clients erschweren, da es unmöglich ist, ein Singleton durch eine Mock-Implementierung zu ersetzen, es sei denn, sie implementiert eine Schnittstelle, die als ihr Typ fungiert.

Es gibt zwei verbreitete Möglichkeiten, Singletons zu implementieren. Beide basieren darauf, den Konstruktor als private zu deklarieren und einen öffentlichen statischen Member zu exportieren, um Zugriff auf die einzige Instanz zu gewähren. Im ersten Ansatz ist der Member ein final-Feld:

Der private Konstruktor wird nur einmal aufgerufen, um das publicstaticfinal-Feld Elvis.INSTANCE zu initialisieren. Das Fehlen eines public- oder protected-Konstruktors garantiert ein »monoelvistisches« Universum, das heißt, es existiert nach der Initialisierung der Elvis-Klasse genau eine Elvis-Instanz – nicht mehr und nicht weniger. Daran ist nicht zu rütteln, mit einer Ausnahme: Ein privilegierter Client kann mithilfe der Methode AccessibleObject.setAccessible den privaten Konstruktor reflexiv aufrufen (Thema 65). Wenn Sie einen solchen Angriff abwehren wollen, ändern Sie den Konstruktor dahingehend, dass er eine Ausnahme wirft, wenn die Erzeugung einer zweiten Instanz angefordert wird.

Im zweiten Ansatz zur Implementierung von Singletons ist der öffentliche Member eine statische Factory-Methode:

Alle Aufrufe von Elvis.getInstance liefern die gleiche Objektreferenz zurück, und es wird nie eine andere Elvis-Instanz geben (abgesehen von der oben genannten Ausnahme).

Der Hauptvorteil des Ansatzes mit dem öffentlichen Feld ist, dass die API klarstellt, dass die Klasse ein Singleton ist: Das öffentliche statische Feld ist final, was bedeutet, dass es immer die gleiche Objektreferenz enthält. Der zweite Vorteil ist, dass dieser Ansatz einfacher ist.

Der Ansatz mit der statischen Factory hat den Vorteil, dass er Ihnen die Möglichkeit bietet, Ihre Entscheidung für ein Singleton zurückzunehmen, ohne die API ändern zu müssen. Die Factory-Methode liefert die einzige Instanz zurück, könnte aber dahingehend geändert werden, eine separate Instanz für jeden Thread zurückzuliefern, der sie aufruft. Ein zweiter Vorteil ist, dass Sie eine generische Singleton-Factory schreiben können, wenn Ihre Anwendung dies erfordert (Thema 30). Und der letzte Vorteil einer statischen Factory ist, dass eine Methodenreferenz als Supplier verwendet werden kann. So ist zum Beispiel Elvis::instance ein Supplier<Elvis>. Allerdings sollten Sie sich für diesen Ansatz nur entscheiden, wenn einer dieser Vorteile von großer Bedeutung ist. Generell wird der Ansatz mit dem öffentlichen Feld empfohlen.

Um eine Singleton-Klasse, die einen dieser Ansätze verfolgt, serialisierbar zu machen (Kapitel 12), reicht es nicht, implements Serializable in die Deklaration zu schreiben. Um die Singleton-Garantie aufrechtzuerhalten, müssen Sie alle Instanzfelder als transient deklarieren und eine readResolve-Methode ergänzen (Thema 89). Ansonsten wird bei jeder Deserialisierung einer serialisierten Instanz eine neue Instanz erzeugt, was in unserem Beispiel dem Auftreten unechter Elvis-Nachahmer entspricht. Dies können Sie verhindern, indem Sie die folgende readResolve-Methode in die Elvis-Klasse einfügen:

// readResolve-Methode bewahrt die Singleton-Eigenschaft

private Object readResolve() {

// Liefere den einzig wahren Elvis zurück und

// überlass die Elvis-Nachahmer dem Garbage Collector.

return INSTANCE;

}

Eine dritte Möglichkeit, ein Singleton zu implementieren, besteht darin, eine enum-Aufzählung mit einem einzigen Element zu deklarieren:

// Enum-Singleton – der bevorzugte Ansatz

public enum Elvis {

INSTANCE;

public void leaveTheBuilding() { ... }

}

Dieser Ansatz entspricht im Großen und Ganzen dem ersten Ansatz mit dem öffentlichen Feld, ist jedoch prägnanter, unterstützt automatisch die Serialisierung und bietet eine absolute Garantie gegen Mehrfachinstanziierung, sogar im Falle von anspruchsvollen Serialisierungs- und Reflection-Angriffen. Dieser Ansatz mag ein wenig ungewöhnlich erscheinen, aber ein Aufzählungstyp mit einem einzigen Element ist oft der beste Weg, einen Singleton zu implementieren. Allerdings können Sie diesen Ansatz nicht nutzen, wenn Ihr Singleton eine andere Superklasse als Enum erweitern muss.

2.4Thema 4: Erzwingen Sie die Nicht-Instanziierbarkeit mit einem private-Konstruktor

Gelegentlich werden Sie eine Klasse schreiben wollen, die nur aus statischen Methoden und statischen Feldern besteht. Klassen dieser Art haben eigentlich einen schlechten Ruf, da einige sie missbrauchen, um nicht in Objekten denken zu müssen. Dennoch haben diese Klassen ihre Daseinsberechtigung. Sie können wie java.lang.Math oder java.util.Arrays dazu verwendet werden, um zusammengehörende Methoden zur Verarbeitung elementarer Werte oder Arrays zusammenzufassen. Man kann damit aber auch wie java.util.Collections statische Methoden, einschließlich Factory-Methoden (Thema 1), für Objekte gruppieren, die eine Schnittstelle implementieren. Ab Java 8 können Sie diese Methoden auch in der Schnittstelle festlegen, vorausgesetzt die Schnittstelle gehört Ihnen und kann von Ihnen bearbeitet werden. Und schließlich können solche Klassen dazu dienen, Methoden in einer finalen Klasse aufzunehmen, da diese nicht in einer Subklasse untergebracht werden können.

Solche Hilfsklassen sind nicht für Instanziierung ausgelegt: Eine Instanz wäre auch nicht besonders sinnvoll. In Ermangelung von expliziten Konstruktoren stellt der Compiler jedoch einen öffentlichen parameterlosen Standardkonstruktor bereit. Für einen Nutzer unterscheidet sich dieser Konstruktor in keiner Weise von den anderen Konstruktoren. Es kommt häufiger vor, dass in öffentlich verfügbaren APIs Klassen zu finden sind, die nur aus Unachtsamkeit instanziierbar sind.

Die Nicht-Instanziierbarkeit einer Klasse zu erzwingen, indem Sie sie als abstract deklarieren, ist keine Lösung. Von einer solchen Klasse können Subklassen abgeleitet und diese dann instanziiert werden. Außerdem könnte dies den Nutzer zu der Annahme verleiten, dass die Klasse vererbt werden kann (Thema 19). Es gibt jedoch ein einfaches Idiom, um Nicht-Instanziierbarkeit zu garantieren. Ein Standardkonstruktor wird nur erzeugt, wenn eine Klasse keine expliziten Konstruktoren enthält, sodass eine Klasse nicht-instanziierbar gemacht werden kann, indem ein privater Konstruktor hinzugefügt wird:

// Nicht-instanziierbare Hilfsklasse

public class UtilityClass {

// Unterdrücke Standardkonstruktor für Nicht-Instanziierbarkeit

private UtilityClass() {

throw new AssertionError();

}

... // Rest ausgelassen

}

Da der explizite Konstruktor privat ist, kann von außerhalb der Klasse nicht darauf zugegriffen werden. AssertionError ist nicht unbedingt erforderlich, bietet aber Sicherheit für den Fall, dass der Konstruktor zufällig von innerhalb der Klasse aufgerufen wird. Er garantiert, dass die Klasse unter keinen Umständen instanziiert wird. Der Code ist leicht widersinnig, da der Konstruktor ausdrücklich bereitgestellt wird, damit er nicht aufgerufen werden kann. Deshalb ist es ratsam, wie oben einen Kommentar einzufügen.

Ein Nebeneffekt dieses Idioms ist, dass es verhindert, dass von dieser Klasse abgeleitet wird. Alle Konstruktoren müssen, explizit oder implizit, einen Superklassenkontruktor aufrufen, und eine Subklasse hätte keinen Superklassenkonstruktor zum Aufrufen.

2.5Thema 5: Arbeiten Sie mit Dependency Injection statt Ressourcen direkt einzubinden

Viele Klassen sind von einer oder mehreren zugrunde liegenden Ressourcen abhängig. Ein Rechtschreibprogramm kommt zum Beispiel nicht ohne ein Wörterbuch aus. Solche Klassen werden nicht selten als statische Hilfsklassen (Thema 4) implementiert:

Ebenfalls anzutreffen sind Implementierungen solcher Klassen als Singletons (Thema 3):

Von beiden Ansätzen ist abzuraten, da sie davon ausgehen, dass es nur ein einzubindendes Wörterbuch gibt. In der Praxis gibt es jedoch zu jeder Sprache ein eigenes Wörterbuch mit zusätzlichen Wörterbüchern für benutzerspezifische Terminologie. Auch kann es sinnvoll sein, zum Testen ein eigenes Wörterbuch zu benutzen. Der Gedanke, dass Sie auf Dauer mit nur einem Wörterbuch auskommen, ist ziemlich unrealistisch.

Sie könnten versuchen, bei Spellchecker die Unterstützung für mehrere Wörterbücher dadurch zu erreichen, dass Sie das dictionary-Feld als nicht-final deklarieren und eine Methode hinzufügen, um das Wörterbuch in einem bestehenden Rechtschreibprogramm zu ändern. Diese Vorgehensweise wäre jedoch äußerst umständlich und fehleranfällig und würde bei Nebenläufigkeit nicht funktionieren. Statische Hilfsklassen und Singletons eignen sich nicht für Klassen, deren Verhalten von einer zugrunde liegenden Ressource parametrisiert wird.

Was wir benötigen, ist die Fähigkeit, mehrere Instanzen der Klasse (hier Spellchecker) zu unterstützen, wobei jede Instanz die vom Client gewünschte Ressource (hier das Wörterbuch) verwendet. Ein einfaches Muster, das diese Anforderung erfüllt, besteht darin, die Ressource beim Erzeugen einer neuen Instanz dem Konstruktor zu übergeben. Dies ist eine Form von Dependency Injection: Das Wörterbuch ist eine Abhängigkeit des Rechtschreibprogramms, die in das Rechtschreibprogramm bei seiner Erzeugung injiziert wird.

Das Dependency-Injection-Muster ist so einfach, dass viele Programmierer es seit Jahren verwenden, ohne zu wissen, dass das Konstrukt einen Namen hat. Auch wenn unser Beispiel eines Rechtschreibprogramms nur über eine einzige Ressource (das Wörterbuch) verfügt, lässt sich Dependency Injection mit einer beliebigen Anzahl an Ressourcen und beliebigen Abhängigkeitsgraphen verwenden. Sie wahrt die Unveränderlichkeit (Thema 17), sodass mehrere Clients abhängige Objekte gemeinsam nutzen können – vorausgesetzt, die Clients benötigen die gleichen zugrunde liegenden Ressourcen. Dependency Injection lässt sich im gleichen Maße auf Konstruktoren, statische Factorys (Thema 1) und Builder (Thema 2) anwenden.

Eine nützliche Variante dieses Musters sieht vor, eine Ressourcen-Factory an den Konstruktor zu übergeben. Eine Factory ist ein Objekt, das mehrfach aufgerufen werden kann, um Instanzen eines Typs zu erzeugen. Solche Factorys basieren auf dem Factory-Method-Muster [Gamma 95]. Die Schnittstelle Supplier<T>, die es seit Java 8 gibt, eignet sich perfekt zur Repräsentation von Factorys.

Methoden, die Supplier<T> als Eingabe entgegennehmen, sollten normalerweise den Typparameter der Factory mit einem eingeschränkten Wildcard-Typ (Thema 31) beschränken, um dem Client die Möglichkeit zu bieten, eine Factory zu übergeben, die einen beliebigen Subtyp eines spezifizierten Typs erzeugt. Die folgende Methode erstellt beispielsweise ein Mosaik, indem sie eine vom Client bereitgestellte Factory verwendet, um die einzelnen Fliesen zu erzeugen:

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

Obwohl Dependency Injection die Flexibilität und Testbarkeit stark verbessert, kann sie große Projekte, die normalerweise Tausende von Abhängigkeiten aufweisen, ziemlich unübersichtlich machen. Dieses Durcheinander lässt sich durch die Verwendung eines Dependency-Injection-Frameworks wie Dagger [Dagger], Guice [Guice] oder Spring [Spring] vermeiden. Ich möchte Sie darauf hinweisen, dass APIs, die für die manuelle Dependency Injection ausgelegt sind, von diesen Frameworks leicht angepasst werden können.

Kurzum, verwenden Sie kein Singleton oder keine statische Hilfsklasse, um eine Klasse zu implementieren, die von einer oder mehreren zugrunde liegenden Ressourcen abhängt, deren Verhalten die Klasse beeinflusst, und lassen Sie die Klasse diese Ressourcen nicht direkt erzeugen. Übergeben Sie stattdessen die Ressourcen – oder die Factorys, mit denen sie erzeugt werden – an den Konstruktor, die statische Factory oder den Builder. Diese als Dependency Injection bezeichnete Praxis wird die Flexibilität, Wiederverwendbarkeit und Testbarkeit Ihrer Klasse immens erhöhen.

2.6Thema 6: Vermeiden Sie die Erzeugung unnötiger Objekte

Oft ist es besser, ein einzelnes Objekt wiederzuverwenden, anstatt jedes Mal ein neues Objekt mit gleicher Funktionalität zu erzeugen. Wiederverwendung kann schneller und eleganter sein. Ein Objekt kann immer wiederverwendet werden, wenn es unveränderlich ist (Thema 17).

Ein extremes Beispiel, wie Sie nicht vorgehen sollten:

Diese Anweisung erzeugt bei jeder Ausführung eine neue String-Instanz. Da das an den String-Konstruktor übergebene Argument ("bikini") ebenfalls eine String-Instanz ist, entspricht es von seiner Funktionalität her allen anderen Objekten, die vom Konstruktor erzeugt werden. Steht diese Anweisung in einer Schleife oder einer häufig aufgerufenen Methode, kann es passieren, dass unnötig Millionen String-Instanzen erzeugt werden.

Nach der Überarbeitung sieht der Code folgendermaßen aus:

Diese Code-Version verwendet eine einzige String-Instanz, anstatt bei jeder Ausführung eine neue zu erzeugen. Außerdem wird das Objekt garantiert von jedem Code wiederverwendet, der in derselben virtuellen Maschine läuft und zufällig das gleiche String-Literal enthält [JLS, 3.10.5].

Sie können das Erzeugen unnötiger Objekte oft vermeiden, indem Sie bei unveränderlichen Klassen statische Factory-Methoden statt Konstruktoren (Thema 1) verwenden, sofern die Klasse beides anbietet. So ist beispielsweise die Factory-Methode Boolean.valueOf(String) dem Konstruktor Boolean(String) vorzuziehen, der seit Java 9 deprecated ist. Während der Konstruktor bei jedem Aufruf ein neues Objekt erzeugen muss, kann die Factory-Methode auch darauf verzichten, was sie in der Praxis sogar meist macht. Doch Sie können nicht nur unveränderliche, sondern auch veränderliche Objekte wiederverwenden, sofern Sie sicher sind, dass diese nicht verändert werden.

In manchen Fällen ist die Erzeugung von Objekten aufwendiger als in anderen. Wenn Sie ein solches teures Objekt wiederholt benötigen, lohnt es sich eventuell, das Objekt zur Wiederverwendung abzuspeichern. Leider ist dies bei der Objekterzeugung nicht immer ersichtlich. Angenommen, Sie wollen eine Methode schreiben, die feststellt, ob ein String eine gültige römische Zahl ist. Die einfachste Form mit einem regulären Ausdruck lautet:

// Leistung lässt sich stark verbessern!

static boolean isRomanNumeral(String s) {

return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"

+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

}

Der Nachteil dieser Implementierung ist, dass sie sich auf die Methode String.matches verlässt. String.matchesist zwar der einfachste Weg, um festzustellen, ob ein String mit einem regulären Ausdruck übereinstimmt, aber diese Methode ist nicht für die wiederholte Verwendung in Performance-kritischen Situationen geeignet. Das Problem dabei ist, dass die Methode intern eine Pattern-Instanz für den regulären Ausdruck erzeugt und diese nur einmal verwendet, um sie dann dem Garbage Collector zu überlassen. Die Erzeugung einer Pattern-Instanz ist teuer, da hierbei der reguläre Ausdruck in einen endlichen Automaten kompiliert werden muss.

Um die Leistung zu verbessern, kompilieren Sie den regulären Ausdruck als Teil der Klasseninitialisierung explizit in eine Pattern-Instanz (die unveränderlich ist), legen diese Instanz in einem Zwischenspeicher ab und verwenden ebendiese für jeden Aufruf der Methode isRomanNumeral:

Die überarbeitete Version von isRomanNumeral bietet eine erhebliche Leistungssteigerung, wenn sie häufiger aufgerufen wird. Auf meinem Rechner benötigt die Originalversion bei einem Eingabestring mit acht Zeichen 1,1 μs, während die verbesserte Version nur 0,17 μs benötigt, was bedeutet, dass sie 6,5-mal schneller ist. Doch nicht nur die Performance ist besser, auch der Code ist klarer. Indem wir die ansonsten unsichtbare Pattern-Instanz zu einem staticfinal-Feld machen, können wir ihr einen Namen geben, was weitaus besser lesbar und verständlicher ist als der reguläre Ausdruck.

Wenn die Klasse mit der verbesserten isRomanNumeral-Methode initialisiert, aber die Methode niemals ausgerufen wird, wird das Feld ROMAN umsonst initialisiert. Es wäre zwar möglich, diese Initialisierung zu verhindern, indem man das Feld später initialisiert (lazy initializiation, Thema 83), wenn isRomanNumeral das erste Mal aufgerufen wird, doch hiervor wird generell abgeraten. Wie so oft bei der späten Initialisierung würde dies die Implementierung unnötig verkomplizieren, ohne dass eine messbare Leistungsverbesserung zu verzeichnen wäre (Thema 67).

Wenn ein Objekt unveränderlich ist, ist klar, dass es problemlos wiederverwendet werden kann. Doch gibt es auch Situationen, wo dies nicht ganz so eindeutig, ja sogar kontraintuitiv sein kann. Betrachten wir den Fall von Adaptern [Gamma 95], die auch als Views