Vom Anfang der Dinge

Java-Trickkiste: Patterns zum Instanziieren von Klassen

Arno Haase

© Shutterstock/Nantawat Chotsuwan

Diesen Monat stelle ich einige Patterns zum Instanziieren von Klassen vor. Bei Coaching und Codereviews erlebe ich, dass viele Entwickler sich weitgehend auf Default-Konstruktoren und DI-Frameworks beschränken – was nicht per se schlecht ist, aber es gibt für manche Situationen bessere Alternativen. Wenn das eine oder andere bekannt vorkommt: umso besser. Es geht hier nicht darum, bahnbrechende neue Ideen zu präsentieren, sondern ein paar im Prinzip bekannte Lösungen in Erinnerung zu rufen.

Initialisierung im Konstruktor

Nicht alle Felder einer Klasse brauchen Setter-Methoden, auch wenn IDEs anbieten, sie zu generieren. Wenn man Felder in einem Konstruktor initialisiert, kann man sie final machen, und der Code ist klarer und bietet weniger Möglichkeiten zur falschen Benutzung. Die Initialisierung per Konstruktor funktioniert unter dem Namen Constructor Injection übrigens auch mit gängigen DI-Frameworks (Listing 1). Spring und CDI unterstützen das schon lange, EJBs inzwischen auch – die Spec erzwingt allerdings, dass zusätzlich ein No-Args-Konstruktor vorhanden ist.

public class CustomerRegistrationService {
  private final EmailService emailService;

  @Inject
  public CustomerRegistrationService (EmailService s) {
    this.emailService = s;
  }
  ... // Methoden zum Registrieren von Kunden
}

Neben der Klarheit des Codes gewinnt man mit diesem Stil der Initialisierung, dass es keine zyklischen Abhängigkeiten zwischen Klassen geben kann (bzw. man sie sofort merkt). Man kann ein Objekt ja erst dann an einen anderen Konstruktor übergeben, wenn es fertig initialisiert ist. Außerdem sieht man gerade beim Schreiben der Unit Tests sofort, welche Abhängigkeiten eine Klasse hat. Man kann sie ggf. leicht mocken, und wenn Abhängigkeiten sich ändern, weist der Compiler darauf hin.

Factory-Methoden

Wenn eine Klasse mehr als einen Konstruktor hat oder das Instanziieren aus anderen Gründen kompliziert ist, machen statische Factory-Methoden den Code oft klarer und robuster. Die JDK-Bibliotheken sind eine reiche Fundgrube für Beispiele, besonders diejenigen APIs, die erst in den letzten Jahren entstanden sind. Ein gutes Beispiel ist die Klasse Instant, die als Teil des neuen Time-API in Java 8 so etwas wie der Nachfolger von java.util.Date ist. Dort gibt es (neben vielen anderen) die statischen Methoden Instant.now(), Instant.ofEpochMilli(long) und Instant.ofEpochSecond(long), die jeweils einen Instant zurückliefern. Das ist besser lesbar als die altbekannten Konstruktoren new Date() bzw. new Date(long). Und während man bei Date wissen muss, dass ein long-Parameter die Millisekunden seit 1970 enthält, hat Instant je eine Variante für Sekunden und Millisekunden mit jeweils sprechenden Namen. Factory-Methoden können auch selektiv Werte aus einem Cache zurückgeben, ohne dass der Aufrufer das auch nur wissen muss. Das tut z. B. die Methode Integer.valueOf(int), die für Werte von -128 bis 127 Instanzen aus einem globalen Cache liefert. Das deckt die häufigsten Zahlen ab und spart signifikant Speicher und entlastet die GC. Die Factory-Methode Integer.valueOf(int) kann – anders als ein Konstruktoraufruf – entscheiden, ob sie eine neue Instanz erzeugt oder einen Cache benutzt. Außerdem können Factory-Methoden Details einer Vererbungshierarchie kapseln. Die Methode NumberFormat.getIntegerInstance(Locale) liefert z. B. eine Instanz von irgendeiner Subklasse von NumberFormat. Typischerweise ist das eine Instanz von DecimalFormat. Aber wie ist das für Chinesisch, Arabisch oder Koreanisch? Anwendungscode muss das nicht wissen, weil die Factory-Methode die Vererbungshierarchie kapselt. Seit Java 8 können statische Factory-Methoden auch in Interfaces stehen. Das kapselt die Details einer Vererbungshierarchie besonders konsequent. So hat das neue Stream-Interface z. B. eine statische Methode Stream.empty(), die einen leeren Stream erzeugt, ohne dass man eine konkrete Klasse dafür kennen müsste.

ServiceLoader

Viele Frameworks bieten Plug-in-Mechanismen, mit denen „fremder“ Code Funktionalität beisteuern kann. Eine Herausforderung ist dabei, wie diese Plug-ins sich beim Framework registrieren – Sinn der Plug-ins ist ja gerade, dass das Framework sie nicht alle kennen muss. Die Klasse java.util.ServiceLoader löst genau dieses Problem. Nehmen wir an, wir schreiben ein Office-Programm, das Daten in einer wie auch immer gearteten Klasse MyDocument hält. Das Programm soll Dokumente in eine Vielzahl von Formaten exportieren können, und wir wollen dafür einen Plug-in-Mechanismus bereitstellen.

Als Erstes muss das Framework ein Interface DocumentExporter definieren (Listing 2). Dort gibt es eine Methode export(), die ein Dokument in einen OutputStream schreibt, und außerdem eine Methode getId(), die einen (hoffentlich) eindeutigen Identifier liefert, anhand dessen der Anwender einen Exporter auswählen kann.

