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

