tifyty

pure Java, what else ?

Dobni, vagy nem dobni, az itt a kérdés

akkor nemesb-e a lélek, ha tűri balsorsa minden nyűgét, s nyilait;
Vagy ha kiszáll tenger fájdalma ellen, s hibát ragadva kivételt dob neki?

Elnézést kérek.

Szóval egy metódus dobjon-e inkább kivételt, vagy inkább adjon vissza olyan értéket, amelyik speciális? Jelezzen null érték visszaadásával, vagy legyen egy olyan metódusa az osztálynak, amelyik segít eldönteni, hogy a másik metódus meghívható-e, és ha nem hívható meg, de mégis meghívjuk, akkor viszont kapjunk kivételt? Sok ilyen kérdést lehet feltenni, és sokáig el lehet gondolkodni azon, hogy melyik API az, amelyik a hívó oldalon olvashatóbb kódot eredményez. Ez azonban nagyrészt pszichológia, hiszen nem azt akarjuk megmondani, hogy egy program működik-e, vagy sem, hanem azt, hogy majd az “átlag programozó”, ha ugyan létezik ilyen, milyen API-val fog jó kódot készíteni.

Csak így magunk közt szólva: semmilyennel, maximum működő kódot fog készíteni, és már az is a szerencsésebb esetek közé tartozik. Láttatok már távoli keleten készült kódot?

Az Effective Java könyv ad tanácsokat, de talán nem árt, ha egy kicsit jobban körbejárjuk ezt a témát. Nem biztos, hogy mindenben teljesen igaza van annak a könyvnek, illetve sok mondása olyan, hogy igaznak ugyan igaz, de amikor “szakemberek” hivatkoznak a könyvre, akkor már nem feltétlenül helyes, amit mondanak a könyv alapján. Pedig csak azt mondják, amit a könyv is mond, csak a környezet más. Ebben a cikkben megnézzük, hogy ez a könyv miket mond a kivételekről, és utána méréseket végzünk, hogy mibe kerül kivételt kezelni.

Effektív Jáva Kivételek

Item 57: Használjunk kivételt kivételes esetben. Ez teljesen igaz. Csak úgy, szíre-szóra nem szabad kivételt dobni, vagy olyan módon használni szerkezeteket, hogy nem ellenőrizzük például a null értéket, mert majd elkapjuk az NPE-t. Ha valahol egy kódban NPE van elkapva: az bűzlik. Az a kód nem optimális, sem sebességben, sem olvashatóságban, és így karbantarthatóságban sem.

Item 58: használjunk ellenőrzött (checked) kivételt minden olyan esetben, amiből ki lehet jönni, és futási kivételt (runtime exception), programozási hibára. Nem akarok ezzel vitába szállni, hallottam már olyan véleményt is, hogy az egész ellenőrzött kivétel, mint fogalom elhibázott. Nekem hiányozna.

Item 59: Ne használjunk feleslegesen ellenőrzött kivételeket. Erre hivatkozva hallottam a fenti állítást. Valójában azt mondja a könyv, hogy ha van rá mód, akkor elgondolkodhatunk azon, hogy a

try{
  obj.action(args);
  }catch(TheCheckedException e){
 ...
  }

szerkezet helyett használhatunk inkább

if( obj.actionPermitted(args) ){
  obj.action(args);
  }else{
 ...
  }

ha lehet. És ha az jó.

Item 60: Ha lehet, inkább használjunk szabványos kivételt és ne készítsünk sajátot. Ez elég evidens: nem csak a kivételeknél van ez így. Ha van egy osztály, amelyik megfelel a célunknak, akkor ne készítsünk magunknak egy másikat, csak azért, hogy legyen egy sajátunk. Egy Java osztály nem státusszimbólum.

