2D adatszerkezet modellezése Javában

…avagy hogyan lehet pozitív tulajdonság a gyengeség és a lustaság. 🙂

Arról a gyakran előforduló modellről van szó, amikor sorok és oszlopok metszeteiben rácselemek csücsülnek, melyek automatikusan létrejönnek a nekik megfelelő sor-oszlop párra való hivatkozáskor, valamint automatikusan megszűnnek soruk vagy oszlopuk törlése esetén.
A megvalósítás kézenfekvőnek tűnik: a sorokban/oszlopokban egy map-ben tároljuk az oszlopokkal/sorokkal indexelve a rácselemeket.

Valahogy így:
[cc_java]
public class Node {
}

public class Column {
}

public class Row {

protected final Map nodes = new HashMap();

public Map getNodes() {
return nodes;
}

}
[/cc_java]

Mi a gond ezzel?

  • Ha törlünk egy oszlopot, az összes sor map-jében még ott marad a rá mutató referencia, mint kulcs. Ez memory leakhez vezet: a szemétgyűjtő nem tudja eltakarítani a “zombi” oszlopokat és a hozzájuk tartozó, ugyancsak zombi rácselemeket. Ez ráadásul az adatstruktúra perzisztálásánál is problémát jelent.
  • Hogyan s mikor hozzuk létre a rácselemeket? Mindig, amikor létrehozunk egy oszlopot? Ezt jobban lehetne automatizálni, ráadásul ha nem minden rácspontba akarunk elemet tenni, fölöslegesen hozzuk létre ezeket.

Több megoldás is lehetséges, gondolhatnánk elsőre:

  • Gondolkodhatnánk egy címkézett gráfban, és a beépített kollekciók helyett alkalmazhatnánk valamiféle gráfkezelő könyvtárat, pl. a JGraphT-t, mely gondoskodik csúcs törlésekor a hozzá tartozó élekről.
  • Kibővíthetnénk a lista adatszerkezetet eseménykezelőkkel, melyek elem hozzáadásakor és törlésekor végrehajtanak egy megadott kódot.

Azonban a probléma megoldható a Java Collections Framework keretein belül, karöltve természetes kiegészítőjével, a Collections Generic API-val (a Commons Collections generikus változata). Nehogy már egy ilyen általános jelenséget ne tudjunk a megannyi hasznos absztrakciót tartalmazó Java nyelv elemeivel modellezni! A kulcsfogalom a gyenge referencia, mellyel befolyásolhatjuk a garbage collector működését, ezzel rábízva az elárvult elemek törlését.

Javában az alapértelmezett referencia erős (strong), de ezenkívül még létezik a gyenge (weak), lágy (soft) és fantom (phantom) referencia. A weak reference lényege, hogy amennyiben egy objektumra csak ilyenek mutatnak, a szemétgyűjtő teljes lelki nyugalommal eltakarítja. Nekünk pont ez a viselkedés jön kapóra: a map-ben gyenge referenciákat fogunk csak tárolni az oszlopokra/sorokra. (A másik két típus most nem érdekes számunkra, pedig azok is hasznosnak bizonyulhatnak más esetekben.)

A Collections API tartalmazza a WeakHashMap osztályt, mely a HashMap-hez hasonló, azonban kulcsai gyenge referenciák. A WeakHashMap ezeket átlátszóan kezeli, önmagától létrehozva és dereferálva nekünk őket. Így programunkban az automatikus törléshez csak két dolgot kell módosítanunk: HashMap helyett WeakHashMap-et használunk, és a kívánt pillanatban meghívjuk a garbage collectort.

A létrehozás pedig lusta map segítségével történik: a rácselemek példányosítása akkor és csak akkor történik, amikor először hivatkozunk az őket azonosító sor-oszlop párosra. Ehhez is kész eszközt kapunk a kezükbe a Collections Generic Libraryben: a LazyMap-et és az InstantiateFactory-t, melyek a Decorator és a Factory pattern alkalmazásával érik el céljukat.

