fbpx

Java 9 újdonságai – 6. rész

Process API újítások

Előzmények

A Java korai verzióiban elég nehézkes volt új folyamatot indítani. Ehhez csak a Runtime.getRuntime().exec() metódus állt rendelkezésünkre. 2004-ben, a Java 5 megjelenésével ez megváltozott, innentől kezdve elérhetővé vált a ProcessBuilder API, amivel könnyebben lehetett létrehozni új folyamatokat. Nézzük meg, hogy mivel bővül a process API repertoárja a Java 9-ben!

process API szemléltetésére szolgáló ábra

ProcessHandle interfész

Az új ProcessHandle interfész új lehetőségeket nyit számunkra a natív folyamatok kezeléséhez. A Java 5 óta elérhető ProcessBuilder által előállított Process objektumoktól elkérhető azok ProcessHandle-je.

A ProcessHandle feladata, hogy azonosítson egy folyamatot és lehetővé tegye, hogy különféle műveleteket végezzünk el rajta. Példányok a következő statikus factory metódusok segítségével hozhatók létre:

Statikus factory metódus Létrehozott ProcessHandle objektum
current() Az aktuális folyamathoz tartozó ProcessHandle objektummal tér vissza.
of(long pid) Optional<ProcessHandle> objektummal tér vissza, ami a megadott natív folyamat azonosítóhoz tartozik.
children() Stream<ProcessHandle> objektummal tér vissza, ami az aktuális folyamathoz tartozó közvetlen gyerek folyamatokat tartalmazza.
descendants() Stream<ProcessHandle> objektummal tér vissza, ami az aktuális folyamathoz tartozó gyerek folyamatokat tartalmazza, rekurzívan azok gyerek folyamataival együtt.
parent() Optional<ProcessHandle> objektummal tér vissza, ami az aktuális folyamat szülő folyamatát tartalmazza.
allProcesses() Egy olyan Stream<ProcessHandle> objektummal tér vissza, ami az aktuális folyamat által látható össze folyamat ProcessHandle-jét tartalmazza.

További hasznos metódusok, amit a ProcessHandle interfész elérhetővé tesz:

ProcessHandle metódus Leírás
info() ProcessHandle.Info objektummal tér vissza, ami az adott folyamathoz tartozó információkat tartalmazza.
isAlive() boolean-nel tér vissza, ami azt jelzi, hogy az adott folyamat él-e még.
pid() A natív folyamat azonosítóval tér vissza, ami alapján az operációs rendszer számon tartja az adott folyamatot.
supportsNormalTermination() boolean-nel tér vissza, ami azt jelzi, hogy támogatja-e a normál leállítást az adott folyamat, vagy rákényszerítve, azonnal állítja le a folyamatot.
onExit() CompletableFuture<ProcessHandle> objektummal tér vissza, ami arra használható, hogy az adott folyamat befejeződésekor, szinkron vagy szinkron módon, elindítsunk egy tetszőleges utasítást.
destroy() boolean-nel tér vissza, ami azt mutatja meg, hogy az adott folyamat leállítási kérelme sikeresen fel lett-e dolgozva.

ProcessHandle.Info interfész

Egy folyamatra egy adott időpillanatban vonatkozó információit tartalmazza. Ezek az információk korlátozottak lehetnek az információkat igénylő folyamatra vonatkozó operációs rendszeri jogosultságok függvényében.

ProcessHandle.Info metódus Leírás
arguments() Az adott folyamat argumentumait tartalmazó String tömböt tartalmazó Optional objektummal tér vissza.
command() Az adott folyamathoz tartozó alkalmazás nevét tartalmazó Optional-lel tér vissza.
commandLine() Az adott folyamathoz tartozó parancssort tartalmazó Optional-lel tér vissza.
startInstant() Az adott folyamat indításának pillanatát tartalmazó Optional-lel tér vissza.
totalCpuDuration() Az adott folyamat által felhasznált CPU-időt tartalmazó Optional-lel tér vissza.
user() Az adott folyamat felhasználóját tartalmazó Optional-lel tér vissza.

Példa

public static void main(String[] args) throws InterruptedException, IOException {
    printProcessInfo("main", ProcessHandle.current());
    Process process = new ProcessBuilder("notepad.exe", "C:/teszt.txt").start();
    printProcessInfo("notepad", process.toHandle());
    process.waitFor();
    printProcessInfo("notepad", process.toHandle());
}

private static void printProcessInfo(String processDescription, ProcessHandle processHandle) {
    System.out.println("---------- Információk a(z) " + processDescription + " folyamatról ----------");
    System.out.printf("Folyamat azonosító (PID): %d%n", processHandle.pid());
    ProcessHandle.Info info = processHandle.info();
    System.out.printf("Parancs: %s%n", info.command().orElse(""));
    String[] arguments = info.arguments().orElse(new String[] {});
    System.out.println("Argumentumok:");
    for (String argument : arguments) {
        System.out.printf("   %s%n", argument);
    }
    System.out.printf("Parancssor: %s%n", info.commandLine().orElse(""));
    System.out.printf("Indítási idő: %s%n", info.startInstant().orElse(Instant.now()).toString());
    System.out.printf("Futási idő: %sms%n", info.totalCpuDuration().orElse(Duration.ofMillis(0)).toMillis());
    System.out.printf("Felhasználó: %s%n", info.user().orElse(""));
    System.out.println();
}

Összefoglalás

A Java 9 process API újításai segítségével szebb kódot írhatunk. A korábbi verzióban bevezetett ProcessBuilder is jelentős lépés volt a helyes irányba, de az új lehetőségek, amiket az új interfészek, illetve az azokat megvalósító osztályok biztosítanak, fontos új eszközöket adnak a programozók kezébe, amikkel könnyebben kezelhetővé váltak a natív folyamatok.

Ha a többi Java 9-es újítás is érdekel, akkor böngészd bátran a többi blog posztot is ebben a témában!

A hivatalos javadoc-ban minden további részletet megtalálsz:

ProcessBuilder, Process, ProcessHandle, ProcessHandle.Info, CompletableFuture.

Java 9 újdonságai – 5. rész

Collection factory metódusok

A Collection factory metódusok olyan új statikus metódusok, amik segítségével egyszerűbben, tömörebben és biztonságosabban inicializálhatunk collection adatstruktúrákat.

