Immutable (nemenné) programovanie v Jave
Zjednodušene povedané, immutable programovanie je spôsob programovania, pri ktorom sa nič (alebo takmer nič) nemení, ale neustále vznikajú nové objekty. Tento článok voľne nadväzuje na článok immutable object (nemenné objekty).
Cieľ
Hlavným cieľom je čitateľovi vysvetliť, ako sa programuje immutable, a to hlavne programátorom, ktorí o immutable programovaní nepočuli, ale sú k nemu z nejakého dôvodu tlačení.
Povieme si, čím sa vyznačujú immutable triedy a metódy, k čomu je to celé dobré. Prečo sa teda takto programuje? Immutable programovanie odstraňuje side-efect (vedľajší efekt). Ten nastáva, ak sa niečo v programe mení. To je problém hlavne u vlákien, immutable triedy sú thread-safe - žiadne vlákno nemôže meniť nejaké dáta inému vláknu.
Ako to funguje?
Nemení sa vnútorný stav, čo pre nás znamená žiadne metódy vracajúci void. Každá metóda musí niečo vracať. Pokiaľ je potrebné niečo zmeniť, nejaký atribút, vytvoríme novú inštanciu triedy, ktorá bude totožná s predchádzajúcou, okrem zmeneného atribútu.
Uveďme si príklad: pre triedu Person
neuvedieme metódu
setAge(int age)
. Ako sa nahradí bude ukázané ďalej. Okrem toho
sa nemení ani referencie. Správna immutable trieda má u každej
premennej final. Aby projekt fungoval, zostáva jedna trieda, zvyčajne
tá, ktorá všetko riadi, ako mutable.
Ak nemôžeme používať void metódy a nefinal premenné, nemôžeme používať Setter, for cyklus, while cyklus, foreach cyklus a samozrejme kolekcie ako List <T>. Čo použiť namiesto toho? Mohli by sme si síce vytvoriť nejakú knižnicu s immutable zoznamy a cykly, ale prečo nepoužiť existujúce? io.vavr (predtým JavaSlang) je knižnica, ktorá nám poskytuje immutable objekty, ktoré nahrádzajú mutable cykly a kolekcie.
Setter
Namiesto setrov sa používajú tzv. Wither.
//metoda nastavi atribut name public Person withName(final String name){ return new Person(name, this); }
Okrem tejto metódy je potreba aj konštruktor. Možno použiť buď hlavný konštruktor, alebo ako v tomto prípade sa použije privátne konštruktor:
private Person(final Person that,final String name){ this.name = name; //další atributy }
List <T>
Miesto mutable Listu je v JavaSlang k dispozícii interface
IndexedSeq<T>
. Funguje podobne ako list.
private final IndexedSeq<Person> persons;
Práca s ním vyzerá asi takto:
// prazdna sekvence persons = Array.empty(); // naplneni sekvence prvky persons = Array.of( new Person("Jan", "Novak"), new Person("Josef","Svoboda") );
Tento interface predpisuje podobné metódy ako List (get (i), size (), ...). Metódy pre prácu s prvkami fungujú takto:
// pridani prvku persons.append(new Person("Petr","Pavel")); // smazani prvku persons.remove(new Person("Petr","Pavel")); // smazani prvku na pozici persons.removeAt(0);
Pamätajte si, že tieto tri metódy nemenia persons
. Tento
zoznam po vykonaní zostane rovnaký. Tieto metódy vráti nový zoznam s
vykonanou zmenou.
IndexedSeq<Person> persons2 = persons.method...
For cyklus
Miesto for cyklu nám JavaSlang poskytuje Stream. Pre neho nie je potrebné ďalšie Konstruktor. Má návratovú hodnotu.
IndexedSeq<Integer> numbers = Stream // vygeneruje posloupnost od 0 do 9 .range(0, 10) // postupne projde radu vygenerovanych cisel .foldLeft( // vyhozi hodnota je prazdne pole Array.empty(), // current - aktualne zpracovana hodnota // i - index zpracovavaneho prvku // i - totozne s i v for cyklu (current, i)->{ // tady muze byt nejaky kod - podminky, dalsi stream.... // pokud jsme na konci streamu // tak se toto vrati // jinak je to current return current.append(i); } );
Aby to bolo zrozumiteľnejšie, skúsme si to ukázať na ďalšom konkrétnejšom príklade.
String outPrint = Stream.range(0, persons.size()).foldLeft("Vypis osob: ", (current, i)->{ return current + "\n" + persons.get(i).toString(); });
Ak by ste chceli prechádzok Stream po väčších krokoch než po jednej, má metóda range preťaženie: range (od, do, krok). Stream má tiež metódu rangeClosed (od, doVčetně).
While
Pre while cyklus sa mi nepodarilo nájsť náhradu v JavaSlang. Sám ho príliš nepoužívam, väčšinou namiesto neho stačí for cyklus, poťažmo Stream. Ukážeme si príklad pre prehľadaní sekvencie čísiel, kde chceme zistiť, či obsahuje nejaké párne číslo:
// sekvence IndexedSeq<Double> numbers = Array.of(1.0,4.0,5.0, 7.8,9.7); boolean result = Stream.range(0, numbers.size()).foldLeft(false, (currentValue, i)->{ // pokud uz byl patricny prvek nalezen // neni potreba kontrolovat dalsi if(currentValue==true) return currentValue; if(numbers.get(i)%2==0) return true;//hodnota se nepridava, nahrazuje return false; } );
Bez ohľadu na index rozhodujúceho prvku má tento algoritmus zložitosť
O(n)
. Preto by som ho nahradil rekurziu.
// spousteci metoda public boolean isEvenThere(final IndexedSeq<Integer> num) { return isEvenThere(num, 0); } // zde probiha rekurze private boolean isEvenThere(final IndexedSeq<Integer> num, final int i) { if (i>= num.size()) return false; if (num.get(i)%2==0) return true; return isEvenThere(num, i+1); }
Pri tomto postupe je priemerná zložitosť n / 2.
Tuple
Veľkou výhodou JavaSlangu je tuple. Interface tuple a niekoľko jeho implementáciou ako Tuple2, Tuple3 a ďalšie, slúži k práci s viacerými hodnotami ako s jednou. Kedy sa to môže hodiť? Predstavme si toto: nejaká trieda potrebuje zoznam manželských párov. Zároveň potrebuje prístup k jednotlivým ľuďom. Začiatočník by definoval dva listy - jeden pre všetky manželmi, druhý pre manželky. Niekto iný by vytvoril list dvouprvkových listov. Posledné by vytvoril triedu pre manželský pár a tie by potom tvorili list. My však nemusíme tvoriť takto jednoduchú triedu ručne, ale využiť práve tuple (prekladom n-tica).
Práve posledný prípad možno riešiť pomocou Tuple2.
Tuple2<T1, T2>(T1 a, T2 b)
Ako je vidieť, táto trieda je generická, môže obsahovať ľubovoľné 2 prvky. Tuple2 je teda nejaká všeobecná dvojica prvkov. Súčasne nemusí prvky byť rovnakého dátového typu.
Práca s immutable triedou
Nakoniec by som rád ukázal, ako sa s immutable triedou pracuje. Kód je z triedy, ktorá je mutable.
// narodil se Jan Novak a je mu 0 let Person p = new Person("Jan", "Novák", 0); // ten se rozhodl prejmenovat p = p.withName("Petr"); p = p.withForName("Lebeda"); // nyni uz Petr zestarl o rok p = p.withAge(p.getAge()+1);
Do mutable premenné sa ukladajú nové a nové inštancie Person, zatiaľ čo tie staré ničí Garbage Collector.
Programovať immutable alebo nie?
Nebudem tu presadzovať jeden alebo druhý spôsob, musíte sa rozhodnúť sami. Ja sám sa snažím immutable programovať, s tým, že sú triedy, ktoré takéto byť nemôžu - napríklad triedy, ktoré ukladajú dokument, pracujú s databázou, alebo potomkovia JFrame.