VS

Immutable vs Mutable Java objektai

Mutable objektas po sukūrimo gali pakeisti savo būseną. Immutable objekto būsena po sukūrimo nebekeičiama – norint kitos reikšmės sukuriamas naujas objektas.

Pagrindinis skirtumas

Keičiamų ir nekeičiamų objektų palyginimas
SavybėMutableImmutable
Būsena po sukūrimoGali keistisNegali keistis
Pakeitimo rezultatasAtnaujinamas tas pats objektasSukuriamas naujas objektas
PavyzdžiaiStringBuilder, ArrayListString, Integer, LocalDate
Dalijimasis nuorodaPakeitimus gali pamatyti visi turintys nuorodąNetikėtų būsenos pakeitimų nėra

Objekto būsena – tai jo atributų reikšmės konkrečiu metu. Pavyzdžiui, pirkinių sąrašo būsena gali būti jame esančių produktų skaičius.

Ar primityvieji tipai yra mutable ar immutable?

Trumpas atsakymas: primityvieji tipai nėra objektai, todėl objektams skirti terminai mutable ir immutable jiems formaliai netaikomi. int, double, char ar boolean kintamasis saugo pačią reikšmę, o ne nuorodą į objektą.

int skaicius = 5;
int kopija = skaicius;

skaicius = 8;

System.out.println(skaicius);
System.out.println(kopija);
Konsolės rezultatas

8
5

  1. skaicius pradžioje saugo reikšmę 5.
  2. kopija = skaicius nukopijuoja pačią skaitinę reikšmę.
  3. skaicius = 8 kintamajam priskiria kitą reikšmę; skaičius 5 nebuvo „pakeistas į“ 8.
  4. kopija tebėra 5, nes primityvieji kintamieji nesidalija vienu keičiamu objektu.
Kintamasis gali keistis, bet tai nėra objekto mutacija.

Priskyrimas skaicius = 8 pakeičia, kurią reikšmę saugo kintamasis. Mutacija reiškia jau sukurto objekto vidinės būsenos pakeitimą.

Keičiamas objektas

class PirkiniuSarasas {
    private int produktuKiekis;

    PirkiniuSarasas(int produktuKiekis) {
        this.produktuKiekis = produktuKiekis;
    }

    void pridetiProdukta() {
        produktuKiekis++;
    }

    int getProduktuKiekis() {
        return produktuKiekis;
    }
}

PirkiniuSarasas sarasas = new PirkiniuSarasas(2);
sarasas.pridetiProdukta();

System.out.println(sarasas.getProduktuKiekis());
Konsolės rezultatas

3

  1. Objektas sukuriamas su dviem produktais.
  2. Metodas pridetiProdukta() pakeičia to paties objekto atributą.
  3. Naujas PirkiniuSarasas objektas nekuriamas.
  4. Vėliau tas pats objektas jau saugo reikšmę 3.

Keičiamas objektas patogus, kai jo būsena natūraliai nuolat kinta: pirkinių krepšelyje, teksto kūrimo buferyje ar vartotojo sąsajos elemente.

Nekeičiamas objektas

final class Temperatura {
    private final double celsijai;

    Temperatura(double celsijai) {
        this.celsijai = celsijai;
    }

    double getCelsijai() {
        return celsijai;
    }

    Temperatura padidinti(double kiek) {
        return new Temperatura(celsijai + kiek);
    }
}

Temperatura ryte = new Temperatura(18.0);
Temperatura diena = ryte.padidinti(4.0);

System.out.println(ryte.getCelsijai());
System.out.println(diena.getCelsijai());
Konsolės rezultatas

18.0
22.0

  1. Klasė pažymėta final, o jos atributas – private final.
  2. Objektas ryte sukuriamas su reikšme 18.0.
  3. padidinti() nekeičia esamo objekto – jis grąžina naują Temperatura.
  4. Todėl ryte lieka 18.0, o diena turi 22.0.

Kodėl keičiamą objektą reikia stebėti atidžiau?

PirkiniuSarasas pirmas = new PirkiniuSarasas(2);
PirkiniuSarasas antras = pirmas;

antras.pridetiProdukta();

System.out.println(pirmas.getProduktuKiekis());
System.out.println(antras.getProduktuKiekis());
Konsolės rezultatas

3
3

pirmas ir antras nurodo tą patį objektą. Pakeitus jį per vieną nuorodą, naują būseną mato ir kita. Tai nėra klaida, tačiau didesnėje programoje toks šalutinis poveikis gali nustebinti.

String ir StringBuilder pavyzdys

String tekstas = "Labas";
String papildytas = tekstas.concat(" rytas");

System.out.println(tekstas);
System.out.println(papildytas);

StringBuilder kuriamas = new StringBuilder("Labas");
kuriamas.append(" rytas");

System.out.println(kuriamas);
Konsolės rezultatas