Előzmények

A Collections Framework létezése óta – vagyis már a JDK 1.2 óta (1998) – sok fejlesztőt elgondolkodtatott, hogyan lehet egy ArrayList-et egy sorban inicializálni. Bár a Java nyelvi támogatást nyújt a String literálok esetén, nem ez a helyzet a Collections Framework-nél.

Szimpla kóderek

Bár Collections literálok nincsenek a Java-ban, ahogy más referenciatípusoknál is, itt is jogos az igény az inicializálásra. Erre a legnyilvánvalóbb – mondhatni triviális – megoldás a következő:

List<String> shoppingList = new ArrayList<>();
shoppingList.add("1 kg kenyér");
shoppingList.add("1 l tej");
shoppingList.add("10 db tojás");

Bár ez a megoldás helyes, mégis nagyon bőbeszédű. Tudsz ennél jobbat?

Leleményes kóderek

A fenti megoldás kicsit túl bőbeszédű, indokolatlan mennyiségű kódot kell leírni ahhoz, hogy egyszerűen alapértékre állítsunk be egy listát. Létezik egyszerűbb megoldás:

List<String> shoppingList = Arrays.asList("1 kg kenyér", "1 l tej", "10 db tojás");
shoppingList.add("1 kg liszt"); // se hozzáadni
shoppingList.remove(0);         // se törölni nem tudunk

Így már sikerült egy sorossá tenni ezt az inicializációt, de érdemes megjegyezni, hogy az előző megoldással ez nem ekvivalens, van egy jelentős különbség az így alapértékre állított változó használatában. Ez a különbség névlegesen az, hogy az így példányosított lista nem méretezhető át, vagyis nem adhatunk hozzá újabb elemet és nem is törölhetünk belőle. Ha makacsak vagyunk és mégis megpróbálnánk, akkor futásidőben a virtuális gép egy UnsupportedOperationException-nel lep meg minket.

Figyelmesen elolvasva az Arrays.asList(T… a) metódus javadoc-ját fény derül ennek okára. Itt elmagyarázzák nekünk, hogy ez a metódus a könnyebb átjárhatóságot hivatott biztosítani a régebbi tömb alapú API-k és az újabb Collection alapú API-k között. A visszaadott lista objektumon végzett változtatások „átíródnak” a lista alapját szolgáló tömbbe. Érthető tehát, hogy mivel a tömbök is létrehozásuk után fix méretűek, így az Arrays.asList(T… a) metódus által visszaadott lista is fix méretű lesz.

Még leleményesebb kóderek

Egy egyszerű csavarral az előző megoldásból kerekíthetünk egy módosítható listás megoldást, ami első bőbeszédű változattal egyenértékű funkcionalitásban:

List<String> shoppingList = new ArrayList<>(Arrays.asList("1 kg kenyér", "1 l tej", "10 db tojás"));
shoppingList.add("1 kg liszt"); // tudunk hozzáadni
shoppingList.remove(0);         // tudunk törölni

Ez nagyszerű, de kezdünk megint kicsit túl bőbeszédűek lenni. Túl sok billentyűt kell leütni ahhoz, hogy inicializáljuk a listánkat. Vajon van jobb megoldás?

Túl leleményes kóderek

Ha alaposan ismered a Java nyelv alapvető építőköveit, akkor bizonyára hallottál a példány inicializátorokról (instance initializer). Ha ezt vegyítjük egy kis névtelen belső osztállyal, akkor eredményül egy extra trükkös, első ránézésre újszerű szintaxist kapunk:

List<String> shoppingList = new ArrayList<String>() {{add("1 kg kenyér"); add("1 l tej"); add("10 db tojás");}};
shoppingList.add("1 kg liszt");
shoppingList.remove(0);

Elértük, amit szerettünk volna, egy sorban inicializáltuk a listánkat. Még mindig van néhány feleslegesnek tűnő karakter és elég sok szintaktikai elem. Ezt a megoldást szokták dupla kapcsoszárójeles inicializálásnak (double brace initialization) hívni. Bár valóban egysoros lett ez a művelet, számos dolog történik a háttérben. Vegyük sorra:

  1. Először is létrejön az ArrayList osztályból leszármaztatott névtelen osztály.
  2. Ennek az új névtelen osztálynak létrejön egy példánya.
  3. Mivel a belső kapcsoszárójel az egy példány inicializátor blokk, ezért ez a kódblokk lefut a 2. lépésben történő példányosodás során.
  4. Az utasításblokk az add() példány metódust meghívja egymás után különböző paraméterekkel, ami mivel nincs override-olva, ezért a szülő osztályból (ArrayList) megörökölt publikus add() metódust hívja meg, ami hozzáadja a paraméterként kapott elemeket a listához.
  5. A most létrejött névtelen osztály példányának referenciáját hozzárendeli a shoppingList változóhoz.

Elég sok dolog végbemegy egyetlen sorban. Ráadásul a névtelen belső osztály miatt a Java fordító külön class fájlokat gyárt a fájlrendszeren. Ha sokat használjuk ezt a trükköt, akkor a túl sok plusz class fájl miatt a programunk lelassulásának lehetünk tanúi. Az objektumorientáltság egyik alapműveletét a leszármaztatást itt nem arra használjuk, amire hivatott, mondhatni visszaélünk vele.

Tudván ezeket kijelenthetjük, hogy bár érdekes módszer ez, mégis kerülendő, anti-patternnek tekintendő.

Java 9 Collection factories to the rescue!

A Collection factory metódusok, amiket a Java 9-ben vezetnek be, elegáns megoldást nyújtanak:

List<String> list = List.of("1 kg kenyér", "1 l tej", "10 db tojás");
Set<String> set = Set.of("1 kg kenyér", "1 l tej", "10 db tojás");

Ez már tényleg sokkal jobb, mint az eddigi megoldások. Nincs túl sok szintaktikai elem és nem is kell sokat gépelnünk. Mégis van egy kis probléma, ami persze a felhasználók szempontjából igazából lényegtelen, de jó tudni róla.

Ennek az új of metódusnak pontosan mennyi paramétert is tudunk átadni? A példánkban épp 3-at adunk át, de mi van, ha valakinek kevesebb vagy több kell?

