tifyty

pure Java, what else ?

Havi archívumok: augusztus 2013

Hogyan hozzunk létre példányt a System osztályból?

Ez a kérdés mindenféle fórumokon elég sokszor előfordul. Legutóbb a LinkedIn-en láttam, és gondoltam egy kicsit érdemes körbejárni a témát. Általában kétféle válasz szokott jönni a kérdésre: vizsgáljuk meg mind a kettőt, és aztán nézzük meg, hogy hova vezetnek ezek a gondolatok.

A kérdés maga azért vetődik fel, mert a System osztály egy utility class, amelyiknek csak statikus metódusai és field-jei vannak, és nem arra készült, hogy példányt hozzunk létre belőle. Ennek okán van is egy argumentum nélküli, privát metódusa, ami miatt nem lehet olyan módon példányosítani, hogy

System system = new System();

mert fordítási hibát kapunk.

Válasz#1, használjunk reflection-t!

Constructor<System> constructor = System.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
System system = (System) constructor.newInstance(null);

Ez egy nagyon egyenes válasz, a kérdésre ad választ és elindíthat azon az úton, hogy motiváljon a reflection megtanulására. De ennél többet ez a válasz nem ad, és aki a címben szereplő kérdést teszi fel, könnyen lehet, hogy nem a megfelelő irányba indul el a Java használata során.

Azok a válaszolók, akik inkább akarnak oktatni, és a helyes irányba terelni a következő választ adják:

Válasz#2, Ne tedd!

Miért akarnánk egy példányt a System osztályból? Nincs semmi értelme. A SUN mérnökei mindent megtettek, hogy ne tudjál belőle példányt létrehozni.

Valóban? Mindent megtettek?

Nos, igen. Mindent, amit értelmesen tenni érdemes. De persze a paranoiások számára lehet többet is.

Hogyan védhetjük meg a utility osztályokat?

A System osztály úgy van védve, ahogyan, és hogy miért nem jobban, arra a cikk végén még visszatérünk. De ezen túl vannak még egyéb lehetőségek is. A lehetőségek, amik eszembe jutottak:

  1. Dokumentáljuk, hogy az osztály nem arra van, hogy példányosítsák. Azt gondolom, hogy már ez is sokat segít, de nem teljes megoldás, és nem állhatunk meg itt, mert előfordulhat, hogy valaki nem olvassa el a dokumentációt, és ez bocsánatos bűn.
  2. Készítsünk egy argumentum nélküli privát konstruktor. Ez meggátolja az egyszerű felhasználót, hogy csak úgy létrehozzon egy példányt. Ha volt annyira botor, hogy nem olvasta el a dokumentációt, akkor is megállítja az a tény, hogy a fordító nem fordítja le a new UtilityClass kódot. De persze ettől még használhat reflection-t. Persze kit érdekel? Aki hülye, haljon meg. De most csak a kíváncsiság kedvvért, minket érdekel.
  3. Ne hívjuk meg ezt a privát konstruktor semmilyen statikus metdusból. Elég triviálisnak tűnik, hiszen ezzel éppen magunkat vágnánk pofon.
  4. Legyen az osztály final. Miért segít ez? Ez nem gátolja meg a direkt felhasználót, hogy reflection-nel hozzon létre egy példányt, de megakadályozza, hogy leszármaztassa az osztályt, és utána a leszármazott osztályból hozzon létre egy példány reflection-nel. Persze mondhatjuk, hogy megérdemli, ha ilyen speciális eszközökhöz nyúl, viszont ha az osztály final akkor legalább azt az ártatlan programozót védjük, aki a leszármazott osztályt használná és nem is tudja, hogy olyan osztály példányát hozta létre, amit nem kellett volna. Amúgy meg az utility osztályban csak statikus metódusok vannak, amik nem öröklődnek: minek a leszármazott. Maximum a rossz géneket örökli.
  5. Végül, de nem utolsó sorban (itt a kegyelemdöfés!) dobjunk egy run-time kivételt a privát konstruktorból. Még ha el is kapja ezt a kivételt a reflection-nel operáló kód, a félkész objektumhoz nem fér hozzá, mert nincs rá referenciája. A konstruktor így nem csak nem hívható meg egyszerűen, de ha reflection-nel mégis meghívják: nem tér vissza.

