obiekty niezmienne

Pytania rekrutacyjne Java – Obiekty niezmienne (immutable)

W Javie zmienność obiektów to coś bardzo powszechnego. Jeszcze do niedawna czymś normalnym było to, że większość beanów ma gettery i settery (być może w niektórych kręgach nadal jest). Ale wszystko powoli się zmienia  i coraz częściej na rozmowach rekrutacyjnych pada pytanie „Co to są obiekty niezmienne (immutable) i jakie są ich zalety i wady?”.

Co to jest obiekt niezmienny?

Obiekt niezmienny (immutable) to taki, który po utworzeniu i inicjalizacji pozostaje niezmienny i co ważne, nie ma możliwości jego zmiany (oczywiście w konwencjonalny sposób). Czyli taki obiekt nie udostępnia metod, które pozwalają na zmianę jego stanu. A jego wszystkie pola są prywatne.

 

Niezmienność ta jest trochę pozorna, ponieważ, przy użyciu refleksji można zmieniać wartości pól w każdej klasie. Wprawdzie wymaga to odrobinę wysiłku i znajomości api refleksji, ale nie jest to rocket science. Jednak to wcale nie oznacza, że z tego powodu powinniśmy zrezygnować z niezmiennych obiektów.

 

Jak zapewnić niezmienność obiektu?

Poza tym, że wszystkie pola powinny być prywatne, to mogą być jeszcze final. A sama klasa powinna być oznaczona jako final tak, żeby nie można było po niej dziedziczyć. Taki obiekt nie powinien udostępniać metod, które pozwalają modyfikować jego wewnętrzny stan, np. setterów.

Ale prywatne pola to jeszcze nie wszystko. Ważne jest, jakie typy danych przechowujemy w tych polach. One też powinny być niezmienne! Jeśli pola w twoim obiekcie są prymitywami, wrapperami prymitywów lub stringami to nie ma problemu. Wszystkie te typy są niezmienne. Ale jeśli używasz własnych obiektów, to musisz zapewnić ich niezmienność.

Jeśli korzystasz z kolekcji, to też musisz zapewnić ich niezmienność. I tutaj są różne sposoby na osiągnięcie tego:

  • możesz skorzystać ze specjalnej metody z klasy Collections,  która zwraca odpowiedni niemodyfikowalny widok na daną kolekcję.
    Np. dla listy będzie to: Collections.unmodifiableList(List<? extends T> list). Metoda ta zwraca tylko widok listy, co oznacza, że jeżeli posiadamy gdzieś w programie referencję do oryginalnej kolekcji, to możemy ją zmienić, a razem z nią zmieni się też widok. Często jest to wystarczające, by zapewnić niezmienność, ale w wielu przypadkach może prowadzić do trudno wykrywalnych błędów.
  • Zastosować niezmienne kolekcje. I można to zrobić co najmniej na kilka sposobów. Jeśli korzystasz z Javy 8 lub starszej, to musisz użyć jakiejś biblioteki, która dostarczy Ci implementację niezmiennych kolekcji (o niezmiennych kolekcjach pisałem w artykule 5 niezbędnych bibliotek języka Java). Jedną z takich bibliotek jest np. Guava.
    Jeśli korzystasz z nowszych wersji Java 9+, to masz do dyspozycji kolekcje niezmienne z biblioteki standardowej, np. List.of("a", "b", "c");. W Javie 10 zostały dodane także metody copyOf(...), które pozwalają skopiować standardową kolekcję i przerobić ją na niezmienną.

 

A co jeśli korzystam z tablic?

Tutaj jest trochę gorzej, ponieważ tablice są z natury zmienne. I korzystanie z nich w niezmiennych obiektach mija się z celem. Oczywiście, przy tworzeniu obiektu możemy kopiować wszystkie tablice, ale to nie zmienia faktu, że skopiowane tablice są też zmienne.

Chyba najlepszym rozwiązaniem w tym przypadku jest zastąpienie tablicy kolekcją.

 

Jak tworzyć obiekty niezmienne?

Takie obiekty można tworzyć na trzy sposoby. Poprzez konstruktor i to jest najprostszy sposób. Ale ma zasadniczą wadę. Im więcej pól do zainicjalizowania, tym więcej parametrów w konstruktorze. Dlatego nie należy w ten sposób tworzyć obiektów, które mają więcej niż 2-3 pola np.:

public final class User {
  private final String username;
  private final String email;

  public User(String username, String email) {
    this.username = username;
    this.email = email;
  }

  public String getUsername() {
    return username;
  }

  public String getEmail() {
    return email;
  }
}