A Java 5-ös verziójában bevezették a változó hosszúságú argumentumokat (varargs). Ez lényegében egy szintaktikai édesítőszer, ami lehetővé teszi, hogy egy metódusnak ne tömböt kelljen átadnunk, hanem vesszővel felsorolhassuk az argumentumokat. A következő két metódus egyenértékű:

void method() {
    String[] shoppingArray = { "1 kg kenyér", "1 l tej", "10 db tojás" };
    methodWithArray(shoppingArray);
    methodWithVarargs(shoppingArray);
    methodWithVarargs("1 kg kenyér", "1 l tej", "10 db tojás");
}

void methodWithArray(String[] shoppingArray) {
}

void methodWithVarargs(String... shoppingArray) {
}

A változó hosszúságú argumentumoknak persze vannak megkötései, például hogy az ilyen paraméter csak a paraméterlista végén szerepelhet, illetve egy metódusnak csak egy ilyen paramétere lehet. Amikor változó hosszúságú argumentumokat váró metódust hívunk, akkor a háttérben létrejön egy tömb, ami hordozza az elemeinket. Ez persze némi teljesítménybeli visszaesést okoz, főleg ha sokszor futtatunk ilyen kódot, például ciklusban.

Hogy ezt elkerüljék, a Java fejlesztői az új of metódusnak nem csak egy változatát készítették el, hanem másik 10 overloadolt változatát is, ahol az első egy paramétert vár, a második kettőt, a harmadik hármat, és így tovább. Így amikor egy, kettő, három … tíz argumentummal hívjuk, akkor a megfelelő overloadolt változat fut le, és így elkerüljük a varargs esetén fellépő overheadet. Cserébe viszont „teleszemetelték” a Java core kódját egy csomó redundánsnak tekinthető résszel, így ezentúl több kódot kell majd karbantartaniuk az elkövetkezendő verziókban. A Java-t fejlesztő szakemberek ezt a megközelítést tartották célszerűnek.

De nem csak a List és Set kapott új factory metódusokat, hanem a Map is:

Map<String, Integer> shoppingMap = Map.of("kenyér", 1, "tej", 1, "tojás", 10);

Tíz argumentumig itt is overloadolt metódusokat használhatunk a kulcs-érték párok felsorolásához, de efelett szintén változó hosszúságú argumentummal rendelkező factory metódust tudunk hívni, amit Map.Entry<K, V> objektumokba kell csomagolnunk, amihez kapunk cserébe statikus entry() metódust:

Map<String, Integer> shoppingMap = Map.ofEntries(entry("kenyér", 1), entry("tej", 1), entry("tojás", 10));

Az új statikus Collection factory metódusok fontos tudnivalói

Nem csak a bőbeszédűség csökkentése volt a cél, bár kétségtelenül nagy előny, hogy tömörebben meg tudjuk fogalmazni a kódunkat és könnyebb is átlátni. A programozói hibák is csökkenthetők néhány általános érvényű szabály bevezetésével, amit az elmúlt években már eszközöltek is, és amire most is ügyeltek.

Megfigyelték, hogy a programhibák jelentős százaléka a null értékek helytelen használatából ered. Épp ezért a collection-ös adatstruktúrákban nem használhatjuk a null-t elemként. Ezt ezek az új statikus factory metódusok se engedélyezik.

A másik hibacsökkentési lehetőség az immutabilitás (módosíthatatlanság) bevezetése. Ezek a Collection factory metódusok módosíthatatlan adatstruktúrákat produkálnak. Ez azért jó, mert sok hiba abból fakad, hogy egy módosítható adatstruktúrát a program hívási láncolata mentén végigpasszolva valahol akaratlanul módosítjuk.

Egy másik nagy előnyt azt a Map.ofEntries() megvalósítása nyújtja, ugyanis itt nem tudjuk elrontani a kulcs-érték párok párosítását, ami csak futásidőben derülne ki, mert már fordítási időben hibát kapunk és hamar kijavíthatjuk azt.

A HashSet-ek és HashMap-ek elemei mindig is látszólag véletlenszerű iterálási sorrendet eredményeztek, de volt egy determinisztikus voltuk, hogy ugyanazt a sorrendet követték minden programfutás esetén. Az új immutábilis collection-ök viszont minden futásnál más és más sorrendben iterálnak végig az elemeiken, így ha hibásan egy kódrészlet a rendezetlen elemek valamilyen sorrendjére támaszkodott, akkor most az ilyen hibákra hamar fény fog derülni (ezt hívjuk fail fast viselkedésnek).

Konklúzió

Összességében az új Collection factory metódusok egy nagyszerű új lehetőséget biztosítanak a programozók számára, amivel tömören tudják a kódjukat megfogalmazni. Érdemes tisztában lenni a részleteikkel, mint például, hogy immutábilis példányokkal térnek vissza, tiltják a null használatát és az iterálási sorrendjük is változó lehet.
A JEP 269-ben olvashatod el angolul a motivációt a megvalósítás mögött.
Ha érdekel a Java 9 többi újdonsága is, akkor azokat a Java 9-es blog posztjainkban olvashatod.

Java 9 újdonságai – 4. rész

Reactive Streams API – reaktív programozás

„A reaktív folyam egy kezdeményezés egy szabvány létrehozására, ami nem blokkoló háttérnyomás esetén is aszinkron folyam feldolgozást tesz lehetővé.” Mi van? – kérdezheted magadban. Pedig ez a lényeg. Bontsuk le kicsit, hogy honnan is származik ez az egész reaktív programozás és mi az értelme! Egy fontos koncepcióról van szó.

A reaktív programozás előzményei

Napjaink alkalmazásainak nagyon más követelményeknek kell megfelelniük, mint néhány évvel ezelőtt. Régebben elég volt, ha a program sok másodperccel később reagált a felhasználói inputra. Az se volt gond, ha néha pár órára leállították karbantartás miatt az egész rendszert. A legnagyobb rendszerek is csak néhány gigabájtnyi adatot kezeltek és az egész néhány szerveren futott.

Ezzel szemben ma a felhasználók inkább milliszekundumban mérhető válaszidőt várnak még a mobiljukról elért webes alkalmazásoktól is. Sokszor már petabájtnyi adatot kezelnek (a gigabájt egymilliószorosa) és felhő alapú klasztereken futnak, ahol több száz vagy ezer magos processzor szolgálja ki a számítási igényüket. A rendelkezésreállás 99,9999% vagy még több kilences a végén, attól függ, hogy milyen rendszerről van szó.