Ezek azok a trükkök, amelyeket egy utility osztály védelmére lehet használni. Lehetnek még más trükkök is, de ezek az eszközök is elegendőek kellenek, hogy legyenek. A SUN mérnökei csak annyit tettek, hogy az osztály final és a konstruktor private. Valószínűleg úgy gondolták, hogy minden egyéb csak túlpörgés. Én meg hajlok arra, hogy egyetértsek velük.

Bónusz kérdés oroszlánszívűeknek:

Hogyan hozol létre egy példányt egy olyan osztályból, amelyik a fent felsorolt ÖSSZES védelemmel rendelkezik ez ellen?

Google ImmutableMap

A Google Guava csomagjában több hasznos dolog is található. Most az ImmutableMap osztályról elménckedem kicsit.

Mi is a mutátor, és az immutable

Egyszerű: mutátor minden olyan metódus, amelyik egy objektum állapotát megváltoztatja. Az objektum immutable, ha nincs olyan metódusa, amelyik meg tudná változtatni, más szavakkal az objektum konstans, nem változik.


Ez nem ugyanaz, mint a final változó. Az csak annyit mond, hogy a változó (már amennyiben változóra használjuk, mert a Java nem feltétlenül szerencsésen ezt a kulcsszót is több, nem egészen ugyanazt jelentő dologra használja), szóval a változó ugyanarra az objektumra mutat. De maga az objektum változhat, és ez az egyik legnagyobb ellenérv a final használata ellen, hogy félrevezetheti az átlag programozót. Könnyen azt lehet hinni, hogy az objektum immutable, pedig nem az.


Miért fontos az, hogy egy objektum megváltozhatatlan legyen? Például vagy egy Map, ami kulcsokhoz rendel értékeket. Persze a kulcs és az érték is objektumok. Ha a kulcs tartalma megváltozik, könnyen előfordulhat, hogy többé nem találjuk meg az elrakott értéket.

import java.util.HashMap;
import java.util.Map;
public class MapExample {
    static class Key {
        String v1;
        Key(String v1) {
            this.v1 = v1;
        }
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((v1 == null) ? 0 : v1.hashCode());
            return result;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            Key other = (Key) obj;
            if (v1 == null) {
                if (other.v1 != null) return false;
            } else if (!v1.equals(other.v1)) return false;
            return true;
        }
    }

    public static void main(String[] args) {
        Map<Key, String> map = new HashMap<Key, String>();
        Key key = new Key("v1");
        map.put(key, "getThisBack");
        key.v1 = "V1";
        System.out.println(map.get(key));
        System.out.println(map.get(new Key("v1")));
        System.out.println(map.get(new Key("V1")));
    }
}

Ez a kis program mind a három sorban azt írja ki, hogy null. Nem kell ezen meglepődni, teljesen korrekt. Az első esetben azért nem találja meg az objektumot, mert nem azzal a hash értékkel lett eltéve, mint amelyikkel keressük. Olyan ez, mint amikor a lámpa alatt keressük a kulcsot, mert ott van világos. Nem ott vesztettük ugyan el, de ha meglátnánk, akkor megismernénk. Csak sajnos esélyünk sincs rá, hogy meglássuk.

A második esetben jó helyen keressük, de valóban nincs benne a map-ben, hiszen már nem ‘v1’ hanem ‘V1’ a kulcsbéli String értéke. A harmadik sor esetén pedig ugyanaz a helyzet, mint az első esetében, nem segít az sem, hogy egy új objektumot hoztunk létre. (A hashCode és equals metódusokat az Eclipse írta, bonyolultabb esetben erre is vannak segítő metódusok a Guava-ban.)

Szóval jó lenne, ha egy Map alatt a kulcsok nem változnának. Persze, tudom, hogy ez egy nagyon szigorú feltétel, és nem kellene ennyire szigorúnak lenni, de amíg nincs az ember tisztában nagyon mélyen a Map rejtelmeivel, addig jobb a szigorúság. És persze utána is, hiszen ki tudja, kinek a kezébe kerül a kód a későbbiekben, és nem várhatjuk, hogy mindenki utolsó regiszterig ismerje a Map-ek lelkivilágát. Még a nőkét sem lehet kiismerni…

