tifyty

pure Java, what else ?

Kategória archívok: juni

Egy kis fejtörő, autós játék

Modellezzünk autókat, és eresszük el egy kicsit az Objektum orientáltságot.

Legyen olyanunk, hogy autó típus. Mit lehet tudni egy autóról? Fogyasztás, ülőhelyek száma, maximális sebesség, gyorsulás. Legyen egy absztrakt Car osztályunk, amelyik ezeket mind tudja tárolni, és visszaadni, és az egyes leszármazott osztályok számolják ki a konkrét értékeket a konstruktorukban. Ez logikusnak tűnik:

  1. Egyrészt minek írnánk meg minden egyes konkrét osztályban azt a rengeteg gettert.
  2. Felesleges az értékeket folyamatosan kiszámolni, valahányszor szükségünk van rá, tárolhatjuk az egyszer kiszámított értéket.
  3. Másrészt az ősosztály nem képes kiszámítani a konkrét értékeket, tehát azt a leszármazottakban kell.

A sok paraméter közül most vegyük a maximális sebességet, amúgy is csak az ami számít a tinédzserek életében, az a szekszi, meg az autóskártyában is a nagyobb sebesség üti a többi lapot. Hogy elférjen a pelenkázó a kombi csomagtartójában, az csak később jön. A maximális sebességgel is lesz problémánk éppen elég (naná, majd arról írok blogot, hogy nézd már, működik a hello world!), lesz traffipax NPE alakjában meg minden szép és jó, a többi paraméter meg nem sokat tenne hozzá, csak elvonná a tisztelt blog olvasó figyelmét a lényegről, ha eddig a locsogásommal még nem sikerült volna.

public abstract class Car {
    private final int maximumSpeed;
    abstract int calculateMaximumSpeed();
    public Car() {
        maximumSpeed = calculateMaximumSpeed();
    }
    public int getMaximumSpeed() {
        return maximumSpeed;
    }
}

Ez, azt hiszem még nem okozott nagy meglepetést. És akkor most jöjjön a Vw Golf GTI.

public class VwGolf extends Car {
    private final Boolean isGti;
    public VwGolf(boolean gti) {
        isGti = gti;
    }
    @Override
    int calculateMaximumSpeed() {
        final int maxSpeed;
        if (isGti) {
            maxSpeed = 246;
        } else {
            maxSpeed = 172;
        }
        return maxSpeed;
    }
}

Ebben sincs nagy varázslat. Ez egy VW Golf, ami lehet GTI vagy sima. Most 16 helyett csak két változatot modellezünk. Az isGti értéke a létrehozott objektumban vagy true, vagy false attól függően, hogy milyen értéket adtunk meg a konstruktorban, és ez nem is változhat az objektum élete során, hiszen a változó final.

Most jött el az a pillanat, amikor unit tesztelünk. A teszt osztályt nem másolom ide: semmi egyéb, mint egy VwGolf létrehozása new operátorral. Hanem a kimenet!

Exception in thread "main" java.lang.NullPointerException
	at example3.VwGolf.calculateMaximumSpeed(VwGolf.java:9)
	at example3.Car.<init>(Car.java:5)
	at example3.VwGolf.<init>(VwGolf.java:3)
	at example3.TestCar.main(TestCar.java:6)

A VwGolf.java 9-es sorában kaptunk NPE-t, ami azt jelenti, hogy akármennyire is lehetetlennek tűnik, az isGti változó értéke bizony null.

Ilyenkor jön a nemtörődöm megoldás, hogy legyen boolean a változó és akkor nem kaphatunk NPE-t.

Hogyan lehet null ez a változó, amikor értéket kap a konstruktorban, és utána úgy is marad? Talán segítheti a megértést, hogy hogyan kerültünk erre a sorra. A Car ötödik sorából jött a hívás, ami stimmel: ez a Car konstruktora. Oda pedig a VwGolf harmadik sorából jutottunk. Ez a konstruktor feje, még az előtt, hogy az isGti változó értéket kapott volna. Akkor pedig már értjük, hogy hogyan született az NPE. Azért ismételjük át, hogy mi történik egy objektum létrehozása közben:

  1. Meghívjuk a konstruktort, de az ahelyett, hogy elkezdene futni, meghívja az ős osztály konstruktorát.
  2. Amikor az ősosztály létrehozása lefutott, még mindig nem kezdi el végrehajtani a konstruktort, hanem nekiáll lefuttatni a dinamikus inicializáló részeket, abban a sorrendben, ahogy a kódban szerepelnek.
  3. Amikor ezek lefutottak, akkor lefut a konstruktor.

Természetesen az ősosztály konstruktorának a meghívása során is minden lefut az ősosztályban, aminek kell, meg annak az ősosztálya is, feltéve, hogy van az osztálynak ősosztálya.

Ugye mindenki tudja, hogy csak egy olyan osztály van, amelyiknek nincs ősosztálya, mert minden más osztálynak van ősosztálya, akkor is, hanem írjuk ki, hogy extends Object

Demonstrációként legyen itt két osztály:

public class Progenitor {
    private int one(String m) {
        System.out.println("Progenitor "+m);
        return 1;
    }
    final int var1 = one("variable initialization 1");
    {
        System.out.println("Progenitor dynamic initializer before construtor");
    }
    final int var2 = one("variable initialization 2");
    public Progenitor() {
        System.out.println("Progenitor Constructor");
    }
    final int var3 = one("variable initialization 3");
    {
        System.out.println("Progenitor dynamic initializer after construtor");
    }
    final int var4 = one("variable initialization 4");
}
public class Offspring extends Progenitor {
    private int one(String m) {
        System.out.println("Offspring "+m);
        return 1;
    }
    final int var1 = one("variable initialization 1");
    {
        System.out.println("Offspring dynamic initializer before construtor");
    }
    final int var2 = one("variable initialization 2");
    public Offspring() {
        System.out.println("Offspring Constructor");
    }
    final int var3 = one("variable initialization 3");
    {
        System.out.println("Offspring dynamic initializer after construtor");
    }
    final int var4 = one("variable initialization 4");
}