Egyszerűen túl nagy az elvárások közti különbség, és a régen bevált módszerek már nem elégítik ki ezeket az új igényeket. Szükség van tehát valami újra. Ez az új megközelítés a reaktív programozás.

Reaktív programozás

Nagyvállalatok egymástól függetlenül fejlesztették le az IT-s rendszereiket, amik között rengeteg hasonlóság volt, bár mindegyik másképp lett implementálva. Az igények már jól körvonalazódtak külön-külön. Milyen alkalmazásra van manapság szükség? Olyanra, ami

  • jól reagál (reactive): A program a vele folytatott kommunikáció során gyorsan ad választ, ezáltal a problémák is gyorsan felfedezhetők és orvosolhatók benne. Az ilyen rendszerek gyors és konzisztens módon adnak választ a kérésekre, megbízható felső korlátot specifikálnak válaszidőnek.
  • öngyógyító (resilient): A program hiba esetén is jól reagáló marad. A hibákat adott komponensen belül kell kezelni és a különböző komponenseket izolálni kell egymástól, ezáltal lehetővé téve, hogy a rendszer részei meghibásodhassanak és helyreállhassanak az egész rendszer kompromittálása nélkül. A komponensek helyreállítását egy másik külső komponensnek való feladatdelegálással lehet megoldani, a magas elérhetőséget pedig replikációval, amikor szükséges. Az adott komponenst használó klienseket nem terheljük meghibásodás kezelésével.
  • alkalmazkodó (elastic): Az alkalmazás akkor is jól reagál, amikor nagy terhelésnek van kitéve (pl. Neptun :-)). A reaktív rendszerek képesek detektálni a terhelés mértékét és ennek megfelelően allokálni erőforrásokat a programhoz. Ennek persze vonzata, hogy globális bottleneckektől mentes architektúrájú legyen a szoftver. Az ilyen rendszerek prediktív és reaktív skálázó algoritmusokat is támogatnak és az alkalmazkodókészségüket költséghatékony módon, mindenki által elérhető hardveren és szoftveren valósítják meg. (Vagyis nem kell hozzá szuperszámítógép.)
  • üzenet vezérelt (message driven): A program aszinkron üzenetátadáson alapul, amivel jól elhatárolhatók a komponensek egymástól és azok laza csatolását eredményezi. Ez az izoláció azt is lehetővé teszi, hogy a hibákat, mint üzeneteket delegáljuk. Az explicit üzenetátadással képessé válik a terheltség kezelésére, illetve alkalmazkodás érhető el azzal, hogy az üzenetsorok hosszát megfigyeli és formálja az igényeknek megfelelően.

Durván 3 évvel ezelőtt (2014. szeptember 16-án) tették közzé a második verzióját a „Reactive Manifesto”-nak, ami ezeket az igényeket fogalmazta meg.

Új programozási paradigma

A reaktív programozás tehát nem más, mint egy új programozási paradigma, ahol adatelemek aszinkron folyamát dolgozzuk fel, és ahol az alkalmazások az új adatelemekre azok megjelenésekor reagálnak.

Az adatok folyama nem más, mint időben egymás után levő adatelemek. Ezzel a módszerrel sok memóriát spórolhatunk meg és így hatékonyabbak lehetünk, mert az adatokat folyamként dolgozzuk fel, és nem kell egy memóriában összegyűlt adathalmazon végigiterálni.

Reaktív folyamok

Amikor a Reactive Manifesto alapján a reaktív folyam megközelítés útjára indult, akkor az volt a cél, hogy egy szabványos megoldást adjanak az aszinkron folyamfeldolgozásra, nem blokkoló háttérnyomás esetén. (Erre a háttérnyomásos dologra rögtön kitérünk részletesebben.) A fő kihívás nem az volt, hogy megoldást találjanak erre a problémára – hisz ekkor már több implementáció is létezett – hanem az, hogy a különböző megoldásokat összeolvasszák és megtalálják azt a minimális halmazát az interfészeknek, metódusoknak és protokolloknak, ami leírja a szükséges műveleteket és entitásokat, hogy elérhessük ezt a célt. Ha még mindig olvasod és idáig eljutottál, akkor megérdemelnél egy ingyen csokit!

Háttérnyomás

A háttérnyomás a kulcs koncepció itt, vagyis angolul a „back pressure”. Képzeld el a termelő (producer) és fogyasztó (consumer) feleket a rendszerben! A termelő adja és a fogyasztó kapja az üzeneteket. A fogyasztó feliratkozik a termelő adatforrására, ha szeretne tőle üzeneteket kapni. Ezek után, amikor egy új üzenet elérhetővé válik, a termelő a fogyasztónak egy callback metódusát meghívva feldolgozásra átadja az üzenetet.

Ha a termelő az üzeneteket nagyobb ütemben adja ki magából, mint ahogy a fogyasztó képes azt feldolgozni, akkor a fogyasztó rá lesz kényszerítve arra, hogy egyre több és több erőforrást ragadjon magához, amely jó eséllyel crash-eli a fogyasztót. Ennek megakadályozására szükség van egy mechanizmusra, hogy a fogyasztó értesíteni tudja a termelőt, hogy az üzenetküldési sebességet csökkentse. A termelő ekkor néhány különböző módszer közül választhat, amivel ezt a helyzetet kezelheti. Ezt a mechanizmust hívjuk háttérnyomásnak, vagyis back pressure-nek.

Blokkoló háttérnyomás

Ezt a legegyszerűbb elérni. Ha a termelő és a fogyasztó azonos szálon futnak, akkor az egyik végrehajtása blokkolja a másik végrehajtását. Probléma megoldva. Persze sok esetben ez nem eszközölhető. Gondolj csak arra, hogy a termelőnek nem csak ez az egy fogyasztója van, hanem több is, és mindegyik más sebességgel képes az üzeneteket feldolgozni. Legtöbbször nem is megoldható, hogy azonos szálon fussanak, mert különböző környezetekben vannak. Ilyen esetekben szükséges egy nem blokkoló háttérnyomási mechanizmus.

Nem blokkoló háttérnyomás