Imhol a forráskód módosított része. Figyelemreméltó, hogy mennyi minden változott…
[cc_java]
public class Row {

protected final Map nodes = LazyMap.decorate(new WeakHashMap(), new InstantiateFactory(Node.class)); // !

}
[/cc_java]

És egy kis tesztosztály, a kiértékelés a kedves olvasó feladata:
[cc_java]
public class GridTest {

public static void main(String[] args) {
Grid grid = new Grid();
for (int i = 0; i < 2; i++) {
grid.getRows().add(new Row());
grid.getColumns().add(new Column());
}
for (Row row : grid.getRows()) {
for (Column column : grid.getColumns()) {
System.out.println(row.getNodes().get(column));
}
}
grid.getColumns().remove(1);
System.gc(); // !
for (Row row : grid.getRows()) {
for (Column column : row.getNodes().keySet()) {
System.out.println(row.getNodes().get(column));
}
}

}

}
[/cc_java]

Hát ilyen power a Collections Framework, amint már Stampie is [intlink id=”587″ type=”post”]rámutatott[/intlink], ami pedig esetleg hiányzik belőle, azt a Collections Generic pótolja. Mi a tanulság? Használjuk ki a nyelvi lehetőségeket, amelyek rendelkezésünkre állnak!

Dokumentumszerkesztés, ahogy Balage csinálja