Aminek a kimenete, amikor egy new Offspring() elsül:

Progenitor variable initialization 1
Progenitor dynamic initializer before construtor
Progenitor variable initialization 2
Progenitor variable initialization 3
Progenitor dynamic initializer after construtor
Progenitor variable initialization 4
Progenitor Constructor
Offspring variable initialization 1
Offspring dynamic initializer before construtor
Offspring variable initialization 2
Offspring variable initialization 3
Offspring dynamic initializer after construtor
Offspring variable initialization 4
Offspring Constructor

Nincs ebben semmi varázslat, és akkor sem hazudtam, amikor azt írtam feljebb, hogy

“Az isGti értéke a létrehozott objektumban vagy true, vagy false attól függően, hogy milyen értéket adtunk meg a konstruktorban, és ez nem is változhat az objektum élete során, hiszen a változó final.”

csak éppen fontos kihangsúlyozni, hogy AZ OBJEKTUM ÉLETE SORÁN. Amikor pedig az NPE robban, akkor az objektumunk még nem él, még csak fetus, még nem csusszant ki a java szülőcsatornáján. Ilyenkor sok minden lehet. Például az is, hogy a final változók még nincsenek inicializálva.

Hogyan oldjuk meg ezt a problémát? Például úgy, hogy a konstruktor nem próbálja meg kiszámolni a maximális sebesség értékét, hanem hagyja, hogy ezt a getter tegye meg az első meghívásnál. Persze ilyenkor előjön a singleton problémához hasonló szinkronizáció. De az már egy másik blog bejegyzés.

Amúgy a JPL_Coding_Standard_Java ajánlás R15 pontja szerint ne hívjunk konstruktorból olyan metódust, amelyik nem privát vagy nem final, mert abból baj lehet. Itt lett is. Na de hát akkor hogyan?

Két megoldást fogok mutatni. Az első egy trükkös, összetett pattern, ami egy kicsit hasonlít a lazy singleton megoldáshoz, legalábbis az adja az ötletet. Ennek megfelelően bonyolult, és nehezen érthető. A második megoldás nem olyan szexi, egy egyszerű gyártó pattern. Ennek megfelelően egyszerűbben érthető, és ezért production kódban csak a második megoldást ajánlom. Az első csak okulásul.

Késleltetett inicializálás

Ugye a gond abból adódott, hogy amikor a Car konstruktora ki akarta számoltatni a végsebességet, akkor a leszármazott konstruktor még nem futott le, nem állt rendelkezésre az inicializált osztály. Csináljuk úgy, hogy legyen egy második, belső osztály a Car osztályon belül, amelyik az autó állapota:

public abstract class Car {
    protected class State {
        int maximumSpeed = 0;
        public State() {
            state = this;
            maximumSpeed = calculateMaximumSpeed();
        }
    };
    private State state;
    abstract int calculateMaximumSpeed();
    public int getMaximumSpeed() {
        return state.maximumSpeed;
    }
}

A State persze magától nem fog létrejönni, de megtehetjük azt, hogy a leszármazott osztály konstruktorában hozzuk létre:

public class VwGolf extends Car {
    protected class State extends Car.State {}
    final Boolean isGti;
    public VwGolf(boolean gti) {
        isGti = gti;
        new State();
    }
    @Override
    int calculateMaximumSpeed() {
        final int maxSpeed;
        if (isGti) {
            maxSpeed = 246;// km/h
        } else {
            maxSpeed = 172;// km/h
        }
        return maxSpeed;
    }
}

Amikor a new State() lefut, akkor az objektumunk már inicializálva lett, bár hivatalosan még nem élő objektum, hiszen a konstruktor még nem tért vissza. A papa osztályban lefut a State konstruktora, és jól el is teszi magát a körülvevő osztály state változójába: a leszármazottaknak ezzel sem kell foglalkozni. Csak annyi a dolguk, hogy létrehozzák a State objektumot, amikor már létre lehet hozni. Ez más működik.

Egyszerű ez a megoldás? Könnyen érthető? Nem. Ha te könnyen megértetted, az azt jelenti, hogy jóval az átlag programozó felett vagy. Ugyanakkor a kódot az átlag programozó fogja karbantartani, tehát KISS.

Gyártósor

Mert hogyan is gyártsunk autót másképp, mint gyártósoron.

public abstract class Car {
    int maximumSpeed = 0;
    abstract int calculateMaximumSpeed();
    public int getMaximumSpeed() {
        return maximumSpeed;
    }
    public void setMaximumSpeed(int maximumSpeed) {
        this.maximumSpeed = maximumSpeed;
    }
}
public class VwGolf extends Car {
    final Boolean isGti;
    public VwGolf(boolean gti) {
        isGti = gti;
    }
    @Override
    int calculateMaximumSpeed() {
        final int maxSpeed;
        if (isGti) {
            maxSpeed = 246;// km/h
        } else {
            maxSpeed = 172;// km/h
        }
        return maxSpeed;
    }
}
public class VwGolfFactory {
    public static VwGolf produceNewCar(boolean gti) {
        VwGolf carGolf = new VwGolf(gti);
        carGolf.setMaximumSpeed(carGolf.calculateMaximumSpeed());
        return carGolf;
    }
}

