Marzec 4, 2019

Java pytanie rekrutacyjne: Obiekt immutable

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