Labas
Labas rytas
Labas rytas

  • String.concat() nekeičia tekstas; metodas grąžina naują String.
  • StringBuilder.append() pakeičia paties StringBuilder turinį.
  • Abu būdai gali pateikti tą patį tekstą, tačiau objektai elgiasi skirtingai.

Dažnos immutable Java klasės

Java turi nemažai klasių, kurių objektai po sukūrimo nekeičiami. Jų metodai, atrodantys kaip pakeitimai, paprastai grąžina naują objektą.

String tekstas = "Labas";
String didziosiomis = tekstas.toUpperCase();

Integer skaicius = 10;
Integer didesnis = skaicius + 5;

System.out.println(tekstas);
System.out.println(didziosiomis);
System.out.println(skaicius);
System.out.println(didesnis);
Konsolės rezultatas

Labas
LABAS
10
15

toUpperCase() nepakeičia pradinio String. Aritmetika su Integer taip pat pateikia kitą reikšmę, o pradinis objektas nekeičiamas.

Dažnų nekeičiamų Java klasių pavyzdžiai
Klasė ar grupėKodėl naudinga, kad objektai nekinta?
StringTekstu galima saugiai dalytis, naudoti kaip kolekcijos raktą ir pakartotinai naudoti String pool reikšmes.
Integer, Long, Double, Boolean, CharacterPrimityviųjų tipų apgaubimo objektai elgiasi kaip stabilios reikšmės.
BigInteger ir BigDecimalKiekvienas aritmetinis metodas grąžina naują tikslų skaičių ir nekeičia pradinio.
LocalDate, LocalTime, LocalDateTime, DurationDatos ir laiko skaičiavimai nekeičia jau turimos datos, todėl rezultatus lengviau sekti.
UUIDIdentifikatoriaus reikšmė po sukūrimo lieka stabili.
OptionalKonteinerio būsena nekinta; operacijos grąžina kitą rezultatą.
record nėra automatiškai giliai immutable.

Įrašo komponentams negalima priskirti kitų nuorodų, tačiau jei record saugo keičiamą List ar kitą keičiamą objektą, jo vidus vis tiek gali pasikeisti. Reikalinga apsauginė kopija.

Dažnos mutable Java klasės ir tipai

  • StringBuilder ir StringBuffer keičia kuriamą simbolių seką.
  • ArrayList, HashSet ir HashMap leidžia pridėti, šalinti ar pakeisti elementus.
  • Masyvai, pavyzdžiui, int[], turi fiksuotą ilgį, tačiau jų elementus galima pakeisti.
  • Senesnės datos klasės Date ir Calendar yra keičiamos; naujesnės java.time klasės – nekeičiamos.
  • Tavo sukurtos klasės su būseną keičiančiais metodais ar setteriais paprastai yra mutable.

Keičiamumas nėra trūkumas savaime. Kolekcijos, buferiai ir modeliai dažnai turi keistis. Svarbu suprasti, kas gali pakeisti objektą ir kas dar turi nuorodą į tą patį objektą.

Kaip kuriama nekeičiama klasė?

  • klasė neleidžiama nesaugiai išplėsti, dažnai pažymint ją final;
  • atributai yra private final;
  • visos reikšmės nustatomos konstruktoriuje;
  • nėra būseną keičiančių setter metodų;
  • keičiami vidiniai objektai apsaugomi kopijomis.
import java.util.List;

final class Receptas {
    private final List produktai;

    Receptas(List produktai) {
        this.produktai = List.copyOf(produktai);
    }

    List getProduktai() {
        return produktai;
    }
}

List.copyOf() sukuria nekintamą pateikto sąrašo kopiją. Jei konstruktorius tiesiog išsaugotų gautą keičiamą sąrašą, išorinis kodas galėtų netikėtai pakeisti Receptas būseną.

final nuoroda dar nepadaro objekto nekeičiamo.

final List<String> neleidžia nuorodai parodyti į kitą sąrašą, tačiau pats sąrašas vis tiek gali būti keičiamas. Todėl reikalinga nekintama arba apsauginė kopija.

Kada kurį pasirinkti?

Praktinis pasirinkimas
Rinkis immutable, kaiRinkis mutable, kai
reikšmė turi būti stabili ir nuspėjamaobjekto būsena natūraliai dažnai keičiasi
objektu dalijasi skirtingos programos dalys ar gijosreikia efektyviai atlikti daug pakeitimų
objektas naudojamas kaip HashMap raktaskuriamas buferis, kaupiklis ar redaguojamas modelis
svarbu sumažinti netikėtus šalutinius poveikiusnaudojama sistema pagrįstai reikalauja setter metodų

Nė vienas pasirinkimas nėra visada geresnis. Gera pradinė taisyklė – rinktis nekintamumą, kai nereikia keisti būsenos, ir sąmoningai naudoti keičiamus objektus ten, kur jų lankstumas teikia aiškią naudą.