package framework;
public interface DocumentExporter {
  String getId();
  void export (MyDocument doc, OutputStream out) throws IOException;
}

Ein geschäftstüchtiger Drittanbieter kann dieses Interface jetzt implementieren (Listing 3). Der Code exportiert Dokumente mittels Java-Serialisierung – ohne jeden Anspruch auf Sinnhaftigkeit.

package thirdparty;
public class SerializingExporter implements DocumentExporter {
  public String getId() {return "Serialization";}
  public void export (MyDocument doc, OutputStream out) throws IOException {
    try (ObjectOutputStream oos = new ObjectOutputStream (out)) {
      oos.writeObject (doc);
    }
  }
}

Damit das Framework den DocumentExporter finden kann, muss der Drittanbieter im Verzeichnis META-INF/services eine Textdatei bereitstellen, die genau wie das Interface heißt, also META-INF/services/framework.DocumentExporter. In dieser Datei steht der Name der Implementierungsklasse, die bereitgestellt wird, hier also thirdparty.SerializingExporter. Klasse und Textdatei müssen im Classpath der Anwendung liegen. Die Klasse java.util.ServiceLoader kann jetzt alle DocumentExporter liefern, die auf diese Weise im Classpath liegen (Listing 4) und die Anwendung kann sie z. B. als Liste im GUI zur Auswahl anbieten.

for (DocumentExporter exporter: ServiceLoader.load (DocumentExporter.class)) {
  // Anzeigen einer Auswahlliste o. Ä.
}

Dieser Mechanismus ist nützlich, um Implementierungen aus verschiedenen Quellen global zu sammeln. Die Standardbibliothek verwendet ihn z. B. zum Registrieren von JDBC-Treibern oder Character Encodings. Das funktioniert gut, wenn das Framework zur Laufzeit gut eine Implementierung auswählen kann (z. B. anhand der JDBC-URL).

Es gibt auch Beispiele, wo der ServiceLoader-Mechanismus besser nicht verwendet worden wäre. So erwartet Hibernate z. B. ab Version 4, dass Anwendungen einen Integrator (eine Art, Hibernate zu konfigurieren) auf diese Weise registrieren. Dadurch ist jeder Integrator, der im Classpath liegt, automatisch immer aktiv. Auch zu Testzwecken o. Ä. kann man ihn nicht deaktivieren oder durch eine andere Implementierung ersetzen.

Singleton

Singletons sind fast immer eine schlechte Idee. Sie machen Unit Tests sehr schwierig, ihr Lebenszyklus ist fest an den der ganzen JVM gekoppelt, und sie sind effektiv globale Variablen mit all deren Nachteilen. Viele Entwickler verwenden sie leider trotzdem, deshalb hier ein paar Bemerkungen dazu, wie man sie sinnvoll initialisiert. Man kann nämlich einfach eine public static final-Variable INSTANCE verwenden, die die einzige Instanz der Klasse enthält (Listing 5).

public class MySingleton {
  public static final MySingleton INSTANCE = 
      new MySingleton();

  private MySingleton() {
    System.out.println ("Konstruktor");
  }

  public void machWas() {
    System.out.println ("mach was");
  }
}

Java initialisiert diese Variable zum spätest möglichen Zeitpunkt, also erst bei ihrer ersten Verwendung. Listing 6 verwendet das Singleton, und die Reihenfolge der Ausgaben zeigt: Der Konstruktor des Singletons wird tatsächlich erst aufgerufen, nachdem „vorher“ ausgegeben wurde.

System.out.println("vorher");
MySingleton.INSTANCE.machWas();

Diese Art der Initialisierung des Singletons hat eine Reihe von Vorteilen: Sie ist einfach, gut lesbar und lazy. Außerdem garantiert die JVM die Threadsicherheit, und das bei minimalem Overhead. Wenn also schon Singletons, dann zumindest auf diese Weise.

Fazit

Es gibt eine Reihe bewährter Muster zum Instanziieren und Initialisieren von Objekten. Und auch moderne Anwendungen mit DI-Frameworks profitieren davon, sie mit ihren jeweiligen Vor- und Nachteilen zu kennen.

Aufmacherbild: Seamless Black & White Close up Butterfly wing Pattern Background von Shutterstock / Urheberrecht: Nantawat Chotsuwan

Geschrieben von
Arno Haase
Arno Haase
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationaler Datenbank und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
Kommentare

Hinterlasse einen Kommentar

3 Kommentare auf "Java-Trickkiste: Patterns zum Instanziieren von Klassen"

avatar
400
  Subscribe  
Benachrichtige mich zu:
joschi
Gast

Erstaunlich, dass sich diese Variante des Singleton Musters in Java immer noch hartnäckig hält.

Dabei gibt es seit Java 5 die wunderbar einfache Möglichkeit Singletons als `enum` zu implementieren. Details dazu unter http://javarevisited.blogspot.de/2012/07/why-enum-singleton-are-better-in-java.html

Patrick Gotthard
Gast

Wenn Singletons schon angesprochen werden, sollte wenigstens die deutlich bessere enum Variante aufgezeigt werden.

Sebastian Dietrich
Gast

Das Entwurfsmuster Bilder sollte auch erwähnt werden, da es insbesondere bei komplex, oder auf viele Arten zu instanziierenden Klassen, zu deutlich wartbarerem Code führt.