Entdecken Sie Millionen von E-Books, Hörbüchern und vieles mehr mit einer kostenlosen Testversion

Nur $11.99/Monat nach der Testphase. Jederzeit kündbar.

Effective Java: Best Practices für die Java-Plattform
Effective Java: Best Practices für die Java-Plattform
Effective Java: Best Practices für die Java-Plattform
eBook786 Seiten7 Stunden

Effective Java: Best Practices für die Java-Plattform

Bewertung: 0 von 5 Sternen

()

Vorschau lesen

Über dieses E-Book

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
SpracheDeutsch
Herausgeberdpunkt.verlag
Erscheinungsdatum1. Okt. 2018
ISBN9783960886396
Effective Java: Best Practices für die Java-Plattform

Ähnlich wie Effective Java

Ähnliche E-Books

Programmieren für Sie

Mehr anzeigen

Ähnliche Artikel

Rezensionen für Effective Java

Bewertung: 0 von 5 Sternen
0 Bewertungen

0 Bewertungen0 Rezensionen

Wie hat es Ihnen gefallen?

Zum Bewerten, tippen

Die Rezension muss mindestens 10 Wörter umfassen

    Buchvorschau

    Effective Java - Joshua Bloch

    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:

    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:

    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.

    Mittels statischer Factory-Methoden, die bei wiederholten Aufrufen das immer gleiche Objekt zurückliefern, können Klassen überdies strenger kontrollieren, zu welchem Zeitpunkt welche Instanzen von ihnen existieren. Klassen, die dies machen, bezeichnet man als instanzenkontrolliert. Es gibt verschiedene Gründe, eine instanzenkontrollierte Klasse zu schreiben: Via Instanzenkontrolle kann eine Klasse sicherstellen, dass sie ein Singleton (Thema 3) oder nicht-instanziierbar (Thema 4) ist. Außerdem können unveränderliche Klassen (Thema 17) mit dieser Technik garantieren, dass es von ihnen keine zwei gleichen Instanzen gibt: a.equals(b) gilt genau dann, wenn a == b. Dies ist die Grundlage des Flyweight-Musters [Gamma95]. Aufzählungstypen (Thema 34) bieten diese Garantie.

    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 ohne public- oder protected-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:

    from: Eine Typkonvertierungsmethode, die einen einzelnen Parameter übernimmt und eine korrespondierende Instanz dieses Typs zurückliefert, zum Beispiel:

    Date d = Date.from(instant);

    of: Eine Aggregationsmethode, die mehrere Parameter übernimmt und in eine Instanz dieses Typs packt, zum Beispiel:

    Set faceCards = EnumSet.of(JACK, QUEEN, KING);

    valueOf: Eine klarer benannte Alternative zu from und of, zum Beispiel:

    BigInteger prime =

    BigInteger.valueOf(Integer.MAX_VALUE);

    instance oder getInstance: Liefert eine Instanz, die durch ihre Parameter beschrieben wird (falls vorhanden), nicht aber unbedingt immer denselben Wert hat, zum Beispiel:

    StackWalker luke = StackWalker.getInstance(options);

    create oder newInstance: Wie instance oder getInstance, nur dass die Methode garantiert, dass bei jedem Aufruf eine neue Instanz zurückgeliefert wird, zum Beispiel:

    Object newArray = Array.newInstance(classObject, arrayLen);

    getType: Wie getInstance, für Fälle, in denen sich die Factory-Methode in einer anderen Klasse befindet. Type ist der Typ des von der Factory-Methode zurückgelieferten Objekts, zum Beispiel:

    FileStore fs = Files.getFileStore(path);

    newType: Wie newInstance, für Fälle, in denen sich die Factory-Methode in einer anderen Klasse befindet. Type ist der Typ des von der Factory-Methode zurückgelieferten Objekts, zum Beispiel:

    BufferedReader br = Files.newBufferedReader(path);

    type: Eine knapper formulierte Alternative zu getType und newType, zum Beispiel:

    List litany =

    Collections.list(legacyLitany);

    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:

    // Teleskopkonstruktor – skaliert nicht gut!

    public class NutritionFacts {

    private final int servingSize;  // (mL) obligatorisch

    private final int servings;    // (pro Packung) obligatorisch

    private final int calories;    // (pro Portion)  optional

    private final int fat;          // (g/Portion)    optional

    private final int sodium;      // (mg/Portion)    optional

    private final int carbohydrate; // (g/Portion)    optional

    public NutritionFacts(int servingSize, int servings) {

    this(servingSize, servings, 0);

    }

    public NutritionFacts(int servingSize, int servings,

    int calories) {

    this(servingSize, servings, calories, 0);

    }

    public NutritionFacts(int servingSize, int servings,

    int calories, int fat) {

    this(servingSize, servings, calories, fat, 0);

    }

    public NutritionFacts(int servingSize, int servings,

    int calories, int fat, int sodium) {

    this(servingSize, servings, calories, fat, sodium, 0);

    }

    public NutritionFacts(int servingSize, int servings,

    int calories, int fat, int sodium, int carbohydrate) {

    this.servingSize  = servingSize;

    this.servings    = servings;

    this.calories    = calories;

    this.fat          = fat;

    this.sodium      = sodium;

    this.carbohydrate = carbohydrate;

    }

    }

    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:

    NutritionFacts cocaCola =

    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:

    // JavaBeans-Muster – birgt die Gefahr inkonsistenter

    // Objekte, erlaubt nur veränderliche Objekte

    public class NutritionFacts {

    // Parameter mit Standardwerten initialisiert (sofern vorhanden)

    private int servingSize  = -1; // obligatorisch; kein Standardwert

    private int servings    = -1; // obligatorisch; kein Standardwert

    private int calories    = 0;

    private int fat          = 0;

    private int sodium      = 0;

    private int carbohydrate = 0;

    public NutritionFacts() { }

    // Set-Methoden

    public void setServingSize(int val)  { servingSize = val; }

    public void setServings(int val)    { servings = val; }

    public void setCalories(int val)    { calories = val; }

    public void setFat(int val)          { fat = val; }

    public void setSodium(int val)      { sodium = val; }

    public void setCarbohydrate(int val) { carbohydrate = val; }

    }

    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.

    NutritionFacts cocaCola = new NutritionFacts();

    cocaCola.setServingSize(240);

    cocaCola.setServings(8);

    cocaCola.setCalories(100);

    cocaCola.setSodium(35);

    cocaCola.setCarbohydrate(27);

    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;

    public static class Builder {

    // Obligatorische Parameter

    private final int servingSize;

    private final int servings;

    // Optionale Parameter – mit Standardwerten initialisiert

    private int calories      = 0;

    private int fat          = 0;

    private int sodium        = 0;

    private int carbohydrate  = 0;

    public Builder(int servingSize, int servings) {

    this.servingSize = servingSize;

    this.servings    = servings;

    }

    public Builder calories(int val)

    { calories = val;      return this; }

    public Builder fat(int val)

    { fat = val; return this; }

    public Builder sodium(int val)

    { sodium = val;        return this; }

    public Builder carbohydrate(int val)

    { carbohydrate = val;  return this; }

    public NutritionFacts build() {

    return new NutritionFacts(this);

    }

    }

    private NutritionFacts(Builder builder) {

    servingSize  = builder.servingSize;

    servings    = builder.servings;

    calories    = builder.calories;

    fat          = builder.fat;

    sodium      = builder.sodium;

    carbohydrate = builder.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:

    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)

    .calories(100).sodium(35).carbohydrate(27).build();

    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:

    // Builder-Muster für Klassenhierarchien

    public abstract class Pizza {

    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }

    final Set toppings;

    abstract static class Builder> {

    EnumSet toppings = EnumSet.noneOf(Topping.class);

    public T addTopping(Topping topping) {

    toppings.add(Objects.requireNonNull(topping));

    return self();

    }

    abstract Pizza build();

    // Subklassen müssen diese Methode überschreiben, um this

    // zurückzuliefern

    protected abstract T self();

    }

    Pizza(Builder builder) {

    toppings = builder.toppings.clone(); // Siehe Thema 50

    }

    }

    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:

    public class NyPizza extends Pizza {

    public enum Size { SMALL, MEDIUM, LARGE }

    private final Size size;

    public static class Builder extends Pizza.Builder {

    private final Size size;

    public Builder(Size size) {

    this.size = Objects.requireNonNull(size);

    }

    @Override public NyPizza build() {

    return new NyPizza(this);

    }

    @Override protected Builder self() { return this; }

    }

    private NyPizza(Builder builder) {

    super(builder);

    size = builder.size;

    }

    }

    public class Calzone extends Pizza {

    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder {

    private boolean sauceInside = false; // Standard

    public Builder sauceInside() {

    sauceInside = true;

    return this;

    }

    @Override public Calzone build() {

    return new Calzone(this);

    }

    @Override protected Builder self() { return this; }

    }

    private Calzone(Builder builder) {

    super(builder);

    sauceInside = builder.sauceInside;

    }

    }

    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.Builder Calzone zurü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:

    NyPizza pizza = new NyPizza.Builder(SMALL)

    .addTopping(SAUSAGE).addTopping(ONION).build();

    Calzone calzone = new Calzone.Builder()

    .addTopping(HAM).sauceInside().build();

    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:

    // Singleton mit einem public final-Feld

    public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { ... }

    public void leaveTheBuilding() { ... }

    }

    Der private Konstruktor wird nur einmal aufgerufen, um das public static final-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:

    // Singleton mit statischer Factory

    public class Elvis {

    private static final Elvis INSTANCE = new Elvis();

    private Elvis() { ... }

    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }

    }

    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. 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:

    // Unsaubere Verwendung einer statischen Hilfsklasse – unflexibel und

    // untestbar!

    public class SpellChecker {

    private static final Lexicon dictionary = ...;

    private SpellChecker() {} // Nicht-instanziierbar

    public static boolean isValid(String word) { ... }

    public static List suggestions(String typo) { ... }

    }

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

    // Unsaubere Verwendung eines Singleton – unflexibel und untestbar!

    public class SpellChecker {

    private final Lexicon dictionary = ...;

    private SpellChecker(...) {}

    public static INSTANCE = new SpellChecker(...);

    public boolean isValid(String word) { ... }

    public List suggestions(String typo) { ... }

    }

    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

    Gefällt Ihnen die Vorschau?
    Seite 1 von 1