Język Java często jest krytykowany za konieczność pisania kodu, który mógłby być automatycznie wygenerowany przez JDK. Ciągłe pisanie getterów i setterów do każdej klasy POJO, tworzenie toStringów, czy projektowanie builderów, choć początkującemu programiście daje wiele satysfakcji, to po pewnym czasie staje się bardzo nudne. W takiej sytuacji na pomoc przychodzi biblioteka Lombok, której celem jest uprościć kod Java.
Instalacja
Aby wykorzystać bibliotekę w kodzie korzystającym z Mavena, należy dodać zależność poprzez wpis:
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <scope>provided</scope> </dependency> </dependencies>
Jeśli korzystasz z innego menadżera projektu (np. Gradle’a) to zajrzyj na oficjalną stronę projektu Lombok. Dodatkowo, jeśli korzystasz z IDE (a zakładam, że tak ;)), potrzebna Ci będzie poprawnie zainstalowana wtyczka do biblioteki. W przypadku IntelliJ nie powinno być problemu, bowiem twórcy oprogramowania zaprojektowali całkiem dobrze działający plugin. Niestety trochę gorzej jest, jeśli korzystasz z drugiego popularnego IDE Eclipse. Tutaj należy ją samodzielnie pobrać i skonfigurować*.
Generowanie składowych klasy
Lombok pozwala na wygenerowanie wiele powtarzalnych składowych klasy, stosując odpowiednie adnotacje. Największą zaletą tego rozwiązania jest możliwość zastosowania różnej ich kombinacji. Dlatego, jeśli chcesz napisać klasę z samymi getterami i jednym konstruktorem, to bez problemu możesz to zrobić. Dodatkowo Twój kod będzie dużo czytelniejszy i krótszy.
Spójrz na poniższy przykład:
public class RealEstate { private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(RealEstate.class.getName()); private Long id; private String code; private String address; public RealEstate() { } public RealEstate(Long id, String code, String address) { this.id = id; this.code = code; this.address = address; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public void writeFile() { Path filePath = Paths.get("real_estate.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) { bufferedWriter.write(toString() + "\n"); } catch (IOException e) { LOGGER.warning("Can't write result to file with content:" + e.getMessage()); } } public void showMessage() { Logger.getGlobal().info("Real estate class"); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RealEstate that = (RealEstate) o; return Objects.equals(id, that.id) && Objects.equals(code, that.code); } @Override public int hashCode() { return Objects.hash(id, code); } @Override public String toString() { return "RealEstate{" + "id=" + id + ", address='" + address + '\'' + '}'; } }
Powyższa klasa jest bardzo długa. Zawiera pola, konstruktory, metody equals, hashcode, toString, użyty logger oraz metodę obsługującą wyjątek.
Akcesory
Jedną z najczęściej powtarzających się składowych klasy są gettery i settery dla pól. Często te proste metody zajmują połowę lub więcej miejsca, a z reguły nie posiadają żadnych wewnętrznych reguł poza przypisaniem wartości lub jej zwracaniem. Adnotacje Getter i Setter nakładane nad klasą wygenerują wszelkie gettery i settery do pól (oczywiście można zastosować także tylko jedną z nich)
@Getter i @Setter
Po zastosowaniu adnotacji:
@Getter @Setter public class RealEstateLombok { private Long id; private String code; private String address; }
@Accessors
Ta adnotacja jest rozwinięciem @Getter i @Setter. Posiada kilka ciekawych flag. Omówię tu dwie najczęściej używane. Pierwsza o nazwie fluent umożliwia, używanie akcesorów bez prefiksu get i set. Kolejna chain, pozwala stosować akcesory w jednej linii, podobnie jak w wzorcu projektowym „budowniczy”.
Klasa RealEstate zaktualizowana o adnotację Accessors:
@Getter @Setter @Accessors(fluent = true, chain = true) public class RealEstateLombok { private Long id; private String code; private String address; }
Przy przykład użycia flagi fluent ustawionej na true:
realEstateLombok.address("ul. Nowowiejska 54/3 Katowice");
Flaga chain ustawiona na true:
realEstateLombok.address("ul. Nowowiejska 54/3 Katowice").code("123");
Konstruktory
Kolejnym często schematycznym blokiem kodu w klasie są konstruktory. Bardzo rzadko zachodzi tu coś więcej, niż zwykłe przypisanie danych do pól. Lombok pozwala generować zarówno konstruktor zawierający wszystkie pola, jak i konstruktor bezargumentowy. Jeśli dziwisz się, do czego może służyć ten drugi, to przypominam Ci, że w przypadku napisania jakiegokolwiek konstruktor Java nie będzie już niejawnie tworzyć konstruktora bezargumentowego. Niestety czasami posiadanie takiego konstruktora jest wymagane przez niektóre frameworki.**
Zastosowanie: @NoArgsConstructor, @AllArgsConstructor
@Getter @Setter @Accessors(fluent = true, chain = true) @AllArgsConstructor @NoArgsConstructor public class RealEstateLombok { private Long id; private String code; private String address; }
Teraz możesz stworzyć zarówno instancję klasy, wypełniając wszystkie pola:
RealEstateLombok realEstateLombok = new RealEstateLombok(1L, "12345", "ul. Nowowiejska 54/3 Katowice");
jak i „pusty” obiekt:
RealEstateLombok emptyRealEstate = new RealEstateLombok();
Equals, hashCode oraz toString
Jak wiesz z poprzednich lekcji, aby poprawnie korzystać ze struktur opierających się o algorytmy haszujące, potrzebne są właściwie zaimplementowane metody hashCode i equals. Często ich wewnętrzna struktura jest podobna do siebie, więc dlaczego ich również nie generować? Za pomocą frameworku Lombok można to wykonać używając @EqualsAndHashCode.
Używając tej adnotacji należy uważać na pola, będące typami referencyjnymi. Powodem jest po pierwsze częste niepotrzebne spowolnienie działanie aplikacji, po drugie w przypadku cyklu (referencja do tej samej referencji) framework może zwariować. Dlatego polecam wypełnić adnotacje stosujące opcje exclude lub of. Do pierwszej z nich, wpisuje się pola, które mają być pominięte podczas generacji. Analogicznie flaga of, będzie korzystać tylko z tych pól, które zostaną w niej zawarte.
Podobnie jak @EqualsAndHashCode można używać @ToString.
Klasa po dodaniu powyższych dwóch adnotacji:
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString(of={"id", "address"}) @EqualsAndHashCode(exclude={"address"}) public class RealEstateLombok { private Long id; private String code; private String address; }
Efekt skorzystania z toString z wykorzystaniem flagi of:
RealEstateLombok(id=1, address=ul. Nowowiejska 54/3 Katowice)
Testowanie kolizji za pomocą kolekcji HashSet:
Set<RealEstateLombok> realEstates = new HashSet<>(); realEstates.add(new RealEstateLombok(1L, "12345", "ul. Nowowiejska 17 Katowice")); realEstates.add(new RealEstateLombok(1L, "12345", "ul. Nowowiejska 17 Katowice")); System.out.println(realEstates);
Prawidłowy wynik (brak kolizji) na konsoli
[RealEstateLombok(id=1, address=ul. Nowowiejska 17 Katowice)]
Generacja gotowych klas
Klasa typu builder
@Builder tworzy z Twojego POJO klasę typu budowniczy. Pozwala ona na szybkie tworzenie rozbudowanych obiektów i jednoczesne unikanie pisania wielu konstruktorów lub setterów. Niestety sama implementacja wzorca budowniczy zajmuje trochę czasu. Na szczęście w projekcie Lombok wystarczy jedna adnotacja, która robi robotę.
@Builder public class CarBuilder { private String brand; private Long age; private String colour; }
Przykład użycia:
CarBuilder.builder() .brand("Opel") .age(5) .colour("red") .build();
Klasa typu data
@Data to kombinacja wielu znanych już adnotacji: @Getter, @Setter, @ToString, @EqualsAndHashCode oraz @RequiredArgsConstructor.
@Data public class CarDataObject { private String brand; private Long age; private String colour; }
Powyższe adnotacje były już omawiane, więc nie będę wrzucał dodatkowych przypadków użycia.
Klasa typu immutable
Analogicznie do @Data, @Value to adnotacja tworząca obiekt immutable za pomocą kombinacji adnotacji, takich jak: @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter. Dodatkowo klasa będzie klasą finalną. Ciekawą adnotacją jest też FieldDefaults, która pozwala zaoszczędzić czasu przy pisaniu modyfikatorów dostępu (flaga level), a także podczas ustawianiu pól jako finalne (flaga makeFinal).
@Value public class CarValueObject { private String brand; private Long age; private String colour; }
Użycie:
CarValueObject carVO = new CarValueObject("Opel", 5, "red");
Jako klasa niemodyfikowalna CarValueObject musi inicjować wszystkie pola w konstruktorze (są typu final).
Inne ciekawe adnotacje
Walka z nullpointerami
Częstym problemem w programowaniu obiektowym jest brak kontroli nad występującymi wyjątkami typu nullpointer. Pewnym rozwiązaniem tego problemu może być stosowanie adnotacji NonNull (podobne adnotacje występują nie tylko w bibliotece Lombok). Bardzo dobrze ta adnotacja sprawdza się razem z adnotacją RequiredArgsConstructor, która tworzy konstruktor na podstawie pól, które nie mogą być puste (nullem).
Wykorzystanie @NonNull oraz @RequiredArgsConstructor
@RequiredArgsConstructor public class NonNullCar { @NonNull private String brand; private Integer age; @NonNull private String colour; }
Wyjątki sprawdzane (checked)
Biblioteka Lombok zawiera też „featury”, które są uważane za eksperymentalne (używaj na własne ryzyko). Jedną z takich adnotacji jest @SneakyThrows, która pozwala obejść regułę wymagalność obsługiwania wyjątków typu „checked”.
Przykładowa obsługa wyjątku sprawdzalnego:
public void writeFile() { Path filePath = Paths.get("real_estate.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) { bufferedWriter.write(toString() + "\n"); } catch (IOException e) { LOGGER.warning("Can't write result to file with content:" + e.getMessage()); } }
Po zastosowaniu adnotacji SneakyThrows:
@SneakyThrows public void writeFile() { Path filePath = Paths.get("real_estate.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) { bufferedWriter.write(toString() + "\n"); } }
Logger
Kolejną bardzo przydatnymi adnotacjami są wszelkie adnotacje dotyczące logowania. Są nimi m. in. @Log, @Log4j, @Slf4j. Upraszczają one odrobinę kod. Tutaj przykład działania adnotacji Log, która jest tożsama z użyciem standardowego loggera Javy.
private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(RealEstate.class.getName()); public void showMessage() { Logger.getGlobal().info("Real estate class"); }
Z wykorzystaniem @Log:
public void showMessage() { log.info("Real estate class"); }
Klasa pomocnicza
Ostatnią ciekawą klasą jest możliwość tworzenia tzw. klasę pomocniczą (ang. helper class). Klasa tego typu jest z reguły klasą finalną, co zabezpiecza przed dziedziczeniem, oraz każda metoda publiczna powinna być statyczną. Taka klasa powinna też posiadać jedynie stałe, jak i prywatny konstruktor. Dzięki takim założeniom zastosowane jest tu bardziej funkcjonalne podejście do programowania. Możesz utworzyć taką klasę automatycznie poprzez użycie @UtilityClass
Wersja podstawowa:
public final class FibonacciCalculator { private static final int FIRST_POSITION = 0; private static final int SECOND_POSITION = 1; private FibonacciCalculator() { } public static int countValue(int position) { int previouspreviousNumber, previousNumber = FIRST_POSITION, currentNumber = SECOND_POSITION; for (int i = 1; i < position ; i++) { previouspreviousNumber = previousNumber; previousNumber = currentNumber; currentNumber = previouspreviousNumber + previousNumber; } return currentNumber; } }
Z wykorzystaniem biblioteki:
@UtilityClass public class FibonacciCalculatorLombok { private final int FIRST_POSITION = 0; private final int SECOND_POSITION = 1; public int countValue(int position) { int previouspreviousNumber, previousNumber = FIRST_POSITION, currentNumber = SECOND_POSITION; for (int i = 1; i < position ; i++) { previouspreviousNumber = previousNumber; previousNumber = currentNumber; currentNumber = previouspreviousNumber + previousNumber; } return currentNumber; } }
To wszystkie omawiane przeze mnie adnotacje, dostarczane przez projekt Lombok. Nie są to jednak jedyne przykłady, bo sam framework jest dość rozbudowany. W kwestii rozszerzenia sobie wiedzy o nim tradycyjnie polecam przeczytać oficjalną dokumentację twórców.
*Instrukcja konfiguracji Lomboka na przykładzie Eclipse: https://projectlombok.org/setup/eclipse
**Przykładem jest tworzenie encji w Hibernate
Kod do lekcji: https://github.com/developeronthego/java-frameworks/tree/main/src/main/java/frameworks/lombok