// użycie
User user = new User("user", "user@test.pl");

 

Kolejny sposób to metoda fabryczna. I tutaj jest podobnie jak z konstruktorem, im więcej pól, tym więcej parametrów. Ale podejście to ma taką zaletę, że możemy stworzyć kilka takich metod o różnych nazwach, z różnym zestawem parametrów, co poprawia znacznie czytelność.

Za to najbardziej wygodnym sposobem do tworzenia obiektów niezmiennych jest skorzystanie ze wzorca builder. Żeby użycie buildera było możliwe, musi on być zaimplementowany wewnątrz danej klasy, tak by miał dostęp do prywatnych pól klasy. I to też można zrobić na wiele sposobów np.:

public class Person {
  private String name;
  private int age;

  public static Builder builder() {
    return new Builder();
  }

  public static class Builder {
    private Person person = new Person();

    public Builder name(String name) {
      person.name = name;
      return this;
    }

    public Builder age(int age) {
      person.age = age;
      return this;
    }

    public Person build() {
      return person;
    }
  }
  // ... getters ...
}

// użycie
Person person = Person.builder()
    .name("test")
    .age(20)
    .build();

Możemy napisać taki builder ręcznie lub skorzystać z jakiegoś plugina do IDE, który wygeneruje go dla nas. Kolejną opcją jest użycie Lomboka i adnotacji @Builder.

 

Zalety obiektów niezmiennych

Obiekty te sprawiają, że unikamy przypadkowych zmian, często w bardzo nieodpowiednich miejscach. Jeśli obiekt jest zmienny, to na pewno znajdzie się ktoś, kto będzie chciał go zmieniać tam, gdzie nie powinien tego robić.

Dobrym przykładem na tego typu sytuację jest obiekt, który przekazujemy jako parametr metody. Taki obiekt może być przekazany pomiędzy wieloma warstwami aplikacji, pomiędzy wieloma wywołaniami metod. Może być przekazany bardzo głęboko w hierarchii wywołań. Przez co identyfikacja, gdzie został zmieniony, okaże się bardzo trudna. Może to prowadzić do wielu dziwnych i trudno rozwiązywalnych błędów.

Używając obiektów niezmiennych nie mamy takich problemów i poprawia się konstrukcja naszej aplikacji.

Obiekty niezmienne są także bezpieczne do użytku wielowątkowego. Skoro obiekt nie jest zmieniany, to można go bezpiecznie przekazywać pomiędzy wątkami bez konieczności synchronizacji.

Kolejną zaletą jest to, że takie obiekty są idealne jako obiekty klucza w mapach. W sytuacji, gdy klucze są zmienne, po zmianie obiektu klucza zmienia się jego hashcode, przez co odnalezienie zapisanej wartości w HashMapie staje się niemożliwe.

 

Kiedy używać obiektów niezmiennych?

W zasadzie możesz używasz obiektów niezmiennych wszędzie i zawsze. Nic nie stoi na przeszkodzie, żeby przeciętna aplikacja webowa działała na obiektach niezmiennych. Nie musisz korzystać też z wielowątkowości, żeby korzystać z obiektów niezmiennych. Aplikacje, które działają w jednym wątku też wiele zyskują przy zastosowaniu obiektów niezmiennych.

Możesz zrezygnować z nich dopiero w sytuacji, gdy faktycznie jest potrzebna zmiana stanu jakiegoś obiektu.

 

Trudności w stosowaniu obiektów niezmiennych

Jedną z trudności w stosowaniu obiektów niezmiennych jaką zauważyłem, jest zmiana mentalna, która jest potrzebna do ich stosowania. Wielu developerów przez lata nauczyło się stosować obiekty zmienne i przyzwyczaiło się do zmieniania ich stanu wszędzie tam, gdzie tylko zechcą, co jest bardzo złą praktyką.

Po latach przyzwyczajeń bardzo trudno jest im przestawić się na niemodyfikowanie obiektów. Jak już wspomniałem, wymusza to trochę inną konstrukcję aplikacji, przez co niektórzy mają problem z przestawieniem się na nowy model. Poza tym, nie każdy do końca rozumie zalety jakie płyną z korzystania z takich obiektów.

Kolejną trudnością, tym razem techniczną, może być tworzenie nowego obiektu za każdym razem, kiedy chcemy zmienić jakieś pole w danym obiekcie. Czyli skopiowanie go. Jest to o wiele bardziej niewygodne, niż po prostu wywołanie settera na zmiennym obiekcie. Dodatkowo, jeśli takich operacji będziemy robić dużo, może to prowadzić do spadku wydajności w aplikacji. Najlepiej w miarę możliwości unikać takich sytuacji.

 