Folytatva Stampie [[http://cubussapiens.hu/node/597|írását]], most szeretném én is a világ elé tárni az eszközök listáját, amely irodai jellegű munkáimat segíti (vagy gátolja, ez nem mindig triviális). A feladataim, melyek ilyen eszközöket követelnek, nagyjából hasonlóak, mint Stampie esetében, házi feladat dokumentációk, ritkán bemutatók egy-egy előadáshoz, illetve táblázatkezelés a pénzügyeim követésére, és néhány egyszeri számítás elvégzésére (ez alatt tipikusan a “mennyit fogok költeni karácsonyra” jellegű kalkulációkra gondolok).

Folytatva [intlink id=”597″ type=”post”]Stampie írását[/intlink], most szeretném én is a világ elé tárni az eszközök listáját, amely irodai jellegű munkáimat segíti (vagy gátolja, ez nem mindig triviális). A feladataim, melyek ilyen eszközöket követelnek, nagyjából hasonlóak, mint Stampie esetében, házi feladat dokumentációk, ritkán bemutatók egy-egy előadáshoz, illetve táblázatkezelés a pénzügyeim követésére, és néhány egyszeri számítás elvégzésére (ez alatt tipikusan a “mennyit fogok költeni karácsonyra” jellegű kalkulációkra gondolok).

Office csomagok

Linuxon nincs sok értelmesnek nevezhető alternatíva, próbálkoztam KOffice-al is, de a funkcionális gyengeségét nem ellensúlyozza a felhasználói felület egyszerűsége. Amit a jelenleg alpha állapotban lévő KOffice2-ről olvastam, a helyzet sokkal jobb lesz, de ebben az állapotban természetesen még nem alkalmas a komoly használatra.

Így jelenleg szinte kizárólagosan OpenOffice-t használok. Az általam ígényelt funkciókat tudja, a beépített PDF export meg biztosítja, hogy bárhol meg tudják tekinteni az alkotásaimat. A képletszerkesztője kifejezetten jó (tudom TeX-es kollegák most köhécselnek..), az ábrakészítője is használható, bár annál megesett, hogy káromkodva bezártam, és elővettem helyette az Inkscape-et, ami kifejezetten nem erre való, de mégis könnyebben megoldottam vele a feladatomat.

Néha felmerül olyan igény, hogy egy dokumentum jó lenne, ha interneten keresztűl megosztható lenne (megfelelő jogosultsággal, nem publikusan). Ekkor veszem elő a Google Docs nevű eszközt, amelyet az egyszerűség kedvéért Prism-ben futtatok. Ezt az eszközt 90%-ban táblázatkezelésre használtam, amivel meg vagyok elégedve, egyszerű formázásokat, és a megszokott parancsokat tudja. A dokumentumszerkesztő részét alig használtam párszor, de azt hiszem nem is fogom, egyszerűen túl keveset tud (egy képet nem sikerült rendesen beillesztenem a szöveg közé).

Egyéb eszközök

Én sajnos nem tudtam még eleget foglalkozni LaTeX-el, így ez nem szerepel a pallettámon, de DokuWiki-t előszeretettel használok. Bár jelenleg nincs összerakva erre offline rendszerem, de tervbe van véve..

Továbbá kiemelném még gyakran használt eszközként a KNotes nevezetű csodát, mely rövid emlékeztetőkre, feljegyzésekre kiválló. Támogat időzített figyelmeztetéseket, illetve opcionálisan RTF jellegű formázást is.

Körülbelül ennyi lenne. Jelenleg ezek az eszközök merülnek fel bennem hasonló feladat felmerülésekor, bár megjegyezném, ezek korántsem elégítik ki maradéktalanul az igényeimet, a közeljövőben (ahogy az időm engedi) szeretnék a témában változtatni, többek között jó lenne összerakni helyi jegyzetelésre egy wiki-t, illetve el kéne kezdenem foglalkozni LaTeX-el is.

Dokumentumszerkesztés OSX-en Stampie módra

Ez az írás egy tervezett sorozat első darabja: az ötlet (köszönet érte D-neenek) az volt, hogy írjuk le, hogy hogyan/miért úgy használjuk a gépünket különböző feladatok elvégzésére. Miután elméletben erről többen is írunk, ezért van rá esély, hogy a sorozat mások számára is hasznos lehet. Ha nem, akkor csak mi gondoltuk végig, hogy mit hogyan használunk, és ez segíthet abban, hogy még jobban összeszedjük a feladatainkat.

Ennyi bevezető után bele is csapnék a lecsóba: első alkalommal arról beszélnék beszélek írok, hogy mit használok dokumentumszerkesztésre. Illetve még inkább először is azt írnám le, hogy pontosan mit is értek itt dokumentumszerkesztésen. Első körben nagyjából azokat a feladatokat, amiket az Ms Office csomag elemeivel el lehet végezni (többé-kevésbé jó minőségben).

Kicsit részletesebben, illetve az általam végzett feladatokra szabva ez legfőképpen valamiféle szöveg készítése: a tipikus feladatok számomra mérési jegyzőkönyv, házi feladat, esetleg dokumentáció készítése, illetve egy-egy esetben prezentációt, illetve táblázatot készítek. Képeket, ábrákat lényegesen ritkábban készítek, ráadásul az egy külön kategóriát is alkothat a feladatok sokszínűsége miatt, ezért ebbe nem megyek most bele.

Office csomagok

A feladatokhoz nem csak egyféle programot használok, méghozzá azért, mert a különböző részfeladatoknál más-más igényeket kell kielégíteni. De az általánosságban elmondható, hogy Microsoft Office nincs a gépen (egy 2004-es Mac-es változatból volt egy demo fenn, de szerintem egyszer sem indítottam még el…). Office csomagból 2+1-féle van telepítve: egyrészt megvettem az Apple iWork csomagját, másfelől az OpenOffice csomagot használom (ebből nagyjából párhuzamosan használom a hivatalos OO.o buildet, illetve egy alternatív, régebb óta az OSX-re fejlesztett NeoOffice csomagot).

A csomagok közti választás viszonylag egyértelmű: szövegszerkesztési feladatokhoz sokkal egyértelműbb, hogy az OpenOffice/NeoOffice csomag a nyerő, mert a saját formátuma is hordozható, és mellesleg ismerik valamilyen szinten a Word dokumentumot is (meglepően jól egyébként – persze messze nem 100%-osan). Az pedig meglehetősen általános felhasználási scenario, hogy közösen készül egy házi feladat – és a partner nem (mindig) ismeri a LaTeXet… Szóval marad a kompromisszum.

Érdekes módon prezentációkészítésre ha csak egy lehetőségem van, mindig az Apple programját használom. Ez talán azzal magyarázható, hogy a prezentációim gyakorlatilag mindig egyedül készülnek, nem annyira gond az, hogy meg kell osztani másokkal, és így jobban merek engedni a kompatibilitásból. De ez a kompatibilitási probléma azért a bemutathatóságot nem befolyásolja: ugyan a ppt-exportálási opcióban nem hiszek (ha az import sem igazán tökéletesen működik, ez miért lenne jobb?), de a pdf export minden igényt kielégít. Egy prezentációnak amúgy sem az animációkról kell, hogy szóljon – minden másra meg tökéletesen megfelelő formátum a pdf.

De akkor miért is áldozom fel a hordozhatóságot (legalábbis részben)? Azért, mert az Apple Keynote egy hihetetlenül kiforrott program, jól megtervezett felhasználói felülettel. Tele van apró, ügyes megvalósításokkal, mint az elemek egymáshoz való igazítása (bármelyik szélhez, illetve középvonalhoz lehet illeszteni, szükség esetén akár többhöz egyszerre) vagy éppen az Inspector logika a formázások kezelésére. Ez az Inspector egy külön ablak, amiben a formázásokat lehet egységesen kezelni. Hasonló szerepet tölt be, mint az MS Office 2007-ben a Szalag: tematikusan rendezetten tárja elénk a funkciókat, de ugyanakkor kevésbé forradalmian új a felület, így gyorsabb megszokni… Mindenesetre, akinek lehetősége van az iWorks 08-at kipróbálni (értsd: mindenki, akinek Mac OSX van a gépére telepítve), érdemes megpróbálni.

Táblázatkezelés: na, itt nincs egyértelmű preferenciám. Tetszik a Numbers (iWork) új megoldása, miszerint egy munkalaphoz több táblázat is kapcsolódhat, stb., de ennél a pontnál azért lényegesen fontosabb a hordozhatóság, valamint az is problémát okozhat, hogy a Numbers még csak az 1.0-s verziónál tart, szóval nagyon kiforrottnak még nem tekinthető, és ráadásul a funkciókészlete is korlátozott egy kissé – de ha csak alapdolgokról van szó, de azt nagyon színesen-szagosan akarjuk végrehajtani, arra kiváló eszköz (lehet – de részletesen nem teszteltem még).

Egyéb szerkesztőeszközök

Persze jól kifejlett kockafejként nem csak az irodai szoftvercsomagokat használom szerkesztésre. Egyik érdekes koncepció, amit elég hosszú ideje próbálgatok, az a wiki alapú tárolás. Előnyei közé sorolható az abszolút hordozhatóság: megfelelően telepítve akár még a saját gépemre sincs szükség a szerkesztéshez – hátrányai közé viszont az, hogy egy saját leíró nyelvet kell hozzá megtanulni – ami a wikik között nem is hordozható. Az én személyes preferenciám a Dokuwiki – ez egyszerű dokumentáció írására kiváló, de komoly, sokfelhasználós rendszernek (a sok itt nem a több szinonímájaként szerepel – egy pár fős Dokuwiki telepítést évek óta – sztem – sikerrel üzemeltetek) véleményem szerint nem igazán alkalmas.

És végezetül megemlíteném még azt a tényt is, hogy LaTeX szerkesztést is csinálok időnként – korábban erre az Aquamacs program AucTeX környzetét használtam. Ügyes volt benne, hogy inline previewt képes készíteni sokféle formázáshoz, valamint a gyárilag egészen korrektül összelőtt pdfsync támogatás – ez a támogatás annyit jelent, hogy a TeX forrás és a generált pdf fájl kapcsolódó részei között gyorsan tudtam ugrálni.

A múlt idő viszont azért van, hogy az Emacs sosem állt túlságosan közel hozzám, és az Aquamacs nem Macesítette eléggé, hogy könnyen megszokhassam. Viszont helyette segítségemre sietett a fejlesztők abszolút nehéz súlyú eszköze – az Eclipse. Igen, abszolút nehéz súlyú, mert gyakorlatilag csak kávéfőzésre nem lehet megtanítani (ok, ilyen alapon az Emacs-ről is lehetne beszélni, lásd még az XKCD kapcsolódó epizódját), valamint ehhez jobban is értek. Szóval a TeXlipse környezet mellett döntöttem, és ebben próbáltam összelőni a megfelelő kapcsolatot az Eclipse és az általam használt pdfsync kompatibilis szerkesztőprogram között. Némi dokumentációböngészés, valamint egy gyors shell script megírása után (pontosabban a dokumentációban szereplő módosítása után, de ez aztán senkit sem érdekel) már működött is. A részleteket külön írásban fogom közzétenni (ide behivatkozva), hogy annyira ne dobjam szét az írás témáját – noha ez mostanra már nyilván tökéletesen sikerült…

Nagyjából ez az arzenálom, ami a dokumentumszerkesztést illeti – nyilván még egynéhány segédprogrammal támogatom meg ezt az arzenált, hogy a káosz teljes legyen – de ezt megintcsak nem akarom túl részletesen mellékelni, mert akkor nyilván az írás lényegét borítanám. Remélem, van, akinek segített valamit ez az írás – bár gyanítom, hogy aki elolvasta, mostanra a pokolba kíván, hogy ilyenekkel húzom az idejét – de úgy érzem, lesz ez még jobb is. Főleg, ha olyan témát választok, amiről többet tudok írni. (Akkor még több időt fogok elrabolni azoktól, akik végigolvassák, amit írtam… 🙂 )

Update: elkészült a [intlink id=”604″ type=”post”]LaTeX szerkesztésről[/intlink] az ígért cikk.

Platformfüggő fejlesztés Java-ban

Ritka eset, hogy szükségünk van rá, de velem megesett. Szükségem volt a java-t futtató rendszer típusára és architektúrájára. Ráadásul futásidőben. Az ok egyszerű. SWT alkalmazást fejlesztek, és nem akarok minden platformra külön kiadást csinálni, ne a felhasználó találja ki, hogy milyen rendszert futtat. De ez mellékes, a probléma adott, futásidőben kell meghatározni, melyik platformfüggő csomagot töltsem be?

Ritka eset, hogy szükségünk van rá, de velem megesett. Szükségem volt a java-t futtató rendszer típusára és architektúrájára. Ráadásul futásidőben. Az ok egyszerű. SWT alkalmazást fejlesztek, és nem akarok minden platformra külön kiadást csinálni, ne a felhasználó találja ki, hogy milyen rendszert futtat. De ez mellékes, a probléma adott, futásidőben kell meghatározni, melyik platformfüggő csomagot töltsem be?

Egyszerű a megoldás – írják a lelkes és segítőkész, ám nem teljesen tájékozott fórumozók – A System.getProperty() paranccsal le lehet kérni a szabványosított, így minden JVM-ben létező “os.name”, “os.version” és “os.arch” tulajdonságokat, amikkel egyszerű beazonosítani a futtató rendszert. A szabvány valóban rögziti, hogy milyen tulajdonságmezőknek kell lenniük, azonban ezeknek a lehetséges értékeit nem. Most jön csak a szenvedés.

A kisebb problémát az “os.name” mező jelenti, ami többé-kevésbé konzisztens értékeket ad vissza a Sun és az alternatívEzen a ponton megjegyezném, hogy csak abból az egyszerű tényből kifolyólag hívom a nem a Sun által készített JVM-eket alternatívoknak, mert a Java nyelv szabványát a Sun saját szabványügyi hivatala adja ki. Az igazsághoz hozzátartozik, hogy az említett hivatal úgy jött létre, hogy az ISO visszadobta a Java szabványt, és a Sun ezt a megoldást találta ki a nyelv szabványosítására. JVM-eknél is. Ezek közül egyetlen kakukktojás a Windows (mi más), aminek minden verziójához tartozik egy saját “os.name” érték, és ezen felül az “os.version” is tartalmazza a rendszer ritkábban látott verziószámát. Hogy ezzel mi a gond? Csupán az, hogy az 1.5-ös JVM a Vista-t “Windows NT (Unknown)”-nak hívja, míg az 1.6-os rendesen “Windows Vista”-nak. Értelmesebb lett volna, ha a “Mac OS” / “Mac OS X”-hez hasonlóan külön jelzik a rendszer két fő ágát, és a verziószám ad bővebb információt a rendszer mibenlétéről. Más rendszerek (pl. Linux) esetén a helyzet jobb, ott tényleg nincs eltérés a verziók között. Íme egy rövid felsorolás az “os.name” lehetséges értékeiről: [[http://mindprod.com/jgloss/properties.html#OSNAME]]

Ennél sokkal komolyabb fejtörést okoz a rendszer architektúrájának meghatározása. Ennél rendkívül változatos értékeket tapasztaltam különböző gépeken, rendszertől függetlenül. Még a Sun JVM-ek se konzisztensek ebben. Az még nem túlzottan meglepő, hogy a 64 bites, intel-alapú rendszeremet “amd64”-nek hívja, hiszen a disztróm hivatalosan amd64-re van fordítva. Más gépeken viszont szinte véletlenszerűen kerül elő a fent említett megnevezés mellett “x86_64”, illetve “x64” is. További érdekesség, hogy mivel “Mac OS X”-en még nincs 64 bites 1.6os Java, így azt “i386”-osnak jelzi. Ebből látszik, hogy a kérdéses mező nem a rendszer, hanem a JVM tulajdonságát adja vissza. Ennél a pontnál elérkeztünk a régebbi, 32 bites rendszerekhez, ami lehet “x86”, “i386”, “i686”, és hasonszőrű társai bármelyike. Utolsóként megemlíteném a Power PC architektúrát, ami lehet “ppc”, vagy “PowerPC” elnevezésű is.

A fenti mezők értékeire vonatkozólag láthatóan a Sun berkein belül sincs megegyezés, különböző rendszereken, vagy akár csak különböző verziók más-más értéket adhatnak. A legnagyobb probléma mégis az, hogy ez nincs sehol dokumentálva! A néhány kicsit használható gyűjteményt lelkes felhasználók hozták össze, de ezek hiányosak, és/vagy régen frissültek. Íme az általam talált legjobb, ami határozottan kevés: [[http://lopica.sourceforge.net/os.html]].

Végső megoldás lehet kitaposott úton járni, azaz felhasználni egy már létező megoldást, ami talán megmondja, mégis milyen rendszeren vagyok. Hasonló megoldásokban szinte kizátólagosan az [[http://lopica.sourceforge.net/os.html|Ant]] projekt kerül szóba, ami tartalmaz egy osztályt, mely egy szimpla enumerált értéket állít elő a létező operációs rendszereknek megfelelően. Az “os.arch” változót pedig csak nemes egyszerűséggel kivezeti a felhasználó számára az összes JVM tulajdonsággal egyetemben. Továbbá az Ant csak fordítás-idejű megoldást kínál, ezzel én nem lettem okosabb.

Végső elkeseredésemben azon kezdtem agyalni, hogy a program első futásakor végigpróbálgatom az összes rendelkezésre álló SWT lib-et, és amelyik nem dob kivételt, az működik. Ha ehhez hozzávesszük, hogy felismerhetünk pár “os.name”, “os.arch” értéket, akkor nem is olyan elvetemült ötlet.

(Update) A megoldás

[[http://cubussapiens.hu/user/30|Happy]] [[http://cubussapiens.hu/node/593#comment-117|hozzászólása]] alapján a zseniálisan egyszerű megoldás:


package proba;

import org.eclipse.core.runtime.internal.adaptor.EclipseEnvironmentInfo;

public class EnvInfo {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
EclipseEnvironmentInfo eei = EclipseEnvironmentInfo.getDefault();
System.out.println(eei.getWS()+"."+eei.getOS()+"."+eei.getOSArch());
System.out.println(eei.getNL());
}

}

, ami nálam a következő kimenetet adja:


linux.gtk.x86_64
hu_HU

Stampie jól gondolta, ezt a problémát már megoldották az Eclipse-ben, csak akkor én ezt nem találtam meg. Az [[http://mobius.inria.fr/eclipse-doc/org/eclipse/core/runtime/internal/adaptor/EclipseEnvironmentInfo.html|EclipseEnvironmentInfo]] osztály a org.eclipse.osgi_3.4.0.v20080605-1900.jar csomagban található meg, és ez az swt csomagok elnevezésével kompatiblis jelölésekkel adja meg a rendszer jellemzőit.

Java rendezés – avagy hogyan ne tegyük

A Java Collection könyvtára remek munka – könnyen használható, mégis sokoldalú API-t biztosít. Megfelelő listákhoz, gráfreprezentációkhoz és feltételezem, hogy még sok minden egyébhez.

Ehhez képest nem (sokkal) bonyolultabb kezelni, mint egy tömböt. Sőt, a C nyelv tömbkezeléséhez képest sokkal egyszerűbb is… 🙂

Ami nekem különösen tetszik, az a beépített rendezési lehetőségek: a Collections.sort statikus metódus használatával saját Comparator segítségével lehet rendezéseket definiálni.

A Java Collection könyvtára remek munka – könnyen használható, mégis sokoldalú API-t biztosít. Megfelelő listákhoz, gráfreprezentációkhoz és feltételezem, hogy még sok minden egyébhez.

Ehhez képest nem (sokkal) bonyolultabb kezelni, mint egy tömböt. Sőt, a C nyelv tömbkezeléséhez képest sokkal egyszerűbb is… 🙂

Ami nekem különösen tetszik, az a beépített rendezési lehetőségek: a Collections.sort statikus metódus használatával saját Comparator segítségével lehet rendezéseket definiálni.

Ezután szinte magától adódott már, hogy amikor nekem egy objektumhierarchia reprezentálására van szükségem, akkor ahhoz ezt a beépített könyvtári osztályokat használjam. Az általam használt algoritmus (most ezt nem részletezném a szükséges tömörség miatt) feltételezte, hogy a hierarchia elemeit felülről lefelé adom be (azaz mindig előbb az őst adom be, és csak utána a leszármazottakat). Miután ezt a bemenetemről nem tudtam feltételezni, ezért adódott a dolog, írjunk rá egy rendezést, saját komparátorral.

public int compare(IEntity i1, IEntity i2){
if (i1.isSubtypeOf(i2)) return -1;
else if (i2.isSubtypeOf(i1)) return 1;
else return i1.getFullyQualifiedName().compareTo(i2.getFullyQualifiedName());
}

Szép, olvasható kód, és pontosan azt írja le, amit szeretnék. Igaz? Hisz itt látszik, hogy szépen az öröklési hierarchia szerint rendezi a mezőket. Amikor ezt a kódot úgy nagyjából egy hónapja megírtam, akkor működött is – igaz, csak erőteljesen korlátozott tesztelés volt, mert ez csak egy nagyon parányi része volt a dolognak, amit csináltam, és utána pedig egy kicsit ezt a részt jegeltem.

Ehhez képest most jött a feketeleves, hogy amikor egy kicsit változott a környezet, amiben az új funkciót teszteltem, kaptam egy hatalmas nullpointer exception-t. Remek…

Némi játék után (ugyanis a futási környezet lenyelte a kivételt, és csak az üzetenet jelenítette meg, ami nullpointer exception-nél mindig “”, azaz nem látszik), kiderítettem, hogy a gondot az okozza, hogy egy objektum kapcsán próbálok hivatkozni az ősére egy saját adatszerkezetben, de ebbe az adatszerkezetbe nem került bele. Ez pedig csak úgy lehetséges, ha az eredeti rendezésnél valami miatt a leszármazott előbbre kerül, mint az őse (általában nem, de az én programomban igen).

Kíváncsi vagyok, hogy hány ember látja, hogy hol is van a probléma a fenti komparátorral, hogyan jöhet össze, hogy a beépített Java sort algoritmus is ezt a rossz eredményt adja ki…

Tökéletes magyarázatom nincs a problémára, de a hiba oka nagyjából két dolog lehet. Egyfelől a definiált komparátor csak egy részben rendezéshez ad megfelelő támpontot, másrészt a Java feltehetőleg a qsort algoritmust használja, ami nem végzi el az összehasonlítást az összes párra, így tipikusan nem hasonlít össze minden egyes elemet minden másik elemmel.

Van még egy tényező, ami a rendezési eljárást még kevésbé egyértelművé teszi, ez pedig az egymástól akár teljesen független ágak a hierarchiában. Az eredeti példáimban ezekből kevés volt, míg a példa, amin kiakadt a program, viszonylag sok, egymástól független ágat tartalmazott, ezzel jócskán megnehezítve az algoritmus munkáját.

Ötletem nem volt, hogyan kellene működővé varázsolni a rendezést egy átírt komparátorral, ezért más, korábbi tanulmányokhoz nyúltam vissza: a bsz-en/algoritmuselméleten bemutatott topologikus rendezéshez nyúltam vissza. Ugyan az jóval erősebb rendezési feltétel, mint amire nekem konkrétan szükségem volna, de ezért cserébe viszonylag kevés munkával lekódolható, és nem is túlságosan lassú (jó, ez persze relatív).

Persze ennek a kódolásakor is segítségemre volt a Java Collections API, még így is kevesebb, mint 20 sor volt a teljes rendezési eljárás, amit még zárásnak beszúrok az írás végé elé.

/**
* Creates a topological ordering on the modelelements
*/
private List orderModelElements(List elementsToOrdered){

List orderedElements = new ArrayList();
while (!elementsToOrdered.isEmpty()){
//The currentLevel is needed in order to remove all the elements that
//do not have any ancestors, not only the first one
//If we delete the elements straight from the elementsToOrdered list,
//the iterator in the for cycle will not work.
List currentLevel = new ArrayList();
for (IEntity element : elementsToOrdered){
Collection ancestors = new ArrayList();
ancestors.addAll(element.getSupertypes());
ancestors.retainAll(entities);//remove all non-entities
if (ancestors!=null && orderedElements.containsAll(ancestors)){
currentLevel.add(element);
}
}
orderedElements.addAll(currentLevel);
elementsToOrdered.removeAll(currentLevel);
}
return orderedElements;
}

És hogy miért is írtam ezt így ide meg? Ennek két oka is van: egyrészt egy nagyon önző, magamat szem előtt tartó indok: ha valaki el tudja esetleg mondani nekem, hogyan kellene a komparátort kijavítani, esetleg részletesebben elmagyarázza csak, hogy miért is hasal el az a komparátor a quick sorton (ha quick sort), nagy örömmel veszem akármilyen formában a reakcióját. Másfelől pedig okulásnak is akart menni mások számára, hogy esetleg nekik ne kelljen órákat bogarászniuk egy pár ezer soros kódbázisban, hogy pontosan hogyan is adódhat az a nullpointerexception.