És persze az immutabilitás nem áll meg a Map-ek kulcsainál. Általában is jó, ha vannak olyan objektumok, amelyek nem változnak. Például a string-ek, vagy az Integer objektumok, amik ha változnak, igen érdekes dolgok történhetnek. És néha például maguk a Map-ek is ilyenek. Milyen jó lenne, például, ha átadhatnánk egy Map-et egy metódusnak, és biztosak lehetnénk benne, hogy a hívott kód nem változtatja meg a tartalmát. Vagy kapunk a saját metódusunkba egy Map-et, és biztosak lehetünk benne, hogy nem változik meg (például egy másik thread által) amíg mi használjuk.

Miér kell a Guava, miért nem jó az unmodifiableCollection

Az első igen régi igény, már a bronzkorban is felmerült az emberekben, hogy ilyen kellene. Ha pedig valamelyik archeológus affinitással rendelkező programozó elmélyül a Java Collections osztályban, akkor találhat ott olyan metódust, név szerint az unmodifiableCollection és tsi., amelyik olyan nem módosítható nézetet ad vissza, amelyik garantálja, hogy aki csak ehhez fér hozzá, és nem az eredeti gyűjteményhez, az nem fogja tudni megváltoztatni a gyűjteményt.

De ez még nem jelenti azt, hogy valami mágia, vagy csak egy aszinkron thread, vagy egy sima call-back folyamán ne változna meg a gyűjtemény. Amit ez a metódus előállít az csak egy nézet, amelyik minden olyan esetben, amikor módosítani akarjuk EZEN KERESZTÜL az eredeti gyűjteményt elküld egy UnsupportedOperationException körútra. Olvasni is az eredetit olvassuk a nézeten keresztül.

Ennek persze van előnye is. A hátrány, mint mondottam az, hogy nincs garancia, hogy valaki, aki az eredeti gyűjteményhez hozzáfér nem változtatja meg az eredeti gyűjteményt. Az előny, hogy mivel ez csak egy nézet, ezért a saját gyűjtemények — például egy Map implementáció, amelyik alapvetően változtatja meg az emberiség sorsát az újonnan implementált kereső algoritmusunkkal, a HashMap O(1) aszimptotikus időigényű algoritmusát nagyságrendekkel gyorsítva nagy gyűjtemény méretek mellett — algoritmusa a nézeten keresztül is elérhető, és ezt használja a nézet keresés során.

A Guava csomagban található Immutable... osztályok nem ilyenek. Azok nem nézetek, hanem fogják az eredeti gyűjteményt (maradjunk példaképpen a Map-oknál), és létrehoznak egy újat belőle, ami már immutable. Ez gyönyörűen le is van írva ebben a blogban (eredeti meg itt).

Hogyan használjuk?

A kérdés alatt nem a normál “usage”-ra gondolok, arra ott van a man page. Inkább az a kérdés, hogy mikor használjuk?

Háááááttt… ha Java-ban programozunk…

és normális kódot akarunk írni, amelyik nem hasal el. A Guava használata ugyan nem fogja meggátolni, hogy elhasaljon a program, de lecsökkenti az esélyét. Ha átkonvertáljuk a Map-ünket immutable map-pé, és így adjuk át egy metódusnak, akkor még azt a kicsi esélyt is elvesszük a metódusól, hogy álnok módon reflection-nel szerezze meg az eredeti Map-et, amit egy unmodifiableMap esetén még megtehetne. (Mondjuk ez már paranoia szint, mert ha annyira félek, akkor inkább nem is állok neki programozni, hiszen lehet írni olyan JNI modult, amelyik feltúrja az egész heap-et meg stack-et, és bármibe belenyúl.)

Viszont a másik kérdés, hogy a metódusban ImmutableMap argumentumot várjunk-e, vagy jó lesz a sima Map? Általában az a szokásunk, hogy nem az implementációt használjuk az argumentumoknál, hanem az interfészt. Ezt azért tesszük, mert általában nem érdekel minket, hogy milyen algoritmust használ az implementáció, lehet az HashMap vagy TreeMap vagy akármi más (AVL fa, vagy platán), minket csak az érdekel, hogy a Map az Map, lehet belőle kiolvasni, és lehet bele tenni kulcs, érték párokat.

