Omówienie
Tradycyjna metoda konstruowania instancji klasy to publiczny konstruktor.
```javascript
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;
}
Publiczny konstruktor jest zazwyczaj wystarczający, ale często zapewnia użytkownikowi wygodniejszy sposób tworzenia instancji w zamian za konstruktora, zapewniając statyczną metodę fabryczną.
Typowy przykład statycznej metody fabrycznej to metoda Boolean.valueOf().
```javascript
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Powyższa metoda przyjmuje wartość boolean typu boolean i zwraca ją jako obiekt Boolean.
Zalety statycznej metody fabrycznej
Może mieć nazwę.
Same parametry przekazywane do konstruktora i sam konstruktor nie opisują wystarczająco dobrze specyfiki zwracanego obiektu. Na przykład, trudno określić, jakie cechy ma Member, patrząc tylko na główny konstruktor klasy Member (name, age, hobby, memberStatus).
Ponadto dla jednego podpisu można utworzyć tylko jeden konstruktor, podczas gdy statyczna metoda fabryczna może mieć nazwę, co oznacza, że możesz utworzyć wiele statycznych metod fabrycznych z jednym podpisem w celu zwrócenia instancji.
```javascript
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);
}
}
Jak widać, tworzenie wielu statycznych metod fabrycznych z różnymi sygnaturami zamiast konstruktora do rozróżniania MemberStatus umożliwia użytkownikowi tworzenie instancji Member z konkretnymi umiejętnościami bez ryzyka pomyłki.
Rozważ bibliotekę zdefiniowaną w JDK, statyczną metodę fabryczną probablePrime() typu BigInteger.
```javascript
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));
}
W porównaniu do ogólnego konstruktora BigInteger i statycznej metody fabrycznej probablePrime(), ta ostatnia wyraźnie mówi, że zwraca BigInteger, który jest liczbą pierwszą.
Nie trzeba tworzyć nowych instancji za każdym razem, gdy są wywoływane.
```javascript
public static Boolean valueOf(boolean b) {
return (b ? Boolean.TRUE : Boolean.FALSE);
}
Można zauważyć, że metoda valueOf() Booleana wstępnie buforuje instancje i zwraca je. Ta funkcja może znacznie poprawić wydajność w scenariuszach, w których obiekt jest często tworzony,wzorca Flyweightktóry można postrzegać jako podobną technikę.
Klasa, która używa tej statycznej metody fabrycznej, aby zwracać ten sam obiekt dla powtarzających się żądań, może kontrolować cykl życia instancji, dlatego nazywana jest klasą kontroli instancji. Kontrola instancji umożliwia tworzenie klas singletonów lub klas, których nie można tworzyć instancji. Ponadto zapewnia, że tylko jedna instancja jest taka sama w klasach wartości niezmiennych.
Kontroler instancji jest podstawą wzorca Flyweight, a typy wyliczeniowe zapewniają utworzenie tylko jednej instancji.
Musisz zasadzić drzewo w Minecrafcie. Jeśli tworzysz nowy obiekt dla każdego drzewa, może to prowadzić do переполнения pamięci.
Dlatego lepiej jest przechowywać obiekty drzew i zmieniać tylko ich lokalizacje. Oczywiście kolor może być większy niż dwa, więc użycie struktury danych, takiej jak mapa, aby przechowywać drzewa według kolorów, byłoby skuteczne.
```javascript
public class Tree {
// Drzewo ma następujące 3 informacje.
private String color;
private int x;
private int y;
// Konstruktor tworzony tylko z kolorem.
public Tree(String color) {
this.color = color;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
// Podczas sadzenia drzewa
public void install(){
System.out.println("x:"+x+" y:"+y+" Posadzone drzewo w kolorze "+color+"!");
}
}
public class TreeFactory {
// Zarządzaj stworzonymi drzewami za pomocą struktury danych HashMap.
public static final Map<String, Tree> treeMap = new HashMap<>();
public static Tree getTree(String treeColor){
// Sprawdź, czy drzewo o podanym kolorze istnieje w mapie. Jeśli tak, zwróć obiekt.
Tree tree = (Tree)treeMap.get(treeColor);
// Jeśli w mapie nie ma jeszcze drzewa o tym samym kolorze, utwórz nowe.
if(tree == null){
tree = new Tree(treeColor);
treeMap.put(treeColor, tree);
System.out.println("Nowy obiekt utworzony");
}
return tree;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Wpisz żądany kolor :)");
for(int i=0;i<10;i++){
// Uzyskaj kolor drzewa
String input = scanner.nextLine();
// Pojedynczo zapewnij drzewo z fabryki
Tree tree = (Tree)TreeFactory.getTree(input);
// Ustaw x,y drzewa i
tree.setX((int) (Math.random()*100));
tree.setY((int) (Math.random()*100));
// Zainstaluj drzewo
tree.install();
}
}
}
Różnica ze wzorcem Singleton
Wzorzec Singleton umożliwia utworzenie tylko jednego drzewa dla klasy. Tak więc, jeśli używany jest wzorzec Singleton, należy zmienić kolor utworzonego pojedynczego obiektu. Oznacza to, że wzorzec Singleton może mieć tylko jeden rodzaj, niezależnie od ilości.
Wzorzec Flyweight jest używany w puli stałych ciągów Java.
Może zwracać obiekty podtypu typu zwracanego.
Zrozumiesz tę zaletę, jeśli kiedykolwiek korzystałeś z metody asList() klasy narzędziowej Arrays.
```javascript
public static List asList(T... a) [
return new ArrayList<>(a);
}
Zwraca ArrayList, który jest podtypem Listy, owijając wartość, ale użytkownik nie musi znać tego podtypu. Oznacza to, że możesz swobodnie wybierać klasę obiektu zwracanego, co zapewnia elastyczność deweloperom, ponieważ nie muszą ujawniać implementacji, więc API może być małe.
O statycznych metodach interfejsu Java
Przed Javą 8 nie można było deklarować statycznych metod w interfejsie, więc jeśli potrzebna była statyczna metoda zwracająca interfejs o nazwie “Typ”, musiałeś utworzyć dołączoną klasę o nazwie “Typy”, w której definiowano metody.
Typowy przykład to 45 implementacji narzędziowych provided JCF, z których prawie wszystkie uzyskuje się za pomocą statycznej metody fabrycznej z jednej dołączonej klasy java.util.Collections. Zwłaszcza wśród tych implementacji znajdują się te, które nie są publiczne, więc instancje można tworzyć tylko za pomocą statycznej metody fabrycznej i nie można odziedziczyć tej implementacji (oczywiście).
Nie ujawniają również 45 implementacji, co znacznie zmniejsza rozmiar API.
```javascript
// Przykład interfejsu i towarzyszącej mu klasy
List empty = Collections.emptyList();
Jednak od Java 8 można dodawać statyczne metody bezpośrednio do interfejsów, więc nie trzeba definiować oddzielnej klasy towarzyszącej.
Może zwracać różne obiekty podklas w zależności od parametrów wejściowych.
Poza prostym zwracaniem podtypów, możesz zwracać różne podtypy w zależności od wartości parametru. Na przykład, jeśli chcesz zwrócić MemberStatus w oparciu o wynik, możesz utworzyć statyczną metodę fabryczną w następujący sposób i umieścić w niej logikę porównania.
```javascript
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("Nie znaleziono odpowiadającego obiektu MemberStatus."));
}
private static Predicate<MemberStatus> decideMemberStatus(int score) {
return element -> element.minScore <= score && element.maxScore >= score;
}
}
@DisplayName("Test MemberStatus")
class MemberStatusTest {
@ParameterizedTest
@CsvSource(value = {"0:BASIC", "30:BASIC", "50:INTERMEDIATE", "70:INTERMEDIATE", "80:ADVANCED", "100:ADVANCED"}, delimiter = ':')
@DisplayName("Zwraca różne MemberStatus w zależności od wyniku.")
void of(int input, MemberStatus expected) {
assertThat(MemberStatus.of(input)).isEqualTo(expected);
}
}
Statyczna metoda fabryczna nie musi istnieć w momencie tworzenia obiektu zwracanego.
W zdaniu obiekt zwracanyjest plikiem klas, który piszemy.
Należy zauważyć, że Class> reprezentuje obiekt Class, który ładowarka klas przypisuje do obszaru sterty podczas ładowania klasy. Ten obiekt klasy zawiera różne metadane o klasie, którą napisaliśmy.
```javascript
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"); // Refleksja
temp = (StaticFactoryMethodType) childClass.newInstance(); // Refleksja
} catch (ClassNotFoundException e) {
System.out.println("Nie ma takiej klasy.");
} catch (InstantiationException e) {
System.out.println("Nie można umieścić w pamięci.");
} catch (IllegalAccessException e) {
System.out.println("Błąd dostępu do pliku klasy.");
}
return temp;
}
}
Jak widać, możesz utworzyć obiekt Class za pomocą lokalizacji implemented interfejsu i zainicjalizować rzeczywistą implementację za pomocą techniki refleksji. W tym momencie statyczna metoda fabryczna nie musi istniećKlase. StaticFactoryMethodTypeChild.
Jeśli implementacja nie istnieje pod ścieżką algorithm.dataStructure.StaticFactoryMethodTypeChild w momencie użycia statycznej metody fabrycznej, wystąpi błąd, ale ponieważ nie ma problemu w momencie utworzenia statycznej metody fabrycznej, mówi się, że jest elastyczna.
```javascript
public interface Test {
int sum(int a, int b);
// Test jest interfejsem, a statyczna metoda fabryczna może być utworzona, nawet jeśli nie ma implementacji.
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)); // Rzuca NPE
}
}
Nawet jeśli nie używasz refleksji, możesz uzyskać tę samą elastyczność. Patrząc na metodę create() statycznej metody fabrycznej Test, nie ma problemu w momencie tworzenia, nawet jeśli nie ma implementacji. Oczywiście w rzeczywistym momencie użycia wystąpi NPE, więc musisz wtedy zwrócić implementację.
Ta elastyczność stanowi podstawę tworzenia frameworka providera usług, a jego typowym przykładem jest JDBC. Framework providera usług JDBC provider jest implementacją usługi, a framework kontroluje providerów w celu dostarczenia implementacji klientom (DIP).
- Komponenty frameworka providera usług
- Interfejs usługi
- Definiuje zachowanie implementacji
- Połączenie JDBC
- API rejestracji providera
- Register provider implementację
- JDBC DriverManager.registerDriver()
- API dostępu do usługi
- Używany, gdy klient uzyskuje instancję usługi; jeśli nie zostanie określony żaden warunek, zwraca domyślną implementację lub iteruje po dostępnych implementacjach.
- Odpowiednik statycznej metody fabrycznej
- JDBC DriverManager.getConnection()
- (opcjonalnie) Interfejs providera usługi
- Bez niej refleksja byłaby używana do tworzenia instancji każdej implementacji
- JDBC Driver
Wzór frameworka providera usług ma wiele wariantów, takich jak wzorzec mostu i framework wstrzykiwania zależności.
```javascript
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connection = null;
connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORA92", "root", "root");
// Logika sql z różnymi instrukcjami
Ogólnie JDBC jest napisane w powyższy sposób. Driver jest rejestrowany za pomocą Class.forName(), a OracleDriver, który jest jedną z implementacji Driver, a Connection jest pobierane za pomocą DriverManager.getConnection(), które jest jedną z implementacji Connection, OracleDriver.
Tutaj Connection jest interfejsem usługi, DriverManager.getConnection() jest API dostępu do usługi, a Driver jest interfejsem providera usługi. Jednak API rejestracji providera DriverManager.registerDriver() nie zostało użyte. Mimo to możemy zarejestrować OracleDriver, implementację Driver, za pomocą Class.forName(). Jak to jest możliwe?
Zasada działania Class.forName()
Ta metoda pobiera fizyczną nazwę pliku klasy jako argument i prosi JVM o załadowanie tej klasy. Następnie ładowarka klas zapisuje metadane klasy w obszarze metod, a także przydziela obiekt Class do obszaru sterty. Ponadto po zakończeniu ładowania klasy,inicjowane są statyczne pola i bloki statyczne, a tutaj używane jest API rejestracji providera.
```javascript
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); // Rejestracja OracleDriver
}
} catch (RuntimeException runtimeexception) {
} catch (SQLException sqlexception) {
}
}
...
}
Rzeczywiście, patrząc na OracleDriver, możesz stwierdzić, że rejestruje OracleDriver za pomocą DriverManager.registerDriver() w bloku statycznym.
Analiza klasy DriverManager
```javascript
public class DriverManager {
private DriverManager() {
}
private static final Map<String, Driver> drivers = new ConcurrentHashMap<String, Driver>();
public static final String DEFAULT_DRIVER_NAME = "default";
public static void registerDefaultPrivider(Driver d) {
System.out.println("Registered Driver");
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();
}
}
Klasa DriverManager jest w rzeczywistości znacznie bardziej skomplikowana, ale jeśli podsumujemy kluczowe punkty i ułatwimy je, będzie to wyglądać podobnie jak powyżej. Jak wspomniano powyżej, OracleDriver jest rejestrowany, wywołując DriverManager.registerDriver() w bloku statycznym OracleDriver, a użytkownik może uzyskać implementację Connection wywołując getConnection().
Patrząc dokładniej na metodę API dostępu do użytkownika getConnection(), możesz zobaczyć, że uzyskuje Connection z interfejsu Driver. Jeśli nie ma interfejsu Driver, który jest interfejsem providera usługi, możesz użyć Class.forName() lub podobnej refleksji, aby zwrócić żądaną implementację Connection. Tutaj Implementacja Connection nie musi istnieć w momencie tworzenia statycznej metody fabrycznej.
Zamiast tego używamy interfejsu Driver, dynamicznie rejestrujemy implementację Driver, a następnie możemy łatwo uzyskać implementację Connection odpowiadającą temu Driverowi.
Nawiasem mówiąc, spojrzałem na rzeczywisty kod metody JDK getConnection() klasy DriverManager, ale jeśli nie jesteś szczególnie zainteresowany, możesz go pominąć.
```javascript
@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()));
}
Najpierw wywoływana jest publiczna statyczna metoda getConnection(), a url, Properties i CallerClass są przekazywane do prywatnej statycznej metody getConnection(). W tym czasie Reflection.getCallerClass() służy do uzyskania klasy, która wywołała publiczną statyczną metodę getConnection(). Na przykład, jeśli klasa Car wywołała getConnection(), Reflection.getCallerClass() zwróci obiekt Class.
```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();
}
}
if(url == null) {
throw new SQLException("URL nie może być nullem", "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("Nie znaleziono odpowiedniego sterownika dla "+ url, "08001");
}
Najpierw callerCL staje się obiektem klasy ładowarki klas, a jest tworzony przez klasę ładującą lub bieżący wątek. Następnie iteruje po liście zarejestrowanych sterowników w bieżącej aplikacji, aDriver. Jeśli aDriver jest uznany przez isDriverAllowed() za prawdziwe, uzyskuje obiekt Connection za pomocą tego sterownika i zwraca go. isDriverAllowed() sprawdza, czy aDriver istnieje w klasie ładującej.
Kluczową zaletą JDBC jest to, że interfejs Driver, interfejs Connection i jego rzeczywiste klasy implementacji są całkowicie oddzielone i dostarczane. Tworząc ramkę za pomocą interfejsu i tworząc każdą klasę implementacji pasującą do tej ramki, jest to bardzo elastyczne.
Dlatego nawet jeśli pojawi się inny DBMS, ten dostawca może zapewnić sterownik i implementację interfejsu Connection, aby programiści Java mogli używać tego samego API sterownika JDBC.
Wady statycznej metody fabrycznej
Nie można tworzyć podklas, ponieważ publiczny lub chroniony konstruktor jest wymagany podczas dziedziczenia.
Jednak to ograniczenie może być również zaletą, ponieważ zachęca do kompozycji zamiast dziedziczenia i można tego wymagać do tworzenia typów niezmiennych.
Statycznych metod fabrycznych trudno zaleźć programistom.
Ponieważ nie są one wyraźnie wyświetlane w opisie API, jak konstruktorzy, programiści muszą dobrze napisać dokumentację API i nadać metodom nazwy, które są powszechnie znane w tej konwencji.
Konwencja nazewnictwa statycznych metod fabrycznych
- z
- Przyjmuje jeden argument i zwraca instancję tego typu.
- Date date = Date.from(instant);
- z
- Przyjmuje wiele argumentów i zwraca instancję odpowiedniego typu.
- Set faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf
- Bardziej szczegółowa wersja z i of.
- BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
- instance lub getInstance
- Zwraca wskazaną instancję, ale nie gwarantuje, że jest to ta sama instancja.
- StackWalker luke = StackWalker.getInstance(options);
- create lub newInstance
- Podobnie jak instance lub getInstance, ale gwarantuje, że każdorazowo zwracana jest nowa instancja.
- Object newArray = Array.newInstance(classObject, arraylen);
- getType
- Podobie jak getInstance, ale zdefiniować statyczną metodę fabryczną w innej klasie niż tworzona klasa.
- FileStore fs = Files.getFileStore(path);
- newType
- Podobie jak newInstance, ale zdefiniować statyczną metodę fabryczną w innej klasie niż tworzona klasa.
- BufferedReader br = Files.newBufferedReader(path);
- typ
- Prosta wersja getType i newType.
- List litany = Collections.list(legacyLitany);
Wniosek
Statyczne metody fabryczne i publiczne konstruktory mają swoje zastosowania, więc używaj ich mądrze.
Źródło