Ezt úgy érhetjük el, hogy a push stratégiánkat lecseréljük pull stratégiára. Vagyis nem akkor történik küldés, amikor a termelő készen áll az új üzenetekkel, hanem akkor, amikor a fogyasztó szól, hogy készen áll x üzenet fogadására. Ekkor a termelő pontosan ennyi üzenetet küld és vár, mielőtt továbbiakat küldene.

JDK 9 Flow API

A Java 9-ben megalkotott, a fentieket minimális méretű interfészekkel támogató API-ja a JEP 226: More Concurrency Updates keretein belül valósult meg. A következő 4 interfészt tartalmazza:

@FunctionalInterface
public static interface Flow.Publisher<T> {
    void subscribe(Flow.Subscriber<? super T> subscriber);
}

public static interface Flow.Subscriber<T> {
    void onSubscribe(Flow.Subscription subscription);
    void onNext(T item);
    void onError(Throwable throwable);
    void onComplete();
}

public static interface Flow.Subscription  {
    void request(long n);
    void cancel() ;
}

public static interface Flow.Processor<T,R>extends Flow.Subscriber<T>, Flow.Publisher<R> {  
}

Java reaktív programozás Publisher és Subscriber szerepeket szemléltető ábra

Nomenklatúra

Korábbi elnevezéseink alapján (termelő, fogyasztó) a Subscriber a fogyasztó, de a Java 9 Flow API nomenklatúrája alapján a feliratkozó, a Publisher pedig a termelő, vagyis a publikáló (kiadó). Ezeket az elnevezéseket magyarul nem tartom szerencsésnek, így ezekre az angol nevükkel, vagy a magyarban meghonosult általános nevükkel fogok hivatkozni a továbbiakban.

Subscriber

A Subscriber feliratkozik a Publisher-hez annak subscribe metódusának meghívásával. Az adatelemek nem kerülnek kiküldésre a Subscriberhez, amíg azokat nem kéri explicite. A Subscriber Subscription-ön hívott metódusai szigorúan rendezettek. Az alkalmazás a következő callback-ekre tud reagálni, amik a Subscriberen vannak definiálva:

Callback Leírás
onSubscribe Ez a metódus kerül meghívásra minden egyéb Subscriber metódus előtt az adott Subscription esetén.
onNext Ez a metódus kerül meghívásra egy Subscription következő eleménél.
onError Ez a metódus kerül meghívásra egy helyreállíthatatlan hiba esetén, ami a Publisher-ben vagy Subscription-ben történt, ami után más Subscriber metódus már nem lesz hívva a Subscription által.

Ha egy Publisher hibába ütközik, ami nem teszi lehetővé adatelemek küldését a Subscriber-nek, akkor ezen a Subscriber-en az onError metódus kerül meghívásra és további üzeneteket nem fog kapni.

onComplete Ez a metódus akkor hívódik meg, amikor már ismertté válik, hogy további Subscriber metódushívások nem lesznek egy olyan Subscription-höz, ami még nem zárult le hibával.

Subscriber példa

import java.util.concurrent.Flow.*;
...

public class MySubscriber<T> implements Subscriber<T> {

    private Subscription subscription;

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(T item) {
        System.out.println("Megérkezett elem: " + item);
        subscription.request(1);
    }

    @Override
    public void onError(Throwable t) {
        t.printStackTrace();
    }

    @Override
    public void onComplete() {
        System.out.println("Kész");
    }
    
}

Publisher

A Publisher az adatelemek folyamát továbbítja a feliratkozóknak. Ezt aszinkron módon teszi, általános esetben egy Executor segítségével. A Publisher-ek biztosítják, hogy a Subscriber metódushívások minden Subscription esetén szigorúan rendezett sorrendben történnek. Ha még mindig itt vagy velem, akkor az adatfolyamon érkezik neked egy újabb csoki reaktív módon.

Publisher példa a JDK SubmissionPublisher osztályával

import java.util.concurrent.SubmissionPublisher;
...

    // Publisher példányosítása
    SubmissionPublisher<String> publisher = new SubmissionPublisher<>();

    // Subscriber regisztrálása
    MySubscriber<String> subscriber = new MySubscriber<>();
    publisher.subscribe(subscriber);

    // Elemek küldése
    System.out.println("Elemek küldése...");
    String[] items = {"1", "alma", "2", "körte", "3", "banán"};
    Arrays.asList(items).stream().forEach(i -> publisher.submit(i));

Subscription

Egy Publisher-t és egy Subscriber-t köt össze. A Subscriber-ek csak akkor kapnak adatelemeket, amikor explicit kérik azokat, és bármikor visszavonhatják a Subscription segítségével.

Metódus Leírás
request Hozzáadja az adott n számú elemet az aktuálisan be nem teljesített kérelmekhez az adott Subscription-nél.
cancel A Subscriber-t leállítja, ami ezek után nem kap több üzenetet. A visszavonási kérelem nem azonnal történik.

Processor

A Processor egy olyan komponens, ami Subscriber-ként és Publisher-ként is viselkedik. A Publisher és Subscriber között helyezkedik el és átalakítja az egyik folyamot egy másikra. Több Processor-t is egymás után lehet kötni, és az utolsó Processor eredményét dolgozza fel a Subscriber. A JDK nem tartalmaz konkrét Processor implementációt, így ezt az API-t felhasználó programozónak kell megvalósítania.

Processor megvalósítására egy példa, ami String-ből Integer-t készít

import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;
...

public class MyTransformProcessor<T,R> extends SubmissionPublisher<R> implements Processor<T, R> {

    private Function function;
    private Subscription subscription;
    
    public MyTransformProcessor(Function<? super T, ? extends R> function) {
        super();
        this.function = function;
    }

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(T item) {
        submit((R) function.apply(item));
        subscription.request(1);
    }

    @Override
    public void onError(Throwable t) {
        t.printStackTrace();
    }

    @Override
    public void onComplete() {
        close();
    }
  
}

Példa a Processor-ral történő folyam átalakításhoz

