Niemutowalność cz.1

Niemutowalność – wstęp

Niemalże legendarne słowo!! Większość programistów trzyma pytanie o niemutowalności obiektów jako ostatnią deskę ratunku w przedłużającej się dyskusji na temat poprawności napisanego kodu….. 🙃 To oczywiście żart. Sama niemutowalność  jest niezmiernie istotna! Należy posiadać dobre zrozumienie istoty zmiany stanu obiektów wchodzących w skład tworzonego systemu.

Z czym się je…. niemutowalność? 🍝

Co to wogóle znaczy, że obiekt jest niemutowalny??
Mutacja jest objawem zmiany stanu wejściowego. Innymi słowy jezeli obiekt jest niemutowalny jego STAN jest niezmienny.
Po co mamy taki niezmienny stan? Jest to dość istotne w przypadku gdy tworzymy aplikacje używająca wielu wątków (multithread), a także z punktu widzenia bezpieczeństwa aplikacji.

Jeżeli obiekt jest mutowalny (jego stan, tzn pola mogą zmieniać wartość np. ma settery) to zmiany wprowadzane przez odrębne wątki odwołujące się do niego, mogą wywołać duże problemy. W wyniku tego może być brak konsystenji danych w stanie końcowym, a w niektórych przypadkach mogą pojawić się look’i 😲

Przykłady

Zazwyczaj kursy dla początkujących pokazują klasę w takiej postaci

public class Cargo {

   private String id;
   private Owner owner;
   private CargoType type;
   private int capacity;

 // konstruktor
 //getters
 //setters
}

Jest to klasyczny przykład mutowalnej klasy. Oczywiście nie da się bezpośrednio nadpisać pól przez ich wywołanie ponieważ wszystkie pola jw. są prywatne. Jednak posiadanie setterów w klasie powoduje, że wszystkie bądź część wartości przypisanych do pól mogą zostać zmienione w trakcie życia obiektu.

Może zatem dojść do takiej sytuacji:

public class Main {

   public static void main(String[] args) {
       Owner owner = new Owner(“John”, 20);
       Cargo cargo = new Cargo("12", owner, CargoType.BIG, 50);
       cargo.setType(CargoType.SMALL);
       System.out.println(cargo.getType()); // tutaj otrzymamy wartość SMALL
   }
}

Jak tworzyć obiekty niemutowalne?

Dość często znajdujemy przykład o tym, że wystarczy dodać słowo kluczowe final do… wlasnie gdzie?
final” można dodać zarówno do klasy, nazwy pól  jak i method.

Klasa finalna

Jeśli klasa nie ma metod mutujących (zmieniających stan), pola są prywatne. Jedna jeżeli klasa nie jest oznaczona jako final, to i tak może dojść do mutowania!!!

public class Cargo {

// pola jak wcześniej
 //konstruktor
   }

   public CargoType getType() {
       return type;
   }

   public class FakeCargo extends Cargo {
       public FakeCargo() {
           super(id, type, capacity);
       }
       public void fakeSetType(CargoType fakeType) {
           type = fakeType;
       }
   }
}

Jako resultat wywołania settera klasy FakeCargo otrzymamy:

public class Main {

   public static void main(String[] args) {
       Owner owner = new Owner("John", 20);
       Cargo cargo = new Cargo("12", owner, CargoType.BIG, 20);
       Cargo.FakeCargo fakeCargo = cargo.new FakeCargo();
       fakeCargo.fakeSetType(CargoType.SMALL);
       System.out.println(cargo.getType()); /// cargo zmienia typ na SMALL
   }
}

Dodanie pola final do klasy Cargo powoduje, że klasa nie może rozszerzać innych klas. Wobec czego FakeCargo nie będzie mogło nadpisać pola klasy Cargo! 💪💪

Pola finalne

Jeżeli pole jest typu prymitywnego (int, double…) to dodanie do niego słowa final powoduje, że staje się ono niemutowalne. Jednakże jeżeli polem jest typ obiektowy to niestety nie jest już tak różowo (wyjątkiem jest obiekt typu String, który jest zawsze niemutowalny).
Popatrzmy na przykład:

Klasa Owner wygląda następująco:

public class Owner {
   private String firstName;
   private int age;

   public Owner(String firstName, int age) {
       this.firstName = firstName;
       this.age = age;
   }
// gettery
//settery
}

,a klasa Cargo ma wszystkie pola finalne

public class Cargo {

   private String id;
   private final Owner owner;
   private final CargoType type;
   private final int capacity;

…..
}

Zauważmy, że pole typu obiektowego Owner jest oznaczone jako final. Użycie settera do zmiany tego pola wywoła błąd kompilacji (o ile IDE pozwoli na stworzenie tego settera). Natomiast pobranie z klasy Cargo obiektu Owner używając gettera jest możliwe. Następnie na tym obiekcie można wywołać jego setter i zmienić jedno z pól. 😲

public class Main {

   public static void main(String[] args) {
       Owner owner = new Owner("John", 20);
       final Cargo cargo = new Cargo("12", owner, CargoType.BIG, 50);
      
       var myOwner = cargo.getOwner();
       myOwner.setAge(30);
       System.out.println(cargo.getOwner().getAge()); // zwroci 30!!!
   }
}

Zmutowaliśmy obiet Cargo a dokładnie rzecz ujmując jego pole Owner 😱 😱 😱 !

Do następnego razu… 👋

W następnym odcinku zobaczymy jeszcze przykład dla metody ze słowem kluczowym final i przejdziemy do tego jak radzić sobie z niemutowalnością, tam gdzie samo użycie słowa final nie wystarczy.

Do sułyszenia

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *