제이온

[Effective Java] Item 1: Statische Factory-Methoden anstelle von Konstruktoren in Betracht ziehen

  • Verfasst in: Koreanisch
  • Land: Alle Ländercountry-flag
  • IT

Erstellt: 2024-04-27

Erstellt: 2024-04-27 00:45

Überblick

Die traditionelle Methode, um eine Instanz einer Klasse zu erhalten, ist der öffentliche Konstruktor.


```javascript public class Member {

}

public enum MemberStatus {

}


Normalerweise reichen öffentliche Konstruktoren aus, aber wenn neben Konstruktoren auch statische Factory-Methoden (static factory method) bereitgestellt werden, ist es für Benutzer oft einfacher, Instanzen gemäß ihren Absichten zu erstellen.


Ein typisches Beispiel für statische Factory-Methoden ist die valueOf()-Methode von Boolean.


```javascript public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }


Diese Methode akzeptiert einen Wert vom Basistyp boolean und gibt ein Boolean-Objekt zurück.


Vorteile von statischen Factory-Methoden

Sie können einen Namen haben.

Die an den Konstruktor übergebenen Parameter und der Konstruktor selbst beschreiben die Eigenschaften des zurückgegebenen Objekts nicht ausreichend. Nehmen wir zum Beispiel die Hauptklasse des Member-Konstruktors (name, age, hobby, memberStatus). Es ist schwierig zu erkennen, welche Eigenschaften ein Mitglied hat, wenn man nur den Konstruktor betrachtet.


Außerdem kann man mit einer Signatur nur einen Konstruktor erstellen, während statische Factory-Methoden einen Namen haben und somit mit einer Signatur mehrere statische Factory-Methoden erstellen können, um Instanzen zurückzugeben.


```javascript public class Member {

}


Wenn man mehrere statische Factory-Methoden mit der gleichen Signatur erstellt, anstatt den MemberStatus über den Konstruktor zu unterscheiden, kann der Benutzer eine Instanz eines Mitglieds mit bestimmten Fähigkeiten erstellen, ohne Verwirrung zu stiften.

Betrachten wir die in JDK definierte Bibliothek, so gibt es die statische Factory-Methode probablePrime() von BigInteger.


```javascript public static BigInteger probablePrime(int bitLength, Random rnd) { if (bitLength < 2) throw new ArithmeticException("bitLength < 2");

}


Wenn man den Standardkonstruktor von BigInteger mit der statischen Factory-Methode probablePrime() vergleicht, ist es klar, dass Letztere den Satz „Gibt ein BigInteger-Objekt zurück, dessen Wert eine Primzahl ist“ besser beschreibt.


Sie müssen bei jedem Aufruf keine neue Instanz erstellen.

```javascript public static Boolean valueOf(boolean b) { return (b ? Boolean.TRUE : Boolean.FALSE); }


Man kann sehen, dass die valueOf()-Methode von Boolean eine Instanz zwischenspeichert und dann zurückgibt. Dieses Merkmal kann die Leistung erheblich verbessern, wenn Objekte mit hohen Erstellungskosten häufig angefordert werden.Flyweight-Musterkann als eine ähnliche Technik betrachtet werden.


Klassen, die statische Factory-Methoden verwenden, um bei wiederholten Anfragen dasselbe Objekt zurückzugeben, werden als Instanzsteuerungsklassen bezeichnet, da sie die Lebensdauer von Instanzen steuern können. Die Instanzsteuerung ermöglicht es, Singleton-Klassen zu erstellen oder Klassen zu erstellen, die nicht instanziiert werden können. Außerdem kann es in unveränderlichen Klassen sichergestellt werden, dass es nur eine einzige Instanz gibt.

Die Instanzsteuerung ist die Grundlage des Flyweight-Musters, und Aufzählungsdatentypen gewährleisten, dass nur eine einzige Instanz erstellt wird.


Beispiel

In Minecraft müssen Bäume gepflanzt werden. Wenn für jedes Baumobjekt ein neues Objekt erstellt wird, besteht die Gefahr eines Memory-Overflows.

Daher kann man die roten und grünen Baumobjekte speichern und nur ihre Positionen ändern, bevor man sie zurückgibt. Natürlich kann es mehr als zwei Farben geben, daher wäre es effizient, die Bäume nach Farbe in einer Datenstruktur wie einer Map zu speichern.


```javascript public class Tree {

}

public class TreeFactory { // HashMap-Datenstruktur zum Verwalten von erstellten Bäumen. public static final Map<String, Tree> treeMap = new HashMap<>();


}