Az ImmutableMap esetében kicsit más a helyzet. Itt ugyanis igen sokszor érdekel, hogy a Map ne változzon amíg a metódus matat körülötte. Végigmegyünk az elemeken, és közben ne jöjjenek létre új elemek, és ne tűnjenek el régiek. Ebben az esetben, logikusan egy java.util.ImmutableMap interfészt kellene használni, amelyiket a java.util.Map kiterjeszt. De ezt nem lehet, több okból is kifolyólag. Az első, hogy ilyen interfész nincs, a többi ok meg ezek után nem érdekes. Anno nem került bele az öröklődési láncba ilyen interfész, és utólag már hiába vágyakozunk rá, hogy legyen nekünk egy gazdag nagypapánk. Ezért kénytelenek vagyunk azt az implementációt használni, ami van: az öreg iszákos nagypapát, aki azért mégiscsak szereti az unokáját: ez a com.google.common.collect.ImmutableMap.

Ha viszont mi hívunk egy metódust, amelyik sima Map-et vár, akkor nem biztos, hogy az ImmutableMap a legjobb megoldás. Ebben az esetben elegendő, és kevesebb erőforrást igényel ha a unmodifiableMap metódussal hozunk létre egy nézetet, és megbízunk annyira a hívott metódusban, hogy nem tesz gonosz dolgokat.

És akkor még mindig ott a kérdés, hogy mi gátolja meg, hogy maguk a kulcs és érték objektumok megváltozzanak?

Szegény ember JsUnit-ja

Szegény ember JsUnit-ja

Mondják: van, amiben pont a gazdagok szűkölködnek. Nem népmeséről van szó, hanem hogy bizonyos cégek, tipikusan a pénzügyi szektorban, szigorúan leválasztják a belső hálót a külsőről. Emiatt talán biztonságosabbak lesznek a rendszerek. Szintén emiatt van, hogy nem lehet csakúgy akármit letölteni. Ha a senior fejlesztő valamit fel akar használni, akkor ahhoz meg kell győznie a főnökét, a főnöke főnökét, majd a biztonsági vezetőt is. Ez egy hosszú és bizonytalan kimenetű folyamat, úgyhogy a senior fejlesztő gyakran inkább gányol, újra feltalálja a kereket, rosszul.

A projekt

Kicsit konkrétabban: néhány hónapja szembe jött egy javascriptes projekt. Egészen jól el volt eresztve, mert használhatott jQuery-t, meg néhány másik libet is. Persze nem volt fenékig tejfel, jelentkeztek a szokásos legacy kódos problémák, pl. a logika és a view totál összenőtt. Ez persze még mind nem lett volna akkor nagy baj, csak a fejlesztés-feedback loop kicsit hosszabb volt a kelleténél:

  1. Js szerkesztése, speckuláció
  2. War forgatása, deploy
  3. Site tesztelése browserből

Akkor már tudtam, mi az a TDD, és azt is, hogy miért jó ez nekem (és a főnökömnek, a főnököm főnökének, a CEO-nak, a felhasználóknak, stb.)  Ezen a SO oldalon egészen sokat összeszedtek, szépen részletezték, hogy melyik miért jó/nem jó. Még azon is vitatkoztak, hogy jó-e browser-ből futtatni a unit teszteket. (Jó, mert élesben is ott fut. Rossz, mert komoly függőség a CI szervernek.) Rákattinthatnék bármelyikre, és le is tölthetném – otthonra.

A megoldás

A megoldás: addig maszíroztam a javascriptet, amíg jól nem működött. Ez van, szegény ember vízzel főz. Más szavakkal: az ügyfél örül, az igazi problémamegoldást viszont elhalasztottuk (és fizetgetjük a technikai törlesztőket).

Azóta időről időre elgondolkodom, mit tehetnék, amikor legközelebb ezen kell dolgoznom. Az egyik ötlete a RhinoUnit adta: Hiszen a Rhino Javascriptet tud futtatni java-ból! Sőt, java 6-tól fölfelé még külön jart se kell hozzá importálni, csak beírjuk, hogy

String javaScriptExpression = "helloWorld();";
Reader javaScriptFile = new StringReader(
    "function helloWorld() {\n"
        + "    println('Hello, World!');\n" + "}");
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");

engine.eval(javaScriptFile);
engine.eval(javaScriptExpression);

És kiírja, hogy “Hello, World!”. A fenti kódot amúgy ebből egyszerűsítettem.