import java.util.concurrent.SubmissionPublisher;
...

    // Publisher példányosítása
    SubmissionPublisher<String> publisher = new SubmissionPublisher<>();

    // Processor és Subscriber példányosítása
    MyFilterProcessor<String, String> filterProcessor = new MyFilterProcessor<>(s -> s.equals("x"));

    MyTransformProcessor<String, Integer> transformProcessor = new MyTransformProcessor<>(s -> Integer.parseInt(s));

    MySubscriber<Integer> subscriber = new MySubscriber<>();

    // Processor és Subscriber egymás után kötése
    publisher.subscribe(filterProcessor);
    filterProcessor.subscribe(transformProcessor);
    transformProcessor.subscribe(subscriber);

    System.out.println("Elemek küldése...");
    String[] items = {"1", "alma", "2", "körte", "3", "csoki"};
    Arrays.asList(items).stream().forEach(i -> publisher.submit(i));
    publisher.close();

Konklúzió

A reaktív programozás megvalósulásához a Java 9 Flow API-ja egy jó kezdet. Megadja a fejlesztőknek azt a minimális interfész csomagot, amivel reaktív programok készíthetők, de időre lesz szükség, mire ennek az ökoszisztémája kialakul. Ha más termékekben is elérhetővé válik majd a reaktív programozás API-ja – mint például adatbáziskezelő rendszerekben – akkor megvalósulhatnak majd azok a modern alkalmazások, amik a Reactive Manifesto-ban leírt feltételeknek megfelelnek.
Ha érdekel a Java 9 további nyelvi újításai, akkor olvasd el a korábbi blog posztokat is a Java 9 témában!

Forrás:
https://community.oracle.com/docs/DOC-1006738
https://aboullaite.me/java-9-new-features-reactive-streams/

Java 9 újdonságai – 3. rész

Java 9 linkelés

A Java 9 újdonságai cikksorozat előző részében a Java platform modul rendszeréről olvashattál. Ez az új feature lehetővé teszi alkalmazásaink különböző módokon való telepítését. A Java 9 linkelés új, opcionális statikus linkelési lépést épít be a programunk fordítási lépései közé. De kezdjük az elején!

Java 8 kompakt profilok

Ahhoz, hogy megértsük, milyen új képességekre teszünk szert a Java 9 kibővült eszközkészletével, előbb érdemes megnéznünk az előzményeket.

A Java 8 2014. március 18-án jelent meg, és rengeteg várva várt új nyelvi elemet tartalmazott. A legismertebb talán a funkcionális programozást egyszerűbb formában lehetővé tevő lambda kifejezések és a Stream API volt, de számos más újítás is ezzel a verzióval jelent meg. Ezek egyike volt a kompakt profilok bevezetése (compact profiles).

3 kompakt profilt definiáltak, amelyeknek mérhetetlenül izgalmas neveket adtak:

  • compact1
  • compact2
  • compact3

Java 8 előtt

A Java 8 előtt, amikor lefordítottuk a programunkat, nem volt lehetőségünk megadni a Java fordítónak (javac), hogy ne a Java futtatókörnyezet (JRE) teljes eszközkészletére fordítson, hanem annak csak egy részhalmazára. Ez mondjuk asztali számítógépek esetén ma már nem olyan nagy gond, mert itt bőséges memória áll rendelkezésre, viszont ha a mobil eszközökre gondolunk, már más a helyzet. Itt erősen korlátozottak az erőforrásaink. Nem csak a memória, de a processzor, tárhely, hálózati kapcsolat, akkumulátor is. Ezekre az eszközökre nem lenne célszerű a folyamatosan növekvő teljes JRE-t telepíteni, főleg, ha ki sem használjuk annak képességeit.

Java 8 után

A Java 8-ban viszont már megadhattuk a programunk fordításakor valamelyik kompakt profil nevét, ami a teljes Java futtatókörnyezetnek csak valamilyen részhalmazát tartalmazta. A kompakt profilok kibővítik egymást. A legszűkebb részhalmaz a compact1. A compact2 profil tartalmaz mindent, amit a compact1 és egyéb csomagokat is. Ugyanígy a compact3 tartalmazza a teljes compact2-t és egyéb package-eket is. Hogy melyik profil pontosan mit tartalmaz, azt az alábbi táblázatban láthatod:

compact1 compact2 compact3 Teljes JRE SE
Core (java.lang.*) JDBC Security (kerberos, acl, sasl) Beans
Security RMI JMX Input Methods
Serialization XML JAXP JNDI IDL
Networking XML JAXP (adds crypto) Preferences
Ref Objects Management Accessibility
Regular Expressions Instrumentation Print Service
Data and Time RMI-IIOP
Input / Output CORBA
Collections Java 2D
Logging Sound
Concurrency Swing
Reflection AWT
JAR Drag and Drop
ZIP Image IO
Versioning JAX-WS
Internationalization (I18N)
JNI
Override Mechanism
Extension Mechanism
Scripting

Megtakarított tárhely

Hogy legyen egy valós képünk arról, hogy mennyi tárhelyet is takaríthatunk meg a kompakt profilokkal, íme egy rövid mérés:

  • compact1 profil: ~14 MB
  • compact2 profil: ~18 MB
  • compact3 profil: ~21 MB
  • Teljes JRE (Java SE Embedded): ~45 MB

Jól látható, hogy a compact1 profil az amúgy is tárhelyoptimalizált teljes Java SE Embedded kiadás egyharmada csupán.
A kompakt profilok már sokat segítettek a problémán, de még mindig nem jelentettek végleges megoldást.

Java 9 linkelés – linkelési idő

A Java 9 linkelés szemléltetésére szolgáló lánc ábraNéha úgy érzem kár lefordítani magyarra a fontosabb angol szakkifejezéseket. Talán most is jobban tettem volna, ha link time-nak hagyom.

Eddig fordítási időről (compile time) és futási időről (run time) beszélhettünk programjaink kapcsán, amik a szoftverünk életciklusának két különböző fázisa. E kettő közé a Java 9-ben beférkőzött a linkelési idő (link time). A Java nyelv a kezdetektől fogva egy dinamikusan linkelő nyelv volt, vagyis amikor a forráskódot lefordítottuk bájtkódra, akkor csak a külső library-kra való hivatkozások neve került a lefordított gépi kódba. A függőségek a program futtatásakor töltődtek be. Ez lehetővé tette, hogy több Java program is használja ugyanazt a külső library-t.

