További pdfTeX gyöngyszemek

A múltkor beszéltem arról, hogyan érdemes szerintem [intlink id=”604″ type=”post”]LaTeX szerkesztést végezni OSX-en[/intlink] Eclipse kiterjesztés segítségével. Azóta némi további érdekességet sikerült összeszedni a témáról – és az ötletek szerintem a TeX környezetek többségére alkalmazhatóak.

Egyik alapvető érdekesség, hogy a pdfTeX motor csak a jpg, png és pdf fájlokat támogatja képfájlként. Ez egyfelől jó (lehet), mert bizonyos képek könnyebben elérhetőek ilyen formátumban, másrészt viszont rossz, mert ezek nem szerkeszthetők vektorosan olyan egyszerűen, ha úgy hozná a sors. Ezért lenne jó vagy svg vagy pedig eps támogatás hozzá.

EPS támogatás beizzítása pdfTeX motorhoz

Abban az esetben, ha az ember gépére telepítve van az imageMagick program, annak a convert parancsát ki lehet hívogatni, és ezzel szinte bármilyen formátumból bármilyen formátumot elő lehet állítani. A megoldás nyilvánvaló előnye, hogy hihetetlenül rugalmas, hátránya, hogy igényli a convert parancs jelenlétét a rendszerben – azaz a hordozhatóság csökken. A következő parancs megmutatja, hogy milyen jellegű utasítást kell kiadni a LaTeX preambulumban:

[cc]\DeclareGraphicsRule{.tif}{png}{.png}{`convert #1 `dirname #1`/`basename #1 .tif`.png}[/cc]

A DeclareGraphicsRule parancs során meg kell adni fájlkiterjesztést, amire figyelni kell, egy típust, egy beolvasási fájlkiterjesztést és egy parancsot. Részletek a következő pdf fájlban: http://www.ctan.org/tex-archive/macros/latex/required/graphics/grfguide.pdf

De tegyük fel, hogy eggyel hordozhatóbb megoldást szeretnénk. Itt jöhet szóba az az opció, hogy a legtöbb TeX disztribúció tartalmaz egy epstolatex parancssori alkalmazást – amint a neve is mutatja, ez egyszerűen eps fájlokat konvertál pdf-re. Külön előnye a programnak, hogy van egy package, ami ennek az egyszerűbb felparaméterezését teszi lehetővé. A package a könnyen megjegyezhető epstopdf névre hallgat.

Az alapértelmezett könyvtárstruktúra és az epstopdf parancs elérhetősége esetén mindössze a package-et kell importálni, és működik, némileg komplexebb felállás esetén a megoldás tovább hegeszthető. Én például azt valósítottam meg, hogy vegye figyelembe az img, src és temp mappákat, amikbe dolgozom, és miközben a LaTeX könyvtára az src, az eps-t az img-ből vegye, míg a pdf-et a temp-be dobja. Ezt a következőképpen sikerült elérni:

  1. Először is megadtam a preambulumban a [cci_latex]\epstopdfsetup{outdir=../temp/,verbose}[/cci_latex] parancsot – ez a temp mappába teszi a pdf-et, és a verbose segítségével kicsit részletesebben kifejti a hasfájásait…
  2. És ezután a képek beszúrásánál az [cci_latex]\includegraphics[width=10cm]{../img/csp.eps}[/cci_latex] módon adom meg a képfájlokat.

Ezután már a rendszer megfelelő hegesztés után elkezd működni. Az epstopdf package dokumentációja alapján a package-et rá lehet venni a DeclareGraphicsRule-lal, hogy mégse az eps2pdf-et használja, vagy ne az alapértelmezett paraméterezéssel.

PDF-specifikus beállítások a rendszerben

Ha csak simán pdfTeX-hel lefordítjuk a LaTeX forrásunkat, akkor már egy használható pdf fájlt kapunk – lehet böngészni meg továbbítani. De mi van, ha mi szeretnénk néhány fancy pdf feature-t kihasználni (na, ez szép hunglish lett 🙂 )? Tipikusan olyasmikre gondoltam most, mint az, hogy a pdf metaadatokat töltsük ki, illetve tegyük klikkelhetővé a hivatkozásokat, valamint a pdf megjelenítő számára is tegyük elérhetővé a dokumentum szerkezetét. Nem hangzik túl extrának, de nagyban növelheti az online olvashatóságot (aki kinyomtatja, annak nyilvánvalóan nem számít…)

Na, csapjunk is bele a lecsóba: a hyperref package jelenti a megoldást a problémára. A bekapcsolt állapot mellett automatikusan klikkelhetővé válnak a hivatkozások, és értelmezni fogja a dokumentum szerkezetét. A metaadatokat nem tölti ki azért automatikusan (noha nem lenne rossz 🙂 ). Mindenesetre ezeket a hyperref csomag opcionális paraméterei között lehet megadni, vagy pedig a hypersetup parancs segítségével. Részletek itt: http://andy-roberts.net/misc/latex/pdftutorial.html

Két fontos megjegyzés a hyperref használatával kapcsolatban: egyrészt opcionális paraméterről van ugyan szó, mégis érdemes megadni a fordítót (esetemben pdftex), ugyanis többféleképpen is keletkezhet pdf a kimeneten, és nem lehet ránézésre eldönteni, hogy melyik fog következni. Az előző oldalon szerepelnek a használható fordítók.

A másik: a hyperref csomagot olyan későn importáljuk, amennyire csak tudjuk (gyk. csak azt tegyük utána, amit feltétlenül szükséges), mert ellenkező esetben sok-sok warningot kapunk, hogy bizonyos hivatkozások megkettőződtek, stb. ami nem könnyíti meg a tényleges hibák felderítését. Ha a végére kerül, akkor ez a probléma nem lép fel (de legalábbis kevésbé). Igen, szép dolog a makrónyelv, ott ilyen szép kis függőségek is kialakulhatnak.

Java kimenet UTF-8 kódolásban – második felvonás

Ezt nem lehet megunni, újra és újra hasonló problémákba ütközök. Most kivételesen nem parancssorban kellett UTF-8 kimenetet generálnom, hanem a Java 6 beépített HTTP szerveréről a kliens felé adott generált válaszban merült fel ugyanez a probléma. A különös az volt, hogy működött, mindenféle konverzió nélkül, tehát teljesen nyugodt voltam, hogy ez a kódrészletet jól írtam meg. Órákba tellet rájönnöm, hogy ez a feltételezés hibás volt.

Ezt nem lehet megunni, újra és újra hasonló problémákba ütközök. Most kivételesen nem parancssorban kellett UTF-8 kimenetet generálnom, hanem a Java 6 beépített HTTP szerveréről a kliens felé adott generált válaszban merült fel ugyanez a probléma. A különös az volt, hogy működött, mindenféle konverzió nélkül, tehát teljesen nyugodt voltam, hogy ez a kódrészletet jól írtam meg. Órákba tellet rájönnöm, hogy ez a feltételezés hibás volt.

A problémakör egyszerű, bár érdekes. Widget-alapú AJAX-os webalkalmazást fejlesztek az önálló laboromhoz, de erről később. A lényeg az, hogy amikor a felhasználó csatlakozik a szerverhez, az egy generált HTML oldallal válaszol, és minden további feladatra JavaScript kódot küld, amit a kliens lefuttat. A javascript kódban a szövegeket előrelátóan Base64 kódolásban vittem át, így ezekkel probléma nem volt. Nem ez volt a helyzet azonban a generált HTML kóddal. Arra lettem figyelmes, hogy a webalkalmazást megváltoztatva a böngészőben nem jelenik meg semmi..

A helyzet felettébb furcsa volt, a generálás során nem merült fel hiba, sőt, a generált kimenet a szerver oldalon jó volt és az átvitel sem dobott kivételt. A kliens oldalon megérkeztek a fejléc sorai, de nem kapott kimenetet. Nem értettem a problémát, hiszen addig jó volt, és azon a részen nem változtattam. Hosszas próbálgatás és debugolás után rájöttem, hogy a hiba megjelenése erősen korrelál azzal a ténnyel, hogy a generált kimenetben szerepel-e ékezetes karakter. Ekkor merült fel bennem, hogy a probléma a kódolással van.

Tehát, a megoldás az, hogy előkeresem a [intlink id=”556″ type=”post”]korábbi cikkemet[/intlink], és az ott szereplő megoldást extrapolálom az én esetemre. Azaz, amikor a HttpExchange kimeneti adatfolyamát lekérem, egy egyszerű PrintStream helyett a következő módon wrappolom, és a böngészőt is értesítem róla, hogy milyen kódolásban kap adatot:


PrintStream out = new PrintStream(arg0.getResponseBody(),true,"UTF-8");
arg0.getResponseHeaders().add("Content-Type","text/html; charset=UTF-8");

A nagyobbik fejtörést az okozta, hogy még így se működött, a hiba ugyanaz maradt, csak annyi változott, hogy a Web dev toolbar már kijelezte az újonann beadott header sort is. Ekkor megakadt a szemem a következő soron:


arg0.sendResponseHeaders(200, text.toCharArray().length);
out.print(text);

Ezzel ugye elküldöm a kliensnek hibakódot (200 = OK), illetve a küldött tartalom hosszát. Ez automatikusan hozzáad egy “Content-Length” sort a header-ekhez. Amikor ezt írtam, jó ötletnek tünt. De! A java belső UCS kódolásában a karakterek száma nem egyezik meg a byte-ok számával a karakterlánc UTF-8 -beli kódolásában. A megoldás: a sendResponseHeaders() metódus második argumentuma legyen 0. Ez azt jelenti, hogy tetszőleges hosszúságú tartalmat küldhetünk el, a bytesorozat végét az jelzi, hogy bezárjuk a kapcsolatot.

Így tényleg működik. csak az érdekesség kedvéért egy rövid idézet a HttpExchange dokumentációjából:

"If the call to sendResponseHeaders() specified a fixed response body length, then the exact number of bytes specified in that call must be written to this stream. If too many bytes are written, then write() will throw an IOException. If too few bytes are written then the stream close() will throw an IOException. In both cases, the exchange is aborted and the underlying TCP connection closed."

Hogy mi ezzel a probléma? Csak az, hogy a dokumentáció szerint kivételt kellett volna kapnom, amikor úgy zárom le, hogy a küldött byte-ok száma nem egyezik az előre megadottal. Ez viszont nem történt meg, hanem egy furcsa, és visszakövethetetlen hibajelenségbe ütköztem. Talán gyorsabban megtalálom a hibát, ha kapok egy értelmes hibaüzenetet.

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!

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.