public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in);

}


Unterschied zum Singleton-Muster

Das Singleton-Muster erlaubt es, nur einen einzigen Baum in der Baumklasse zu erstellen. Daher müsste man die Farbe des erstellten einzelnen Objekts ändern, wenn man das Singleton-Muster verwendet. Das bedeutet, dass man mit dem Singleton-Muster nur einen einzigen Baum haben kann, unabhängig von der Art.


Einsatzmöglichkeiten

Das Flyweight-Muster wird im String Constant Pool von Java verwendet.


Sie können ein Objekt eines Untertyps des Rückgabetyps zurückgeben.

Wenn Sie die asList()-Methode der Arrays-Dienstprogramm-Klasse verwendet haben, können Sie diesen Vorteil verstehen.


```javascript public static List asList(T... a) [ return new ArrayList<>(a); }


Es wird ein Wert in eine ArrayList, eine Unterimplementierung von List, verpackt und zurückgegeben, wobei der Benutzer diese Implementierung nicht kennen muss. Mit anderen Worten, die Flexibilität, die Klasse des zurückgegebenen Objekts frei wählen zu können, ermöglicht es dem Entwickler, die Implementierung zu verbergen und die Implementierung zurückzugeben, wodurch die API klein gehalten werden kann.


Über statische Methoden in Java-Schnittstellen

Vor Java 8 konnten in Schnittstellen keine statischen Methoden deklariert werden. Wenn also eine statische Methode erforderlich war, die eine Schnittstelle mit dem Namen „Type“ zurückgab, wurde eine Begleitklasse namens „Types“ erstellt, die nicht instanziiert werden konnte und die Methode enthielt.


Ein bekanntes Beispiel sind die 45 Dienstprogramm-Implementierungen, die von JCF bereitgestellt werden, von denen die meisten über statische Factory-Methoden in einer einzigen Begleitklasse namens java.util.Collections erhalten werden. Insbesondere gibt es unter diesen Implementierungen einige, die nicht öffentlich sind und daher nur über statische Factory-Methoden instanziiert werden können (diese Implementierungen können natürlich nicht vererbt werden).


Außerdem werden die 45 Implementierungen nicht offengelegt, was zu einer viel kleineren API führt.


```javascript // Beispiel für eine Schnittstelle und eine Begleitklasse List empty = Collections.emptyList();


Ab Java 8 können jedoch statische Methoden direkt zu Schnittstellen hinzugefügt werden, sodass keine separate Begleitklasse mehr definiert werden muss.


Es ist möglich, je nach Eingabeparameter ein Objekt einer anderen Klasse zurückzugeben.

Neben der einfachen Rückgabe eines Untertyps kann man je nach Wert des Parameters einen anderen Untertyp zurückgeben. Wenn man beispielsweise den MemberStatus je nach Punktzahl zurückgeben möchte, kann man eine statische Factory-Methode wie folgt erstellen und dort die Vergleichslogik einfügen.


```javascript public enum MemberStatus {

}

@DisplayName("MemberStatus-Test") class MemberStatusTest {

}


Zum Zeitpunkt des Schreibens der statischen Factory-Methode muss die Klasse des zurückzugebenden Objekts nicht existieren.

Im obigen Satz bezieht sichKlasse des Objektsauf unsere erstellte Klassendatei.

Als Hinweis: Class<?> bezieht sich auf das Class-Objekt, das vom Classloader beim Laden einer Klasse auf dem Heap zugewiesen wird. Dieses Class-Objekt enthält verschiedene Metadaten unserer erstellten Klasse.


```javascript package algorithm.dataStructure;

public abstract class StaticFactoryMethodType {

}


Im obigen Code kann man sehen, dass ein Class-Objekt über die Position der Implementierung der Schnittstelle erstellt wird und die tatsächliche Implementierung mithilfe der Reflexionstechnik initialisiert wird. Zu diesem Zeitpunktzum Zeitpunkt des Schreibens der statischen Factory-Methodemuss die Klasse StaticFactoryMethodTypeChild nicht existieren.


Wenn zum Zeitpunkt der Verwendung der statischen Factory-Methode keine Implementierung im Pfad algorithm.dataStructure.StaticFactoryMethodTypeChild vorhanden ist, tritt ein Fehler auf. Aber zum Zeitpunkt des Schreibens der statischen Factory-Methode gibt es kein Problem, daher ist es flexibel.


