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
| Savybė | Mutable | Immutable |
|---|---|---|
| Būsena po sukūrimo | Gali keistis | Negali keistis |
| Pakeitimo rezultatas | Atnaujinamas tas pats objektas | Sukuriamas naujas objektas |
| Pavyzdžiai | StringBuilder, ArrayList | String, Integer, LocalDate |
| Dalijimasis nuoroda | Pakeitimus 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);8
5
skaiciuspradžioje saugo reikšmę5.kopija = skaiciusnukopijuoja pačią skaitinę reikšmę.skaicius = 8kintamajam priskiria kitą reikšmę; skaičius5nebuvo „pakeistas į“8.kopijatebėra5, nes primityvieji kintamieji nesidalija vienu keičiamu objektu.
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());3
- Objektas sukuriamas su dviem produktais.
- Metodas
pridetiProdukta()pakeičia to paties objekto atributą. - Naujas
PirkiniuSarasasobjektas nekuriamas. - 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());18.0
22.0
- Klasė pažymėta
final, o jos atributas –private final. - Objektas
rytesukuriamas su reikšme18.0. padidinti()nekeičia esamo objekto – jis grąžina naująTemperatura.- Todėl
rytelieka18.0, odienaturi22.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());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);Labas
Labas rytas
Labas rytas
String.concat()nekeičiatekstas; metodas grąžina naująString.StringBuilder.append()pakeičia patiesStringBuilderturinį.- 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);Labas
LABAS
10
15
toUpperCase() nepakeičia pradinio String. Aritmetika su Integer taip pat pateikia kitą reikšmę, o pradinis objektas nekeičiamas.
| Klasė ar grupė | Kodėl naudinga, kad objektai nekinta? |
|---|---|
String | Tekstu galima saugiai dalytis, naudoti kaip kolekcijos raktą ir pakartotinai naudoti String pool reikšmes. |
Integer, Long, Double, Boolean, Character | Primityviųjų tipų apgaubimo objektai elgiasi kaip stabilios reikšmės. |
BigInteger ir BigDecimal | Kiekvienas aritmetinis metodas grąžina naują tikslų skaičių ir nekeičia pradinio. |
LocalDate, LocalTime, LocalDateTime, Duration | Datos ir laiko skaičiavimai nekeičia jau turimos datos, todėl rezultatus lengviau sekti. |
UUID | Identifikatoriaus reikšmė po sukūrimo lieka stabili. |
Optional | Konteinerio būsena nekinta; operacijos grąžina kitą rezultatą. |
Į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
StringBuilderirStringBufferkeičia kuriamą simbolių seką.ArrayList,HashSetirHashMapleidž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
DateirCalendaryra keičiamos; naujesnėsjava.timeklasė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?
| Rinkis immutable, kai | Rinkis mutable, kai |
|---|---|
| reikšmė turi būti stabili ir nuspėjama | objekto būsena natūraliai dažnai keičiasi |
| objektu dalijasi skirtingos programos dalys ar gijos | reikia efektyviai atlikti daug pakeitimų |
objektas naudojamas kaip HashMap raktas | kuriamas buferis, kaupiklis ar redaguojamas modelis |
| svarbu sumažinti netikėtus šalutinius poveikius | naudojama 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ą.