Item 61: Az absztrakciós szintnek megfelelő kivételt dobjunk. Ez azt mondja, hogy a kivétel feleljen meg az osztály/metódus szintjének. Például egy kamatszámítási metódus ne dobjon IOException-t, akkor sem, ha nem tudja olvasni a számításhoz szükséges aktuális napi kamatkonfigurációs fájlt. Ilyen esetben az exception-t át kell csomagolni, hogy a metódus szignatúrája egységesen magas (vagy éppen alacsony) szintű legyen. Bár általában alacsony absztrakciós szintről nem szoktunk magas absztrackciós szintű osztályokat hívni.

Item 62: dokumentálni kell a kivételeket, amiket dobunk. Ez evidens, nem csak azt kell dokumentálni, hanem jószerével mindet, amit maga a kód nem dokumentál. Mondjuk nem mindenki szeret annyit gépelni, mint én.

Item 63: Tegyük bele a kivételbe a hibáról szóló információkat a részletes üzenetbe. Ez olyankor fontos, ha a hibát ember (is) kezelni fogja. Bekerül a naplófájlba, és valaki majd el fogja olvasni. Anno még a FORTRAN korban hozták oda hozzám az építész hallgatók a teljes oldal FORTRAN printout-ot, amire csak annyi volt a program alá nyomtatva: SYNTAX ERROR. Aztán keresd meg és holnap add le újra a lyukkártyákat. Szerencsére már nem itt tartunk, és ez a programozók érdeme. Maradjon is így.

Item 64: törekedjünk az atomi hiba kezelésre azaz, ha hibát dobunk, akkor az objektum még maradjon használható állapotban. Ez is elég evidens, de a mostani vizsgálat szempontjából annyira nem fontos.

Item 65: Ne hagyjuk figyelmen kívül a kivételeket. Ismerős ugye:

try{
  obj.action(args);
  }catch(Throwable t){
  }

?

Persze nem ennyire durván, csak ahogy az Eclipse vagy a NetBeans (vagy tetszőleges más IDE) előadja valamelyik kód template-je alapján. Akár még lehet is benne egy e.printStackTrace() ami egy alkalmazás szervere alatt aztán kiköt egy sose nézett napló fájlban, vagy a /dev/null-ban. Hát ne!

Mérjünk!

Mit mérjünk? Mérjünk, hogy merjünk kivételt dobni. Hívjunk meg mondjuk 10milliószor egy metódust, ami csak visszatér egy null értékkel, és mérjük meg, hogy mi történik, ha kivételt dob és azt el kell kapnunk.

Az első kód:

                        final long loopSize = 10_000_000;
...
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				Object z = nullReturner();
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with null");