Unit tesztek

Már csak köré kell rakni egy JUnit-ot, az ismétlődő részeket belerakjuk a setUp() -ba, és voila:

public class TestCalc {

	private ScriptEngine engine;

	@Before
	public void setUp() throws Exception {
		ScriptEngineManager factory = new ScriptEngineManager();
		engine = factory.getEngineByName("JavaScript");

		String[] sources = new String[] {
			"/com/tamasrev/Calc.js"
		};
		for (String src : sources) {
			Reader r = new InputStreamReader(getClass().getResourceAsStream(src));
			engine.eval(r);
		}
	}

	@Test
	public void testSum() throws Exception {
		//GIVEN
		String source = "sum(2, 3)";

		//WHEN
		Object result = engine.eval(source);

		//THEN
		assertEquals(5.0, result);
	}
}

Nézzük meg részletesebben, mi ez a sok betű?

A setUp metódusban összerakunk egy ScriptEngine-t és beletöltjük a teszt javascript dependenciáit. Azt gyanítom, hogy a ScriptEngineManager-t nem kell minden teszt metódushoz újra példányosítani; talán a ScriptEngine-t se. A build és a tesztek futtatása egyelőre tíz percen belül van, úgyhogy ez most még OK. Keretrendszernek rendesen ki kellene tesztelni a dolgokat.

Aztán a testSum() metódus összerak Stringben egy JavaScript hívást, lefuttatja, és a ScriptEngine által visszaadott értékre meghív egy JUnit-os assert-et. Mindez szép és jó, amíg tesztenként egy assertünk van. Van, aki szerint ne is legyen több. És van, aki szerint néha lehet. Szerintem meg mint minden szabály alól, ez alól is vannak kivételek. (Igen, ez is egy szabály volt).

Egyszerre több értéket ki tudunk nyerni pl JavaScript objektumként. Ez Java oldalon egy NativeObject, amit nagyjából, de tényleg csak nagyjából Map-ként kezelhetünk. Azaz:

	@Test
	public void testDividendAndRemainder() throws Exception {
		//GIVEN
		String source = "dividendWithRemainder(5, 2)";

		//WHEN
		Map result = (Map) engine.eval(source);

		//THEN
		assertEquals(2.0, result.get("dividend"));
		assertEquals(1.0, result.get("remainder"));
	}

Tehát Js-ből visszaadunk kulcs-érték párokat, és azokat Java-ban a szokásos módon asszertáljuk. A listák ellenőrzése házi feladat az olvasónak.

Miért

Miért jó ez nekünk? Azért, mert automatikusan futtathatunk unit teszteket Js-re. Nem kellenek hozzá csilli-villi hipszter libek. Sőt, még browser se kell hozzá. Elég a java 6, a jó öreg JUnit, és már mehet is.

Miért nem jó ez nekünk? Egyrészt, mert ezek nem unit tesztek: a legacy kódos könyvben Michael Feathers kifejti, hogy a unit tesztek gyorsak. Nem beszélnek a hálózaton, nem izélgetik a fájlrendszert, adatbázist végképp nem, hanem szépen izolációban futnak. Ami ezeket a szabályokat megszegi, az nem unit teszt. Persze attól még lehet hasznos, teszi hozzá.

Másrészt, ezek poliglott, többnyelvű tesztek, tehát pont elveszítjük az xUnit szépségét: hogy abban a nyelvben írjuk a tesztet, amiben a kódot is.

Összességében vegyes a kép. Ez van, szegény ember vízzel főz. Körbeértünk.

Hogyan tovább?

Ez itt egy proof of concept. Akit érdekel, itt megtalálja a teljes projektet. Ez még pont az a méret, amit bármelyik szegény ember be tud írni a belső hálós gépébe. Mindenki más használjon normális megoldásokat, például JsUnit-ot.

Vallomás

Olvasok egy szakmai blogot, és csak tanulok tanulok, egyre okosabb leszek, és baromira tisztelem a szerzőt: wazze, miket nem tud ez az ember! Igazi nagy koponya!

Aztán eszembe jut, hogy én mikor szoktam és miről blogot írni, és rájövök, hogy valószínűleg most tanulta meg ő is, egyáltalán nem biztos, hogy egy héttel ezelőtt is tudta. Azért írta meg pont most a blog bejegyzést.