A dinamikus linkelés mechanizmusában semmi nem változott, ez továbbra is így működik. Amivel a Java 9-ben most már többet tud a nyelv, az az, hogy lehetőségünk van egy opcionális statikus linkelési lépést is beépíteni a programunk életciklusába. Ez a linkelési idő, ami a fordítási idő és futási idő között van, egy statikus linkelési lépés. A statikus linkelés során modulok halmazait és azok tranzitív függőségeit (transitive dependencies) tudjuk linkelni, hogy egy run-time image-et létrehozzunk.

Java 9 linkelés – jlink parancssori eszköz

Mindezt a jlink nevű új, parancssori eszközzel tehetjük meg, az alább demonstrált módon:

jlink --module-path <modulepath> --add-modules <modules> --limit-modules <modules> --output <path>
  • module-path: Ezzel a kapcsolóval specifikálhatjuk a JDK moduljainak elérési útját. Ezek lehetnek JAR fájlok, jmod fájlok (a JDK 9 új fájlformátuma, ami hasonló a JAR fájlokhoz, de tud kezelni natív kódot és java konfigurációs fájlokat is) vagy exploded modulok.
  • add-modules: Modulok, amikre a run-time image-et le szeretnénk generáltatni.
  • limit-modules: Korlátozhatjuk a linkelést csak azokra a modulokra, amikre az alkalmazásunknak mindenképp szüksége van. Néha, amikor modulokat adunk hozzá, akkor azok tranzitív függőségeit is hozzáadjuk. Ezzel a kapcsolóval limitálhatjuk, hogy melyek ezek a modulok.
  • output: A kimeneti elérési út, ahova a generált run-time image kerül.

Konklúzió

Láthatjuk, hogy a Java 9 új, jlink parancssori eszközével a modularizált Java programokat új módokon tudjuk telepíteni, ami számos helyzetben jól jöhet. Annak köszönhetően, hogy a Java 9-ben maga a core csomagok is modularizálva lettek, ha saját projektünket is modularizálva írjuk meg és modularizált külső library-kat használunk, akkor olyan run-time image elkészítése válik lehetővé, ami a korábbi verziókban nem volt kivitelezhető.

Ha többet szeretnél olvasni a jlink használatáról, akkor azt ezen a hivatalos Oracle oldalon teheted meg.

Java 9 újdonságai – 2. rész

Ez a cikksorozat a Java 9 újdonságait taglalja. Az első részben elkezdtük megnézni, hogy a Java 9 modul rendszere miért annyira fontos, milyen problémát hivatott orvosolni. Lássuk most hát a részleteket!

A Java 9 modul rendszere (Jigsaw)

A dunsztos üvegektől nincs menekvés

A JAR-ok – vagyis Java ARchive-ok ( = dunsztos üveg) – nem mások, mint lefordított Java osztályok, némi metainformációval ellátva, ZIP-re tömörítve és .jar kiterjesztéssel ellátva.

A Java 9 modul rendszere ezt a struktúrát a metainformációk terén egészíti ki. Ezentúl a moduláris JAR fájlok rendelkezni fognak egy modul leíróval (module-info.class). Ebben a leíróban meghatározhatjuk, hogy ez a modul milyen más modultól függ. Ezt a következő szintaxissal tehetjük meg:

module kocsi {
    requires motor;
}

A kocsi modulunk függ a motor modultól, vagyis ez a kocsi modul nem működőképes motor modul nélkül. Ezt grafikusan a következőképp ábrázolhatjuk:

Java 9 modul rendszer requires példa

A requires-ön túl van egy másik kulcsszavunk is, amit használhatunk. Ez a kulcsszó az exports, amivel azt adhatjuk meg, hogy a modulunk melyik csomagja (package-e) érhető el egy másik modulból. Minden, amit nem nevezünk meg az exports kulcsszóval, az alapértelmezés szerint nem exportálódik, vagyis rejtve marad más modulok előtt. Ez megoldja a bevezetőben említett potenciális kódelspagettiasodást. Ha kiegészítjük az előbbi példánkat ezzel, akkor ezt kapjuk:

module kocsi {
    exports hu.ak.tesla.kocsi;

    requires motor;
}

Java 9 modul rendszer exports példa

Mi fog történni a Java core osztályaival?

A jó hír az, hogy ezeket is feldarabolták modulokra, így sokkal könnyebben tudják majd a jövőben kibővíteni. Ez azt is lehetővé teszi, hogy az olyan platformokon, ahol szűkösek az erőforrások – például a mobil alkalmazások – ott az adott platformra fordított kódhoz csak a tényleg szükséges modulokat mellékeljük, ezzel tárhelyet, memóriát, hálózati adatforgalmat, vagy akár akkumulátor energiát is megtakarítva.

Amikor elindítunk egy moduláris Java programot, akkor a requires függőségi lánc mentén a JVM ellenőrzi, hogy minden modul megtalálható-e. Ez sokkal kényelmesebb nekünk, mint a korábbi Java verziókban, amikor a classpath paraméterben az összes függőséget fel kellett kézzel térképeznünk és sorolnunk.

Java modul rendszer konklúzió

kirakó ami a Java 9 modul rendszerét szemléltetiEzen új feature-ök segítségével újonnan írt programjainkban újabb szinten megvalósulhat az OOP egyik alapelve, az egységbezárás (encapsulation), aminek az előnyei régóta jól ismertek.

De mint láthattuk, a JAR-októl nem szabadulunk meg, továbbra is ez lesz a formája a library-k tárolásának, csak kibővítették a leíróképességét, ezzel sok új lehetőséget megnyitva előttünk.

Ebben a blog posztban nem sikerült minden részletre kitérni, mint például a névtelen modulokra illetve az automatikus modulokra. Az említett két kulcsszón kívül vannak még egyebek is, amikkel tovább finomítható a modulok összekapcsolása.

Tanfolyamunkon természetesen gyakorlati példákon keresztül megnézzük a legfontosabb lehetőségeit ennek az új nyelvi elemnek.

Ha szívesen olvasnál még többet a Java 9 modul rendszerről, akkor figyelmedbe ajánlom az angol wiki oldalát.

Java 9 újdonságai – 1. rész

Java 9 megjelenési dátuma