Egyszerű? IGEN. Érthető? IGEN. Karbantartható? IGEN. Akkor ezt. Az eredeti ötlet a cikkhez a JPL_Coding_Standard_Java dokumentumból jött, de egy “kicsit” ki lett dolgozva, mert kérés volt a cikk után, hogy ne csak olyanról írjak, amit akkor tanultam 🙂

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?

Mit csinál ez a kód

Ez a post nem különálló poszt, hanem a posztot egészíti ki. Abban a posztban szerepel egy mintaprogram, de a magyarázat egy kicsit hosszú ahhoz, hogy abban a bejegyzésben szerepeljen, ezért itt van külön. A kódot ide is bemásoltam darabokra vágva magyarázatokkal:

import java.util.concurrent.atomic.AtomicInteger;

public class FinalTest {

	private static AtomicInteger counter = new AtomicInteger(0);

	static void waitFor(int n) {
		while (!counter.compareAndSet(n, n + 1)) {
		}
	}

Amit a kód elején látunk az egy atomi integer, amit arra használunk, hogy a később elindított két szál közül mindig csak az egyik fusson, és így pontosan tudjuk, hogy mi milyen sorrendben történik. A való életben persze éppen nem ez történik, azért indítunk el szálakat, hogy azok párhuzamosan fussanak, itt viszont pont azt akarjuk demonstrálni, hogy mi történik akkor, ha a kód futása során az egyik szál eléri a másik szál által félig létrehozott objektumot.

A waitFor metódus a program futás során tervezett n-edik állapotra vár, és ha ez elérkezett (a másik szál módosította a számlálót), akkor megnöveli a számláló értékét és visszatér. Itt kell megjegyezni, hogy az egyszerűség kedvéért itt csak egy üres while ciklus van, ami igazi kódban elfogadhatatlan. Először is, kellene bele legalább egy sleep, hogy ne egye meg az összes CPU időt azon a magon amelyiken fut, másodszor nem foglalkozik azzal, hogy a szál kap-e interruptot, harmadrészt egy ilyen pollozás nem hatékony, inkább wait és notify, notifyAll metódusokat kell használni, és végül, de elsősorban még azokat sem, hanem valami olyan megoldást, ami ennél magasabb szintű, mondjuk egy aszinkron queue.

A main előtt van két osztályunk. Az egyik a majd később elindítandó két szál kódja, (ez a T), a másik pedig az az osztály, amelyik elköveti azt a kerülendő disznóságot, hogy a konstruktor futása közben kiadja a this referenciát (ez a C).

	private static class C {
		final int x;
		int y;

		C(T other) {
			super();
			other.c = this;
			waitFor(0);
			waitFor(3);
			x = 1;
			y = 1;
			waitFor(4);
		}
	}

	private static class T extends Thread {
		public boolean constructor = false;
		public T other = null;
		public C c;

		@Override
		public void run() {
			if (constructor) {
				c = new C(other);
			} else {
				waitFor(1);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
				waitFor(2);
				waitFor(5);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
			}
		}
	}

A main mind a két szálat létrehozza, az egyiknek azt mondja, hogy legyen ő az, amelyik meghívja a C() konstruktort, a másik meg majd hozzányúl, és nézi, hogy mi az értéke a változóknak. A konstruktort elindító szál fog érdemi dolgot csinálni előbb, a másik első dolga az, hogy megálljon, és várjon arra, hogy a konstruktor őt továbbengedje (waitFor(1)).

A konstruktor beállítja az argumentumként megkapott szálobjektumban az other mezőt, hogy a másik szál hozzá tudjon férni az objektumhoz, ami éppen most jön létre, majd a waitFor(0) hívással nem vár, mert eleve a 0 állapotból indul a program, ellenben növeli vele a számláló értékét, és így a másik szál kódjában a waitFor(1) vissza fog térni, és hogy a konstruktor ne csináljon semmit, amíg az el nem végzett annyit, amit el kell végeznie, ezért elkezd várni a 3-as állapotra.

A másik szál fogja, és kiírja a még nem teljesen létrehozott embrió objektum x és y mezőjének értékét. Mivel ezek még nem kaptak értéket, ezért mind a kettő értéke definíció szerint a default érték. Ezt követően tovább engedi a konstruktort futtató szálat, és vár arra, hogy az befejezze, amit be kell fejeznie. A konstruktor pedig értéket ad a két változónak, majd ezt jelzi a másik szálnak, aki ismét kiírja, most már a beállított értéket a final x mező esetében, az y esetében meg vagy a default-ot, vagy a beállított értéket.