a második pedig:

			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionell();
				} catch (Exception e) {

				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with exception");

(Bevallom őszintén, és bűnbánóan, hogy ezek a benchmark kódok nem mennének át a sonar copy-paste szűrőjén. És bizony, már itt is van a 65. tétel megsértése: a kivétel ignorálása ebben az esetben kivételes eset, tisztelt bíróság …)

A két metódus pedig:

	private static Object exceptionell() throws Exception {
		throw new Exception();
	}

	private static Object nullReturner() {
		return null;
	}

Ha pedig futtatjuk, akkor a következő eredményt kapjuk:

13ms with null
12221ms with exception

Ez azért lényeges különbség! Kivételt dobni és elkapni ezerszer többe kerül, mint csak egy értékkel visszatérni. De igazából mi tart ilyen sokáig? Az kivétel létrehozása, az eldobása, vagy az elkapása?
Dobjunk egy olyan kivételt, amelyik előre el van készítve:

			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionelle();
				} catch (Exception e) {

				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with constant exception");

és a metódus amit hívunk:

	final static private Exception e = new Exception();

	private static Object exceptionelle() throws Exception {
		throw e;
	}

Ennek eredménye:

22ms with constant exception

ami már nem annyira rossz! Persze ebben az esetben ne is álmodjunk arról, hogy az exception meg fogja mondani a stack trace-ben, hogy honnan dobták. Azt tudja csak, hogy hol készült, a stack milyen mélységében:

This is what exceptionelle throws:
java.lang.Exception
	at XceptionTest.<clinit>(XceptionTest.java:133)

Éppen ez a lényeg, hogy nem kell összegyűjtenie a stack tarce-t, szemmel láthatólag az tart sokáig. De ha olyan ellenőrzött kivételt dobunk, amelyiket el fognak kapni, akkor általában nincs szükségünk a stack trace-re. Persze ha egy static final kivételt dobunk, akkor abban nem tudunk átadni paramétereket. Azért ezt néha szeretnénk. (Már aki…)

Java 1.7 óta lehetőségünk van rá, hogy olyan kivételt hozzunk létre, amelyik nem gyűjti össze a stack trace-t magába. Ehhez egy saját kivétel osztályt kell létrehozni, és a megfelelő (protected) super konstruktort meghívni:

	private static Object exceptionella() {
		return new MyException();
	}

	private static class MyException extends Exception {
		private static final long serialVersionUID = 1L;

		public MyException() {
			super("nomessage", null/* cause */, false/* enableSupression */,
					false/* writableStackTrace */);
		}
	}

És a kód ami mér:

			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionella();
				} catch (Exception e) {

				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with exception w/o stack trace");

(Milyen változatos ez a program…)

A futási eredmény

260ms with exception w/o stack trace

No, azért ez már nem olyan villám, mintha csak egy konstans kivétellel térnénk vissza, de mégis csak hússzoros és nem ezerszeres idő a kivétel nélküli esethez képest. De itt mi viszi el az időt? Az új kivétel objektum létrehozása, vagy magát a kivételt drága még így is létrehozni? Vagy a kivétel elkapása a drága? Nézzük meg, hogy mi a helyzet ha csak egy sima Object-t hozok létre:

	private static Object exceptionello() {
		return new Object();
	}

(Ennél egyszerűbb mért kód talán már nem is lehet.) És a mérőkód:

			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				Object z = exceptionello();
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with new Object");

Az eredmény pedig:

15ms with new Object

Nem kerül túl sokba egy új objektum létrehozása. Majdnem semmi. Ebben a teszt környezetben biztonsággal nem is mérhető a különbség, de azért feltehető, hogy nem nulla és nem negatív idő alatt jön létre 🙂

Konklúzió

Kivételt létrehozni drága. Ha a nem elég gyors a programod, és tuningolni kell rajta, és azt mutatja profiler, hogy sok időt visz el a kivételek létrehozása és dobálása, akkor

  • Gondolkozz el azon, hogy lehet-e előre gyártott kivételeket dobni, ez lehet egy gyors tuning.
  • Ha az időt felhasználó kivételek saját kivételek, amelyek hurcolnak információt, akkor Java 1.7 alatt lehet használni a stack trace nélküli konstruktort. Ez egy picit több idő, de még mindig a gyors tuning.
  • Gondolkozz el azon, hogy miért dob ilyen gyakran kivételt a program? (Feltehetőleg nem tiszta a kód.)

Nem szabad, hogy ebbe a problémába ütközz. Ha tényleg a kivételek dobása viszi el az időt, akkor nézd meg, hogy van-e elegendő unit teszted, és ha van, akkor refactor, refactor, refactor. Ha nincs, akkor meg úgyis mindegy…

Prolog

Mint arra bölcs kollégánk az előző cikk kapcsán rámutatott ezek a mérések nem precíz benchmarkok. Nagyságrendileg lehet rájuk támaszkodni, azaz azt lehet mondani, hogy null értékkel visszatérni gyorsabb, mint kivételt dobni, de körülbelül ennyi. Az előző cikkekhez képest áttértem a nanoTime használatára, de nem gondolom, hogy az előző cikkek eredményei s a következtetések ne állnának meg. A teljes mérőprogram egy db. .java fájl, ha valakit érdekel itt van copy/paste:

public class XceptionTest {

	public static void main(String[] args) {
		Long timeStart, timeEnd, dTime;
		final long loopSize = 10_000_000;
		switch (args[0]) {
		case "1":
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				Object z = nullReturner();
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with null");
			break;
		case "2":
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionell();
				} catch (Exception e) {

				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with exception");
			break;
		case "3":
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionelle();
				} catch (Exception e) {

				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with constant exception");
			break;
		case "4":
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionella();
				} catch (Exception e) {

				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with exception w/o stack trace");
			break;
		case "5":
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				@SuppressWarnings("unused")
				Object z = exceptionello();
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime + "ms with new Object");
			break;
		case "6":
			try {
				@SuppressWarnings("unused")
				Object z = exceptionelle();
			} catch (Exception e) {
				System.out.println("This is what exceptionelle throws:");
				e.printStackTrace();
			}
			break;
		case "7":
			timeStart = System.nanoTime();
			for (int i = 0; i < loopSize; i++) {
				try {
					@SuppressWarnings("unused")
					Object z = exceptionelln();
				} catch (Exception e) {
				}
			}
			timeEnd = System.nanoTime();
			dTime = (timeEnd - timeStart) / 1_000_000;
			System.out.println(dTime
					+ "ms null return but could throw exception");
			break;
		case "8":
			try {
				@SuppressWarnings("unused")
				Object z = exceptionell();
			} catch (Exception e) {
				System.out.println("This is what exceptionell throws:");
				e.printStackTrace();
			}
			break;
		case "9":
			try {
				@SuppressWarnings("unused")
				Object z = exceptionella();
			} catch (Exception e) {
				e.printStackTrace();
			}
			break;
		}
	}

	private static Object exceptionelln() throws Exception {
		return null;
	}

	private static Object exceptionello() {
		return new Object();
	}

	private static Object exceptionella() {
		return new MyException();
	}

	private static class MyException extends Exception {
		private static final long serialVersionUID = 1L;

		public MyException() {
			super("nomessage", null/* cause */, false/* enableSupression */,
					false/* writableStackTrace */);
		}
	}

	final static private Exception e = new Exception();

	private static Object exceptionelle() throws Exception {
		throw e;
	}

	private static Object exceptionell() throws Exception {
		throw new Exception();
	}

	private static Object nullReturner() {
		return null;
	}

}