A Java 9 aktuális tervezett kiadási dátuma 2017. szeptember 21. Vagyis itt van a nyakunkon. Elég zűrös Java verzióról van szó, ami a határidőket illeti, ugyanis nem egyszer lett elhalasztva a kiadás dátuma. Ez talán nem is olyan meglepő, ha megnézzük, hogy milyen új feature-ök kerültek bele. Az elkövetkezendő blog posztokban sorra vesszük a leglényegesebb újdonságokat.

Java 9 oktatás az A&K Akadémián

Hogy lehet az, hogy mi már ezt a verziót oktatjuk, amikor még meg sem jelent?

A végleges hivatalos verzió még valóban nem jelent meg, de az early access (EA) verzió már egy ideje elérhető, amely mivel már túl vagyunk a feature complete dátumon, az összes feature-t tartalmazza, ami a végleges verzióban is benne lesz.

Be kell vallanom, hogy nagy mázlista vagy, ha most kezded el a Java-t tanulni, mert az egyik feature – a jshell – nagyban segíti az oktatást és a tanulást, főleg a kezdeti lépéseknél, mert nagyon szemléletes eszköz. De erről részletesen majd egy kicsit később. Először akadna itt egy kis játék…

A Java Platform modul rendszere (Jigsaw) – bevezető

Java 9 - Project JigsawA Java 9 legmeghatározóbb eleme a Project Jigsaw néven futó, a Java modularizációját célzó projekt, amely a többszöri release date csúsztatásért felelős.

Én személy szerint nem bánom, sőt, örülök is neki, hogy a projekt fejlesztése során tartott mérföldköveknél őszintén képesek voltak a projekt aktuális állapotára objektíven tekinteni, és azt mondani, hogy ez így nem elég jó.

Mint tudjuk, a Java nyelvnél erősen koncentrálnak a készítők a visszafele kompatibilitásra, vagyis hogy az új Java verziók, amennyire csak lehet, a korábbi verzióval fordított bájtkódot módosítás nélkül futtatni tudják. Persze ez nem volt mindig tartható.

A modularizáció megoldása a Java nyelv jövőjét tekintve esszenciális feladat. Ami most a Java 9-ben a nyelv részévé válik, az ezentúl mindig szerves részét fogja képezni, radikális módosítások eszközölése jelentős problémákat vetne fel. De úgy tűnik, hogy most már hamarosan tényleg lezárják ezt az alprojektet és az éles kiadás részét fogja képezni.

Miről is szól a Java Platform modul rendszere?

Eddig is volt lehetőség megírt kódunk alap szintű szervezésére. Ha arra gondolunk, hogy megírt osztályainkat már most is külön fájlokban tároljuk, illetve ezeket a fájlokat tudjuk csoportosítani mappákba, vagyis csomagokba (package-ekbe).

A modul rendszer lényege, hogy még egy absztrakciós szinttel kiegészítse ezt a csoportosítás.

Miért jó nekünk még egy absztrakciós szint?

Az alapvető probléma a Java nyelv hozzáférés módosítók limitált képességeiből adódik. Mindannyian ismerjük a public, protected, private és package private scope-okat. A public elérhetőségű osztályaink jelentik itt a problémát, ezek publikus metódusai ugyanis a publikus API-nk részét képezik, és ezáltal bármilyen más osztályból elérhetők. Ez a Java nyelv létrejötte óta hatalmas gubancokat okozott már a komplex rendszereknél, ugyanis amit a nyelv megenged, azt bizony a leleményes programozók ki is használják. Így történt ez ennél a nyelvi feature-nél is, ami átláthatatlan, szövevényes, spagetti kódot eredményezett. A spagetti kódnak súlyos hátrányai vannak.

JAR – JAR

A Java 9 előtt lényegében az egyetlen módszerünk az enkapszulációra a JAR-ok voltak. A JAR – vagyis Java ARchive – a programunk lefordított változatának egy ZIP fájlba tömörített változata, amit néhány metainformációval elláthattunk, mint például, hogy melyik osztály main metódusa a belépési pont. Ha például Java-ban JSON adatstruktúrákat szerettünk volna használni, akkor letöltöttük a megfelelő JAR fájlt, amiben minden osztály benne volt a JSON-ök kezeléséhez. Természetesen kedvünkre választhattunk az implementációk közül, mert több csapat is fejleszt saját JSON kezelőt.

Ezek után persze a programunk már csak ezen JSON kezelő JAR jelenlétében működőképes, amikor kiadjuk éles rendszerünket, akkor ezt a JAR-t se szabad elfelejtenünk.

Érdekesség

A „jar” egy angol főnév is, ami többek között dunsztos üveget jelent, amiben a lekvárt is eltesszük. A JAR elnevezés valószínűleg innen is kapta a nevét, csak mi nem lekvárt, hanem lefordított Java osztályokat teszünk el benne.

Dunsztos üvegek… dunsztos üvegek mindenütt!

Egy nagy rendszer esetén természetesen nem csak egy ilyen külső library-tól való függőségünk van, hanem több, illetve tovább bonyolítja a helyzetet, hogy egy külső library maga is lehet, hogy egy másik library-t igényel a működéséhez. Ezeket a függőségeket, más szóval dependenciákat, feloldani sokszor nem triviális feladat.

Eddig mi tévők lehettünk a JAR-ok támadása ellen?

Ennek kezelésére eddig a modern build toolok adtak elegáns megoldást, mint a Maven és a Gradle. Ezeknél a tooloknál ugyanis egy központi repository-ban számon tartják az összes publikus library (artifact) összes publikusan elérhető verzióját azok függőségeivel együtt. Ez lehetőséget biztosít számunka a build tool használata során, hogy csak azt tüntessük fel, hogy a mi aktuális projektünknek milyen külső library-kra van szüksége, és a dependenciák feloldását már maga a tool végezze. Ez azt jelenti, hogy ha például a JSON kezelő library függ egy dátum kezelő library-tól, akkor ha mi a saját projektünknél megjelöljük, hogy a JSON kezelő library-ra szükségünk van, akkor a tool automatikusan felderíti a függőségi láncolatot és feloldja a függőségeket, vagyis ebben az esetben letölti a projektünkhöz a dátum kezelő library-t is, ezzel működőképessé téve a projektünket. A JAR-ok önmagukban ezt a függőséget nem képesek kifejezni.

Ezt a súlyos problémát orvosolja a Java Platform modul rendszere, amelyről a következő blog posztban olvashatsz.