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 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