A környezet, amin a mérést végeztem, ugyanaz, mint a múltkor:


A tesztek egy 8GB memóriával felszerelt MacBook Pro7,1 gépen futottak OS X 10.7.4, 7-es Java-val (ez utóbbit értő szem több helyen is észrevehette), de azért itt van egy ‘java -version’ kimenete:

verhasp:java verhasp$ java -version
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b21)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)

4 responses to “Dobni, vagy nem dobni, az itt a kérdés

  1. tvik augusztus 7, 2012 2:16 du.

    A konklúzióban a probléma kezelésének módja éppen fordított sorrendben van, mint amilyenben szerintem próbálkozni kellene. Tehát először meg kell nézni, hogy miért van annyi kivétel, lehet-e ezt csökkenteni, aztán a stacktrace nélküli kivételek alkalmazása, aztán a többször felhasználható kivételek.

    -Ha túl sokszor fordul elő egy kivétel, az valójában nem is kivételes eset.

  2. hron84 augusztus 7, 2012 3:24 du.

    Egy Shakespeare veszett el benned. De nagyon 🙂

  3. Kofa augusztus 30, 2012 4:40 du.

    Sajnos a Java néha maga is “optimalizálja” az exceptionöket, és akkor sincs stack trace, ha szeretnéd…
    http://www.javaspecialists.eu/archive/Issue187.html

  4. Robin P (@bootcode) december 12, 2012 6:49 du.

    Én a null object patternt favorizálnám (lásd Java 8 / Guava Optional típus). Ha pedig egyszer sikerül elszakadni Java-tól, akkor kiderül, hogy az Optional (illetve a hibakezelés úgy általában) megoldható a megfelelő monádokkal, s máris vidámabb az élet. Erős idegzetűeknek http://learnyouahaskell.com , amire a “For a few monads more” fejezet végére érsz, tudni fogod mi a helyzet 🙂

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s