Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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.


 

Všetky články v sekcii
Java - Pre pokročilých
Článok pre vás napísal Ondřej Němec
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity