Programozás | Java, JSP » Tajti Ákos - Java fejtörők és trükkök

Alapadatok

Év, oldalszám:2008, 14 oldal

Nyelv:magyar

Letöltések száma:346

Feltöltve:2010. március 18.

Méret:997 KB

Intézmény:
-

Megjegyzés:

Csatolmány:-

Letöltés PDF-ben:Kérlek jelentkezz be!



Értékelések

Nincs még értékelés. Legyél Te az első!


Tartalmi kivonat

Java fejtörõk és trükkök Tajti Ákos Tartalomjegyzék Hogyan érjük el egy osztály privát metódusát? StringTokenizer – Kent Beck nem örülne Háromszor singleton A dátum API hibái Hogyan generáljuk automatikusan a Hibernate konfig fájlokat? Java Stringek interálása Promóció Az OO paradigma lelke a belsõ állapot elrejtése. Mikor érdemes mégis megkerülni és milyen lehetõségeink vannak rá? Errõl szól ez a post. Hogyan érjük el egy osztály privát metódusát? Erre néha szükség lehet olyankor, amikor tudjuk, hgy egy hasznos funkcionalitást már implementáltak egy privát metódussal, és nem szeretnénk újraírni. Ezzel idõt spórolhatunk, de csökkenti a rugalmasságot Egyszóval: mértékkel. De hogyan vihetjük ezt véghez? Egy osztályon belül természetesen nagyon egyszerû. De mi van akkor, ha az A osztályból akarjuk meghívni a B osztály egy privát metódusát? Ilyenkor nem marad más, csak a Java Reflection API-ja. A

java.langreflect csomagban olyan osztályokat találhatunk, amik a nyelv bizonyos elemeit reprezentálják. Ilyen a Class, a Method és a többi osztály Ha ezeket megfelelõ módon használjuk, akkor példányosíthatunk osztályokat és meghívhatunk metódusokat. Az alábbi kód például meghívja a Capsule osztály privát salute metódusát. Capsule reflectand = new Capsule(); Class[] parameterTypes = new Class[]{ String.class }; Method method = reflectand.getClass()getDeclaredMethod("salute", parameterTypes); method.setAccessible(true); String result = (String)method.invoke(reflectand, new Object[]{"ákos"}); System.outprintln(result); Az elsõ sor létrehoz egy példányt az osztályból, ezen fogjuk majd meghívni a metódust. A második sorban egy tömbben összefoglaljuk, hogy milyen típusú paramétereket vár a metódus, amit meg akarunk hívni (a tömb Class típusú elemeket tartalmaz). A harmadik sorban létrehozzuk azt a Method objektumot, ami a

salute metódust reprezentálja. Mivel a metódust a paraméterlistája és a neve egyértelmûen jellemzi (néha azonban mégsem, erre egy késõbbi postban visszatérek) , ezért ezeket kell megadnunk. Ha megvan a method objektum, akkor a setAccessible() metódussal láthatóvá kell tennünk, mivel privát. Ezután az invoke() metódussal hívhatjuk meg. Egyszerû A fenti kód rengeteg kivételt dobhat, ezeket nem tüntettem fel. A dolog mégsem olyan szép, mint amilyennek látszik. Ha ugyanis a rendszerben konfigurálva van egy megfelelõ security policy, akkor a módszer nem fog mûködni. Viszont ekkor sem lehetetlen meghívni a privát metódust: jó esetben a konfiguráción tudunk változtatni, ráadásul gyakran nincs security manager konfigurálva. Mikor lehet szükség privát metódusok használatára? Például akkor, ha JavaBeaneket szeretnénk egyszeren kezelni. Vagy akkor, ha nem szeretnénk (jogosan) újraimplementálni valamit, amit már más megírt. Mindenesetre

érdemes tudni a reflection létezésérõl. StringTokenizer – Kent Beck nem örülne Ebben a bejegyzésben arra láthatunk példát, mekkora galibát okozhat egy rosszul elnevezett paraméter. Nem véletlenül szánt Kent Beck egy külön fejezetet a nevek helyes megválasztására. Mit ír ki az alábbi programrészlet? String input = "a|b||c"; String delim = "||"; StringTokenizer tokenizer = new StringTokenizer(input, delim); System.outprintln(tokenizernextToken()); A StringTokenizer osztályt sztringek feldarabolására használhatjuk. Több konstruktora van, a fent használt változatban a feldarabolni kívánt sztringet (input) és az elválaztó karaktereket (delim) kell megadnunk. A fenti példában a "a|b||c" sztringet szeretnénk feldarabolni a "||" elválasztó karakterrel. Azaz mindenhol el akarjuk vágni, ahol kér | áll Azt várjuk tehát, hogy a program kimenete a "a|b" sztring lesz (mivel csak az elsõ tokent

íratjuk ki). Ha viszont lefuttatjuk a kódot teljesen mást kapunk: a program az "a" sztringet írja ki. Olyan, mintha a nextToken() metódus nem értené, hogy csak a dupla | szimbólumnál kell elvágnia a sztringet. Ez részben igaz is, valóban nem érti, de nem is akarja érteni. A StringTokenizer konstruktoráról ezt mondja az API: The characters in the delim argument delimiters for separating tokens. are the Ez azt jelenti, hogy a tokenizer nem egy elválasztóként kezeli a megadott sztringet, hanem elválasztó karakterek halmazaként. Ha az input sztringben a halmaz bármelyik elemét megtalálja, ott elvágja a sztringet. Ha egy több karakterbõl álló szimbólumot akarunk elválasztásra használni, más módszerekhez kell nyúlnunk (pl.: saját metódus indexOf()-fal). A hiba nyilvánvalóan a paraméter hibás elnevezésébõl ered. Ha delims lenne a neve, a többesszámból nyilvánvalóvá válna, hogy itt több elválasztóról van szó. Ügyeljünk

mindig a paraméterek elnevezésére, hogy megkönnyítsük más programozók munkáját! Kent Beck Implementációs minták címû könyvében Java idiómákat ír le kategóriák szerint elrendezve. Háromszor singleton Biztosan mindenki használta már a Singleton tervezési mintát. Röviden: azt érhetjük el vele, hogy egy osztályból egyszerre csak egy példány létezhessen, azaz senki ne példányosíthassa.Több oka lehet annak, hogy ezt akarjuk Javában többféleképpen is megvalósíthatjuk a mintát. Az egyik lehetõség a publikus statikus mezõ használata privát konstruktorral: public class Highlander{ public static final Highlander INSTANCE = new Highlander(); private Highlander(){ } } A privát konstruktor pontosan egyszer hívódik meg,és az INSTANCE mindig ugyanaz a referencia lesz. A konstruktort nem hívhatja meg senki (kivéve a privilegizált és nagyon rosszindulatú személyeket), így mindig ugyanarra az objektumra fogunk hivatkozni. A másik

lehetõség a factory metódus használata: public class Highlander{ private static final Highlander instance= new Highlander(); private Highlander(){ } public static Highlander getInstance(){ return instance; } } Annyi a különbség az elõzõ megközelítéshez képest, hogy az egyetlen példányt egy statikus metódus adja vissza (factory metódus). Ennek az az elõnye, hogy ha rájövünk, hogy mégsem egy példányra van szükségünk, akkor a metódus kódjának átírásával elérhetjük ezt. Egyébként hatékonyságban ez a megoldás nem jobb az elõzõnél, mivel az új JVM implementációk az elõzõ alakra hozzák a kódot (inline). Mi a probléma ezekkel a megoldásokkal? Tegyük fel, hogy az osztályunkat szerializálni szeretnénk. Ehhez elõször implementálnunk kell a Serializable interfészt Ez eddig nem gond A problémát a példány statikus volta jelenti, ugyanis a statikus mezõk nem szerializálhatók. Azaz ha szerializáljuk az osztályt, majd visszaolvassuk,

akkor egy új objektum jön létre, a régi pedig elveszik. Azaz singletont akartunk, de mégsem azt kapunk Mi a megoldás? Az egyik az, hogy készítünk egy speciális readResolve metódust. Ez igen körülményes megoldás, és szerencsére van ennél jobb is: használhatunk egyelemû enumerációt. Ebben a megközelítésben a fenti kód így nézne ki: public enum Highlander{ INSTANCE; } Ebben az esetben is egyetlen példányunk van (INSTANCE), amit az elsõ változathoz hasonlóan érhetünk el (Highlander.INSTANCE) Az eredeti metódusokat az enumerációban is változatlanul implementálhatjuk. Amit nyerünk: rövidebb kód és automatikus szerializáció. Az enumerációnál nem kell a szerializációból adódó problémákkal foglalkoznunk, az már meg van oldva. A legutolsó megoldás a Java 1.5-tõl létezik, de elõnyei ellenére még most sem annyira elterjedt, mint az elsõ kettõ. A tervezési minták programozási nyelvtõl független megoldások gyakran

felmerülõ problémákra. Az osztályok és objektumok kapcsolatával foglalkoznak. A dátum API hibái Mit ír ki az alábbi programrészlet? public class DatingGame { public static void main(String[] args) { Calendar cal = Calendar.getInstance(); cal.set(1999, 12, 31); // Year, Month, Day System.outprint(calget(CalendarYEAR) + " "); Date d = cal.getTime(); System.outprintln(dgetDay()); } } Ezt a fejtörõt Bloch könyvébõl vettem, és ez az egyik kedvencem, mivel rávilágít arra, hogy mennyire érdemes az API-t olvasnunk, mikor nem vagyunk biztosak egy metódus funkciójában. Nem szabad csak a metódusnevekre hagyatkoznunk, mivel azok félrevezetõek lehetnek. Ráadásul azt is megerõsíti, hogy nagyon ráférne már egy felújítás a Java dátum API-jára (erres talán sor is kerül, ha a JodaTime bekerül a Java7-be). Az elsõ hiba ebben a sorban van: cal.set(1999, 12, 31); // Year, Month, Day A kód írója úgy akarta beállítani a Calendar objektumot, hogy

az 1999 december 31 dátumot tartalmazza. Teljesen természetesnek tûnik, hogy a set metódust ezekkel a paraméterekkel hívta meg. De sajnos nem az A hónapok sorzámozása ugyanis 0-val kezdõdik, így a decembernek a 11 felel meg. Azaz mikor a hónapparaméternek a 12 értéket adjuk meg, akkor valójában a (nem létezõ) 13. hónapra utalunk Semmi probléma, gondolnánk, akkor kiváltódik egy kivétel, hogy jelezze a hibát. Sajnos nem ez történik Ilyenkor az év továbbgörgetõdik, azaz itt nem 1999, hanem 2000 kerül beállításra, a hónap pedig nem a 13., hanem az elsõ lesz. Azaz itt 2000 január 31 kerül beállításra Azonban ez még mindig nem a végleges kimenet. Van még egy hiba ebben a sorban: System.outprintln(dgetDay()); Ha elolvassuk a getDay() metódus leírását, kiderül, hogy az nem a hónap napját adja vissza, hanem azt, hogy az adott dátum a hétnek mely napjára esik. Azaz, mivel 2000 jabuár 31 hétfõre esett, a visszaadott érték 1. A

megfelelõ metódus a getDate() (elég szerencsétlenül lett elnevezve). Ez a hónap napját adja vissza, azaz 31-ét A tanulság az, hogy ne bízzunk vakon a metódusok elnevezésében, mindig olvassuk el figyelmesen a leírást. Ez fokozottan igaz akkor, ha dátumkezeléssel van dolgunk Hogyan generáljuk automatikusan a Hibernate konfig fájlokat? A hibernate alapvetõen az objektumorientált és a relációs szemléletmód közti szakadékot hidalja át. Erre azért van szükség, hogy Java objektumainkat (azaz belsõ állapotuknak bizonyos részleteit) tetszõleges adatbázisba menthessük. Ehhez természetesen szükség van valamilyen segédállományokra, amelyek leírják, hogy az objektumokat hogyan kell leképezni az adatbázisra. Meg kell mondanunk, hogy melyik táblába akarjuk menteni az objektumot, és azt is, hogy melyik mezõnek melyik relációs attribútum felel meg. A hibernate esetében ezek a segédállományok az ún mapping fájlok. Ezek xml állományok

meghatározott szerkezettel A tutorialban arról lesz szó, hogyan generálhatjuk a szükséges állományokat automatikusan. Ehhez a hibernate és az ant alapvetõ ismeretére lesz szükség. A mapping fájlokat szerencsére nem kell mindig kézzel megírnunk. A hibernate biztosít eszközöket arra, hogy az osztályokból automatikusan generáljuk õket, majd a mappingek alapján elkészíti az adatbázissémát. Ha viszont fordított a helyzet, azaz, ha egy adatbázisséma alapján akarunk mappingeket generálni, majd azokból osztályokat, akkor más eszközöket kell segítségül hívnunk. Kell valami, amivel az adatbázisból generáljuk a mappingeket, majd abból könnyen elkészíthetjük a Java osztályokat. A mappingek generálására jól használható a middlegen eszköz. Ezt letölthetjük innen A middlegen jar állományait helyezzük a CLASSPATH környezeti változóba, vagy a build.xml állományba (a projektünk build fájlja) vegyük fel a következõ részletet:

<path id="middlegen.classpath"> <fileset dir="${basedir}/lib/middlegen"> <include name="*.jar"/> </fileset> </path> Ezzel a build.xml-ben késõbb felhasználható middlegenclasspath nevû változót hoztunk létre, amivel a lib/middlegen könyvtár jar fájljaira hivatkozunk (feltételezzük, hogy a middlegen összes jar fájlját elhelyeztük itt). A middlegen egyik jar állományában van egy middlegen.MiddlegenTask osztály Ez egy ant task (ilyet úgy lehet írni, hogy kiterjesztjük a Task osztályt), azaz felhasználhatjuk a build.xmlben, de elõször meg kell mondanunk, hogy használni akarjuk: <taskdef name="middlegen" classname="middlegen.MiddlegenTask" classpathref="middlegen.classpath"/> A task neve middlegen lesz, a middlegen.MiddlegenTask osztály valósítja meg a logikáját, amit a middlegen.classpath elérési útvonalon találunk meg Ezt azért kell megadni, mert

alapértelmezésben az ant minden taskot saját lib könyvtárában keres, ez pedig nem ott van. Ezután a taskot ugyanúgy használhatjuk, mint bármeyik másikat, és használjuk is a mappingek generálására. <target name="generate.hbm"> <middlegen appname="faktum" prefsdir="${basedir}/prefs" gui="false" databaseurl="jdbc:oracle:thin:@localhost:1521/db" driver="oracle.jdbcOracleDriver" username="user" password="pass" > <!-- Hibernate Plugin --> <hibernate destination="${basedir}/model" package="tajti.exmodel" javaTypeMapper= "middlegen.pluginshibernateHibernateJavaTypeMapper" standardGeneratorScheme="sequence" standardGeneratorArg="sequence a"/> </middlegen> </target> A mappingek generálását a generate.hbm végzi A middlegen task attribútumai között meg kell adni az alkalmazás nevét (appname), azt a

könyvtárat, ahol a middlegen tárolhatja ideiglenes állományait (prefsdir), azt, hogy akarunk-e a generáláshoz GUI-t igénybevenni (gui), az adatbázis url-jét (databaseurl), a JDBC meghajtót (driver), a felhasználónevet (user), és a hozzá tartozó jelszót. Ezen kívül van még több opcionális paraméter, ezekrõl részletesebben ír a middlegen dokumentációja. A beágyazott hibernate elemben specifikálhatjuk a generálás lépéseit. Itt kell megadnunk, hogy milyen könyvtárba kerüljenek a generált mappingek (destination), hogy melyik csomagban lesznek majd a mappingekhez tartozó osztályok (package) és típusleképezést akarunk használni hogy milyen (javaTypeMapper). Ez utóbbi általában mindig ugyanaz, viszont kedvünk szerint kiterjeszthetjük az osztályt, és használhatjuk azt. Erre például akkor van szükség, ha a hibernate-l elmentett objektumok kulcsát egy szekvenciából akarjuk elõvenni. A nagy egészeket a fenti típusleképezõ

osztály BigIntegerként képezi le. Ehhez a típushoz viszont nem használhatjuk a szekvenciás megoldást. Ilyenkor érdemes új típusleképezõ osztályt írni A hibernate elem többi része opcionális. A példában a standardGeneratorScheme attribútum azt jelenti, hogy a perzisztált objektumok kulcsát egy szekvenciából akarjuk elõvenni, a standardGeneratorArg pedig megmondja, hogy melyik szekvenciából. Ha a parancssorban, a projekt könyvtárában ezután kiadjuk az ant generate.hbm parancsot, akkor a middlegen a model könyvtárban elkészíti a mappingeket. A mappingekbõl a org.hibernatetoolantHibernateToolTask taskkal generálhatunk Java osztályokat. Ugyanúgy, ahogy az elõbb, definiáljuk az elérési utat, majd a taskot: <path id="hibernate.toolclasspath"> <fileset dir="${basedir}/lib/hibernate"> <include name="*.jar"/> </fileset> </path> <!-- hibernate tool task--> <taskdef

name="hibernatetool" classname="org.hibernatetoolantHibernateT oolTask" classpathref="hibernate.toolclasspath"/> A Java osztályok generálásához a következõ szkriptrészlet szükséges: <target name="generate.java"> <hibernatetool destdir="${basedir}/tajti/ex/model" classpath="${hibernate.toolclasspath}" > <configuration> <fileset dir="${basedir}/model"> <include name="*/.hbmxml"/> </fileset> </configuration> <hbm2java/> </hibernatetool> </target> A destrdir attribútumban a mappingek generálásánál használt csomagnevet kell megadnunk. Ha ezután az ant generatejava parancsot kiadjuk, akkor minden mappinghez (ha azok helyesek) kapunk egy Java állományt a egfelelõ helyen. Ezután még generálhatjuk a hibernate.cfgxml konfigurációs állományt Ehhez az alábbi szkriptrészlet szükséges: <target

name="generate.config"> <hibernatetool destdir="${basedir}/model"> <configuration> <fileset dir="${basedir}/model"> <include name="*.hbmxml"/> </fileset> </configuration> <hbm2cfgxml/> </hibernatetool> </target> Ezt az ant generate.config paranccsal futtathatjuk FIGYELMEZTETÉS! Ahogy látszik, megtehetjük, de nem a legszerencsésebb az adatbázisból generálni az osztályokat. A hibernate-t a fordított esetre találták ki Ha mégis ezt az utat választjuk, akkor késõbb rengeteg problémánk lesz az generált osztályok közötti öröklõdés kialakítására. Továbbá az is probléma, hogy a middlegen nem támogatja a mappingek inkrementális generálását. A végeredmény az, hogy alkalmazásunk nagyon érzékeny lesz az adatbázis változásaira. Csak akkor használjuk ezt a megoldást, ha úgy gondoljuk, hogy az adatbázis szerkezete már végleges! Java Stringek

internálása Adott az alábbi Java program. public class InterningStrings { static String s1 = new String("valami"); static String s2 = new String("valami"); public static void main(String[] args) { s1 = s1.metódus(); s2 = s2.metódus(); System.outprintln(s1 == s2); } } A String osztály melyik metódusának a nevével kell helyettesítenünk a kódban a "metódus" szót, hogy a program kimenete true legyen? Ha a main elsõ két sorát elhagyjuk, akkor a program kimenete false. Igaz ugyan, hogy a két objektum ugyanazt a sztringet tartalmazza, de mivel példányosítással hoztuk létre õket, nem pedig a sztringeknél megengedett speciális lehetõséggel, ezért a két objektum identitása nem egyezik meg (s1 és s2 különbözõ objektumokra hivatkoznak). Az == operátor az identitás egyezõségét vizsgálja, így erre a két objektumra false értéket ad, és ez lesz a program kimenete. Ha azt akarjuk, hogy a kimenet true-ra változzon, akkor

találnunk kell egy olyan metódust a String osztályban, amely valahogy ugyanazt a referenciát adja vissza s1-re és s2-re meghívva. Szerencsére van ilyen metódus Ha elkezdjük böngészni a String osztály dokumentációját, az "i" betûnél megtaláljuk a szükséges metódust. A neve: intern() Leírásából megtudjuk, hogy a String osztálynak van egy privát sztring kollekciója, ami kezdetben üres. Amikor az intern() metódust meghívjuk, két dolog történhet. Vagy benne van már a sztring, amit az objektum tartalmaz, a kollekcióban, vagy nincs Ha benne van (ezt a metódus a String osztály equals() metódusával, tehát érték egyezõség vizsgálattal dönti el), akkor a metódus visszaadja a kollekcióból a megfelelõ objektum referenciáját. Ha nincs benne, akkor létrejön egy új String objektum a megfelelõ sztringgel, és annak referenciáját adja visza a metódus. Azt is megtudjuk, hogy minden sztring literál alapértelmezetten

"internált", ezért a "valami" == "valami" kifejezés értéke true. Ha a fenti példában a "metódus" szót mindenhol az "intern" szóval helyettesítjük, akkor a következõk történnek. Az s1intern() hatására kiderül, hogy a "valami" sztring még nincs a kollekcióban. Ekkor létrejön egy új String objektum (new String("valami")), és referenciája a kollekcióba kerül. Az intern() ezt a referenciát adja vissza, és mi ezt adjuk értékül s1-nek. Az s2intern() hívásnál a metódus megnézi, hogy szerepel-e már a szting a kollekcióban (s2.equals(element)) Mivel a "valami" sztring már benne van a kollekcióban, ezért a metódus annak a referenciáját adja vissza. Azaz most s1 és s2 ugyanarra az objektumra mutat, azaz az s1 == s2 kifejezés true értékû, a feladatot megoldottuk Ha érték típusú osztályokat implementálunk (például nagy számokat reprezentáló osztályok)

érdemes felhasználnunk a belsõ kollekció ötletét. Ezt azért érdemes megtennünk, mivel a == operátor gyorsabb, mint egy equals() hívás. Promóció Mit ír ki az alábbi programrészlet? byte b1 = 3; byte b2 = 4; byte b3 = b1 + b2; System.outprintln(b3); A megfejtés egyszerûnek tûnik: összeadunk két byte típusú változót, ennek 7 az eredménye, így a programrészlet ezt fogja kiírni. Azonban mégsem ez történik. Ha megpróbáljuk lefordítani a kódot fordítási hibát kapunk a következõ üzenettel: PromotionTest.java:5: possible loss of precision found : int required: byte byte b3 = b1 + b2; Ez azt jelenti, hogy a fordító a harmadik sorban az értékeadás jobb oldalán int típusú kifejezést talált, miközben byte-ra lenne szüksége. Hogyan lehetséges ez? A választ a Java nyelvi specifikációban találhatjuk meg, a numeric promotion-rõl szóló szakaszban. A szakasz ezt mondja a bináris aritmetikai operátoroknál alkalmazott

konverziókról: • If any of the operands is of a reference type, unboxing conversion (§5.18) is performed. Then: • If either operand is of type double, the other is converted to double. • Otherwise, if either operand is of type float, the other is converted to float. • Otherwise, if either operand is of type long, the other is converted to long. • Otherwise, both operands are converted to type int. Ebben a kódban az utolsó pontnak van jelentõsége. Ez azt mondja ki, hogy a bináris aritmetikai mûveletek legkisebb egysége az int. Ha egy ilyen mûveletben int-nél kisebb operandus szerepel, az automatikusan intre konvertálódik. Tehát a fenti esetben a két byte összeadása helyett két int összeadása áll. Így a kifejezés típusa int Az értékadás baloldalán viszont byte áll, ami kisebb, mint az int. Ez a pontosság romlásához vezetne, így ezt a fordító nem engedheti. A numerikus promóció egy bináris aritmetikai operátor operandusait azonos

típusúra alakítja. Erre az egyes nyelvek eltérõ szabályokat használnak