```javascript public interface Test {

}

public class Main {

}


Man kann dieselbe Flexibilität erreichen, ohne Reflexion zu verwenden. Die statische Factory-Methode create() von Test zeigt, dass zum Zeitpunkt des Schreibens kein Problem auftritt, auch wenn keine Implementierung vorhanden ist. Natürlich tritt zum Zeitpunkt der tatsächlichen Verwendung eine NPE auf, daher muss später eine Implementierung zurückgegeben werden.


Diese Flexibilität ist die Grundlage für die Erstellung von Service Provider Frameworks, von denen JDBC ein typisches Beispiel ist. Der Anbieter eines Service Provider Frameworks ist die Implementierung des Service, und das Framework steuert die Bereitstellung dieser Implementierungen an den Client, um den Client von der Implementierung zu trennen (DIP).


  • Komponenten eines Service Provider Frameworks
    • Service-Schnittstelle
      • Definiert das Verhalten der Implementierung.
      • JDBC-Verbindung
    • API zur Anbieterregistrierung
      • Der Anbieter registriert die Implementierung.
      • JDBC-DriverManager.registerDriver()
    • API zum Zugriff auf Dienste
      • Wird verwendet, wenn der Client eine Instanz des Service abruft. Wenn keine Bedingung angegeben wird, wird die Standardimplementierung oder die unterstützten Implementierungen abwechselnd zurückgegeben.
      • Relevanz von statischen Factory-Methoden
      • JDBC-DriverManager.getConnection()
    • (optional) Service Provider Interface
      • Wenn dies nicht vorhanden ist, muss die Reflexion verwendet werden, um jede Implementierung zu instanziieren.
      • JDBC-Treiber


Das Service Provider Framework-Muster hat viele Variationen, darunter das Bridge-Muster, Dependency Injection Frameworks usw.


Typisches JDBC-Beispiel

```javascript Class.forName("oracle.jdbc.driver.OracleDriver"); Connection connection = null; connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORA92", "root", "root");

// SQL-Logik mit verschiedenen Anweisungen


Normalerweise wird JDBC wie oben geschrieben. Class.forName() wird verwendet, um OracleDriver zu registrieren, eine der Implementierungen von Driver. DriverManager.getConnection() wird verwendet, um eine Verbindung zur Implementierung von OracleDriver zu erhalten, eine der Implementierungen von Connection.


Dabei ist Connection die Service-Schnittstelle, DriverManager.getConnection() ist die Service-Zugriffs-API und Driver ist die Service Provider-Schnittstelle. Die API zur Anbieterregistrierung, DriverManager.registerDriver(), wurde jedoch nicht verwendet. Trotzdem können wir OracleDriver, eine Implementierung von Driver, mit Class.forName() allein registrieren. Wie ist das möglich?


Funktionsweise von Class.forName()

Wenn man diese Methode mit dem Namen der physischen Klassendatei als Argument aufruft, wird die JVM aufgefordert, diese Klasse zu laden. Der Classloader speichert die Metadaten der Klasse im Methodenbereich und weist gleichzeitig ein Class-Objekt auf dem Heap zu. Außerdem wird nach Abschluss des Klassengeladensder statische Feld- und Blockinitialisierungsvorgang durchgeführt, und in diesem Schritt wird die API zur Anbieterregistrierung verwendet.


```javascript public class OracleDriver implements Driver {

}


Tatsächlich kann man sehen, dass in OracleDriver im statischen Block DriverManager.registerDriver() verwendet wird, um OracleDriver, die Implementierung von Driver, zu registrieren.


Analyse der DriverManager-Klasse

```javascript public class DriverManager {

}


Die DriverManager-Klasse ist in Wirklichkeit viel komplexer, aber wenn man sich auf das Wesentliche konzentriert, ist sie der oben beschriebenen ähnlich. Wie bereits erwähnt, wird registerDriver() in der statischen Blockanweisung von OracleDriver aufgerufen, um OracleDriver zu registrieren, und der Benutzer kann eine Implementierung von Connection abrufen, indem er getConnection() aufruft.


Wenn man sich die Service-Zugriffs-API getConnetion() genauer ansieht, stellt man fest, dass eine Verbindung aus der Driver-Schnittstelle abgerufen wird. Wenn es die Service Provider-Schnittstelle Driver nicht gäbe, müsste man Reflexion wie Class.forName() verwenden, um die gewünschte Connection-Implementierung zurückzugeben. Zu diesem Zeitpunktmuss die Connection-Implementierung zum Zeitpunkt der Erstellung der statischen Factory-Methode nicht existieren.


