
Czym jest obiekt immutable?
Obiekt immutable jak sama nazwa wskazuje jest niezmienny czyli podczas swojego “życia” nie może zmienić swojego stanu. Oczywiście czasami jest możliwa zmiana stanu obiektu np. String:
String s = "qwe"; s = "any";
Jednak wiąże się to za każdym razem z utworzeniem nowego obiektu. W skrócie oznacza to, że adres zmiennej s się zmienia po przypisaniu do niej innego stringa – za każdym razem jest tworzony nowy obiekt typu String (pomijając kwestię poolingu).
Przykład niezmienności
Prześledźmy jak zmienia się stan obiektu podanego np. do metody bazując na dwóch prostych klasach: Person i Address. Następnie postarajmy się z nich stworzyć obiekty immutable. 😉
Address:
package pl.blog.java; public class Address { private String street; private int homeNumber; public Address(String street, int homeNumber) { this.street = street; this.homeNumber = homeNumber; } public String getStreet() { return street; } public int getHomeNumber() { return homeNumber; } public void setStreet(String street) { this.street = street; } }
Oraz Person:
package pl.blog.java; public class Person { private String name; private String lastName; private Address address; public Person(String name, String lastName, Address address) { this.name = name; this.lastName = lastName; this.address = address; } public String getName() { return name; } public String getLastName() { return lastName; } public Address getAddress() { return address; } }
Przekażmy jeden z nich do metody oraz “przypadkiem” zmieńmy jego stan:
package pl.blog.java; public class Main { public static void main(String[] args) { Address address = new Address("Piotrkowska", 133); Person person = new Person("Pablo", "Escabo", address); System.out.println(person.getAddress().getStreet()); makeSth(person); System.out.println(person.getAddress().getStreet()); } private static void makeSth(Person person) { Address address = person.getAddress(); address.setStreet("Łódzka"); System.out.println("Zmiana adresu: " + address.getStreet() + ", " + address.getHomeNumber()); } }
A następnie sprawdźmy stan obiektu, który został przesłany jako argument:
Piotrkowska Zmiana adresu: Łódzka, 133 Łódzka
Jak widać stan obiektu, który został przekazany do metody uległ zmianie pomimo tego, że zmiany wykonaliśmy na obiekcie typu Address, a nie Person – jak możemy temu zapobiec?
Pierwszy pomysł to usunięcie getterów w klasie Person, jednak zazwyczaj ich potrzebujemy. Lepszym pomysłem jest zwracanie kopii obiektów w getterach, dzięki temu uchronimy się przed niepotrzebnymi zmianami obiektów.
W tym celu warto wykorzystać tzw. konstruktor kopiujący (w sumie konstruktor jak każdy inny, nazwa pochodzi z języka C++).
public Address(Address address) { this(address.street, address.homeNumber); }
A w getterze zwracamy już kopię:
public Address getAddress() { return new Address(address); }
Stosując taki prosty zabieg już nie mamy problemów z pilnowaniem stanu obiektu Person:
Piotrkowska Zmiana adresu: Łódzka, 133 Piotrkowska
Chociaż, nie do końca… Spójrz na taki przykład:
public class Main { public static void main(String[] args) { Address address = new Address("Piotrkowska", 133); Person person = new Person("Pablo", "Escabo", address); System.out.println(person.getAddress().getStreet()); makeSth(address); System.out.println(person.getAddress().getStreet()); } private static void makeSth(Address address) { address.setStreet("Łódzka"); System.out.println("Zmiana adresu: " + address.getStreet() + ", " + address.getHomeNumber()); } }
Tym razem nie używamy gettera z klasy Person, tylko podczas tworzenia obiektu typu Person zapamiętujemy referencję obiektu Address, którego stan później możemy bez problemu zmieniać – co widać poniżej:
Piotrkowska Zmiana adresu: Łódzka, 133 Łódzka
Przed tym też można się łatwo zabezpieczyć – oczywiście kopiując obiekt podczas tworzenia obiektu typu Person.
public Person(String name, String lastName, Address address) { this.name = name; this.lastName = lastName; this.address = new Address(address); }
No i jak widać podziałało:
Piotrkowska Zmiana adresu: Łódzka, 133 Piotrkowska