Czy encje Hibernate mogą być niezmienne?

To pytanie sam chętnie bym zadał kandydatowi, jako pytanie uszczegóławiające. Hibernate to jeden z najpopularniejszych ORMów. Pobiera dane z bazy i tworzy obiekty, więc warto wiedzieć jak zachowuje się pod tym względem.

I odpowiedź tutaj nie jest jednoznaczna. Jeśli pobieramy dane i je tylko wyświetlamy, to encje tworzone przez Hibernate mogą być niezmienne. Hibernate mocno bazuje na refleksji i wszystkie wartości w encjach może ustawiać przez refleksję. Settery dla pól nie są więc potrzebne.

Ale w sytuacji, gdy chcemy modyfikować już zapisane w bazie dane, to jest już gorzej. Najpierw musimy pobrać daną encję, zmodyfikować odpowiednie pola i Hibernate będzie mógł ją zapisać (oczywiście można to omijać na różne sposoby, ale jest to mało wygodne).

Niestety zmienność obiektów to cecha na której Hibernate został zbudowany.

W takim razie, jak można zintegrować Hibernate’a który bazuje na zmiennych obiektach z aplikacją, która bazuje na niezmiennych obiektach?

Jednym z rozwiązań jest używanie encji Hibernate’a tylko w warstwie dostępu do danych i konwertowanie ich do niezmiennego modelu aplikacji. W ten sposób ograniczamy zasięg encji do minimum i w całej aplikacji możemy korzystać z obiektów niezmiennych. Może to być jednak trochę problematyczne w niektórych sytuacjach. A alternatywą dla tego rozwiązania jest rezygnacja z Hibernate’a.

 

Podsumowanie

Obiekty niezmienne to bardzo ważny element budowania aplikacji, który sprawia, że aplikacje zyskują na czytelności i spójności, a także są bardziej odporne na błędy. Każdy developer powinien wiedzieć, jak korzystać z obiektów niezmiennych i jakie korzyści wiążą się z ich stosowaniem. Jeśli jeszcze nie miałeś okazji ich używać, to może czas zacząć?

 

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<

4 thoughts to “Pytania rekrutacyjne Java – Obiekty niezmienne (immutable)”

  1. Przy Hibernate bym wspomniał jeszcze ,,mo annotacji @Immutable oraz JPAowej @Entity(mutable=false). Nie dają one niemutowalności z sensie Javowym. Bardziej daje niemutowalność w sensie logicznym nie updateując stanu encji w bazie lub waląc wprost Exceptionem.

  2. Hipotetyczne pytanie –
    Twój Builder jest zagnieżdżoną klasą statyczną. Pola w klasie User nie mogą być finalne – bo ustawia je nasz Builder.
    Istnieje możliwość że ktoś bedzie używał Twojego User.Builder bez .build() – działając sobie na Klasie Builder – przekazując pomiędzy metodami itd.. W końcu gdzieś zrobi .build() , ale nie wiemy dokładnie co i jak zmieniało naszą Builder.

    Druga sprawa – pola chyba nie muszą być prywatne. Co z public final?

    1. Oczywiście zawsze jest takie ryzyko, że ktoś będzie przekazywał builder jako parametr w różne miejsca. Ale to już kwestia niezrozumienia po co builder się stosuje i po co stosuje się niemutowalne obiekty, tworzone za pomocą takiego buildera.

      Builder taki można napisać jeszcze inaczej:

      public final class PersonAlt {
          private final String name;
          private final int age;
      
          private PersonAlt(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          public static Builder builder() {
              return new Builder();
          }
      
          public static class Builder {
              private String name;
              private int age;
      
              public Builder name(String name) {
                  this.name = name;
                  return this;
              }
      
              public Builder age(int age) {
                  this.age = age;
                  return this;
              }
      
              public PersonAlt build() {
                  return new PersonAlt(name, age);
              }
          }
          // ... getters ...
      }
      

      I teraz masz private final, ale jest to trochę mniej estetyczne bo musisz używać konstruktora obiektu (jak masz dużo pól w klasie, to też i będzie dużo parametrów w konstruktorze). Ale problem z przekazywaniem buildera pozostanie.

      Co do public final to nie widziałem, żeby ktoś tak robił. Wszyscy trzymają się raczej konwencji getterów i dostępu przez nie do pól.

      1. Warto pamiętać, że jak jest dużo pól w klasie to warto się zastanowić nad podzieleniem tej klasy na mniejsze (tak radzi wujek Bob).

Komentarze są zamknięte.