pytania rekrutacyjne Flashcards
Equals i hashcode
equals() i hashCode() to dwie metody, które są związane z implementacją równości obiektów w języku Java. Oto krótka wyjaśnienie obu metod:
equals():
Metoda equals() jest używana do porównywania dwóch obiektów pod kątem ich treści (wartości).
Domyślna implementacja metody equals() w klasie Object porównuje referencje do obiektów, co oznacza, że porównuje, czy oba obiekty są dokładnie tym samym obiektem w pamięci.
W praktyce często konieczne jest dostosowanie tej metody w własnych klasach, aby porównywać obiekty na podstawie ich zawartości, a nie referencji.
Implementacja equals() powinna spełniać pewne warunki, takie jak: być refleksyjna, symetryczna, przechodzić test równości z obiektem null, i innych.
Przykład dostosowanej implementacji equals():
java
Copy code
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
// Porównanie pól obiektów
// …
return true; } hashCode(): Metoda hashCode() zwraca wartość liczbową, która reprezentuje "skróconą" wersję obiektu. Ta wartość liczbowo identyfikuje obiekt i jest wykorzystywana w mechanizmach haszujących, takich jak tablice mieszające (hash tables). Implementacja hashCode() powinna być zgodna z implementacją equals(), tj. obiekty, które są równe według equals(), powinny mieć również takie same wartości hashCode(). Domyślna implementacja w klasie Object zwraca różne wartości dla różnych obiektów, nawet jeśli są one równe według equals(). Przykład dostosowanej implementacji hashCode():
java
Copy code
@Override
public int hashCode() {
int result = 17;
result = 31 * result + field1.hashCode();
result = 31 * result + field2.hashCode();
// Dodaj inne pola, jeśli istnieją
return result;
}
Wartości 17 i 31 są wybranymi liczbami pierwszymi, które pomagają w uzyskaniu rozróżnialnych wartości hashCode() dla różnych kombinacji pól. Działanie tych liczb przyczynia się do lepszej dystrybucji wartości haszy.
Co to jest tablica haszująca
Tablica haszująca (ang. hash table) to struktura danych, która umożliwia efektywne przechowywanie i wyszukiwanie elementów w oparciu o klucze. Tablica haszująca wykorzystuje funkcję haszującą do przekształcenia klucza w indeks tablicy, gdzie wartość związana z danym kluczem jest przechowywana. Ta technika pozwala na szybkie odnajdywanie elementów przy użyciu klucza.
Podstawowe operacje na tablicy haszującej to dodawanie (wstawianie), usuwanie i wyszukiwanie elementu. Klucz jest przekształcany za pomocą funkcji haszującej, która zwraca indeks w tablicy. W przypadku kolizji, tj. sytuacji, gdy dwa różne klucze mają tę samą wartość hasza i prowadziłyby do zajęcia tego samego indeksu, stosuje się różne metody rozwiązywania konfliktów. Popularne techniki to:
Separate Chaining (łańcuchy oddzielone): Każdy indeks tablicy zawiera listę (łańcuch) elementów, które mają ten sam wartość hasza. W przypadku kolizji, nowy element jest po prostu dodawany do odpowiedniej listy.
Open Addressing (otwarte adresowanie): Jeśli nastąpi kolizja, algorytm poszukuje następnego dostępnego indeksu w tablicy, aż znajdzie wolne miejsce. Różne metody otwartego adresowania obejmują liniowe przeszukiwanie, kwadratowe przeszukiwanie czy podwójne haszowanie.
Ważne jest, aby funkcja haszująca była dobrze zdefiniowana i zapewniała jak najmniejszą liczbę kolizji, aby zachować efektywność tablicy haszującej. Optymalny wybór funkcji haszującej zależy od charakterystyki danych, jakie są przechowywane w tablicy. Tablice haszujące są szeroko stosowane w praktyce do implementacji różnych struktur danych, takich jak słowniki, zbiory czy cache.
Relacja wiele do wielu na przykładzie agent ubezpieczenie klient
Order:
Relacja z Agent: Wiele do jednego (Many-to-One). Wiele zamówień może być przypisanych do jednego agenta.
Relacja z Ubezpieczenie: Wiele do jednego (Many-to-One). Wiele zamówień może być przypisanych do jednego ubezpieczenia.
Relacja z Client: Wiele do jednego (Many-to-One). Wiele zamówień może być przypisanych do jednego klienta.
Relacja z OrderLine: Jeden do wielu (One-to-Many). Jedno zamówienie może mieć wiele linii zamówienia.
OrderLine:
Relacja z Order: Wiele do jednego (Many-to-One). Wiele linii zamówienia może być przypisanych do jednego zamówienia.
Relacja z Agent: Wiele do jednego (Many-to-One). Wiele linii zamówienia może być przypisanych do jednego agenta.
Relacja z Ubezpieczenie: Wiele do jednego (Many-to-One). Wiele linii zamówienia może być przypisanych do jednego ubezpieczenia.
Relacja z Client: Wiele do jednego (Many-to-One). Wiele linii zamówienia może być przypisanych do jednego klienta
Joiny
SQL umożliwia stosowanie różnych rodzajów joinów, aby łączyć dane z różnych tabel w zapytaniach. Poniżej znajdziesz przykłady różnych joinów z wykorzystaniem dwóch tabel: Orders i OrderDetails.
INNER JOIN:
Zwraca tylko wiersze, które mają odpowiadające wartości w obu tabelach.
sql
Copy code
SELECT Orders.OrderID, Customers.CustomerName
FROM Orders
INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID;
LEFT JOIN (lub LEFT OUTER JOIN):
Zwraca wszystkie wiersze z lewej tabeli (Orders) i pasujące wiersze z prawej tabeli (Customers).
sql
Copy code
SELECT Orders.OrderID, Customers.CustomerName
FROM Orders
LEFT JOIN Customers ON Orders.CustomerID = Customers.CustomerID;
RIGHT JOIN (lub RIGHT OUTER JOIN):
Zwraca wszystkie wiersze z prawej tabeli (Customers) i pasujące wiersze z lewej tabeli (Orders).
sql
Copy code
SELECT Orders.OrderID, Customers.CustomerName
FROM Orders
RIGHT JOIN Customers ON Orders.CustomerID = Customers.CustomerID;
FULL JOIN (lub FULL OUTER JOIN):
Zwraca wiersze, gdy istnieje pasujące wartości w jednej z tabel.
sql
Copy code
SELECT Orders.OrderID, Customers.CustomerName
FROM Orders
FULL JOIN Customers ON Orders.CustomerID = Customers.CustomerID;
CROSS JOIN:
Tworzy iloczyn kartezjański dwóch tabel, zwracając wszystkie możliwe kombinacje wierszy.
sql
Copy code
SELECT Orders.OrderID, Customers.CustomerName
FROM Orders
CROSS JOIN Customers;
SELF JOIN:
Tworzy połączenie między dwiema instancjami tej samej tabeli.
sql
Copy code
SELECT a.OrderID, b.OrderID
FROM Orders a, Orders b
WHERE a.CustomerID = b.CustomerID AND a.OrderID <> b.OrderID;
Te przykłady używają tabeli Orders i Customers, ale możesz dostosować je do swoich konkretnych tabel i relacji. Warto również pamiętać, że efektywność i zrozumienie zapytań zależą od konkretnych wymagań i struktury danych w danej bazie.
group by
Grupa GROUP BY w SQL jest używana do grupowania wyników zapytania według jednego lub więcej kryteriów. Poniżej znajdziesz przykłady zastosowania GROUP BY na przykładowej tabeli Orders z polami CustomerID i TotalAmount.
Podstawowe użycie GROUP BY:
Poniższe zapytanie grupuje zamówienia według identyfikatora klienta (CustomerID) i oblicza sumę łącznej kwoty (TotalAmount) dla każdego klienta.
sql
Copy code
SELECT CustomerID, SUM(TotalAmount) as TotalSpent
FROM Orders
GROUP BY CustomerID;
Zastosowanie funkcji agregującej z GROUP BY:
W tym przypadku używamy funkcji COUNT() do zliczenia liczby zamówień dla każdego klienta.
sql
Copy code
SELECT CustomerID, COUNT(OrderID) as OrderCount
FROM Orders
GROUP BY CustomerID;
Grupowanie według wielu kolumn:
Możemy grupować wyniki według kilku kolumn jednocześnie.
sql
Copy code
SELECT CustomerID, ProductCategory, AVG(ProductPrice) as AvgPrice
FROM Orders
GROUP BY CustomerID, ProductCategory;
Grupowanie po wynikach funkcji agregujących:
Możemy również grupować po wynikach funkcji agregujących.
sql
Copy code
SELECT COUNT(OrderID) as OrderCount, CustomerID
FROM Orders
GROUP BY CustomerID
HAVING COUNT(OrderID) > 5; – Ograniczenie wyników za pomocą HAVING
Zastosowanie GROUP_CONCAT():
Funkcja GROUP_CONCAT() może być używana do konkatenacji wartości w obrębie grupy.
sql
Copy code
SELECT CustomerID, GROUP_CONCAT(ProductName SEPARATOR ‘, ‘) as PurchasedProducts
FROM Orders
GROUP BY CustomerID;
Grupowanie według wyrażeń:
Możemy także grupować wyniki według wyników wyrażeń.
sql
Copy code
SELECT CASE WHEN TotalAmount > 1000 THEN ‘High Spender’ ELSE ‘Regular’ END as SpendingCategory, COUNT(CustomerID) as CustomerCount
FROM Orders
GROUP BY SpendingCategory;
Powyższe przykłady ilustrują różne przypadki użycia GROUP BY w zapytaniach SQL. Warto zauważyć, że grupowanie często używane jest z funkcjami agregującymi, takimi jak SUM(), COUNT(), AVG(), itp.
1 level cache 2 nd level cache
W kontekście Spring oraz technologii ORM (Object-Relational Mapping) takich jak Hibernate, pojawiły się dwa poziomy pamięci podręcznej (cache), które są wykorzystywane w celu zwiększenia wydajności operacji dostępu do bazy danych. Są to:
First Level Cache (Pamięć podręczna pierwszego poziomu):
Jest to cache, który jest specyficzny dla sesji (transakcji) i istnieje tylko w jej zakresie.
Każda sesja Hibernate ma swój własny cache pierwszego poziomu.
Kiedy aplikacja pobiera obiekt z bazy danych, Hibernate zapisuje go w pamięci podręcznej pierwszego poziomu, a następnie przy kolejnym żądaniu pobrania tego samego obiektu w ramach tej samej sesji, Hibernate sprawdza najpierw cache pierwszego poziomu zamiast wysyłać zapytanie do bazy danych.
Cache pierwszego poziomu jest efektywny w trakcie pojedynczej sesji, ale nie przechodzi poza jej granice.
Przykład użycia cache pierwszego poziomu w Spring i Hibernate:
java
Copy code
// Przy pobieraniu obiektu, Hibernate zapisuje go w cache pierwszego poziomu
MyEntity entity = entityManager.find(MyEntity.class, entityId);
// Przy kolejnym pobieraniu tego samego obiektu w ramach tej samej sesji, Hibernate korzysta z cache pierwszego poziomu
MyEntity cachedEntity = entityManager.find(MyEntity.class, entityId);
Second Level Cache (Pamięć podręczna drugiego poziomu):
Jest to cache współdzielony pomiędzy różnymi sesjami i transakcjami.
Przechowuje obiekty oraz dane z bazy danych w pamięci aplikacji, aby unikać częstego korzystania z bazy danych i poprawić ogólną wydajność.
Cache drugiego poziomu jest bardziej globalny, ponieważ przechodzi poza zakres pojedynczej sesji i może być używany przez różne części aplikacji.
Popularnym narzędziem do zarządzania pamięcią podręczną drugiego poziomu w Hibernate jest Ehcache.
Przykład konfiguracji cache drugiego poziomu w pliku persistence.xml:
xml
Copy code
<property></property>
<property></property>
Warto zauważyć, że korzystanie z pamięci podręcznej, zarówno pierwszego, jak i drugiego poziomu, wymaga starannego zarządzania, aby uniknąć problemów związanych z niezgodnością danych w przypadku modyfikacji w bazie danych z poziomu innych aplikacji lub procesów.
n+1
Problem N+1 jest sytuacją, która często występuje w kontekście relacji jeden do wielu w bazie danych. Główna koncepcja tego problemu polega na tym, że podczas pobierania danych z relacji jeden do wielu, dla każdego głównego obiektu jest wykonywane dodatkowe zapytanie, aby pobrać powiązane obiekty. Ostateczna liczba zapytań jest równa N+1, gdzie N to liczba głównych obiektów.
Przykład:
Rozważmy dwie encje: Author (Autor) i Book (Książka), gdzie jeden autor może napisać wiele książek (relacja jeden do wielu).
Problem N+1:
sql
Copy code
– Zapytanie 1: Pobranie wszystkich autorów
SELECT * FROM Author;
– Dla każdego autora wykonuje się dodatkowe zapytanie:
– Zapytanie 2: Pobranie książek dla autora o ID 1
SELECT * FROM Book WHERE author_id = 1;
– Zapytanie 3: Pobranie książek dla autora o ID 2
SELECT * FROM Book WHERE author_id = 2;
– …
W rezultacie otrzymujemy N+1 zapytań, co może prowadzić do znacznego obciążenia bazy danych, zwłaszcza gdy mamy dużą ilość danych.
Rozwiązanie:
Fetch Join:
Użyj JOIN FETCH w zapytaniu JPQL lub HQL, aby załadować powiązane obiekty w jednym zapytaniu.
sql
Copy code
SELECT a FROM Author a JOIN FETCH a.books;
Batch Loading:
Użyj mechanizmów ładowania danych wsadowych, takich jak @BatchSize w Hibernate, aby zoptymalizować sposób, w jaki dane są pobierane z bazy danych.
java
Copy code
@OneToMany(mappedBy = “author”)
@BatchSize(size = 10) // Przykładowa wartość
private List<Book> books;
Eager Loading:</Book>
Ustaw relację jeden do wielu jako FetchType.EAGER, jeśli istnieje pewność, że zawsze chcemy pobierać powiązane obiekty wraz z głównym obiektem.
java
Copy code
@OneToMany(mappedBy = “author”, fetch = FetchType.EAGER)
private List<Book> books;
Zastosowanie jednego z tych rozwiązań pozwala uniknąć nadmiernego obciążenia bazy danych poprzez minimalizację liczby zapytań wykonywanych podczas pobierania danych z relacji jeden do wielu.</Book>
lazy loading loading exception LAZY EAGER
Rozwiązanie problemu dotyczącego błędów związanych z leniwym (lazy) i chciwym (eager) ładowaniem zwykle zależy od konkretnego kontekstu i struktury aplikacji. Poniżej przedstawiam kilka wskazówek i potencjalnych rozwiązań.
Wyjątek LazyInitializationException:
Oznaczanie Leniwych Relacji:
Upewnij się, że relacje, które są leniwie ładowane (FetchType.LAZY), są oznaczone jako transakcyjne, a dostęp do nich odbywa się w ramach aktywnej sesji Hibernate.
java
Copy code
@OneToMany(mappedBy = “author”, fetch = FetchType.LAZY)
private List<Book> books;
Praca w Kontekście Transakcji:</Book>
Upewnij się, że dostęp do leniwie ładowanych relacji odbywa się wewnątrz otwartej transakcji. W Springu można to zrealizować przy użyciu adnotacji @Transactional.
java
Copy code
@Transactional
public Author getAuthorWithBooks(Long authorId) {
Author author = authorRepository.findById(authorId).orElse(null);
// Relacja books zostanie leniwie załadowana wewnątrz tej transakcji.
return author;
}
Wyjątek Hibernate:
Błąd org.hibernate.LazyInitializationException:
Jeśli otrzymujesz błąd LazyInitializationException przy próbie dostępu do leniwie ładowanej relacji poza sesją Hibernate, możesz rozważyć kilka rozwiązań.
java
Copy code
@OneToMany(mappedBy = “author”, fetch = FetchType.LAZY)
private List<Book> books;
a. OpenSessionInView (OSIV):</Book>
W przypadku aplikacji webowej możesz rozważyć użycie wzorca Open Session In View (OSIV), który utrzymuje sesję otwartą przez całe życie żądania HTTP.
b. Eager Loading lub Fetch Join:
W niektórych przypadkach rozważ zastosowanie chciwego ładowania (FetchType.EAGER) lub Fetch Join, aby zminimalizować ryzyko błędów LazyInitializationException.
java
Copy code
@OneToMany(mappedBy = “author”, fetch = FetchType.EAGER)
private List<Book> books;
Używanie FetchType.LAZY z Uważnością:</Book>
Używaj FetchType.LAZY z pełną świadomością. Upewnij się, że relacje są leniwie ładowane tylko tam, gdzie jest to uzasadnione, a dostęp do nich odbywa się w odpowiednich kontekstach transakcyjnych.
Zastosowanie FetchType.EAGER:
Ostrzeżenie dotyczące FetchType.EAGER:
Używanie FetchType.EAGER dla wszystkich relacji może prowadzić do tzw. “problemu N+1”, gdzie pobierane są wszystkie powiązane obiekty nawet w przypadku prostych operacji.
java
Copy code
@OneToMany(mappedBy = “author”, fetch = FetchType.EAGER)
private List<Book> books;
Rozważ zastosowanie FetchType.LAZY dla większości relacji, a jedynie w przypadkach, gdzie pełne chciwe ładowanie jest uzasadnione, używaj FetchType.EAGER.
Podsumowanie:
Ostateczny wybór między FetchType.LAZY a FetchType.EAGER zależy od wymagań aplikacji, skomplikowania modelu danych i konkretnego przypadku użycia. Dobre zrozumienie leniwego ładowania, kontekstu transakcji i odpowiednie zarządzanie sesją Hibernate są kluczowe dla uniknięcia błędów i zoptymalizowanego dostępu do danych.</Book>
@Transactional
@Transactional to adnotacja w Springu, która informuje kontener Springa o tym, że metoda powinna być transakcyjna. Adnotacja ta może być stosowana do metody lub klasy, co oznacza, że wszystkie metody w danej klasie będą transakcyjne, jeśli tylko ta klasa zostanie użyta jako komponent Springa (na przykład poprzez wstrzykiwanie zależności).
Kluczową cechą @Transactional jest zarządzanie transakcjami w kontekście aplikacji. Pozwala na definiowanie granic transakcji, propagację, izolację i obsługę błędów transakcyjnych.
Najważniejsze atrybuty adnotacji @Transactional:
propagation:
Określa, jak ma się zachować metoda, gdy jest wywoływana w kontekście istniejącej transakcji. Na przykład: Propagation.REQUIRED, Propagation.REQUIRES_NEW, itp.
isolation:
Określa poziom izolacji transakcji. Definiuje stopień, w jakim jedna transakcja może “widzieć” zmiany dokonane przez inne transakcje. Na przykład: Isolation.READ_COMMITTED, Isolation.SERIALIZABLE, itp.
readOnly:
Określa, czy transakcja powinna być oznaczona jako tylko do odczytu (true/false). Oznaczenie transakcji jako tylko do odczytu może umożliwić pewne optymalizacje w zarządzaniu bazą danych.
timeout:
Określa czas, po którym transakcja powinna zostać automatycznie zakończona, jeśli nie została zakończona wcześniej.
rollbackFor:
Określa wyjątki, które powinny prowadzić do automatycznego wycofania transakcji.
noRollbackFor:
Określa wyjątki, które nie powinny prowadzić do automatycznego wycofania transakcji.
Przykład użycia adnotacji @Transactional:
java
Copy code
@Service
public class MyService {
@Autowired private MyRepository myRepository; @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) public void performTransactionalOperation() { // Logika biznesowa myRepository.save(entity1); // Inna operacja myRepository.save(entity2); } } W tym przykładzie, metoda performTransactionalOperation() jest oznaczona adnotacją @Transactional z określonymi atrybutami. Wszystkie operacje wykonywane w ramach tej metody są objęte jedną transakcją. Jeśli wystąpi błąd, cała transakcja zostanie automatycznie wycofana (rollback). Adnotacja ta jest szczególnie przydatna w środowiskach, gdzie korzysta się z baz danych i wymaga zarządzania transakcjami w sposób spójny i niezawodny.
@preDestroy
@PreDestroy to adnotacja w Springu używana do oznaczania metody, która powinna zostać wywołana przed zniszczeniem obiektu zarządzanego przez kontener Springa. Jest to często wykorzystywane do wykonania operacji czyszczenia, zwalniania zasobów czy zamykania połączeń przed zakończeniem cyklu życia danego beana. Poniżej znajdziesz prosty przykład:
java
Copy code
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class MyDatabaseConnection {
// Inne pola, metody itp. @PreDestroy public void closeConnection() { System.out.println("Closing database connection..."); // Logika zamykania połączenia z bazą danych } } W tym przykładzie, klasa MyDatabaseConnection jest oznaczona adnotacją @Component, co oznacza, że jest zarządzana przez kontener Springa. Metoda closeConnection z adnotacją @PreDestroy zostanie wywołana przed zniszczeniem beana. Wewnątrz tej metody można umieścić wszelkie operacje związane z zamykaniem połączenia z bazą danych lub innymi działaniami przy zamykaniu beana.
Kiedy kontener Springa zauważa, że bean ma być zniszczony (na przykład w wyniku zamykania aplikacji), automatycznie wywołuje metody oznaczone adnotacją @PreDestroy na tym beanie. To zapewnia kontrolowane i porządkowe zamykanie zasobów przed zniszczeniem beana
@PostConstruct
@PostConstruct to adnotacja w Springu, która jest używana do oznaczania metody, która powinna być wykonana po zakończeniu procesu konstrukcji bean’a, ale przed udostępnieniem go do klienta. Metoda oznaczona adnotacją @PostConstruct zostanie wykonana zaraz po tym, jak obiekt zostanie skonstruowany, ale przed tym, jak zostanie użyty w aplikacji. Oto prosty przykład:
java
Copy code
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
public class MyService {
@PostConstruct public void init() { System.out.println("Initializing MyService..."); // Logika inicjalizacyjna } // Inne metody, pola itp. } W tym przykładzie, klasa MyService jest oznaczona adnotacją @Component, co oznacza, że jest zarządzana przez kontener Springa. Metoda init z adnotacją @PostConstruct zostanie automatycznie wywołana po zakończeniu konstrukcji obiektu, co pozwala na wykonanie inicjalizacyjnych operacji.
Główne zastosowania @PostConstruct obejmują:
Inicjalizację zasobów, takich jak połączenia z bazą danych czy pliki konfiguracyjne.
Inicjalizację stanu obiektu przed użyciem w aplikacji.
Wykonanie operacji, które muszą być wykonane w momencie inicjalizacji, ale po konstrukcji obiektu.
Warto zauważyć, że metoda oznaczona @PostConstruct powinna być bezparametrowa i nie powinna zwracać wartości. Jeśli używasz Java 9 lub nowszej, możesz również zastąpić javax.annotation.PostConstruct przez javax.annotation.processing.Processing z pakietu java.annotation.
Scope beanów
W kontekście Springa, “scope” określa zakres istnienia beana i kontroluje, jak długo bean pozostaje aktywny oraz jakie ma właściwości w kontekście cyklu życia aplikacji. Spring oferuje różne zakresy (scope) dla beana, a wybór odpowiedniego zależy od wymagań aplikacji. Poniżej przedstawiam najważniejsze zakresy beana w Springu:
- Singleton (Domyślny):
Zakres (@Scope): @Scope(“singleton”)
Opis:
Domyślny zakres beana w Springu.
Oznacza, że tylko jedna instancja beana jest tworzona dla każdego kontekstu aplikacji.
Singleton jest jednym z najczęściej używanych zakresów i jest odpowiedni dla wielu przypadków użycia. - Prototype:
Zakres (@Scope): @Scope(“prototype”)
Opis:
Oznacza, że dla każdego żądania utworzenia beana zostaje stworzona nowa instancja.
Każda instancja bean jest niezależna od innych, co jest przydatne w przypadku, gdy chcemy uniknąć współdzielenia stanu między różnymi częściami aplikacji. - Request:
Zakres (@Scope): @Scope(“request”)
Opis:
W kontekście aplikacji webowej, oznacza, że dla każdego żądania HTTP jest tworzona nowa instancja bean.
Po zakończeniu żądania, instancja bean jest usuwana. - Session:
Zakres (@Scope): @Scope(“session”)
Opis:
W kontekście aplikacji webowej, oznacza, że dla każdej sesji HTTP (sesji użytkownika) jest tworzona nowa instancja bean.
Instancja jest usuwana po zakończeniu sesji. - Global Session:
Zakres (@Scope): @Scope(“globalSession”)
Opis:
Podobne do session, ale stosowane w kontekście portletów w środowisku portletowym. - Application:
Zakres (@Scope): @Scope(“application”)
Opis:
W kontekście aplikacji webowej, oznacza, że dla całej aplikacji jest tworzona jedna instancja bean.
Instancja jest usuwana po zakończeniu aplikacji. - WebSocket:
Zakres (@Scope): @Scope(“websocket”)
Opis:
Dotyczy kontekstu WebSocket w aplikacjach webowych. - Custom:
Zakres (@Scope): @Scope(“customScopeName”)
Opis:
Możliwość zdefiniowania własnego zakresu dla specyficznych wymagań aplikacji.
Przykład użycia:
java
Copy code
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(“prototype”)
public class MyPrototypeBean {
// …
}
W powyższym przykładzie, MyPrototypeBean będzie miał zakres prototype, co oznacza, że dla każdego żądania utworzenia nowej instancji beana będzie tworzona nowa kopia.
Cykl życia beana
Cykl życia beana w Springu obejmuje szereg kroków, od momentu utworzenia do zniszczenia, a każdy krok jest zarządzany przez kontener Springa. Poniżej przedstawiam podstawowy cykl życia beana w Springu:
- Utworzenie Beana (Instantiation):
Moment, w którym Spring tworzy nową instancję beana. W przypadku Singletona dzieje się to tylko raz, podczas startu kontenera. W przypadku Prototypu, nowa instancja jest tworzona za każdym razem, gdy bean jest żądany. - Ustawienie Właściwości (Populating Properties):
Spring ustawia właściwości (zależności) beana, używając konstruktorów, setterów lub pól (w zależności od konfiguracji). - Wywołanie Metody @PostConstruct lub Interfejsu InitializingBean:
Jeśli bean ma metody oznaczone adnotacją @PostConstruct lub implementuje interfejs InitializingBean, te metody są wywoływane tuż po ustawieniu właściwości i przed udostępnieniem beana. - Użycie Beana przez Aplikację (In Use):
W tym momencie bean jest dostępny dla innych komponentów w aplikacji i może być używany. - Zakończenie Używania Beana (Disposal):
W przypadku singletonów, bean jest przechowywany przez cały okres życia kontenera. W przypadku prototypów, kiedy bean nie jest już używany, może być zniszczony. - Wywołanie Metody @PreDestroy lub Interfejsu DisposableBean:
Jeśli bean ma metody oznaczone adnotacją @PreDestroy lub implementuje interfejs DisposableBean, te metody są wywoływane przed zniszczeniem beana.
Diagram Cyklu Życia Beana w Springu:
sql
Copy code
+——————-+
| Utworzenie Beana |
+——–|———-+
|
v
+——————-+
| Ustawienie |
| Właściwości |
+——–|———-+
|
v
+———————|——————+
| @PostConstruct / v |
| InitializingBean +——————+
+———————+
|
v
+——————-+
| Użycie Beana |
+——–|———-+
|
v
+——————-+
| Zakończenie |
| Używania Beana |
+——–|———-+
|
v
+———————|——————+
| @PreDestroy / v |
| DisposableBean +——————+
+———————+
Przykład:
java
Copy code
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class MyBean {
@PostConstruct public void postConstruct() { System.out.println("Bean is being constructed..."); // Logika inicjalizacyjna } // Inne metody, pola itp. @PreDestroy public void preDestroy() { System.out.println("Bean is being destroyed..."); // Logika przed zniszczeniem beana } } W tym przykładzie, postConstruct jest wywoływane po utworzeniu beana, a preDestroy przed zniszczeniem. Ta metoda zarządzania cyklem życia jest bardzo przydatna do wykonywania działań przed inicjalizacją i przed zniszczeniem obiektu zarządzanego przez kontener Springa.
bean
W kontekście Springa, termin “bean” odnosi się do obiektów, które są zarządzane przez kontener Springa. Beany w Springu są komponentami, które są tworzone, konfigurowane i zarządzane przez kontener. Są one używane do reprezentowania różnych części aplikacji, takich jak usługi, repozytoria, kontrolery, czy też obiekty dostępu do danych. Beany są podstawowym elementem wstrzykiwania zależności i zarządzania cyklem życia komponentów w Springu. Poniżej przedstawiam kilka przykładów, aby lepiej zrozumieć, co to jest bean w Springu:
Przykład 1: Prosty Bean
java
Copy code
import org.springframework.stereotype.Component;
@Component
public class MyBean {
// Logika beana
}
W tym przykładzie, klasa MyBean jest oznaczona adnotacją @Component, co oznacza, że jest to bean zarządzany przez kontener Springa.
Przykład 2: Bean z Zależnościami
java
Copy code
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyService {
private MyRepository myRepository; @Autowired public MyService(MyRepository myRepository) { this.myRepository = myRepository; } // Logika usługi } W tym przykładzie, klasa MyService posiada zależność MyRepository, a adnotacja @Autowired oznacza, że Spring automatycznie wstrzyknie odpowiednią instancję MyRepository podczas tworzenia beana MyService.
Przykład 3: Konfiguracja XML Bean
xml
Copy code
<!-- plik applicationContext.xml -->
<beans>
<bean></bean>
</beans>
W tym przykładzie, beana są definiowane i konfigurowane w pliku XML. myBean jest instancją klasy MyBean.
Przykład 4: Bean z Cyklem Życia
java
Copy code
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class LifecycleBean {
@PostConstruct public void init() { // Logika inicjalizacyjna } // Logika beana @PreDestroy public void destroy() { // Logika przed zniszczeniem beana } } W tym przykładzie, klasa LifecycleBean ma metody oznaczone adnotacjami @PostConstruct i @PreDestroy, co umożliwia wykonanie pewnych operacji przed inicjalizacją beana oraz przed jego zniszczeniem.
Beany w Springu są wszechstronnymi komponentami, które można skonfigurować, wstrzykiwać zależności, zarządzać ich cyklem życia i używać w różnych obszarach aplikacji.
orElse a orElseGet
orElse i orElseGet to metody dostępne w interfejsie Optional w języku Java, które pozwalają obsługiwać sytuacje, gdy opcjonalna wartość jest pusta (null). Oto główne różnice między nimi:
- orElse:
Sygnatura:
java
Copy code
T orElse(T other)
Działa:
Zwraca wartość opcjonalną, jeśli istnieje, lub podaną wartość domyślną, jeśli opcjonalna wartość jest pusta.
Wywoływane Zawsze:
Metoda orElse zawsze wywołuje przekazaną wartość, niezależnie od tego, czy opcjonalna wartość jest obecna czy pusta.
Przykład:
java
Copy code
Optional<String> optionalValue = Optional.ofNullable(someValue);
String result = optionalValue.orElse("defaultValue");
2. orElseGet:
Sygnatura:</String>
java
Copy code
T orElseGet(Supplier<? extends T> supplier)
Działa:
Zwraca wartość opcjonalną, jeśli istnieje, lub wywołuje dostawcę (funkcję) i zwraca wynik tej funkcji jako wartość domyślną, jeśli opcjonalna wartość jest pusta.
Wywoływane Tylko, Gdy Potrzebne:
Metoda orElseGet wywołuje dostawcę tylko wtedy, gdy opcjonalna wartość jest pusta, co może być bardziej efektywne, jeśli obliczenia domyślnej wartości są kosztowne.
Przykład:
java
Copy code
Optional<String> optionalValue = Optional.ofNullable(someValue);
String result = optionalValue.orElseGet(() -> calculateDefaultValue());
Kiedy Wybrać Którąś Z Metod?
orElse:</String>
Użyj, gdy wartość domyślna jest prostą wartością lub nie wymaga obliczeń.
Metoda ta jest zawsze wywoływana, nawet jeśli opcjonalna wartość jest obecna.
orElseGet:
Użyj, gdy wartość domyślna wymaga obliczeń lub jest wynikiem operacji.
Dostawca (lambda lub referencja do metody) zostanie wywołany tylko wtedy, gdy opcjonalna wartość jest pusta.
W zależności od kontekstu i specyfiki sytuacji, wybór między orElse a orElseGet zależy od preferencji i efektywności obliczeń domyślnej wartości. Jeśli wartość domyślna jest stała lub obliczenia są niewielkie, obie metody mogą być używane zamiennie. Jednakże, gdy wartość domyślna jest wynikiem złożonych obliczeń, orElseGet może być bardziej efektywne.
Optional Optional to klasa wprowadzona w języku Java 8 w pakiecie java.util, która służy do obsługi potencjalnie pustych (null) wartości. Optional zapewnia bardziej bezpieczne i ekspresywne podejście do obsługi sytuacji, w których wartość może być obecna lub też nie. Pozwala to unikać potencjalnych błędów związanych z dostępem do null oraz ułatwia programowanie defensywne.
Oto kilka kluczowych cech i sposobów użycia Optional:
- Tworzenie Optional:
Z of (gdy wartość nie może być null):
java
Copy code
Optional<String> optionalValue = Optional.of("Hello, World!");
Z ofNullable (gdy wartość może być null):</String>
java
Copy code
String value = // …
Optional<String> optionalValue = Optional.ofNullable(value);
Pusty Optional:</String>
java
Copy code
Optional<String> emptyOptional = Optional.empty();
2. Sprawdzanie Obecności Wartości:
isPresent - Sprawdzanie czy wartość jest obecna:
java
Copy code
if (optionalValue.isPresent()) {
// Wartość jest obecna
String result = optionalValue.get();
} else {
// Wartość jest pusta
}
3. Dostęp do Wartości:
get - Pobieranie Wartości (uwaga: unikaj bezpośredniego get):</String>
java
Copy code
String result = optionalValue.get(); // Unikaj tego, sprawdź obecność przed użyciem
orElse - Dostarczanie Wartości Domyślnej:
java
Copy code
String result = optionalValue.orElse(“Default Value”);
orElseGet - Dostarczanie Wartości Domyślnej za pomocą Dostawcy (Supplier):
java
Copy code
String result = optionalValue.orElseGet(() -> calculateDefaultValue());
4. Operacje na Wartości:
map - Wykonywanie Operacji na Wartości:
java
Copy code
Optional<String> upperCaseValue = optionalValue.map(String::toUpperCase);
filter - Filtracja Wartości:</String>
java
Copy code
Optional<String> filteredValue = optionalValue.filter(s -> s.startsWith("H"));
5. Obsługa Pustych Wartości:
orElseThrow - Rzucanie Wyjątku dla Pustego Optional:</String>
java
Copy code
String result = optionalValue.orElseThrow(() -> new RuntimeException(“Value not present”));
ifPresent - Wykonywanie Operacji, Jeśli Wartość Jest Obecna:
java
Copy code
optionalValue.ifPresent(val -> System.out.println(“Value is present: “ + val));
Podsumowanie:
Optional w języku Java jest przydatnym narzędziem do lepszego zarządzania potencjalnie pustymi wartościami. Jest często używany w metodach zwracających wartości, które mogą być nullem, a także w sytuacjach, gdzie wymagane jest bezpieczne operowanie na wartościach. Należy jednak unikać nadużywania Optional i stosować go tam, gdzie faktycznie ma sens, a nie w każdej sytuacji.
Stream i operacje na streamach
Stream w języku Java to sekwencyjny i potencjalnie nieograniczony zestaw elementów, który można przetwarzać w oparty na funkcyjnych operacjach sposób. Streamy pozwalają na wygodne i efektywne przetwarzanie danych, stosując różne operacje takie jak filtrowanie, mapowanie, sortowanie czy redukcję.
Oto kilka kluczowych cech i operacji związanych ze Stream:
- Tworzenie Strumieni:
Z Kolekcji:
java
Copy code
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
Stream<String> streamFromList = myList.stream();
Z Tablicy:</String></String>
java
Copy code
String[] myArray = {“a1”, “a2”, “b1”, “c2”, “c1”};
Stream<String> streamFromArray = Arrays.stream(myArray);
Z Elementów:</String>
java
Copy code
Stream<String> streamOfElements = Stream.of("a1", "a2", "b1", "c2", "c1");
Z Funkcji Generatora:</String>
java
Copy code
Stream<String> generatedStream = Stream.generate(() -> "generated").limit(5);
2. Operacje Pośrednie:
filter - Filtracja:</String>
java
Copy code
Stream<String> filteredStream = stream.filter(s -> s.startsWith("a"));
map - Mapowanie:</String>
java
Copy code
Stream<String> uppercasedStream = stream.map(String::toUpperCase);
sorted - Sortowanie:</String>
java
Copy code
Stream<String> sortedStream = stream.sorted();
distinct - Usunięcie Duplikatów:</String>
java
Copy code
Stream<String> distinctStream = stream.distinct();
3. Operacje Końcowe:
forEach - Iteracja po Elementach:</String>
java
Copy code
stream.forEach(System.out::println);
collect - Zbieranie Wyników do Kolekcji:
java
Copy code
List<String> collectedList = stream.collect(Collectors.toList());
toArray - Zbieranie do Tablicy:</String>
java
Copy code
String[] array = stream.toArray(String[]::new);
reduce - Redukcja:
java
Copy code
Optional<String> concatenated = stream.reduce((s1, s2) -> s1 + s2);
4. Operacje na Liczbach:
sum, average, count, max, min - Operacje na Liczbach:
java
Copy code
int sum = intStream.sum();
OptionalDouble average = doubleStream.average();
long count = longStream.count();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();
5. Strumienie Nieskończone:
iterate - Generowanie Nieskończonego Strumienia:</String>
java
Copy code
Stream.iterate(0, n -> n + 1).limit(10).forEach(System.out::println);
generate - Generowanie Nieskończonego Strumienia z Funkcji Generatora:
java
Copy code
Stream.generate(() -> “generated”).limit(5).forEach(System.out::println);
6. Operacje na Strumieniach z Obiektami:
allMatch, anyMatch, noneMatch - Sprawdzanie Warunków:
java
Copy code
boolean allMatch = stream.allMatch(s -> s.startsWith(“a”));
boolean anyMatch = stream.anyMatch(s -> s.startsWith(“a”));
boolean noneMatch = stream.noneMatch(s -> s.startsWith(“z”));
findFirst, findAny - Znajdowanie Pierwszego lub Dowolnego Elementu:
java
Copy code
Optional<String> firstElement = stream.findFirst();
Optional<String> anyElement = stream.findAny();
groupBy - Grupowanie Elementów:</String></String>
java
Copy code
Map<Integer, List<String>> groupedByLength = stream.collect(Collectors.groupingBy(String::length));
Podsumowanie:
Strumienie (Stream) w Javie dostarczają eleganckie i funkcyjne narzędzie do przetwarzania kolekcji danych. Umożliwiają bardziej deklaratywne i czytelne podejście do operacji na danych, co przyczynia się do bardziej ekspresywnego i czytelnego kodu. Strumienie są integralną częścią programowania funkcyjnego w Javie i pozwalają na bardziej wydajne oraz zwięzłe manipulowanie danymi.</String>
Heap stack - sterta stos
Heap (sterta) i Stack (stos) to dwa rodzaje pamięci używane w trakcie wykonywania programów komputerowych. Oto krótkie omówienie każdego z tych rodzajów pamięci:
- Heap Memory (Pamięć Sterta):
Charakterystyka:
Jest obszarem pamięci komputera, gdzie dynamicznie alokowane są obiekty i dane, które mają dłuższy czas życia niż lokalne zmienne przechowywane na stosie.
Obiekty w pamięci sterty są zarządzane ręcznie lub automatycznie przez mechanizmy takie jak Garbage Collector.
Użycie:
Wykorzystywana jest głównie do przechowywania obiektów o zmiennej wielkości i długo żyjących danych, takich jak instancje klas, tablice, itp.
Przykłady:
Obiekty stworzone za pomocą operatora new w Javie.
Dynamicznie alokowane struktury danych, takie jak listy, mapy, itp.
Żywotność:
Obiekty w pamięci sterty pozostają “żywe” tak długo, jak są dostępne z programu i nie zostaną one ręcznie zwolnione.
2. Stack Memory (Pamięć Stos):
Charakterystyka:
Jest to obszar pamięci, w którym przechowywane są lokalne zmienne oraz dane związane z bieżącymi wywołaniami funkcji.
Stos jest stosunkowo niewielki, ale operuje bardzo szybko, ponieważ dostęp do danych na stosie jest prostszy i szybszy.
Użycie:
Przechowuje lokalne zmienne oraz dane związane z bieżącymi wywołaniami funkcji.
Każde wywołanie funkcji powoduje utworzenie nowego “ramienia” na stosie z lokalnymi zmiennymi dla tej funkcji.
Przykłady:
Wszystkie lokalne zmienne w trakcie wykonywania funkcji.
Adresy powrotu z funkcji.
Wartości przekazywane do funkcji jako argumenty.
Żywotność:
Zmienne na stosie mają krótki czas życia. Są tworzone przy wejściu do funkcji i usuwane przy jej opuszczeniu.
Podsumowanie:
Heap i Stack to dwa główne obszary pamięci używane w trakcie działania programów.
Heap jest używany do przechowywania dynamicznie alokowanych obiektów o dłuższym czasie życia.
Stack jest używany do przechowywania lokalnych zmiennych i danych związanych z bieżącymi wywołaniami funkcji.
Zarządzanie pamięcią na stosie jest zazwyczaj bardziej efektywne i szybsze niż na stercie, ale stos ma ograniczoną wielkość.
Heap umożliwia dynamiczne alokowanie i zwalnianie pamięci, co może prowadzić do fragmentacji pamięci.
W językach programowania takich jak Java, zarządzanie pamięcią na stercie jest często zautomatyzowane, a programiści rzadko muszą się martwić o ręczne zwalnianie pamięci.
System.gc()
System.gc() to wywołanie metody w języku Java, które służy do sugestii dla systemu o konieczności wykonania operacji zbierania śmieci (Garbage Collection). Jednak, ważne jest zaznaczenie, że wywołanie System.gc() to jedynie sugestia, a nie bezpośrednie polecenie do natychmiastowego rozpoczęcia procesu zbierania śmieci. Ostateczna decyzja o wykonaniu zbierania śmieci pozostaje w gestii JVM (Java Virtual Machine).
Garbage Collector (Zbieracz Śmieci):
Garbage Collector (GC) to mechanizm wewnątrz JVM, który odpowiada za automatyczne zarządzanie pamięcią, zwalnianie niepotrzebnych obiektów (śmieci) i przywracanie dostępnego miejsca w pamięci. Główne zadania Garbage Collectora to:
Identyfikacja Śmieci:
Rozpoznawanie obiektów, które nie są już dostępne dla programu i mogą być bezpiecznie usunięte.
Zbieranie Śmieci:
Usuwanie obiektów, które zostały zidentyfikowane jako nieosiągalne, zwolnienie ich pamięci i przywrócenie jej do puli dostępnej dla nowych obiektów.
Zapobieganie Wyciekom Pamięci:
Zapobieganie wyciekom pamięci poprzez automatyczne usuwanie obiektów, które nie są już używane, ale nie zostały ręcznie zwolnione przez programistę.
Działanie Garbage Collectora:
Generational Garbage Collection:
Większość współczesnych Garbage Collectorów, w tym ten używany w JVM, opiera się na podejściu generacyjnym. Działa ono na zasadzie podziału obszaru pamięci na dwie główne części: młodą generację (Young Generation) i starą generację (Old Generation).
Kroki Procesu Zbierania Śmieci:
Młoda Generacja:
Nowo utworzone obiekty są umieszczane w młodej generacji.
W trakcie zbierania śmieci w młodej generacji, usuwane są obiekty, które są już nieosiągalne.
Pozostałe obiekty przechodzą do starej generacji.
Stara Generacja:
Obiekty, które przetrwały kilka cykli zbierania śmieci w młodej generacji, są przenoszone do starej generacji.
W starej generacji odbywa się bardziej kosztowne zbieranie śmieci, ponieważ tu znajdują się obiekty o dłuższym czasie życia.
Różnice W Efektywności:
Procesy zbierania śmieci mogą być wywoływane automatycznie w odpowiednich momentach przez JVM. Wpływ System.gc() na wywołanie Garbage Collectora zależy od implementacji JVM i konfiguracji systemu.
System.gc():
Sugestia do GC:
System.gc() służy do wywołania Garbage Collectora, ale nie gwarantuje natychmiastowego wykonania operacji zbierania śmieci.
Jest to jedynie sugestia dla JVM, a decyzja o realizacji tej sugestii zależy od wielu czynników, takich jak obciążenie systemu, strategia Garbage Collectora, itp.
Użycie z Ostrożnością:
Zazwyczaj nie jest zalecane ręczne wywoływanie System.gc(), ponieważ współczesne Garbage Collectory są zdolne do efektywnego zarządzania pamięcią bez ingerencji programisty.
Może to prowadzić do nadmiernego obciążenia systemu i wpływać na wydajność aplikacji.
W skrócie, System.gc() to sugestia dla JVM, a nie bezpośrednie polecenie do natychmiastowego rozpoczęcia procesu zbierania śmieci. Jeśli program jest odpowiednio napisany, Garbage Collector powinien efektywnie zarządzać pamięcią bez potrzeby ręcznego interweniowania.
Garbage collector
Garbage Collector (GC) to mechanizm w językach programowania, który jest odpowiedzialny za automatyczne zarządzanie pamięcią, usuwanie obiektów, które nie są już używane przez program, i zwalnianie zasobów, które były im przypisane. GC eliminuje konieczność ręcznego zwalniania pamięci przez programistów, co pomaga w uniknięciu wycieków pamięci i związanych z nimi problemów.
Jak działa Garbage Collector?
Identyfikacja Nieosiągalnych Obiektów:
GC identyfikuje obiekty, które nie są już dostępne z programu, a zatem nie mają referencji do nich.
Oznaczanie Obiektów:
GC oznacza obiekty uznane za nieosiągalne, stosując różne algorytmy, takie jak algorytm znacznikowy (mark-and-sweep) lub śledzenie od korzenia (tracing from roots).
Usuwanie Obiektów:
Zidentyfikowane i oznaczone jako nieosiągalne obiekty są usuwane z pamięci. Mogą one być albo zwolnione od razu, albo przeniesione do specjalnej sterty, gdzie zostaną zwolnione w późniejszym czasie.
Zwalnianie Pamięci:
GC zwalnia pamięć zajmowaną przez usunięte obiekty. W przypadku algorytmu znacznikowego, obszar pamięci, na którym znajdują się usunięte obiekty, może ulec fragmentacji.
Rodzaje Garbage Collector w Javie (Java Virtual Machine - JVM):
Serial Garbage Collector:
Wykorzystuje algorytm mark-and-sweep.
Przeznaczony głównie do prostych aplikacji, testów i małych serwerów.
Parallel Garbage Collector (Parallel GC):
Równoległa wersja mark-and-sweep.
Stosowany w środowiskach, w których ważna jest wydajność dla wielu rdzeni CPU.
Concurrent Mark-Sweep (CMS) Collector:
Oznacza i sprząta obiekty równolegle z działaniem aplikacji.
Bardziej odpowiedni dla aplikacji, w których ważna jest niska latencja.
G1 (Garbage First) Collector:
Zastosowanie zaawansowanych algorytmów, takich jak garbage-first heap.
Bardziej efektywny w obszarze dużej pamięci i niskiej latencji.
ZGC (Z Garbage Collector):
Projektowany z myślą o niskiej latencji i dużych obszarach pamięci.
Introdukuje algorytmy kompresji i oczyszczania.
Dlaczego Garbage Collection jest Ważny?
Unikanie Wycieków Pamięci:
Zapobiega wyciekom pamięci poprzez usuwanie obiektów, które nie są już używane.
Usprawnianie Programowania:
Eliminuje potrzebę ręcznego zwalniania pamięci przez programistów, co ułatwia pisanie bezpiecznego i wydajnego kodu.
Optymalizacja Pamięci:
Pomaga w optymalizacji wykorzystania pamięci poprzez zwalnianie zasobów, gdy są one już niepotrzebne.
Redukcja Ryzyka Błędów:
Zmniejsza ryzyko błędów związanych z zarządzaniem pamięcią, takich jak wycieki pamięci czy dostęp do zwolnionej pamięci.
Warto pamiętać, że choć Garbage Collector automatyzuje zarządzanie pamięcią, to nie zwalnia programistów od odpowiedzialności za pisanie efektywnego i dobrze zoptymalizowanego kodu. Zrozumienie, jak działa GC, może być przydatne przy optymalizacji i unikaniu potencjalnych problemów z wydajnością.
Try catch finally
Kod, który dostarczyłeś, sugeruje użycie bloku try-with-resources wraz z blokami catch i finally. Przedstawmy, jak działa każda z tych części:
- try-with-resources:
java
Copy code
try (ResourceType resource = new ResourceType()) {
// Kod, który korzysta z zasobu (resource)
} catch (ExceptionType e) {
// Obsługa wyjątku (jeśli wystąpił)
} finally {
// Kod, który zostanie wykonany zawsze, niezależnie od tego, czy wyjątek wystąpił czy nie
}
try-with-resources jest mechanizmem wprowadzonym w języku Java 7, który ułatwia zarządzanie zasobami, takimi jak strumienie, pliki, połączenia do baz danych itp.
W bloku try-with-resources, zasoby są otwierane w nawiasach okrągłych. Java automatycznie zajmuje się zamykaniem tych zasobów po zakończeniu bloku try. - catch:
Blok catch jest używany do obsługi wyjątków, które mogą zostać zgłoszone w trakcie wykonania bloku try.
Jeśli w bloku try wystąpi wyjątek odpowiadający typowi określonemu w bloku catch, to ten blok zostanie wykonany. - finally:
Blok finally jest opcjonalny i zawiera kod, który zostanie wykonany zawsze, niezależnie od tego, czy wystąpił wyjątek, czy nie.
Jest to miejsce do umieszczenia kodu, który musi zostać wykonany, niezależnie od tego, czy wystąpił wyjątek, czy nie.
Przykładowe Użycie:
java
Copy code
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Example {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader(“example.txt”))) {
String line = reader.readLine();
// Kod, który korzysta z zasobu (reader)
} catch (IOException e) {
// Obsługa wyjątku IOException
e.printStackTrace();
} finally {
// Kod, który zostanie wykonany zawsze
}
}
}
Out of Memory Error
OutOfMemoryError to rodzaj wyjątku, który jest zgłaszany, gdy program osiąga limit dostępnej pamięci i nie jest w stanie zaalokować więcej miejsca. Ten błąd wskazuje, że program zużył całą dostępną pamięć i nie może już zaalokować dodatkowej przestrzeni, co prowadzi do awarii aplikacji.
Przyczyny OutOfMemoryError:
Wycieki Pamięci (Memory Leaks): Gdy obiekty nie są odpowiednio usuwane przez Garbage Collector, mogą pozostawać w pamięci, co z czasem prowadzi do wycieku pamięci.
Niewystarczająca Pamięć:
Brak dostępnej pamięci na stercie (Heap) dla nowych obiektów.
Brak dostępnej pamięci na stosie podczas rekurencyjnych wywołań funkcji.
Zbyt Duże Obiekty:
Próba utworzenia bardzo dużego obiektu, który przekracza dostępną pamięć.
Obsługa OutOfMemoryError:
Zidentyfikuj Przyczynę:
Spróbuj zidentyfikować, dlaczego doszło do wyczerpania pamięci. Czy to w wyniku wycieku pamięci, czy może próby utworzenia zbyt dużego obiektu?
Monitoruj Pamięć:
Używaj narzędzi do monitorowania zużycia pamięci, takich jak VisualVM, jconsole lub narzędzia dostarczone przez środowisko uruchomieniowe JVM.
Optymalizuj Kod:
Spróbuj zoptymalizować kod w celu zmniejszenia zużycia pamięci, zwłaszcza jeśli wyciek pamięci jest wynikiem błędu programistycznego.
Zwiększ Dostępną Pamięć:
W przypadku aplikacji Java można rozważyć zwiększenie dostępnej pamięci za pomocą parametrów uruchomieniowych JVM, takich jak -Xmx (maksymalna dostępna pamięć dla sterty).
java
Copy code
java -Xmx512m MyApp
Popraw Zarządzanie Pamięcią:
Zapewnij, że zarządzanie pamięcią w aplikacji jest poprawnie zaimplementowane, a obiekty są zwalniane, gdy nie są już potrzebne.
System.gc():
System.gc() to metoda w języku Java, która wywołuje Garbage Collector w celu próby zwolnienia nieużywanej pamięci i usunięcia niepotrzebnych obiektów. Jednakże, nie ma gwarancji, że Garbage Collector zostanie natychmiast uruchomiony po wywołaniu tej metody. Użycie System.gc() nie jest zalecane, ponieważ JVM (Java Virtual Machine) zazwyczaj samodzielnie zarządza procesem usuwania nieużywanych obiektów.
Blok try-catch-finally:
Konstrukcja try-catch-finally jest używana do obsługi wyjątków w języku Java. Struktura ta pozwala na wykonanie pewnych działań w bloku finally niezależnie od tego, czy wystąpił wyjątek, czy nie.
java
Copy code
try {
// Kod, który może wywołać wyjątek
} catch (Exception e) {
// Obsługa wyjątku
} finally {
// Kod, który zostanie wykonany niezależnie od tego, czy wystąpił wyjątek, czy nie
}
Blok finally jest opcjonalny, ale jeśli jest obecny, kod w nim zawarty zostanie wykonany zawsze, niezależnie od tego, czy w bloku try wystąpił wyjątek czy nie. Jest to przydatne, na przykład, do zwolnienia zasobów, które muszą być zwolnione, niezależnie od tego, czy doszło do wyjątku.
Exceptions
W Javie, zarówno błędy (errors), jak i wyjątki (exceptions) są rodzajami obiektów, które dziedziczą z klasy Throwable. Oba te typy mają pewne różnice w kontekście ich przeznaczenia i obsługi. Oto krótkie omówienie różnic między błędami a wyjątkami w Javie:
Błędy (Errors):
Charakterystyka:
Błędy w Javie reprezentują sytuacje, które są poza kontrolą programisty i zazwyczaj wskazują na poważne problemy w środowisku wykonawczym JVM.
Reprezentują błędy, które są trudne lub niemożliwe do naprawy podczas działania programu.
Przykłady Błędów:
OutOfMemoryError: Występuje, gdy brakuje pamięci.
StackOverflowError: Występuje, gdy stos wywołań jest przepełniony.
LinkageError: Występuje, gdy występuje problem z linkowaniem klas.
Obsługa Błędów:
Błędy zazwyczaj nie są obsługiwane w kodzie programu, ponieważ wskazują na poważne błędy systemowe.
Nie jest zalecane próbowanie obsługić lub naprawić błędy, ponieważ ich wystąpienie wskazuje na poważne problemy.
Wyjątki (Exceptions):
Charakterystyka:
Wyjątki w Javie reprezentują sytuacje, które można obsłużyć w kodzie programu.
Dziedziczą z klasy Exception i mogą wystąpić w wyniku błędów wykonawczych, które mogą być naprawione.
Przykłady Wyjątków:
NullPointerException: Występuje, gdy program próbuje odwołać się do obiektu, którego wartość jest null.
ArithmeticException: Występuje, gdy wykonujemy nieprawidłowe operacje matematyczne, np. dzielenie przez zero.
FileNotFoundException: Występuje, gdy próbujemy otworzyć plik, który nie istnieje.
Obsługa Wyjątków:
Wyjątki mogą być obsługiwane za pomocą bloków try-catch, gdzie kod w bloku catch wykonuje się, jeśli wyjątek zostanie zgłoszony.
Obsługa wyjątków umożliwia programowi reakcję na nieprzewidziane sytuacje i podjęcie działań naprawczych.
Piramida Wyjątków:
W programowaniu obiektowym, często mówi się o “piramidzie wyjątków”, co oznacza, że istnieje hierarchia klas wyjątków. Na szczycie piramidy znajduje się klasa bazowa Throwable, a potem są klasy Error i Exception. Klasy Exception dzielą się na checked (kontrolowane) i unchecked (niekontrolowane) wyjątki. Unchecked wyjątki dziedziczą z klasy RuntimeException.
plaintext
Copy code
Throwable
|– Error
|– Exception
|– RuntimeException (Unchecked)
|– Checked Exceptions
Podsumowując:
Błędy wskazują na poważne problemy w środowisku wykonawczym i zazwyczaj nie są obsługiwane.
Wyjątki reprezentują sytuacje, które można obsłużyć, i są podzielone na wyjątki kontrolowane i niekontrolowane.
Obsługa wyjątków odbywa się za pomocą bloków try-catch, a ich brak może prowadzić do przerwania działania programu.
Hierarchia wyjątków
Hierarchia wyjątków w języku Java jest zorganizowana w sposób hierarchiczny, a najważniejszą klasą bazową dla wszystkich wyjątków jest klasa Throwable. Poniżej przedstawiam uproszczoną hierarchię wyjątków w języku Java:
plaintext
Copy code
Throwable
|– Error
| |– AssertionError
| |– LinkageError
| |– VirtualMachineError
| |– OutOfMemoryError
| |– StackOverflowError
|– Exception
|– RuntimeException
| |– ArithmeticException
| |– ArrayIndexOutOfBoundsException
| |– ClassCastException
| |– ConcurrentModificationException
| |– IllegalArgumentException
| |– IllegalStateException
| |– NoSuchElementException
| |– NullPointerException
| |– IndexOutOfBoundsException
| |– ArrayIndexOutOfBoundsException
| |– StringIndexOutOfBoundsException
|– IOException
| |– FileNotFoundException
| |– IOException
| |– EOFException
| |– SocketException
|– SQLException
Throwable: Klasa bazowa dla wszystkich wyjątków i błędów w języku Java.
Error: Reprezentuje poważne błędy, z którymi programista zazwyczaj nie powinien próbować sobie radzić, np. OutOfMemoryError, StackOverflowError.
Exception: Jest to klasa bazowa dla większości wyjątków. Działa jako kategoria ogólna dla różnych wyjątków, które nie są błędami systemowymi.
RuntimeException: Reprezentuje wyjątki, które są niekontrolowane (unchecked). Programista nie jest zobowiązany do ich obsługi. Obejmuje wiele popularnych wyjątków, takich jak NullPointerException, ArithmeticException, itp.
IOException: Reprezentuje wyjątki związane z operacjami wejścia/wyjścia, np. FileNotFoundException, EOFException, itp.
SQLException: Reprezentuje wyjątki związane z bazą danych SQL.
Powyższa hierarchia to tylko fragment pełnej hierarchii wyjątków w języku Java. Programista ma również możliwość tworzenia własnych klas wyjątków, które dziedziczą z Exception lub RuntimeException, zależnie od tego, czy chce, aby były kontrolowane czy niekontrolowane. Hierarchia ta jest bardzo przydatna podczas obsługi wyjątków w kodzie, ponieważ pozwala na bardziej precyzyjne przechwytywanie i obsługę różnych rodzajów błędów i wyjątków.
checked unchecked
W języku Java wyjątki można podzielić na dwie główne kategorie: kontrolowane (checked) i niekontrolowane (unchecked). Podział ten wprowadza dwie różne gałęzie w hierarchii klas wyjątków: Exception dla wyjątków kontrolowanych i RuntimeException dla wyjątków niekontrolowanych. Oto krótkie omówienie obu rodzajów wyjątków:
- Wyjątki Kontrolowane (Checked Exceptions):
Charakterystyka:
Są to wyjątki, które muszą być obsługiwane przez programistę w kodzie programu.
Dziedziczą bezpośrednio z klasy Exception (lub z klas dziedziczących z Exception).
Przykłady Wyjątków Kontrolowanych:
IOException: np. FileNotFoundException, EOFException.
SQLException: związane z operacjami na bazie danych.
ClassNotFoundException: gdy klasa nie może zostać znaleziona podczas ładowania.
Obsługa:
Wyjątki kontrolowane muszą być obsługiwane za pomocą bloku try-catch lub deklarowane w sygnaturze metody za pomocą klauzuli throws.
java
Copy code
try {
// Kod, który może zgłosić wyjątek kontrolowany
} catch (IOException e) {
// Obsługa wyjątku
}
2. Wyjątki Niekontrolowane (Unchecked Exceptions):
Charakterystyka:
Są to wyjątki, które nie muszą być obsługiwane lub deklarowane przez programistę.
Dziedziczą z klasy RuntimeException (lub z klas dziedziczących z RuntimeException).
Przykłady Wyjątków Niekontrolowanych:
NullPointerException: próba odwołania się do obiektu, który ma wartość null.
ArithmeticException: błąd arytmetyczny, np. dzielenie przez zero.
ArrayIndexOutOfBoundsException: próba dostępu do tablicy z nieprawidłowym indeksem.
Obsługa:
Nie jest wymagane, aby wyjątki niekontrolowane były obsługiwane za pomocą bloku try-catch lub deklarowane w sygnaturze metody. Jednakże, mogą być obsługiwane, jeśli programista tego chce.
java
Copy code
try {
// Kod, który może zgłosić wyjątek niekontrolowany
} catch (NullPointerException e) {
// Obsługa wyjątku
}
Podsumowanie:
Wyjątki kontrolowane to te, które wymagają interwencji programisty i muszą być obsługiwane lub deklarowane.
Wyjątki niekontrolowane to te, które mogą wystąpić podczas działania programu, ale programista nie jest zobowiązany do ich obsługi.
W praktyce, wyjątki niekontrolowane są zazwyczaj błędami programistycznymi, takimi jak błędy logiki biznesowej, podczas gdy wyjątki kontrolowane są związane z sytuacjami, które mogą wystąpić w wyniku zewnętrznych czynników, takich jak operacje wejścia/wyjścia czy obsługa baz danych.
klasa abstrakcyjna a interfejs
Klasy abstrakcyjne i interfejsy są dwoma konceptami w języku Java, które umożliwiają tworzenie abstrakcji i definiowanie kontraktów dla klas implementujących. Oto krótka charakteryzacja obu tych koncepcji:
Klasa Abstrakcyjna:
Słowo Kluczowe:
Używa się słowa kluczowego abstract przed definicją klasy.
Konstruktory:
Może mieć konstruktory, które mogą być wywoływane przez podklasy.
Pola:
Może zawierać pola (zmienne instancji).
Metody:
Może zawierać zarówno metody abstrakcyjne (bez implementacji) jak i metody z implementacją.
Metody abstrakcyjne są oznaczane słowem kluczowym abstract.
Dziedziczenie:
Może rozszerzać jedną klasę (tylko jedno dziedziczenie).
Klasy Potomne:
Klasy potomne (dziedziczące) muszą dostarczyć implementację dla wszystkich metod abstrakcyjnych.
java
Copy code
abstract class Animal {
private String name;
public Animal(String name) { this.name = name; } abstract void makeSound(); void eat() { System.out.println(name + " is eating."); } } Interfejs: Słowo Kluczowe:
Używa się słowa kluczowego interface przed definicją interfejsu.
Konstruktory:
Nie może zawierać konstruktorów. Interfejsy definiują jedynie kontrakty, a nie stan.
Pola:
Może zawierać tylko stałe (zmienne oznaczone jako final i static).
Metody:
Metody w interfejsie są domyślnie abstrakcyjne, nie posiadają implementacji.
Od Javy 8, interfejsy mogą również zawierać metody z domyślną implementacją oraz statyczne metody.
Dziedziczenie:
Może dziedziczyć wiele interfejsów (wielokrotne dziedziczenie).
Klasy Implementujące:
Klasy implementujące interfejs muszą dostarczyć implementację wszystkich metod zadeklarowanych w interfejsie.
java
Copy code
interface Vehicle {
void start();
void stop(); } Kiedy Wybrać Klasę Abstrakcyjną lub Interfejs: Klasa Abstrakcyjna:
Jeśli chcesz dostarczyć pewną wspólną implementację dla klas dziedziczących.
Jeśli planujesz dodawać więcej funkcji w przyszłości do klasy bazowej.
Gdy potrzebujesz konstruktorów w klasie bazowej.
Interfejs:
Jeśli klasa potrzebuje dziedziczyć z kilku źródeł (wielokrotne dziedziczenie).
Jeśli chcesz stworzyć kontrakt bez dostarczania implementacji (szczególnie w przypadku interfejsów w programowaniu zorientowanym obiektowo).
Gdy chcesz zdefiniować metody z domyślną implementacją bez konieczności dostarczania implementacji w klasach implementujących (od Javy 8).
Array list a linked list
ArrayList i LinkedList to dwie popularne implementacje interfejsu List w języku Java, które różnią się strukturą danych i zasadami działania. Oto krótka charakteryzacja obu struktur:
ArrayList:
Implementacja:
Bazuje na tablicy dynamicznej.
Dostęp do Elementów:
Dostęp do elementów za pomocą indeksów.
Operacje odczytu są efektywne, ponieważ tablica umożliwia bezpośredni dostęp do elementów.
Dodawanie Elementów:
Dodawanie na końcu listy jest szybkie i efektywne.
Dodawanie lub usuwanie elementów w środku listy może być kosztowne, ponieważ wymaga przesunięcia elementów w tablicy.
Pamięć:
Zużywa mniej pamięci, ponieważ nie wymaga dodatkowej przestrzeni na wskaźniki.
Iteracja:
Iteracja jest szybka za pomocą pętli for i indeksów.
java
Copy code
List<String> arrayList = new ArrayList<>();
arrayList.add("One");
arrayList.add("Two");
arrayList.add("Three");
LinkedList:
Implementacja:</String>
Bazuje na liście dwukierunkowej.
Dostęp do Elementów:
Dostęp do elementów odbywa się poprzez przeglądanie listy od początku do końca lub od końca do początku.
Operacje odczytu mogą być wolniejsze w porównaniu do ArrayList.
Dodawanie Elementów:
Dodawanie i usuwanie elementów w środku listy jest szybkie, ponieważ nie wymaga przesunięcia elementów.
Dodawanie na końcu listy może być wolniejsze niż w przypadku ArrayList.
Pamięć:
Zużywa więcej pamięci, ponieważ każdy element wymaga dodatkowego miejsca na przechowywanie wskaźników do poprzedniego i następnego elementu.
Iteracja:
Iteracja odbywa się za pomocą iteratora i operacji przesuwania wskaźników.
java
Copy code
List<String> linkedList = new LinkedList<>();
linkedList.add("One");
linkedList.add("Two");
linkedList.add("Three");
Kiedy Wybrać ArrayList a kiedy LinkedList:
ArrayList:</String>
Dostęp do elementów poprzez indeksy.
W przypadku częstych operacji odczytu.
Gdy lista jest rzadko modyfikowana (dodawanie/usuwanie elementów).
LinkedList:
Dla częstych operacji dodawania i usuwania elementów, zwłaszcza w środku listy.
Gdy wymagane jest wielokrotne przeszukiwanie listy od początku do końca lub od końca do początku.
W przypadku operacji wymagających przesunięcia elementów, LinkedList jest bardziej efektywny.
Listy, sety, mapy
Listy, sety i mapy to trzy różne rodzaje kolekcji w języku Java, dostępne w ramach frameworku kolekcji (java.util), które oferują różne funkcje do przechowywania i manipulowania danymi. Oto krótkie omówienie każdej z tych kolekcji:
- Listy:
Reprezentacja:
List to kolekcja, która przechowuje elementy w określonej kolejności.
Każdy element jest przypisany do indeksu, co umożliwia dostęp do elementów za pomocą indeksów.
Implementacje:
ArrayList: Implementacja oparta na tablicy, co oznacza, że dostęp do elementów jest szybki, ale operacje dodawania/usuwania w środku listy mogą być kosztowne.
LinkedList: Implementacja oparta na liście dwukierunkowej, co umożliwia szybkie dodawanie/usuwanie elementów, ale dostęp do elementów jest wolniejszy niż w przypadku ArrayList.
Przykład:
java
Copy code
List<String> arrayList = new ArrayList<>();
arrayList.add("Element 1");
arrayList.add("Element 2");</String>
List<String> linkedList = new LinkedList<>();
linkedList.add("Element 1");
linkedList.add("Element 2");
2. Sety:
Reprezentacja:</String>
Set to kolekcja, która przechowuje unikalne elementy, eliminując duplikaty.
Implementacje:
HashSet: Implementacja, która nie gwarantuje zachowania kolejności elementów.
LinkedHashSet: Implementacja, która utrzymuje kolejność elementów zgodnie z kolejnością ich dodania.
TreeSet: Implementacja, która przechowuje elementy w posortowanej kolejności.
Przykład:
java
Copy code
Set<String> hashSet = new HashSet<>();
hashSet.add("Element 1");
hashSet.add("Element 2");</String>
Set<String> treeSet = new TreeSet<>();
treeSet.add("Element 1");
treeSet.add("Element 2");
3. Mapy:
Reprezentacja:</String>
Map to kolekcja, która przechowuje pary klucz-wartość.
Każdy klucz w mapie jest unikalny, a wartości są przypisane do odpowiadających im kluczy.
Implementacje:
HashMap: Implementacja, która nie gwarantuje zachowania kolejności par klucz-wartość.
LinkedHashMap: Implementacja, która utrzymuje kolejność par klucz-wartość zgodnie z kolejnością ich dodania.
TreeMap: Implementacja, która przechowuje pary klucz-wartość w posortowanej kolejności względem kluczy.
Przykład:
java
Copy code
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put(“Klucz 1”, 1);
hashMap.put(“Klucz 2”, 2);
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put(“Klucz 1”, 1);
treeMap.put(“Klucz 2”, 2);
Podsumowując, wybór między ArrayList a LinkedList, HashSet, LinkedHashSet, TreeSet oraz różnymi implementacjami map (HashMap, LinkedHashMap, TreeMap) zależy od konkretnych wymagań dotyczących dostępu, dodawania, usuwania elementów i zachowania kolejności w danej sytuacji.
D Dependency Inversion solid
Dependency Inversion to jeden z pięciu zasad S.O.L.I.D., który został zaproponowany przez Roberta C. Martina. Ta zasada dotyczy organizacji relacji między komponentami w systemie i jest kluczowym elementem projektowania obiektowego. Dependency Inversion jest skoncentrowany na tym, aby zależności między klasami były odwrócone w stosunku do tradycyjnego podejścia. Oto główne założenia zasady Dependency Inversion:
Główne Założenia:
Wysokopoziomowe moduły nie powinny zależeć od niskopoziomowych modułów. Obydwa powinny zależeć od abstrakcji.
W tradycyjnym podejściu często widzimy, że klasy wysokopoziomowe zależą od klas niskopoziomowych. Dependency Inversion sugeruje, aby oba rodzaje modułów zależały od abstrakcji.
Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.
Oznacza to, że interfejsy i klasy abstrakcyjne, które definiują abstrakcje, nie powinny zależeć od implementacji. Implementacje powinny zależeć od abstrakcji.
Przykład:
Załóżmy, że mamy system do zarządzania pojazdami, gdzie mamy Car (samochód) jako niskopoziomowy moduł i Driver (kierowca) jako wysokopoziomowy moduł. W tradycyjnym podejściu Driver zależałby bezpośrednio od Car:
java
Copy code
class Car {
void start() {
// implementacja
}
}
class Driver {
Car car;
Driver() { this.car = new Car(); } void drive() { car.start(); // kierowca jedzie } } W podejściu Dependency Inversion użylibyśmy abstrakcji, aby odwrócić te zależności. Moglibyśmy stworzyć interfejs Vehicle jako abstrakcję dla pojazdów:
java
Copy code
interface Vehicle {
void start();
}
class Car implements Vehicle {
@Override
public void start() {
// implementacja startu samochodu
}
}
class Driver {
Vehicle vehicle;
Driver(Vehicle vehicle) { this.vehicle = vehicle; } void drive() { vehicle.start(); // kierowca jedzie } } Teraz Driver zależy od abstrakcji Vehicle, a nie bezpośrednio od konkretnej implementacji Car. Możemy łatwo rozszerzyć system o nowe rodzaje pojazdów, które implementują interfejs Vehicle, nie zmieniając kodu w klasie Driver. To umożliwia elastyczność i łatwość rozbudowy systemu.
I interface segregation
Interface Segregation Principle (ISP) to jedna z pięciu zasad S.O.L.I.D., wprowadzonych przez Roberta C. Martina, mających na celu poprawę projektowania obiektowego. ISP koncentruje się na tym, aby interfejsy były zorganizowane w taki sposób, aby klienci korzystający z interfejsów nie byli zmuszeni implementować metod, których nie potrzebują. Zasada ta przeciwdziała tworzeniu “grubych” interfejsów, które wymagają implementacji wielu metod, nawet jeśli nie są one używane przez wszystkich klientów. Oto główne założenia Interface Segregation Principle:
Główne Założenia:
Duże interfejsy są złe:
Interfejsy nie powinny być “grube” i zawierać zbyt wielu metod. Jeśli klient nie potrzebuje wszystkich metod z interfejsu, nie powinien być zmuszany do ich implementacji.
Rozbijanie na mniejsze interfejsy:
Lepiej jest tworzyć kilka mniejszych interfejsów, z których każdy zawiera zestaw metod skorelowanych tematycznie. Klient może implementować tylko te interfejsy, które są dla niego istotne.
Unikanie redundancji:
Unikaj sytuacji, w których jedna klasa musi implementować wiele metod, z których wiele nie jest używanych w danym kontekście.
Przykład:
Załóżmy, że mamy interfejs Worker, który zawiera metody związane zarówno z pracą biurową, jak i pracą na budowie:
java
Copy code
interface Worker {
void doOfficeWork();
void doConstructionWork();
}
Problem pojawia się, gdy mamy dwie różne klasy klientów: OfficeWorker i ConstructionWorker. Każda z tych klas implementuje interfejs, ale często korzystają tylko z jednej grupy metod. To prowadzi do niepotrzebnej implementacji i redundancji.
Lepsze podejście to rozbicie interfejsu na dwa mniejsze:
java
Copy code
interface OfficeWorker {
void doOfficeWork();
}
interface ConstructionWorker {
void doConstructionWork();
}
Teraz klasy klientów implementują tylko te interfejsy, które są dla nich istotne, eliminując tym samym niepotrzebną redundancję i skomplikowanie implementacji.
Korzyści Interface Segregation Principle:
Elastyczność: Pozwala na elastyczność w implementacji, ponieważ klienci implementują tylko te metody, których potrzebują.
Unikanie redundancji: Eliminuje konieczność implementacji niepotrzebnych metod.
Łatwiejsze utrzymanie: Skomplikowane interfejsy są trudniejsze do utrzymania, a rozbicie ich na mniejsze części ułatwia zarządzanie kodem.
Interface Segregation Principle pomaga projektantom i programistom tworzyć bardziej modularne, zrozumiałe i elastyczne systemy.
L liskov substitution priniciple solid
Zasada podstawiania Liskov (ang. Liskov Substitution Principle - LSP) to jedna z pięciu zasad S.O.L.I.D., które zostały zaproponowane przez Barbarę Liskov. Zasada ta odnosi się do dziedziczenia klas w programowaniu obiektowym i zakłada, że obiekty powinny być w stanie być zastąpione (podstawione) obiektami swoich klas pochodnych bez zmiany poprawności programu. Sformułowanie zasady LSP brzmi:
“Jeśli S jest podtypem T, to obiekt typu T może być zastąpiony obiektem typu S bez zmiany poprawności programu.”
W praktyce oznacza to, że każdy obiekt klasy potomnej powinien być w stanie zastąpić obiekt klasy bazowej, nie powodując żadnych nieoczekiwanych błędów ani zmian w działaniu programu. W skrócie, jeśli klasa B dziedziczy po klasie A, to obiekt typu A może być zastąpiony obiektem typu B.
Przykład:
java
Copy code
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int calculateArea() { return width * height; } }
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override public void setHeight(int height) { this.width = height; this.height = height; } } W powyższym przykładzie Square dziedziczy po Rectangle. Mimo że z punktu widzenia dziedziczenia jest to poprawne, z punktu widzenia zasady LSP może prowadzić do problemów. Na przykład:
java
Copy code
void processRectangle(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(10);
int area = rectangle.calculateArea();
System.out.println(“Area: “ + area);
}
// Użycie zasady LSP
Rectangle rectangle = new Rectangle();
processRectangle(rectangle); // Poprawne
Square square = new Square();
processRectangle(square); // Niespodziewany wynik, naruszona zasada LSP
W przypadku Square modyfikacja jednego z wymiarów wpływa na drugi, co prowadzi do niespodziewanych wyników. Właśnie to narusza zasadę Liskov Substitution Principle, ponieważ obiekt Square nie zachowuje się tak, jak oczekuje się od ogólnego obiektu Rectangle. Optymalne rozwiązanie tego problemu wymagałoby przemyślenia projektu klas, aby poprawnie zastosować zasadę LSP.
O open-closed priniciple
Zasada otwarte/zamknięte (Open/Closed Principle, OCP) to jedna z zasad projektowania obiektowego, wprowadzona przez Bertranda Meyera, która jest często skrócona do zdania: “Otwarte na rozszerzenia, zamknięte na modyfikacje”. Zasada ta jest jedną z pięciu zasad SOLID i skupia się na tym, aby klasa była dostosowana do rozszerzania, ale jednocześnie zamknięta na zmiany w swoim istniejącym kodzie.
Główne założenia zasady otwarte/zamknięte:
Otwarte na Rozszerzenia (Open for Extension):
Klasa powinna być zaprojektowana w taki sposób, aby można było ją rozszerzać bez modyfikacji jej istniejącego kodu.
Zamknięte na Modyfikacje (Closed for Modification):
Istniejący kod klasy powinien być zamknięty na zmiany, aby unikać wprowadzania niepotrzebnych modyfikacji, które mogą wpłynąć na stabilność i funkcjonalność istniejącego systemu.
Przykładowy Scenariusz:
Załóżmy, że mamy klasę Shape z metodą area(), która oblicza pole powierzchni dla różnych kształtów. Zastosowanie zasady OCP oznacza, że powinniśmy być w stanie dodawać nowe kształty, nie modyfikując kodu istniejącej klasy Shape.
java
Copy code
// Przykład przed zastosowaniem OCP
class Shape {
String type;
double area() { if (type.equals("Circle")) { // obliczenia pola powierzchni dla okręgu } else if (type.equals("Rectangle")) { // obliczenia pola powierzchni dla prostokąta } // inne przypadki } } java Copy code // Zastosowanie OCP interface Shape { double area(); }
class Circle implements Shape {
double radius;
@Override public double area() { // obliczenia pola powierzchni dla okręgu } }
class Rectangle implements Shape {
double width;
double height;
@Override public double area() { // obliczenia pola powierzchni dla prostokąta } } W powyższym przykładzie zastosowaliśmy interfejs Shape, który jest otwarty na rozszerzenia poprzez implementowanie go w różnych klasach reprezentujących różne kształty, takie jak Circle i Rectangle. Dzięki temu możemy dodawać nowe kształty, jednocześnie nie modyfikując kodu istniejącej klasy Shape. To jest zgodne z zasadą otwarte/zamknięte.
S single responsibility principle
Zasada pojedynczej odpowiedzialności (Single Responsibility Principle, SRP) to jedna z zasad SOLID, której głównym celem jest zachęcanie do projektowania klas o jednej, klarownej odpowiedzialności. SRP mówi, że klasa powinna mieć tylko jeden powód do zmiany, co oznacza, że powinna mieć tylko jedno zadanie lub odpowiedzialność.
Główne założenia zasady pojedynczej odpowiedzialności:
Jedna Odpowiedzialność:
Klasa powinna mieć tylko jedną odpowiedzialność, tj. jedno konkretne zadanie do wykonania.
Unikanie Zmiany z Kilku Powodów:
Kiedy klasa ma tylko jedną odpowiedzialność, zmiany w systemie, które wpływają na tę odpowiedzialność, nie powinny wymagać modyfikacji klasy z innych powodów.
Wysoki Poziom Koherenji:
Klasa powinna być kohernetna, czyli wszystkie jej metody i pola powinny być ze sobą powiązane w ramach jednej konkretnej odpowiedzialności.
Przykład:
Załóżmy, że mamy klasę Report, która ma generować raport i jednocześnie zapisywać go do pliku. Narusza to zasadę pojedynczej odpowiedzialności, ponieważ klasa ma dwa powody do zmiany: zmiana formatu raportu i zmiana sposobu zapisu do pliku.
java
Copy code
// Naruszające SRP
class Report {
void generateReport() {
// generowanie raportu
}
void saveToFile() { // zapisywanie raportu do pliku } } W zgodzie z SRP możemy rozdzielić te dwie odpowiedzialności do dwóch różnych klas:
java
Copy code
// Zgodne z SRP
class ReportGenerator {
void generateReport() {
// generowanie raportu
}
}
class FileSaver {
void saveToFile() {
// zapisywanie raportu do pliku
}
}
W ten sposób, jeśli zmieni się sposób generowania raportu, nie będzie to miało wpływu na klasę FileSaver, a zmiana sposobu zapisu do pliku nie wpłynie na klasę ReportGenerator. To zwiększa elastyczność i utrzymywalność kodu. Zasada pojedynczej odpowiedzialności pomaga unikać sytuacji, w których klasa staje się “zbyt rozbuchana” i trudna do zrozumienia, ponieważ próbuje wykonywać zbyt wiele różnych zadań.
Singleton
Wzorzec Singleton jest jednym z wzorców projektowych, które wprowadzają jedyny punkt instancji dla danej klasy, zapewniając, że klasa ta ma tylko jedną instancję i dostarcza globalny punkt dostępu do niej. Wzorzec Singleton jest szczególnie przydatny, gdy chcemy upewnić się, że tylko jedna instancja klasy istnieje w systemie i dostęp do niej jest możliwy z dowolnego miejsca.
Podstawowe cechy wzorca Singleton:
Konstruktor prywatny:
Singleton posiada prywatny konstruktor, co oznacza, że nie jest możliwe utworzenie instancji tej klasy z zewnątrz.
Statyczna metoda dostępu:
Singleton dostarcza statyczną metodę, która zwraca jedyną instancję obiektu (najczęściej nazywaną getInstance).
Pola prywatne:
Często instancja singletona jest przechowywana w prywatnym statycznym polu w klasie.
Leniwa inicjalizacja (Lazy Initialization):
Instancja singletona jest inicjalizowana dopiero w momencie pierwszego wywołania metody getInstance, co nazywane jest leniwą inicjalizacją.
Poniżej przedstawiam prosty przykład implementacji wzorca Singleton w języku Java:
java
Copy code
public class Singleton {
// Prywatne statyczne pole przechowujące jedyną instancję klasy
private static Singleton instance;
// Prywatny konstruktor, aby uniemożliwić tworzenie instancji z zewnątrz private Singleton() { // Inicjalizacja instancji (może zawierać inne operacje) } // Statyczna metoda dostępu do instancji Singletona public static Singleton getInstance() { if (instance == null) { // Leniwa inicjalizacja, jeśli instancja jeszcze nie istnieje instance = new Singleton(); } return instance; } } Warto zauważyć, że ten przykład używa leniwej inicjalizacji, co oznacza, że instancja zostanie utworzona dopiero wtedy, gdy zostanie wywołana metoda getInstance. Istnieje także inne podejście, tzw. inicjalizacja eager, w której instancja jest tworzona od razu podczas ładowania klasy. Wybór pomiędzy leniwą a eager inicjalizacją zależy od wymagań danego przypadku użycia.
Wzorzec Factory
Wzorzec Factory (Fabryka) to wzorzec projektowy z kategorii creational, który zajmuje się procesem tworzenia obiektów. Celem tego wzorca jest dostarczenie interfejsu do tworzenia obiektów w klasie bazowej, jednocześnie pozostawiając podklasy odpowiedzialne za wybór typu tworzonego obiektu.
Główne elementy wzorca Factory to:
Produkt (Product):
Interfejs lub klasa abstrakcyjna definiująca obiekt, który ma być utworzony przez konkretne fabryki.
Konkretne Produkty (Concrete Products):
Klasy implementujące interfejs lub dziedziczące po klasie abstrakcyjnej produktu, reprezentujące rzeczywiste obiekty, które fabryka będzie tworzyć.
Fabryka (Factory):
Interfejs lub klasa abstrakcyjna definiująca metodę (lub zestaw metod), które są odpowiedzialne za tworzenie obiektów (produktów).
Może istnieć wiele różnych fabryk, każda tworząca różne rodzaje produktów.
Konkretne Fabryki (Concrete Factories):
Klasy implementujące interfejs lub dziedziczące po klasie abstrakcyjnej fabryki, dostarczające konkretną implementację tworzenia produktów.
Przykład:
Załóżmy, że mamy hierarchię produktów, takich jak Button (przycisk) i Checkbox (pole wyboru), a także chcemy stworzyć różne rodzaje fabryk, takie jak WindowsFactory i MacOSFactory, które będą tworzyć konkretne produkty dla systemów Windows i macOS.
java
Copy code
// Interfejs Produktu
interface Button {
void render();
}
// Konkretne Produkty
class WindowsButton implements Button {
@Override
public void render() {
System.out.println(“Render Windows Button”);
}
}
class MacOSButton implements Button {
@Override
public void render() {
System.out.println(“Render MacOS Button”);
}
}
// Interfejs Fabryki
interface GUIFactory {
Button createButton();
}
// Konkretne Fabryki
class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
}
class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
}
Teraz możemy użyć fabryk do tworzenia produktów w zależności od wymagań systemowych:
java
Copy code
public class Client {
public static void main(String[] args) {
// Wybór fabryki w zależności od systemu
GUIFactory factory;
if (isWindows()) {
factory = new WindowsFactory();
} else {
factory = new MacOSFactory();
}
// Użycie fabryki do utworzenia produktu Button button = factory.createButton(); button.render(); } private static boolean isWindows() { // Sprawdzenie warunku dla systemu Windows return true; } } W ten sposób wzorzec Factory pozwala na elastyczne tworzenie obiektów różnych rodzajów, przy jednoczesnym uniezależnieniu kodu klienta od konkretnej implementacji produktów.
Wzorzec builder
Wzorzec Builder to wzorzec kreacyjny, który pozwala na konstruowanie złożonych obiektów krok po kroku. Głównym celem wzorca Builder jest dostarczenie elastycznego interfejsu do tworzenia różnych konfiguracji złożonych obiektów, pozwalając klientom na konstruowanie obiektów z różnymi właściwościami bez konieczności bezpośredniego operowania na ich wewnętrznych detalach.
Główne składniki wzorca Builder to:
Produkt (Product):
Reprezentuje złożony obiekt, który ma być zbudowany. Wzorzec Builder jest używany do konstrukcji różnych wariantów tego produktu.
Interfejs Builder:
Zawiera deklaracje metod do konstruowania poszczególnych części produktu.
Konkretny Builder:
Implementuje interfejs Builder i dostarcza konkretne implementacje metod do budowy poszczególnych części produktu.
Dyrektor (Director):
Zarządza procesem konstruowania obiektu za pomocą interfejsu Builder. Może definiować różne kroki procesu konstrukcji i korzystać z różnych konkretnych builderów.
Klient (Client):
Korzysta z dyrektora i/lub bezpośrednio z builderów do konstrukcji różnych wariantów produktu.
Poniżej przedstawiam prosty przykład implementacji wzorca Builder w języku Java. Rozważmy budowę obiektu Computer z różnymi właściwościami, takimi jak procesor, pamięć RAM, dysk twardy itp.
java
Copy code
// Produkt
class Computer {
private String processor;
private int ram;
private int storage;
public Computer(String processor, int ram, int storage) { this.processor = processor; this.ram = ram; this.storage = storage; } // Gettery // ... }
// Interfejs Builder
interface ComputerBuilder {
void buildProcessor(String processor);
void buildRAM(int ram);
void buildStorage(int storage);
Computer getResult();
}
// Konkretny Builder
class DesktopBuilder implements ComputerBuilder {
private Computer computer;
public DesktopBuilder() { this.computer = new Computer("Standard Processor", 4, 500); } @Override public void buildProcessor(String processor) { computer = new Computer(processor, computer.ram, computer.storage); } @Override public void buildRAM(int ram) { computer = new Computer(computer.processor, ram, computer.storage); } @Override public void buildStorage(int storage) { computer = new Computer(computer.processor, computer.ram, storage); } @Override public Computer getResult() { return computer; } }
// Dyrektor
class ComputerDirector {
private ComputerBuilder builder;
public ComputerDirector(ComputerBuilder builder) { this.builder = builder; } public void construct() { builder.buildProcessor("High-Performance Processor"); builder.buildRAM(16); builder.buildStorage(1000); } }
// Klient
public class Client {
public static void main(String[] args) {
ComputerBuilder desktopBuilder = new DesktopBuilder();
ComputerDirector director = new ComputerDirector(desktopBuilder);
director.construct(); Computer desktop = desktopBuilder.getResult(); System.out.println("Desktop Configuration:"); System.out.println("Processor: " + desktop.getProcessor()); System.out.println("RAM: " + desktop.getRAM() + "GB"); System.out.println("Storage: " + desktop.getStorage() + "GB"); } } W tym przykładzie ComputerDirector zarządza procesem konstruowania obiektu Computer za pomocą interfejsu ComputerBuilder. DesktopBuilder dostarcza konkretne implementacje metod do budowy poszczególnych części Computer. Klient (Client) używa dyrektora i/lub bezpośrednio builderów do konstrukcji różnych wariantów Computer.