	public static void main(String[] args) {
		T t = new T();
		t.constructor = true;
		t.other = new T();
		t.start();
		t.other.start();
		try {
			t.join();
			t.other.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
}

A main működése triviális.

final, fin ül (update 2013-02-11 14:02)

Mire jó a final? Sok mindenre. Például segít programozni, és segít felfedezni olyan hibákat, amikor egy változó nem csak egyszer kaphat értéket, pedig mi azt szerettük volna. Ha például interfészünk van, akkor abban minden változó alapból static és final, ki sem kell írni. Check style alapbeállítás szerint (de lehet, hogy csak helyi default) a check style rád is kiabál, hogy “Ugyanmár ki ne írd! Tudja azt mindenki!” (Ja! Hiszed te! A check style nem szokott felvételiztetni!) Aztán meg a javac fordító olyan okos is tud lenni, mint ebben a bejegyzésben mutattam, hogy egy final változóról tudja már fordítási időben, hogy mi az értéke, és ezért oda se nyúl a kód, például egy int 127-es értékért, hanem egyenesen belefordítja a kódba, mint konstanst.

Szóval erre jó a final. De jó másra is. Mielőtt azonban ebbe belemennénk tisztáznunk kell valamit:

Attól, hogy egy változót final kulcsóval definiáltunk, az objektum, amit ezen keresztül el lehet érni még megváltozhat!

A final tehát nem garantál immutabilitást. Mi is az immutable, azaz megváltoztathatatlan objektum? Amit nem lehet megváltoztatni. Miért szeretjük az ilyen objektumokat? Azért, mert több szálon futó programok esetében kevésbé kell, hogy fájjon a fejünk a szinkronizálással. Ilyen a String, Integer és még sok egyéb objektum. Persze mi magunk is készíthetünk ilyen objektumokat. Ennek egyik módja például az, hogy az objektumon belül minden egyes mező legyen final. Ami presze nem garantálja, hogy azok az objektumok, amelyekre ezek a mezők tartalmaznak referenciát (emlékeztek gyerekek: milyen gyengék tudunk lenni, ha referenciákról van szó), szóval, hogy azok ne változnának. Aztán persze akkor sem járható ez az út, ha az objektum nem igazi immutable, hanem elkészítjük, csinálunk vele ezt, azt és csak ezután már többet nem változik.

Van olyan szokás is a mi utcánkban, vagy máshol (nálunk csak ha van értelme), hogy egyes osztályok két interfészt implementálnak: egy mutátor, és egy immutátor interfészt. A mutátor deklarálja az összes olyan metódust, amelyik meg tudja változtatni az objektum tartalmát, általában ki is terjeszti az immutátor interfészt, amelyik viszont csak olyan metódusokat deklarál, amelyek lekérdezésre, és még véletlenül sem objektum állapot változtatásra valók. Amikor pedig referenciát ad át egy ilyen objektumra a program egy metódusnak, akkor ha annak nem szabad megváltoztatni az objektum tartalmát, akkor az immutátor interfész típussal adja át, így (hacsak nagyon nem erőlködik) nem fog belepiszkítani a mi tiszta objektumunkba. Legfeljebb a vízből veszi ki a zoxigént.

Na de eléggé mélyen beástam magam ebbe a gondolati verembe, ki kellene poppolni innen, és mint a kismalac, nem várom meg a létrát, inkább dobok egy ellenőrizetlen kivételt, ami majd jól kivesz a veremből. Vissza a finálra.

Lokális változó is lehet final, objektum instancia változó is lehet final, és statikus változó is lehet finál. Most ne beszéljünk arról, hogy osztály is lehet final.

Lokális változók esetében főleg programozási szépség. Ez persze szubjektív. Viszont ha a metóduson belül létrehozzuk egy név nélküli osztály egy példányát, amelyikben hivatkozunk a metódusunk egy változójára, akkor annak a változónak final-kell lennie, deklaráció szempontjából is, és értéket is kellett kapnia, amikor az objektumot létrehozzuk. (Ez is megért egy misét.) Viszont a lokális változók a veremben vannak, minden szálnak saját verme van, egymás vermébe nem nyúlkálnak, ez az etika, mint a piszoárban. Ezért aztán a többszálúságot a lokális változók nem zavarják. Final, vagy fin ül, mindegy.

Nem így van azonban az osztály és az objektum változókkal! Ezeket, legyenek privát, protected, public vagy egyéb láthatóságúak (ismétlés: mi lehet még?) több szál is láthatja. Ha egy objektum változó nem final (és nem is volatile) értéket kap, akkor a lefordult kódnak szíve joga, hogy ezt az értéket berakja egy processzor regiszterbe, L1,2,3 cache-be és ne írja ki memóriába, amíg a szál azon a processzor(mag)on fut. Ő tudja, hogy mi a változó értéke, és nem jeleztük, hogy azon a szálon kívül mást is érdekelne, így akár amíg világ a világ és véget nem ér a JVM futása, akár sose nem is kerül ki az érték a memóriába, ahol egyébként az objektum van. Ha viszont az objektumváltozó final, akkor értéket kell, hogy kapjon, amikorra a konstruktor lefut, és ekkor kikerül memóriába is (vagy ha oda nem is, ez már processzor hardver kérdése, de annyi biztos, hogy a többi szál is a beállított értéket fogja látni).

A final objektumváltozó egy olyan “volatile” objektumváltozó, amelyik csak egyszer kap értéket a konstruktorban, és legkésőbb a konstruktor lefutásának a végére a többi szál számára is láthatóvá válik az értéke. (Meg ne kövezzetek! Tudom, hogy ez így pongyola megfogalmazás, de ha már kövezésről van szó, tudjátok meg, hogy ti mind egyéniségek vagytok!)

Persze nem, hanem még a volatile mezőknél is hatékonyabb. Azokat ugyanis minden egyes olvasásnál ellenőrizni kell, hogy valaki más nem módosította-e a tartalmukat, a final mezők viszont nem változhatnak, így eszébe sincs a kódnak kinézegetni a lassú memóriába, ha egy final változó értéke már bent van a processzor cache-ben.

A Java specifikáció külön meg is említi, hogy például a String minden mezője final, ezért, amikor létrejön egy új String objektum, akkor egy másik szálon (az egyik amelyik létrehozta, ehhez képest egy másikon) is használható az objektum, mert minden egyes mezője final tehát a másik szál is azt az értéket látja, amit a konstruktor létrehozott. (String témában javaslom ezt a bejegyzést először utána pedig ezt a bejegyzést, amely két bejegyzés igen mélyen belevájkál a String lelkivilágába). És hogy most már sokadik hete ne maradjunk kód nélkül, itt van egy mit ír ki:

import java.util.concurrent.atomic.AtomicInteger;

public class FinalTest {

	private static AtomicInteger counter = new AtomicInteger(0);

	static void waitFor(int n) {
		while (!counter.compareAndSet(n, n + 1)) {
		}
	}

	private static class C {
		final int x;
		int y;

		C(T other) {
			super();
			other.c = this;
			waitFor(0);
			waitFor(3);
			x = 1;
			y = 1;
			waitFor(4);
		}
	}

	private static class T extends Thread {
		public boolean constructor = false;
		public T other = null;
		public C c;

		@Override
		public void run() {
			if (constructor) {
				c = new C(other);
			} else {
				waitFor(1);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
				waitFor(2);
				waitFor(5);
				System.out.println("x=" + c.x);
				System.out.println("y=" + c.y);
			}
		}
	}

	public static void main(String[] args) {
		T t = new T();
		t.constructor = true;
		t.other = new T();
		t.start();
		t.other.start();
		try {
			t.join();
			t.other.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
}

részletesebb magyarázat

Nos, mint többszálúság esetében ez szokott lenni, nem elegendő lefuttatni a programot, és megnézni, hogy mit ír ki. Mert amit kiír, az egy dolog, egy futáskor, egy gépen. Máskor, másik gépen esetleg mást ír ki. Az a tudomány, olyan programot írni, amelyik többszálú, de mégis garantálhatjuk, hogy minden gépen mindig ugyanazt produkálja. Ez a program nem ilyen, és ez lényeg benne. Amit ez kiír, az vagy

x=0
y=0
x=1
y=1

vagy éppen

x=0
y=0
x=1
y=0

Akinek a másodikat sikerül reprodukálnia, írja meg. Elvileg nem kizárt, és nagy terhelés esetén, amikor legkevésbé kellene, akkor fog megtörténni. No nem ez a kód, mert ez a kód, mint tapasztaltabbaknak egyből lejöhetett: nem production kód. Rövid, egybetűs, ugyan könnyen átlátható, de semmitmondó változónevek, e.printStackTrace és társai. Ami azonban a legfontosabb, hogy ez a kód direkt arra készült, hogy demonstrálja (sajnos a dolog természetéből adódóan kevés sikerrel), hogy mi az amit nem szabad. A javasolt eljárás ennek az ellentéte. A konstruktorokat úgy kell megírni, hogy azok ne adják ki a this értékét magukból, így az objektumhoz más szálból csak akkor lehessen hozzáférni, amikor a konstruktor már lefutott, és minden final mező értéke be lett állítva. Miért is tennénk mást? Mert észre sem vesszük. Készítünk egy factory-t, amelyik minden létrehozott objektumnak átadja magát, így minden objektumtól el lehet kérni, hogy melyik factory hozta létre. Aztán ahogy fejlődik a kód egyes osztályok még néhány dolgot be akarnak jegyezni magukról a factory-ban, a konstruktornak átadott factory objektumba visszahínak átadva paraméterként a this-t, és már itt is az antipattern. De azért működik a dolog, amíg egyszer egy távoli ügyfél malájföldön többszálú környezetben el nem kezdi használni, és Murfy-nek megfelelően crash. És ilyenkor tör elő a szupportos programozó szájából a másik Brian élete idézet: “Menjetek a …”

Vagyis a final egy rafinalt dolog, és nem csak szépészet!

Házi feladat: Gondold át, hogy mi a helyzet a static final mezőkkel. Gondolj arra, hogy az osztályok maguk is objektumok.

UPDATE

Kofa felhívta a figyelmemet, hogy a waitFor metódusban a meghívott compareAndSet metódus változtat egy volatile változót, aminek a hatására az y változó megváltozott értéke is garantáltan elérhetővé válik az összes szál számára. Így ez a példaprogram nem jó, soha nem fordulhat elő a

x=0
y=0
x=1
y=0

eredmény.

StringBuffer és StringBuilder

Az a cikk arról szól, hogy mennyivel gyorsabb a StringBuilder, mint a StringBuffer, és mikor melyiket érdemes használni. Seniorok számára nem lesz benne újdonság, talán a mérések eredménye hordoz némi tanulságot.

Amikor a Java-t tanuljuk, akkor szigorúan a fejünkbe verik, hogy a String az bizony immutable, és a string konkatenáció az ördögtől való, eszi a memóriát, másolgatja a stringet a memóriában ide-oda, új objektumokat hoz létre, és ez így igaz is. Amikor nem csak éppen két stringet akarunk konkatenálni egy Exception üzenet összerakására, amikor a program futásának már úgyis annyi, és ezért a teljesítmény sem különösebben érdekes akkor ott van a StringBuffer és a StringBuilder.

Mi különbség a kettő között?

A StringBuffer támogatja a multi-thread hívásokat, míg a StringBuilder nem. Ettől eltekintve teljesen kompatibilis a két osztály, api szinten ahol az egyik használható, ott használható a másik is. A metódusok azonban a StringBuffer-ben synchronize-áltak, és ez azt jelenti, hogy különböző szálak használhatnak biztonsággal közös buffert, de ugyanakkor minden metódushívás előtt és után a változók értékeinek szinkronizálása a memória és a processzor regiszterek és a processzor cache között megtörténik. Ez valamennyit lassít a futáson. Persze valamit, valamiért.


A StringBuffer a Java 1.0 verzióban már megjelent, míg a StringBuilder csak az 1.5-ben. Miért nem jelent meg rögtön az 1.0-ban mind a kettő, illetve miért a szinkronizált változat volt az első? Biztos válasz erre nincs, hiszen ez egy olyan döntés, amit a Java tervezői és fejlesztői hoztak, és ha csak valahol egy jegyzőkönyvben le nem írták, hogy miért döntöttek úgy, hogy a StringBuffer szinkronizált legyen, akkor valószínűleg már senki nem emlékszik rá.

De képzeljük magunkat egy pillanatra 1996. január 23. elé (ekkor jött ki az 1.0 Java), és tegyük fel a kérdést: legyen-e szinkronizált a tervezett StringBuffer minden metódusa? Mit is jelentett ekkor a szinkronizáltság? Azt amit ma? Nem egészen. Elvileg ugyan igen, de erősen máshol voltak a hangsúlyok. A szinkronizált metódusokba nem léphet be párhuzamosan két szál. Ez nem gond, mert a StringBuffer esetében ez baj is lenne. Akkor már inkább jobb, ha az egyik száll megáll, a másik meg vár, mintha egy rosszul megírt multi-thread program összekutyulja a buffert, aztán szidják a Java-t, hogy az szar. (Amúgy akkor még elég szar volt, de ez már a mellékvágány mellékvágánya lenne.) A másik dolog, hogy többprocesszoros rendszerekben a processzor cache és regiszterek szinkronizálását a memória felé el kell végezni, hogy ha a buffert egy másik processzoron futó szál piszkálta utoljára, akkor is jó állapottal tudjon a mi szálunk dolgozni. Ez akkoriban, amikor a 600MHz Alpha processzorok voltak a csúcstartók, és Linux még nyolc évre volt attól, hogy igazán elfogadhatóan fusson többprocesszoros gépen nem sok mindenkit érintett. A Java történet nem arról szólt, hogy mennyire jó, hanem arról, hogy életben marad-e egyáltalán.

Nézzük meg a gyakorlatban, hogy milyen sebesség különbségek lehetnek az egyes string kezelési megközelítések között. Vegyünk egy nagyon egyszerű példát: Vegyünk ki minden felesleges szóközt egy stringből. Más szavakkal, ahol egymás után több szóköz van, ott azokat cseréljük le egy szóközre.

Ez a feladat különösen kedves a szívemnek, mert először a BME-n jelent meg nyomtatásban a Z80 programozás jegyzetemben.

Az első megoldásban használjunk String-eket és reguláris kifejezéseket:

	public static String unspacerRegEx1(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

Ez a lehető legrosszabb megoldás, sem nem olvasható, sem nem hatékony. Már algoritmus szinten sem. Javítunk rajta, hogy ha nem két hanem több szóköz van egymás után, akkor azt egyszerre ismerje fel:

	public static String unspacerRegEx2(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s+)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

Tovább javíthatunk a dolgon, ha nem rakjuk össze a részeredmény stringeket, hanem balról jobbra haladva érjük el az eredményt:

	public static String unspacerRegEx3(final String in) {
		String returnValue = "";
		Pattern pattern = Pattern.compile("(.*?)\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		int end = 0;
		while (matcher.find()) {
			returnValue += matcher.group(1) + " ";
			end = matcher.end();
		}
		returnValue += in.substring(end);
		return returnValue;
	}

Persze továbbra is kétségeink lehetnek, hogy ez megfelelő megoldás-e, de azt várjuk, hogy lényeges sebességnövekedés érhető el, ha a string elég nagy. De még nagyobb hatékonyságnövekedést érhetünk el akkor ha megtaláljuk a reguláris kifejezésekhez használhatü replaceAll() metódust:

	public static String unspacerRegEx4(final String in) {
		Pattern pattern = Pattern.compile("\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		String returnValue = matcher.replaceAll(" ");
		return returnValue;
	}

… és persze kísértést érezhetünk arra is, hogy a String osztályban definiált azonos nevű metódust használjuk:

	public static String unspacerString(final String in) {
		return in.replaceAll("\\s\\s+", " ");
	}

Ez nagyon egyszerű megoldás, tiszta, világos, olvasható. Általánosságban (és a mérési eredmények ismeretében) az is az *általános* javaslatom, hogy ezt a verziót érdemes használni és csak akkor keresni valami gyorsabbat, ha nem elég gyors a programunk és a profiler azt mondja, hogy itt van a palack nyaka. De ha ezt mondja, akkor megoldhatjuk a feladatot StringBuilder-rel is:

	public static String unspacerStringBuilder(final String in) {
		StringBuilder sb = new StringBuilder(in);
		boolean previousWasSpace = false;
		int j, i;
		for (i = 0, j = 0; i < sb.length(); i++) {
			sb.setCharAt(j, sb.charAt(i));
			boolean thisIsSpace = Character.isWhitespace(sb.charAt(j));
			if (!previousWasSpace || !thisIsSpace) {
				j++;
			}
			previousWasSpace = thisIsSpace;
		}
		sb.setLength(j);
		return sb.toString();
	}

Ez az algoritmus szépen pakolgatja a karaktereket, és ha két szóközt talál, akkor a második után nem lépteti a j target indexet, hanem rámásolja a következő karaktert (ami lehet szóköz, vagy nem szóköz, de ha szóköz, akkor megint csak nem lépteti).

Az unspacerStringBuffer kopizásától most itt eltekintünk, ugyanaz, mint a unspacerStringBuilder csak StringBuilder helyett StringBuffer-t használ. A fő program elején összeállít egy megfelelően (erre a szóra még visszatérünk) nagy stringet, és utána a parancssori argumentumnak megfelelően meghívja valamelyik metódust:

	public static void main(String[] args) {
		Long timeStart, timeEnd;
		final String testBase = "ap        le  pe                a          ch";
		final int multiplier = 1000;
		final StringBuilder sb = new StringBuilder(multiplier
				* testBase.length());
		for (int i = 0; i < multiplier; i++) {
			sb.append(testBase);
		}
		final String testString = sb.toString();
		final long loopSize = 5;
		switch (args[0]) {
		case "1":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx1(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx1");
			break;
...
		}
	}
}

(A case többi ágától eltekinthetünk. A teljes program megtalálható itt becsukva:

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class provides three different methods that remove the superfluous
 * spaces from a string.
 */
public class StringUnspacer {

	public static String unspacerRegEx1(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

	public static String unspacerRegEx2(final String in) {
		String returnValue = in;
		Pattern pattern = Pattern.compile("^(.*)(\\s\\s+)(.*)$");
		Matcher matcher = pattern.matcher(returnValue);
		while (matcher.find()) {
			returnValue = matcher.group(1) + " " + matcher.group(3);
			matcher = pattern.matcher(returnValue);
		}
		return returnValue;
	}

	public static String unspacerRegEx3(final String in) {
		String returnValue = "";
		Pattern pattern = Pattern.compile("(.*?)\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		int end = 0;
		while (matcher.find()) {
			returnValue += matcher.group(1) + " ";
			end = matcher.end();
		}
		returnValue += in.substring(end);
		return returnValue;
	}

	public static String unspacerRegEx4(final String in) {
		Pattern pattern = Pattern.compile("\\s\\s+");
		Matcher matcher = pattern.matcher(in);
		String returnValue = matcher.replaceAll(" ");
		return returnValue;
	}

	public static String unspacerString(final String in) {
		return in.replaceAll("\\s\\s+", " ");
	}

	public static String unspacerStringBuilder(final String in) {
		StringBuilder sb = new StringBuilder(in);
		boolean previousWasSpace = false;
		int j, i;
		for (i = 0, j = 0; i < sb.length(); i++) {
			sb.setCharAt(j, sb.charAt(i));
			boolean thisIsSpace = Character.isWhitespace(sb.charAt(j));
			if (!previousWasSpace || !thisIsSpace) {
				j++;
			}
			previousWasSpace = thisIsSpace;
		}
		sb.setLength(j);
		return sb.toString();
	}

	public static String unspacerStringBuffer(final String in) {
		StringBuffer sb = new StringBuffer(in);
		boolean previousWasSpace = false;
		int j, i;
		for (i = 0, j = 0; i < sb.length(); i++) {
			sb.setCharAt(j, sb.charAt(i));
			boolean thisIsSpace = Character.isWhitespace(sb.charAt(j));
			if (!previousWasSpace || !thisIsSpace) {
				j++;
			}
			previousWasSpace = thisIsSpace;
		}
		sb.setLength(j);
		return sb.toString();
	}

	public static void main(String[] args) {
		Long timeStart, timeEnd;
		final String testBase = "ap        le  pe                a          ch";
		final int multiplier = 1000;
		final StringBuilder sb = new StringBuilder(multiplier
				* testBase.length());
		for (int i = 0; i < multiplier; i++) {
			sb.append(testBase);
		}
		final String testString = sb.toString();
		final long loopSize = 5;
		switch (args[0]) {
		case "1":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx1(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx1");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "2":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx2(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx2");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "3":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx3(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx3");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "4":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerRegEx4(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerRegEx4");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "5":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerString(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart + "ms with unspacerString");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "6":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerStringBuilder(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart
					+ "ms with unspacerStringBuilder");
			break;
		//
		// ------------------------------------------------------------------------
		//
		case "7":
			timeStart = System.currentTimeMillis();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				String z = unspacerStringBuffer(testString);
			}
			timeEnd = System.currentTimeMillis();
			System.out.println(timeEnd - timeStart
					+ "ms with unspacerStringBuffer");
			break;
		}
	}
}

)

A mérési eredmények:

verhasp:string-test verhasp$ sh target/classes/execute.sh 
112508ms with unspacerRegEx1
110590ms with unspacerRegEx2
846ms with unspacerRegEx3
105ms with unspacerRegEx4
92ms with unspacerString
30ms with unspacerStringBuilder
42ms with unspacerStringBuffer

Értékelés: látszik, hogy a StringBuilder valóban a leggyorsabb, de a String.replaceAll() sem sokkal lassabb. Igaz, hogy a háromszorosa, de mi ez a leglassabb megoldás 3000-szeres futási idejéhez képest.

Megjegyzések: a sebességet nagyon sok minden befolyásolja. Ha egy JVM-ben futtattam egymás után a teszteket, akkor előfordult, hogy az egyik metódus lefutott, elindult a második, és beütött egy GC. Ezt valamennyire kivédte, ha minden metódus hívás előtt hívott a program egy System.gc()-t de ilyenkor is sok minden befolyásolta az egyes futási időket. Külön processzként elindítva egyértelmű és világos eredményt kaptunk. Az azonban, hogy az első próbálkozások során például a unspacerRegex4 rendszeresen gyorsabb volt, mint az unspacerString, illetve kisebb méretű stringekre a StringBuilder-t használó megoldás sokkal lassabb volt, mint a unspacerRegex4 vagy unspacerString azt mutatja, hogy optimalizálni csak a tényleges futó kódot lehet, mérésekkel. Premature optimization is source of evil.

Irodalom:

String immutable reloaded

A korábbi A String immutable, vagy nem? cikk meglehetősen rövid volt. Nem titkolt (bár közzé sem tett) szándékom az volt, hogy az olvasókat gondolkodásra ösztönözzem, és azt hiszem, hogy a visszajelzések alapján (ami személyes levélben is jócskán jött) ez meg is történt. Azonban voltak olyan visszajelzések is, amelyek szememre vetették, hogy nem szabad ilyen hekkeléseket csinálni, holott a cikk nem akarta azt sugallni, hogy a String osztály a Java nyelvben ilyen módon is használandó lehet.

Ezért most “újra” megírom ezt a cikket. Először elmondom, hogy mit is csinál ez a program, majd pedig azt, hogy miért is érdemes ezt tudni, melyek azok az esetek, amikor szükségünk lehet erre az ismeretre. És akkor, mivel elég rövid, hogy ne kelljen lapozgatni, az eredeti cikk mégegyszer:


Mit ír ki a következő program?

package com.verhas.elrettentopelda;

import java.lang.reflect.Field;

public class Wtf {
	public static void main(String[] args) throws Exception {
		String alma = &quot;alma&quot;;
		String korte = &quot;alma&quot;;
		Field value = String.class.getDeclaredField(&quot;value&quot;);
		value.setAccessible(true);
		value.set(alma, new char[] { 'k', 'o', 'r', 't', 'e' });
		System.out.println(korte);
	}
}

Mit csinál ez a program

Először is tisztázzuk, hogy a program olyan dolgot követ el, ami ugyan a gyakorlatban működik, de semmi nem garantálja, hogy valóban működőképes mindig, minden Java környezetben. Ugyanis nem csak a String osztály definiált, és garantált működését használja ki, hanem reflection használatával olyan mezőkhöz nyúl, amelyek ugyan szerepelnek az implementációban, ám minden előzetes figyelmeztetés nélkül a fejlesztők megváltoztathatják ezeket. Nem azért vannak, hogy piszkáljuk, de senki nem akadályoz meg bennünket abban, hogy megtegyük.

UPDATE(2013-11-08): Java8-ban meg is tették a fejlesztők, a fenti példa nem működik 8-as Java alatt!

Tehát ha valaki ilyesmire vetemedik, akkor az megérdemli. Az nem Java-ban programoz, hanem csak kódot állít elő Java nyelven.

A program két String tíusú változót hoz létre. A Java fordító észreveszi, hogy ez a két string literál azonos, ezért a lefordított kódban valóban azonos lesz a két string, annyira, hogy csak egyszer fog szerepelni a string literálok között. Azaz alma==korte. Ezt követően reflection használatával elkérjük a String osztály value nevű mezőjét. Vigyázat, kezdők, és akik nem jártasak a reflection-ben: NEM a value mező értékét kérjük el, már csak azért sem, mert nem egy String típusú objektum value mezőjéről beszélünk, hanem az osztály value mezőjéről. Ez nem egy static mező, így értéke sincs, nem osztálypéldány. Ettől függetlenül még kaphatunk egy Field típusú objektumot, amelyik metódusai ezen a mezőn végeznek műveletet.

Amikor megvan ez az objektumunk, akkor a hozzáférést true értékre állítjuk. Ezzel elérjük, hogy azokat a mezőket is el tudjuk érni, amelyek private módosítóval rendelkeznek, vagy egyéb okok miatt nem férhetnénk hozzá a mezőhöz (pl. protected a mező, és nem azonos csomagban vagyunk, sem pedig leszármazottak nem vagyunk). Innen kezdve a field objektumon keresztül el tudjuk érni egy String objektum value mezőjét.

Ezt követően ezt a mezőt átállítjuk. Az értéke egy karakter tömb lesz, a k, o, r, t, e értékekkel, és a hossza 5. Ez azonban nem a string hossza, csak a string objektum value mezőjének a hossza. A String karakterei ugyanis csupán valahol ebben a tömbben vannak, nem feltétlenül az első elemnél kezdődik a string és nem feltétlenül tart a tömb végéig. A string objektum hosszát nem állítottuk át, az maradt továbbra is a "alma".length() értéke, ami 4.

Ennek folyományaként pedig a a string objektum értéke "kort", mert a hossz érték 4.

Érdekes, hogy a szavazás állása szerint jelen pillanatban (2012-07-26 11:30pm) a korte 12:8-ra vezet a helyes kort-tel szemben, de hát a tények makacs dolgok, ez nem demokrácia, hogy a szavazás döntsön.

Miért érdemes ezt tudni?

Például érdemes tudni, hogy a substring metódus nem fogja a karaktereket átmásolni, hanem az eredeti és a substring által visszaadott string ugyanazt a karakter tömböt fogja használni. Ezért nem különösebben nagy költség, még igen nagy string objektumoktól sem elkérni substring-et. (Stringet konkatenálni persze az már más.)

Azért is érdekes lehet a kérdés, hogy tisztában legyünk azzal, hogy ha kiadunk egy JAR fájlt a kezünkből, akár obfuscated lebutítva, hogy ne nagyon lehessen visszafejteni, és elektronikusan aláírva, hogy ha valaki belepiszkál, akkor a class loader már ne töltse be (melyik class loader?) azzal nagyon sokat nem növeltünk a biztonságon.

Készítettünk licence ellenőrző modult, most is elérhető LGPL License3j néven. Ez abban segíti a felhasználót, hogy ne használja véletlenül a szoftvert olyan esetben, amikor nincs rá joga. De arra izmozni, hogy ne is tudja, nem érdemes, mert azt még C-ben sem lehet. A natív gépi kódot is át lehet írni, a JVM byte kódba is bele lehet piszkálni, lehet class loadert írni, ami kikerüli a betöltési védelmeket, és végül, de nem utolsó sorban át lehet írni reflection-nel a futó Java-ban bármit.

De aki ilyet tesz, az hacker. Nem programozó.