Stattdessen verwenden wir die Driver-Schnittstelle, registrieren dynamisch die Implementierung von Driver und können dann einfach eine Implementierung von Connection abrufen, die zu diesem Driver passt.


Als Hinweis: Ich habe den tatsächlichen JDK-Code der getConnection()-Methode von DriverManager analysiert. Wenn Sie nicht daran interessiert sind, können Sie dies überspringen.


```javascript @CallerSensitive public static Connection getConnection(String url, String user, String password) throws SQLException { java.util.Properties info = new java.util.Properties();

}


Zuerst wird die öffentliche statische Methode getConnection() aufgerufen, wobei url, Properties und CallerClass als Argumente an die private statische Methode getConnection() übergeben werden. Reflection.getCallerClass() dient dazu, die Klasse abzurufen, die die öffentliche statische Methode getConnection() aufgerufen hat. Wenn die Klasse Car getConnection() aufgerufen hat, kann Reflection.getCallerClass() das Class-Objekt abrufen.


```javascript private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } }

}


callerCL ist ein Classloader-Objekt, das vom Classloader von caller oder dem aktuellen Thread erstellt wird. Anschließend wird aDriver nacheinander aus registeredDrivers, der Liste der in der aktuellen Anwendung registrierten Treiber, entnommen. Wenn dieser Driver von isDriverAllowed() als true erkannt wird, wird mithilfe dieses Drivers ein Connection-Objekt abgerufen und zurückgegeben. isDriverAllowed() prüft, ob aDriver in caller vorhanden ist.


Vorteile des JDBC-Frameworks

Der springende Punkt des JDBC-Frameworks ist, dass die Schnittstellen Driver und Connection sowie die Klassen, die diese Schnittstellen implementieren, vollständig getrennt bereitgestellt werden. Indem man die Schnittstellen verwendet, um einen Rahmen zu erstellen, und dann separate Implementierungsklassen zu diesem Rahmen erstellt, erhält man einen sehr flexiblen Ansatz.


So kann jeder DBMS-Anbieter die Schnittstellen Driver und Connection implementieren und diese bereitstellen, so dass Java-Entwickler dieselbe API für verschiedene DBMS-Treiber verwenden können.


Nachteile von statischen Factory-Methoden

Wenn man erbt, benötigt man einen öffentlichen oder geschützten Konstruktor. Wenn man nur statische Factory-Methoden bereitstellt, kann man keine Unterklassen erstellen.


Diese Einschränkung ist jedoch eher ein Vorteil, da sie eher zu Komposition als zu Vererbung führt und diese Einschränkung eingehalten werden muss, um unveränderliche Typen zu erstellen.


Statische Factory-Methoden sind für Programmierer schwer zu finden.

Da sie nicht so deutlich in der API-Beschreibung wie Konstruktoren dargestellt werden, müssen Entwickler die API-Dokumentation gut schreiben und die Methoden benennen, indem sie allgemein bekannte Konventionen befolgen, um das Problem zu entschärfen.


Benennung von statischen Factory-Methoden

  • from
    • Nimmt ein Argument entgegen und gibt eine Instanz dieses Typs zurück.
    • Date date = Date.from(instant);
  • of
    • Nimmt mehrere Argumente entgegen und gibt eine Instanz des entsprechenden Typs zurück.
    • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf
    • Detailliertere Version von from und of.
    • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance oder getInstance
    • Gibt die Instanz zurück, die durch den Parameter angegeben wird, garantiert aber nicht, dass es sich um dieselbe Instanz handelt.
    • StackWalker luke = StackWalker.getInstance(options);
  • create oder newInstance
    • Wie instance oder getInstance, garantiert aber, dass jedes Mal eine neue Instanz erstellt und zurückgegeben wird.
    • Object newArray = Array.newInstance(classObject, arraylen);
  • getType
    • Wie getInstance, wird aber verwendet, wenn die Factory-Methode nicht in der zu erstellenden Klasse, sondern in einer anderen Klasse definiert ist.
    • FileStore fs = Files.getFileStore(path);
  • newType
    • Wie newInstance, wird aber verwendet, wenn die Factory-Methode nicht in der zu erstellenden Klasse, sondern in einer anderen Klasse definiert ist.
    • BufferedReader br = Files.newBufferedReader(path);
  • type
    • Kompakte Version von getType und newType.
    • List<Complaint> litany = Collections.list(legacyLitany);


Zusammenfassung

Statische Factory-Methoden und öffentliche Konstruktoren haben jeweils ihren eigenen Zweck, daher sollten Sie sie entsprechend verwenden.


Quelle

Kommentare0