![translation](https://cdn.durumis.com/common/trans.png)
Questo è un post tradotto da IA.
[Effective Java] Item 1. Consider static factory methods instead of constructors
- Lingua di scrittura: Coreana
- •
-
Paese di riferimento: Tutti i paesi
- •
- Tecnologia dell'informazione
Seleziona la lingua
Testo riassunto dall'intelligenza artificiale durumis
- I metodi di fabbrica statici sono un modo per creare istanze di una classe al posto dei costruttori, offrendo il vantaggio di poter denominare le istanze, migliorare le prestazioni memorizzando nella cache oggetti con un costo di creazione elevato e restituire, se necessario, oggetti di sottotipi diversi.
- In particolare, prima di Java 8, non era possibile dichiarare metodi statici nelle interfacce, quindi era necessario creare una classe companion per definire i metodi di fabbrica statici. Dopo Java 8, è possibile aggiungere metodi statici direttamente alle interfacce, eliminando la necessità di definire una classe companion separata.
- L'utilizzo di metodi di fabbrica statici può incoraggiare la composizione rispetto all'eredità, e il fatto che questo vincolo debba essere rispettato per creare tipi immutabili può essere un vantaggio. Tuttavia, a differenza dei costruttori, non sono esplicitamente visibili nella documentazione dell'API, quindi gli sviluppatori devono mitigare questo problema fornendo una buona documentazione dell'API e utilizzando nomi di metodi che seguono convenzioni ben consolidate.
Panoramica
Il modo tradizionale per ottenere un'istanza di una classe è tramite un costruttore pubblico.
public class Member {
private String name;
private int age;
private String hobby;
private MemberStatus memberStatus;
public Member(String name, int age, String hobby, MemberStatus memberStatus) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.memberStatus = memberStatus;
}
}
public enum MemberStatus {
ADVANCED,
INTERMEDIATE,
BASIC;
Generalmente, un costruttore pubblico è sufficiente, ma a volte, fornendo un metodo statico di fabbrica (static factory method) oltre al costruttore, è più facile per l'utente creare un'istanza come previsto.
Un esempio tipico di metodo statico di fabbrica è il metodo valueOf() di Boolean.
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
Il metodo sopra accetta un valore di tipo primitivo boolean e restituisce un oggetto Boolean.
Vantaggi dei metodi statici di fabbrica
Può avere un nome.
I parametri passati al costruttore e il costruttore stesso non sono in grado di descrivere adeguatamente le caratteristiche dell'oggetto restituito. Ad esempio, è difficile capire da solo quale tipo di Member sia dalla classe Member di cui sopra, solo guardando il costruttore principale (nome, età, hobby, stato membro).
Inoltre, è possibile creare un solo costruttore per una singola firma, mentre i metodi statici di fabbrica possono avere un nome, quindi è possibile creare più metodi statici di fabbrica con la stessa firma per restituire istanze.
public class Member {
private String name;
private int age;
private String hobby;
private MemberStatus memberStatus;
public Member(String name, int age, String hobby, MemberStatus memberStatus) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.memberStatus = memberStatus;
}
public static Member basicMember(String name, int age, String hobby) {
return new Member(name, age, hobby, MemberStatus.BASIC);
}
public static Member intermediateMember(String name, int age, String hobby) {
return new Member(name, age, hobby, MemberStatus.INTERMEDIATE);
}
public static Member advancedMember(String name, int age, String hobby) {
return new Member(name, age, hobby, MemberStatus.ADVANCED);
}
Come sopra, creare più metodi statici di fabbrica con la stessa firma anziché usare il costruttore per distinguere il MemberStatus, consente all'utente di creare un'istanza di Member con specifiche capacità senza confusione.
Guardando le librerie definite nel JDK, esiste un metodo statico di fabbrica, probablePrime(), in BigInteger.
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
Comparando il costruttore generale di BigInteger con il metodo statico di fabbrica probablePrime(), quest'ultimo descrive meglio il fatto che restituisce un BigInteger che è un numero primo.
Non è necessario creare una nuova istanza ogni volta che viene chiamato.
public static Boolean valueOf(boolean b) {
return (b ? Boolean.TRUE : Boolean.FALSE);
Si può vedere che il metodo valueOf() di Boolean mette in cache le istanze e le restituisce. Questa caratteristica può migliorare significativamente le prestazioni, soprattutto quando vengono richieste frequentemente oggetti con elevati costi di creazione, ed è possibile vederla come una tecnica simile aFlyweight Pattern.
Le classi che usano il metodo statico di fabbrica per restituire lo stesso oggetto alle richieste ripetute sono chiamate classi di controllo delle istanze perché possono controllare il ciclo di vita delle istanze. Controllando le istanze, è possibile creare una classe singleton o una classe non istanziabile. Inoltre, è possibile garantire che ci sia una sola istanza nelle classi di valori immutabili.
Il controllo delle istanze è alla base del modello Flyweight e i tipi enumerati garantiscono che venga creata solo una singola istanza.
Esempio
In Minecraft, è necessario piantare alberi. Se si crea un nuovo oggetto per ogni albero, è probabile che si verifichi un overflow di memoria.
Pertanto, come sopra, è possibile memorizzare gli oggetti albero rosso e albero verde e restituire solo la posizione. Ovviamente, poiché il colore può essere più di due colori, sarebbe più efficiente memorizzare gli alberi in base al colore in una struttura dati come Map.
public class Tree {
// Un albero ha le seguenti 3 informazioni
private String color;
private int x;
private int y;
// Il costruttore è creato solo dal colore.
public Tree(String color) {
this.color = color;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
// Quando pianti un albero
public void install(){
System.out.println("x:"+x+" y:"+y+" posizione con un albero di colore "+color+"!");
}
}
public class TreeFactory {
// Gestisce gli alberi creati utilizzando la struttura dati HashMap.
public static final Map treeMap = new HashMap<>();
public static Tree getTree(String treeColor){
// Controlla se c'è un albero del colore immesso nella mappa. Se esiste, fornisce quell'oggetto.
Tree tree = (Tree)treeMap.get(treeColor);
// Se non c'è ancora un albero dello stesso colore nella mappa, crea un nuovo oggetto e forniscilo.
if(tree == null){
tree = new Tree(treeColor);
treeMap.put(treeColor, tree);
System.out.println("Nuovo oggetto creato");
}
return tree;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Inserisci il colore desiderato :)");
for(int i=0;i<10;i++){
// Inserimento del colore dell'albero
String input = scanner.nextLine();
// Ricezione di un albero dalla fabbrica
Tree tree = (Tree)TreeFactory.getTree(input);
// Imposta x,y dell'albero e
tree.setX((int) (Math.random()*100));
tree.setY((int) (Math.random()*100));
// Installa l'albero
tree.install();
}
}
Differenza con il modello Singleton
Il modello Singleton consente di creare un solo albero nella classe albero. Pertanto, se si utilizza il modello Singleton, è necessario modificare il colore dell'unico oggetto creato. In altre parole, il modello Singleton consente di avere un solo oggetto indipendentemente dal tipo.
Casi d'uso
Il modello Flyweight viene utilizzato nel pool di costanti String di Java.
Può restituire un oggetto di un sottotipo del tipo di ritorno.
Se hai mai usato il metodo asList() della classe utility Arrays, puoi capire questo vantaggio.
public static List asList(T... a) [
return new ArrayList<>(a);
Restituisce un valore avvolto in ArrayList, che è un'implementazione di sottotipo di List, ma l'utente non ha bisogno di conoscere questa implementazione. Vale a dire, la flessibilità di poter scegliere la classe dell'oggetto restituito consente allo sviluppatore di restituire l'implementazione senza esporla, il che consente di mantenere l'API di dimensioni ridotte.
Storia dei metodi statici dell'interfaccia Java
Prima di Java 8, non era possibile dichiarare metodi statici nelle interfacce, quindi se fosse stato necessario un metodo statico che restituisse un'interfaccia denominata "Type", sarebbe stato necessario creare una classe associata non istanziabile denominata "Types" per definire il metodo al suo interno.
Un esempio tipico sono le 45 implementazioni di utilità fornite da JCF, la maggior parte delle quali vengono ottenute tramite metodi statici di fabbrica nella classe associata java.util.Collections. In particolare, alcune di queste implementazioni non sono pubbliche e possono essere istanziate solo tramite metodi statici di fabbrica (naturalmente, queste implementazioni non possono essere ereditate).
Inoltre, poiché non vengono esposte 45 implementazioni, l'API può essere molto più piccola.
// Esempio di interfaccia e classe associata
Tuttavia, a partire da Java 8, è possibile aggiungere metodi statici direttamente alle interfacce, quindi non è più necessario definire una classe associata separatamente.
È possibile restituire un oggetto di una classe diversa a seconda del parametro di input.
Oltre a restituire semplicemente un sottotipo, è possibile restituire un sottotipo diverso a seconda del valore del parametro. Ad esempio, se si desidera restituire un MemberStatus diverso a seconda del punteggio, è possibile creare un metodo statico di fabbrica come quello sottostante e inserire la logica di confronto al suo interno.
public enum MemberStatus {
ADVANCED(80, 100),
INTERMEDIATE(50, 79),
BASIC(0, 49);
private final int minScore;
private final int maxScore;
MemberStatus(int minScore, int maxScore) {
this.minScore = minScore;
this.maxScore = maxScore;
}
public static MemberStatus of(int score) {
return Arrays.stream(values())
.filter(decideMemberStatus(score))
.findAny()
.orElseThrow(() -> new NoSuchElementException("Non esiste un oggetto MemberStatus corrispondente."));
}
private static Predicate decideMemberStatus(int score) {
return element -> element.minScore <= score && element.maxScore >= score;
}
}
@DisplayName("MemberStatus Test")
class MemberStatusTest {
@ParameterizedTest
@CsvSource(value = {"0:BASIC", "30:BASIC", "50:INTERMEDIATE", "70:INTERMEDIATE", "80:ADVANCED", "100:ADVANCED"}, delimiter = ':')
@DisplayName("Restituisce MemberStatus diverso a seconda del punteggio.")
void of(int input, MemberStatus expected) {
assertThat(MemberStatus.of(input)).isEqualTo(expected);
}
La classe dell'oggetto da restituire non deve esistere al momento della scrittura del metodo statico di fabbrica.
Nella frase sopra,classe dell'oggettosi riferisce al file di classe che scriviamo.
Per inciso, Class> si riferisce all'oggetto Class che l'oggetto ClassLoader alloca nell'area heap quando carica una classe. Questo oggetto Class contiene vari metadati della classe che abbiamo scritto.
package algorithm.dataStructure;
public abstract class StaticFactoryMethodType {
public abstract void getName();
public static StaticFactoryMethodType getNewInstance() {
StaticFactoryMethodType temp = null;
try {
Class> childClass = Class.forName("algorithm.dataStructure.StaticFactoryMethodTypeChild"); // Riflessione
temp = (StaticFactoryMethodType) childClass.newInstance(); // Riflessione
} catch (ClassNotFoundException e) {
System.out.println("Classe non trovata.");
} catch (InstantiationException e) {
System.out.println("Impossibile caricare in memoria.");
} catch (IllegalAccessException e) {
System.out.println("Errore di accesso al file di classe.");
}
return temp;
}
Nel codice precedente, puoi vedere che un oggetto Class viene creato in base al percorso dell'implementazione dell'interfaccia e la tecnica della riflessione viene utilizzata per inizializzare l'implementazione effettiva. In questo caso,al momento della scrittura del metodo statico di fabbrica, la classe StaticFactoryMethodTypeChild non deve esistere.
Se non esiste un'implementazione nel percorso algorithm.dataStructure.StaticFactoryMethodTypeChild al momento dell'utilizzo del metodo statico di fabbrica, si verificherà un errore, ma non ci saranno problemi al momento della scrittura del metodo statico di fabbrica, quindi è flessibile.
public interface Test {
int sum(int a, int b);
// Test è un'interfaccia e non ci sono problemi a scrivere il metodo statico di fabbrica anche se non ci sono implementazioni.
static Test create() {
return null;
}
}
public class Main {
public static void main(String[] args) {
Test test = Test.create();
System.out.println(test.sum(1, 2)); // Si verifica NPE
}
È possibile ottenere la stessa flessibilità senza utilizzare la riflessione. Guardando il metodo statico di fabbrica create() di Test, non ci sono problemi a scriverlo anche se non ci sono implementazioni. Naturalmente, si verifica un NPE al momento dell'effettivo utilizzo, quindi è necessario restituire l'implementazione in un secondo momento.
Questa flessibilità è alla base della creazione di framework di provider di servizi, di cui JDBC è un esempio tipico. Il provider del framework di provider di servizi è l'implementazione del servizio e il framework controlla la fornitura di queste implementazioni al client, separando il client dall'implementazione (DIP).
- Componenti del framework di provider di servizi
- Interfaccia di servizio
- Definisce il comportamento dell'implementazione.
- JDBC Connection
- API di registrazione del provider
- Il provider registra l'implementazione.
- JDBC DriverManager.registerDriver()
- API di accesso al servizio
- Utilizzato dal client per ottenere un'istanza del servizio, e se non sono specificate condizioni, restituisce l'implementazione predefinita o l'implementazione supportata in modo rotatorio.
- Corrispondenza dei metodi statici di fabbrica
- JDBC DriverManager.getConnection()
- (Opzionale) Interfaccia di provider di servizi
- Se questo non esiste, è necessario utilizzare la riflessione per istanziare ogni implementazione.
- JDBC Driver
- Interfaccia di servizio
Il modello di framework di provider di servizi ha molte varianti, tra cui il modello Bridge, il framework di iniezione di dipendenze, ecc.
Tipico Esempio JDBC
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connection = null;
connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORA92", "root", "root");
Generalmente, JDBC viene scritto come sopra. Tramite Class.forName(), viene registrato uno degli implementatori di Driver, OracleDriver, e tramite DriverManager.getConnection(), viene ottenuto uno degli implementatori di Connection, Connection per OracleDriver.
Qui, Connection è l'interfaccia di servizio, DriverManager.getConnection() è l'API di accesso al servizio e Driver è l'interfaccia di provider di servizi. Tuttavia, l'API di registrazione del provider, DriverManager.registerDriver(), non viene utilizzata. Tuttavia, è possibile registrare OracleDriver, che è un'implementazione di Driver, usando solo Class.forName(). Come è possibile?
Meccanismo di funzionamento di Class.forName()
Questo metodo accetta il nome del file di classe fisico come argomento e richiede alla JVM di caricare la classe. Il caricatore di classi memorizza i metadati della classe nell'area metodo e alloca anche un oggetto Class nell'area heap. Inoltre, una volta completato il caricamento della classe,i campi statici e i blocchi statici vengono inizializzati e l'API di registrazione del provider viene utilizzata in questo momento.
public class OracleDriver implements Driver {
static {
defaultDriver = null;
Timestamp timestamp = Timestamp.valueOf("2000-01-01 00:00:00.0");
try {
if (defaultDriver == null) {
defaultDriver = new OracleDriver();
DriverManager.registerDriver(defaultDriver); // Registrazione di OracleDriver
}
} catch (RuntimeException runtimeexception) {
} catch (SQLException sqlexception) {
}
}
...
In realtà, puoi vedere che OracleDriver utilizza DriverManager.registerDriver() all'interno del blocco statico per registrare OracleDriver, che è un'implementazione di Driver.
Analisi della classe DriverManager
public class DriverManager {
private DriverManager() {
}
private static final Map drivers = new ConcurrentHashMap();
public static final String DEFAULT_DRIVER_NAME = "default";
public static void registerDefaultPrivider(Driver d) {
System.out.println("Driver registrato");
registerDriver(DEFAULT_DRIVER_NAME, d);
}
public static void registerDriver(String name, Driver d) {
drivers.put(name, d);
}
public static Connection getConnection() {
return getConnection(DEFAULT_DRIVER_NAME);
}
public static Connection getConnection(String name) {
Driver d = drivers.get(name);
if (d == null) throw new IllegalArgumentException();
return d.getConnection();
}
La classe DriverManager è in realtà molto più complessa, ma come descritto sopra, è simile a questa. Come descritto sopra, OracleDriver viene registrato chiamando registerDriver() nel blocco statico di OracleDriver, e l'utente può ottenere un'implementazione di Connection chiamando getConnection().
Guardando più da vicino l'API di accesso dell'utente, getConnection(), puoi vedere che ottiene una Connection dall'interfaccia Driver. Se non esiste un'interfaccia Driver di provider di servizi, è possibile utilizzare la riflessione, come Class.forName(), per restituire l'implementazione di Connection desiderata. In questo caso,l'implementazione di Connection non deve esistere al momento della scrittura del metodo statico di fabbrica.
Invece, utilizziamo l'interfaccia Driver e possiamo facilmente ottenere un'implementazione di Connection corrispondente a quel Driver registrando dinamicamente l'implementazione di Driver.
Per inciso, ho analizzato il codice JDK effettivo del metodo getConnection() di DriverManager, ma puoi saltarlo se non sei molto interessato.
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
Innanzitutto, viene chiamato il metodo statico pubblico getConnection() e url, Properties e CallerClass vengono passati come argomenti al metodo statico privato getConnection(). In questo caso, Reflection.getCallerClass() serve a ottenere la classe che ha chiamato il metodo statico pubblico getConnection(). Se la classe Car chiama getConnection(), è possibile ottenere un oggetto Class tramite Reflection.getCallerClass().
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();
}
}
if(url == null) {
throw new SQLException("L'URL non può essere nullo", "08001");
}
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
}
if (reason != null) {
throw reason;
}
throw new SQLException("Nessun driver adatto trovato per "+ url, "08001");
callerCL è un oggetto caricatore di classi e viene creato dal caricatore di classi di caller o dal thread corrente. Successivamente, viene estratto un aDriver dall'elenco di driver registrati nell'applicazione corrente, registeredDrivers. Se questo Driver restituisce true in isDriverAllowed(), viene ottenuta una connessione da questo Driver e restituita. isDriverAllowed() verifica se aDriver esiste in caller.
Vantaggi del framework JDBC
Il punto fondamentale del framework JDBC è che l'interfaccia Driver, l'interfaccia Connection e le classi di implementazione che implementano queste interfacce vengono fornite separatamente. Utilizzando l'interfaccia per creare una cornice, è possibile creare classi di implementazione separate che si adattano a quella cornice, il che è molto flessibile.
Pertanto, anche se viene rilasciato un altro DBMS, il fornitore può fornire Driver e Connection che implementano l'interfaccia, consentendo agli sviluppatori che utilizzano Java di utilizzare la stessa API per i driver di altri DBMS.
Svantaggi dei metodi statici di fabbrica
È necessario un costruttore pubblico o protetto per l'ereditarietà, quindi non è possibile creare sottoclassi se viene fornito solo un metodo statico di fabbrica.
Tuttavia, questo vincolo può essere considerato un vantaggio in quanto incoraggia la composizione piuttosto che l'ereditarietà e deve essere mantenuto per creare tipi immutabili.
I metodi statici di fabbrica sono difficili da trovare per i programmatori.
Poiché non sono esplicitamente evidenziati nell'API come i costruttori, gli sviluppatori devono mitigare il problema scrivendo una buona documentazione API e dando ai metodi nomi che seguono convenzioni ampiamente riconosciute.
Metodi di denominazione dei metodi statici di fabbrica
- from
- Accetta un parametro e restituisce un'istanza del tipo corrispondente.
- Date date = Date.from(instant);
- of
- Accetta più parametri e restituisce un'istanza del tipo appropriato.
- Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf
- Versione più dettagliata di from e of.
- BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
- instance o getInstance
- Restituisce l'istanza specificata come parametro, ma non garantisce che sia la stessa istanza.
- StackWalker luke = StackWalker.getInstance(options);
- create o newInstance
- Simile a instance o getInstance, ma garantisce che venga creata e restituita una nuova istanza ogni volta.
- Object newArray = Array.newInstance(classObject, arraylen);
- getType
- Simile a getInstance, ma utilizzato quando il metodo di fabbrica è definito in una classe diversa da quella da creare.
- FileStore fs = Files.getFileStore(path);
- newType
- Simile a newInstance, ma utilizzato quando il metodo di fabbrica è definito in una classe diversa da quella da creare.
- BufferedReader br = Files.newBufferedReader(path);
- type
- Versione abbreviata di getType e newType.
- List<Complaint> litany = Collections.list(legacyLitany);
Riepilogo
I metodi statici di fabbrica e i costruttori pubblici hanno ciascuno il proprio scopo, quindi usali in modo appropriato.