Mimo iż singleton jest jednym z podstawowych wzorców projektowych, zdecydowana większość jego implementacji zawiera karygodne błędy projektowe. Sam się przekonaj i sprawdź, czy dobrze to robisz. Dzięki temu materiałowi dosłownie w 15 minut nauczysz się implementować singleton poprawnie i unikniesz potencjalnych kłopotów.
Spis treści
- 1 Co to jest singleton?
- 2 Singleton – przykłady zastosowania
- 3 Struktura wzorca – implementacja
- 4 Singleton Java – przykładowa implementacja
- 5 W poszukiwaniu „idealnego” singletonu
- 6 Alternatywne implementacje singletonu
- 7 Gotowe biblioteki a singleton
- 8 Singleton – zalety
- 9 Singleton jako antywzorzec
- 10 Podsumowanie
- 11 20+ BONUSOWYCH materiałów z programowania
Co to jest singleton?
Zacznijmy jednak od drobnego wprowadzenia – zapoznaj się z podstawową definicją singletonu oraz poniższymi przykładami, jak można wykorzystać go w praktyce.
Singleton – to kreacyjny wzorzec projektowy, który ma na celu ograniczenie ilości tworzonych obiektów danej klasy tylko do jednej instancji.
Wzorzec ten może być zastosowany wszędzie tam, gdzie chcemy przechować jeden spójny obiekt dostępny z wielu miejsc aplikacji.
Singleton – przykłady zastosowania
Przykładem wykorzystania singletonu może być klasa przechowująca konfigurację aplikacji. Z każdego miejsca w systemie możemy ją zmodyfikować i chcemy, żeby zmiany były widoczne również z dowolnego miejsca.
Jednocześnie nie możemy pozwolić na to, by w systemie były utrzymywane różne wersje konfiguracji.
Struktura wzorca – implementacja
Jest wiele możliwości implementacji singletonu, jednak wszystkie można sprowadzić do dwóch głównych punktów:
1. Zapewnij tylko jedną instancję
Należy ograniczyć swobodną możliwość tworzenia nowych instancji klasy. Można to osiągnąć przez oznaczenia konstruktora prywatnym modyfikatorem dostępu.
Ponieważ „z zewnątrz” nie można już utworzyć nowej instancji, klasa sama musi zarządzać swoimi instancjami i w odpowiednim momencie ją utworzyć.
2. Przygotuj globalny punkt dostępu
Żeby móc pobrać instancje naszej klasy, należy przygotować jakiś punkt dostępu, który powinien być dostępny globalnie.
Najczęściej realizuje się to przez statyczną metodę zwracającą przygotowaną wcześniej instancję singletonu.
Singleton Java – przykładowa implementacja
Poniżej najprostsza możliwa implementacja singletonu w Javie.
public class SingletonSimpleEager { private static final SingletonSimpleEager instance = new SingletonSimpleEager(); private SingletonSimpleEager() { } public static SingletonSimpleEager getInstance() { return instance; } } // wywołanie SingletonSimpleEager singleton = SingletonSimpleEager.getInstance();
Zgodnie z przedstawioną wcześniej strukturą wzorca mamy prywatny konstruktor oraz globalny punkt dostępu w formie statycznej metody.
Singleton inicjalizacja obiektu eager vs lazy
Powyższa implementacja nie wykorzystuje jednak bardzo ważnej zalety singletonu – mianowicie inicjalizacji obiektu, dopiero kiedy zajdzie taka potrzeba. Instancja zostanie utworzona już w momencie załadowania klasy, nawet jeżeli nigdy nie zajdzie potrzeba jej wykorzystania.
Dzięki implementacji leniwego tworzenia obiektu (ang. lazy loading) można odwlec w czasie moment kosztownego budowania instancji obiektu oraz potencjalnie oszczędzić zasoby, jeżeli nie będzie potrzeby wcale jego utworzenia.
Poniżej zmodyfikujemy przykład z nowymi założeniami.
public class SingletonSimpleLazy { private static SingletonSimpleLazy instance; private SingletonSimpleLazy() { } public static SingletonSimpleLazy getInstance() { if(instance == null){ instance = new SingletonSimpleLazy(); } return instance; } }
W tym przykładzie przenieśliśmy moment generowania instancji do statycznej metody. Kod singletonu powoli nabiera kształtów.
Pytanie jednak, czy jest on już wystarczająco dobry.
W poszukiwaniu „idealnego” singletonu
W kolejnych krokach będziemy próbować złamać pierwotne założenia singletonu oraz postaramy się znaleźć sposób, jak można się przed tym zabezpieczyć.
Każdą kolejną implementację będziemy testować, sprawdzając, czy powstałe instancje znajdują się w tym samym miejscu w pamięci. Jeżeli uda nam się utworzyć dodatkową instancję singletonu, to znaczy, że kod nie jest odporny na tę metodę.
SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance(); SingletonSimpleLazy secondInstance = SingletonSimpleLazy.getInstance(); assertEquals(firstInstance, secondInstance);
Podstawowe wywołanie zadziałało – metoda getInstance() zwróciła dwukrotnie obiekt znajdujący się dokładnie w tym samym miejscu w pamięci.
Poszukajmy teraz czegoś trudniejszego.
1. Refleksja
Refleksja jest bardzo potężnym i potencjalnie niebezpiecznym narzędziem. Przy jej pomocy możemy modyfikować właściwości załadowanego kodu aplikacji jeszcze podczas jej działania.
W naszym wypadku, w celu utworzenia nowej instancji wykonamy trzy kroki:
- przy pomocy refleksji pobierzemy prywatny konstruktor z klasy;
- zmienimy jego modyfikator dostępu na publiczny;
- następnie przy jego pomocy utworzymy nową instancję naszej klasy.
SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance(); // 1 Constructor<SingletonSimpleLazy> constructor = SingletonSimpleLazy.class.getDeclaredConstructor(); // 2 constructor.setAccessible(true); // 3 SingletonSimpleLazy secondInstance = constructor.newInstance(); assertNotEquals(firstInstance, secondInstance);FAILED W kilku stosunkowo prostych krokach udało nam się utworzyć nową instancję klasy, czyli złamać założenia singletonu.
Spróbujmy teraz zabezpieczyć nasz kod przed taką ingerencją. W tym celu zmodyfikujemy konstruktor tak, żeby sprawdzał, czy statyczne pole instance jest już uzupełnione. Jeżeli instancja została już utworzona wcześniej, powinien zgłosić błąd.
public class SingletonReflection { private static SingletonReflection instance; private SingletonReflection() { if (instance != null) { throw new IllegalStateException("Cannot create new instance, please use getInstance method instead."); } } public static SingletonReflection getInstance() { if (instance == null) { instance = new SingletonReflection(); } return instance; } }SUCCEED Udało nam się w ten sposób zabezpieczyć przed utworzeniem nowej instancji. Teraz próba utworzenia kolejnego obiektu tej klasy zostanie przerwana wyjątkiem.
Nie jest to jednak idealne zabezpieczenie. Wyobraź sobie, co się stanie, jeżeli próba utworzenia instancji przy pomocy refleksji zostanie wywołana przed standardowym wywołaniem metody: Singleton.getInstance().
Zapraszam do dyskusji w komentarzach. 👇👇👇
2. Serializacja i deserializacja
Serializacja polega na zamianie obiektu np. na strumień bajtów, który później podczas deserializacji możemy odtworzyć jako nowy obiekt. Więcej o samej serializacji pisałem w tekście o klonowaniu przez serializację.
Co dla nas w tym momencie jest bardzo ważne, podczas deserializacji nie jest zachowany standardowy cykl życia obiektu – podczas jego tworzenia nie jest wywoływany żaden konstruktor!
Dlatego korzystając z tego mechanizmu, również możemy utworzyć nową instancję pierwotnego singletonu.
SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance(); SingletonSimpleLazy secondInstance = null; try (FileOutputStream fos = new FileOutputStream("./SingletonSimpleLazy.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(firstInstance); } try (FileInputStream fis = new FileInputStream("./SingletonSimpleLazy.ser"); ObjectInputStream ois = new ObjectInputStream(fis)) { secondInstance = (SingletonSimpleLazy) ois.readObject(); } assertNotEquals(firstInstance, secondInstance);FAILED Po raz kolejny udało nam się utworzyć nową instancję z pominięciem zabezpieczeń. Całe szczęście specyfikacja Javy przewidziała na taką sytuację specjalną metodę: readResolve.
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
Jeżeli klasa posiada taką metodę, to podczas deserializacji wynik jej działania jest zwracany zamiast standardowej metody: readObject.
public class SingletonSerializable implements Serializable { private static SingletonSerializable instance; private SingletonSerializable() { } public static SingletonSerializable getInstance() { if (instance == null) { instance = new SingletonSerializable(); } return instance; } private Object readResolve() throws ObjectStreamException { return getInstance(); } }SUCCEED Problem z serializacją obiektów występuje tylko wtedy, jeżeli jawnie oznaczymy naszą klasę jako serializowaną przy pomocy interfejsu: java.io.Serializable. W przeciwnym wypadku obiektu wcale nie uda się zserializować.
3. Klonowanie
Java udostępnia wbudowany mechanizm klonowania obiektów, dzięki któremu również można ominąć standardowy cykl budowania tych obiektów.
SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance(); SingletonSimpleLazy secondInstance = firstInstance.clone(); assertNotEquals(firstInstance, secondInstance);FAILED Można się przed tym zabezpieczyć rezygnując z klonowania obiektów lub, podobnie jak w przypadku serializacji, w metodzie clone zwrócić istniejącą już instancję singletonu.
public class SingletonCloneable implements Cloneable { private static SingletonCloneable instance; private SingletonCloneable() { } public static SingletonCloneable getInstance() { if (instance == null) { instance = new SingletonCloneable(); } return instance; } @Override public SingletonCloneable clone() throws CloneNotSupportedException { return getInstance(); } }SUCCEED W przypadku klonowania, podobnie jak przy serializacji, klasa musi najpierw być jawnie oznaczona jako zdolna do klonowania.
4. Wielowątkowość [multi thread]
W wielowątkowym środowisku może dojść do sytuacji, w której kilka niezależnych wątków w tym samym czasie próbuje wykonać nasz kod. Jeżeli nie zabezpieczymy się przed tym odpowiednio, może to prowadzić do nieprzewidzianych sytuacji.
Przeprowadźmy test, który zasymuluje taką sytuację:
Runnable task1 = () -> { firstInstanceForTestBreakWithThreads = SingletonSimpleLazy.getInstance(); }; Runnable task2 = () -> { secondInstanceForTestBreakWithThreads = SingletonSimpleLazy.getInstance(); }; int testSuccess = 0; int testFail = 0; for (int i = 0; i < 1000; i++) { SingletonSimpleLazy.instance = null; ExecutorService service = Executors.newFixedThreadPool(2); service.submit(task1); service.submit(task2); service.shutdown(); service.awaitTermination(1, TimeUnit.SECONDS); if (firstInstanceForTestBreakWithThreads != null && secondInstanceForTestBreakWithThreads != null && firstInstanceForTestBreakWithThreads.equals(secondInstanceForTestBreakWithThreads)) { testSuccess++; } else { testFail++; } } System.out.println(String.format("testSuccess: %d, testFail: %d", testSuccess, testFail));
Test polega na próbie pobrania instancji metodą: getInstance() przez dwa niezależne wątki. Test uznajemy za zaliczony, jeżeli obie instancje znajdują się w tym samym miejscu w pamięci. Natomiast, jeżeli jedna z nich jest pusta lub zostały utworzone dwa obiekty, test uznajemy za niezaliczony.
Dodatkowo test uruchomiłem 1000 razy, żeby zobaczyć uśredniony wynik. Żeby test mógł być uruchamiany wielokrotnie, przed każdym powtórzeniem trzeba jeszcze zresetować zmienną statyczną przechowującą instancję – SingletonSimpleLazy.instance = null;. W ramach testu chwilowo zmieniłem jej modyfikator dostępu na publiczny.
FAILED Wynik pokazuje, że około 15% wywołań kończy się porażką.testSuccess: 854, testFail: 146
Niby większość zaliczona, natomiast jeżeli Ci to nie wystarcza, zapraszam do dalszej lektury.
Dlaczego niektóre testy zakończyły się porażką?
Żeby odpowiedzieć na to pytanie, przypomnijmy sobie, jak wygląda metoda getInstance() .
public static SingletonSimpleLazy getInstance() { if(instance == null){ instance = new SingletonSimpleLazy(); } return instance; }
Jeżeli w kodzie aplikacji nie ma zdefiniowanych żadnych semaforów – czyli blokad wskazujących wątkom, w którym miejscu mają wstrzymać swoje działanie, kolejność, w jakiej poszczególne fragmenty kodu zostaną wykonane przez różne wątki, może być różna. Prowadzi to, do tak zwanych wyścigów.
W naszym przykładzie oba wątki mogły jednocześnie sprawdzić warunek, czy instancja jest równa null i w obu wypadkach była to prawda. Dlatego zostały utworzone dwie instancje.
Żeby się przed tym zabezpieczyć, wprowadzimy odpowiednie blokady/semafory.
Synchronizacja wątków
Warto również zauważyć, że problem ten występuje tylko, jeżeli chcemy skorzystać z leniwego tworzenia instancji.
Metoda synchronized
Najprostszą możliwą forma synchronizacji w tym wypadku jest utworzenie sekcji krytycznej w ramach całej metody getInstance.
public synchronized static SingletonSimpleLazy getInstance() { if (instance == null) { instance = new SingletonSimpleLazy(); } return instance; }
Po tej modyfikacji tylko jeden wątek będzie mógł wejść do tej metody w tym samym czasie.
INFO Uzyskaliśmy dzięki temu zabezpieczenie przed utworzeniem nowej instancji, jednak spowolniliśmy jednocześnie bardzo naszą aplikację. W obecnej wersji nawet jak instancja będzie już utworzona, wszystkie wątki będą musiały poczekać na swoją kolej, żeby ją pobrać.Powstało w ten sposób wąskie gardło, które w kolejnych krokach postaramy się wyeliminować.
Blok synchronizowany
Przenieśmy sekcję krytyczną trochę niżej – za warunek sprawdzający istnieje instancji. Dzięki temu po utworzeniu instancji nie będzie już potrzeby kolejkowania wątków.
public static SingletonSimpleLazy getInstance() { if (instance == null) { synchronized (SingletonSimpleLazy.class) { instance = new SingletonSimpleLazy(); } } return instance; }INFO Niestety to rozwiązanie również nie jest idealne. Może bowiem dojść do sytuacji, w której więcej niż jeden wątek będzie czekał na wejście do sekcji krytycznej. Wtedy, po zwolnieniu blokady, on również utworzy nową instancję.
Blokada z podwójnym zatwierdzeniem [Double-checked locking optimization]
Na takie sytuacje powstał specjalny wzorzec projektowy: blokada z podwójnym zatwierdzeniem. Polega to na podwójnym sprawdzeniu warunku: najpierw przed blokadą i potem jeszcze raz po przejściu przez blokadę.
public static SingletonSimpleLazy getInstance() { if (instance == null) { synchronized (SingletonSimpleLazy.class) { if (instance == null) { instance = new SingletonSimpleLazy(); } } } return instance; }INFO Teraz nawet jak kilku wątkom uda się przejść przez blokadę, to zostaną na niej skolejkowane i nie przejdą przez drugi warunek.
Została nam do poprawy już tylko jedna rzecz. W ramach optymalizacji zmienne w pamięci komputera przechowywane są w keszu procesora. Dlatego może dojść do sytuacji, w której jeden wątek ustawi poprawnie wartość zmiennej, ale drugi podejmie próbę jej odczytania, zanim zostanie ona jeszcze zsynchronizowana.
Zmienna volatile
Specyfikacja Javy pozwala określić, które zmienne mają być zawsze przechowywane we współdzielonej pamięci, a nie w keszu. Dzięki oznaczeniu zmiennej jako volatile niezależnie od tego, który wątek próbuje ją odczytać, jej wartość będzie zawsze spójna.
private static volatile SingletonSimpleLazy instance;SUCCEED Po tych zmianach mamy już pewność, że nasz kod jest odpowiednio przygotowany do działania w środowisku wielowątkowym.
Classloader
Java pobiera klasy przez specjalne classloadery. W prostych aplikacjach desktopowych zazwyczaj mamy do czynienia z jednym domyślnym classloaderem i najczęściej jego istnienie jest nawet niezauważalne. Jednak w środowisku JavaEE, np. w kontenerze serwletów wykorzystywanych jest więcej niż jeden classloader jednocześnie.
Może wtedy dojść do utworzenia niezależnego obiektu singletonu w ramach każdego z nich.
Zasymulujemy taką sytuację w środowisku JavaSE w trzech kolejnych krokach:
SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance(); // 1 String targetJarFile = "file://"+Paths.get(".").toAbsolutePath().resolve("target/singleton-1.0.jar").toString(); ClassLoader classLoader = new URLClassLoader(new URL[]{new URL( targetJarFile)}, null); // 2 Class singletonClass = classLoader.loadClass(SingletonSynchronized.class.getCanonicalName()); // 3 Method getInstanceMethod = singletonClass.getDeclaredMethod("getInstance"); Object secondInstance = getInstanceMethod.invoke(singletonClass); assertNotEquals(firstInstance, secondInstance);
- Klasa naszego singletonu została już pobrana przez domyślny classloader. Spróbujemy pobrać ją jeszcze raz, korzystając z klasy URLClassLoader. W tym celu potrzebujemy podać ścieżkę do zbudowanego wcześniej pliku jar z naszą klasą;
- W drugim kroku pobieramy ponownie klasę na podstawie jej pełnej nazwy (ang. fully qualified class name);
- W ostatnim kroku pobieramy statyczną metodę getInstance i korzystając z niej, tworzymy nowy obiekt singletonu.
private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader == null) { classLoader = SingletonSimpleLazy.class.getClassLoader(); } return (classLoader.loadClass(classname)); }
Różne maszyny wirtualne
Jeżeli aplikacja uruchamiana jest na kilku maszynach wirtualnych, np. w środowisku EJB również dojdzie do utworzenia kilku instancji singletonu na każdej maszynie wirtualnej (JVM).
W takiej sytuacji najlepiej unikać singletonów przechowujących lokalny stan obiektu.
Garbage Collection
Jeżeli klasa singletonu zostanie odśmiecona i potem ponownie załadowana, również dojdzie do utworzenia nowej instancji. Jeżeli na obiekt singletonu nie będzie wskazywała żadna referencja, może on zostać usunięty i utworzony ponownie dopiero, jeżeli zajdzie taka potrzeba.
Ten problem występował w starszych wersjach Javy, obecnie nie powinien już mieć miejsca. Więcej na ten temat można przeczytać tutaj.
Alternatywne implementacje singletonu
W poprzednich krokach rozważaliśmy tylko jedną z możliwości implementacji singletonu. Istnieją jednak alternatywne sposoby, np. z wykorzystaniem statycznej klasy lub enuma.
Static holder pattern
Singleton Static holder pattern jest to bezpieczne rozwiązanie i jednocześnie zapewniające dobrą wydajność w wielowątkowym środowisku. Dzięki niemu mamy również zapewnione leniwe utworzenie instancji.
public class SingletonStaticHolder { private SingletonStaticHolder() { } private static class Holder { private static final SingletonStaticHolder INSTANCE = new SingletonStaticHolder(); } public static SingletonStaticHolder getInstance() { return Holder.INSTANCE; } }
W tym rozwiązaniu cały trud poprawnej implementacji wzorca zrzucamy na maszynę wirtualną Javy. Zmienna INSTANCE zostanie zainicjowana dopiero w momencie załadowania klasy Holder, czyli podczas pierwszego wywołania metody getInstance().
Enum
Implementacja singletonu opartego na enumie również zrzuca cały ciężar poprawnej implementacji na Javę. Enumy charakteryzują się tym, że może istnieć tylko jedna instancja danej klasy.
public enum SingletonEnum { INSTANCE; // public methods }
W tym miejscu warto sprawdzić, jak enum jest wewnętrznie zaimplementowany przez Javę. Posłużymy się w tym celu narzędziem javap:
root@tw:/# javap SingletonEnum.class Compiled from "SingletonEnum.java" public final class pl.stormit.singleton.SingletonEnum extends java.lang.Enum<pl.stormit.singleton.SingletonEnum> { public static final pl.stormit.singleton.SingletonEnum INSTANCE; public static pl.stormit.singleton.SingletonEnum[] values(); public static pl.stormit.singleton.SingletonEnum valueOf(java.lang.String); static {}; }
Enum to zwyczajna finalna klasa dziedzicząca po java.lang.Enum.
Korzystając z tego rozwiązania, osiągnęliśmy wynik bardzo podobny do klasy SingletonSimpleEager, czyli singletonu z zachłannym tworzeniem instancji.
Gotowe biblioteki a singleton
Zamiast implementować wzorzec ręcznie, można również posłużyć się gotowymi już rozwiązaniami.
Immutables
Immutables to biblioteka do pracy z klasami typu: value object. Jedną z jej funkcjonalności jest automatyczna implementacja wzorca singleton.
@Value.Immutable(singleton = true) public class SingletonImmutables { } // wywołanie SingletonImmutables firstInstance = ImmutableSingletonImmutables.builder().build(); SingletonImmutables secondInstance = ImmutableSingletonImmutables.builder().build(); assertTrue(firstInstance == secondInstance);
Singleton – zalety
- Klasa zaimplementowana jako singleton samodzielnie kontroluje liczbę swoich instancji, dzięki czemu, po wprowadzeniu niewielkich modyfikacji, można przerobić ją na pulę zarządzającą większą ilością obiektów.
- Sam proces tworzenia nowej instancji jest niewidoczny dla użytkownika. Dlatego można zaimplementować leniwe tworzenie instancji i przyczynić się do zaoszczędzenia zasobów.
- Łatwo można przerobić go na fabrykę obiektów, uniezależniając się od konkretnej implementacji.
Singleton jako antywzorzec
Singleton przez wielu programistów uważany jest za antywzorzec projektowy. Dzieje się tak głównie dlatego, że jest on dość często nadużywany.
Poniżej zestawienie głównych zarzutów wobec tego wzorca. Niektóre z nich przy odrobienie elastyczności można obejść lub nawet odrzucić.
- Poważnie utrudnia testowanie aplikacji
Testy są tylko utrudnione, jeżeli w singletonie przechowywany jest stan. Należy wtedy pamiętać, by był on odpowiednio zainicjowany lub wyczyszczony przed każdym wywołaniem testu. - Brak elastyczności
Taka jest właśnie specyfika singletonu, że już na poziomie kodu jest na sztywno określona liczba instancji. - Łamie zasadę jednej odpowiedzialności (single responsibility principle)
Klasa zaimplementowana jako singleton z założenia jest już odpowiedzialna za dwie rzeczy: za realizację swoich funkcji biznesowych oraz zarządzanie instancją. - Łamie zasadę otwarte-zamknięte (Open/Closed principle), ponieważ nie można go rozszerzać
W pierwotnej wersji wzorca rzeczywiście ciężko jest go rozszerzać. Można jednak połączyć singleton z fabryką i nie będzie stanowiło to już problemu.interface Singleton { } public class SingletonFactory { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { instance = new Singleton() { // singleton implementation }; } return instance; } }
- Jest to obiektowy zamiennik zmiennej globalnej
Tu ciężko się spierać. Rzeczywiście singleton bywa dla wielu programistów swego rodzaju substytutem nieobsługiwanych przez Javę zmiennych globalnych. Dlatego właśnie jest tak ważne, by go nie nadużywać.
Podsumowanie
Dzięki wykorzystaniu singletonu w swoich aplikacjach można zyskać na wydajności oraz przejrzystości kodu. Należy jednak pamiętać, by go nie nadużywać i nie starać się go wprowadzić wszędzie na siłę. Wzorzec ten powinien być stosowany raczej sporadycznie.
Podobnie sprawa ma się, jeżeli chodzi o wszystkie zabezpieczenia przedstawione w artykule. Tu również należy podejść z rozwagą, nie zawsze jest sens wprowadzać wszystko u siebie. Jeżeli piszemy prostą aplikację, uruchamianą w środowisku jednowątkowym i dodatkowo jesteśmy jedyną osobą odpowiedzialną za jej rozwój, z powodzeniem sprawdzi się u nas najprostsza implementacja. Oszczędzimy w ten sposób czas potrzebny na napisanie oraz utrzymanie nadmiarowego kodu.
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!
6 Comments
Temat opisany chyba z każdej możliwej strony i ciężko tu cokolwiek dodać ale spróbuję 😉
Jako jeden z minusów pierwszej implementacji podajesz eager initialization. Tylko w tym konkretnym przypadku jest ona całkowicie lazy. Nadal do utworzenia obiektu wymagane jest wywołanie dowolnej metody klasy czyli w tym przypadku getInstance(). A skoro już wywołujemy tę metodę to znaczy, że chcemy w tym konkretnym momencie korzystać z obiektu, czyli minus przestaje być minusem.
Elon słuszna uwaga.
Jest to rzeczywiście bardzo specyficzny przypadek i dla tego testu obie implementacje zachowają się bardzo podobnie.
Rozwijając, inicjalizacja w stylu:
private static final SingletonSimpleEager instance = new SingletonSimpleEager();
zostanie wywołana w momencie załadowania klasy SingletonSimpleEager, a klasa jest ładowana dopiero podczas jej pierwszego aktywnego wykorzystania, czyli np. wywołania jednej z metod.
Drugie rozwiązanie jest o tyle lepsze (klasa SingletonSimpleLazy), że możemy wywołać inne statyczne metody klasy lub wykorzystać jej statyczne zmienne, bez tworzenia instancji singletonu.
Bardzo fajny artykuł! Zaktualizuj link do GitHuba 🙂
Cześć kchrusciel. Dzięki za opinię i uwagę. Już poprawione.
kozak totalny, aż chce się czytać!
Dzięki 🙂