Budapesti Műszaki és Gazdaságtudományi Egyetem
Java technológia
Fejezetek az "Effective Java" c. könyvből
Java technológia Fejezetek az "Effective Java" c. könyvből
A könyv
Joshua Bloch, Effective Java, Programming Language Guide, Sun Microsystems Inc., 2001. A könyvben 57 fejezet ("item") található, amelyek egy-egy problémát mutatnak be, tanulságos példákkal illusztrálva. Az előadás a könyvben található fejezetek közül tartalmaz néhányat, minden fejezetnél fel van tüntetve a könyvbeli fejezet száma és címe is. Az előadásban bemutatott példaprogamok részben a könyvből származnak.
© 2002-2005 Wagner Ambrus
2
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 2
Enforce the singleton property with a private constructor. A singletonok olyan objektumok, amelyeket legfeljebb egyszer lehet példányosítani. Ezek gyakran olyan rendszerkomponenseket reprezentálnak, amelyekből csak egy létezik (például a képernyő, vagy a billentyűzet). A singleton osztályokat mindig úgy kell megvalósítani, hogy a singleton tulajdonságot automatikusan kikényszerítse.
© 2002-2005 Wagner Ambrus
3
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 2
A megvalósítások közös jellemzője, hogy a konstruktor privát, így a példányok létrehozását az osztály szigorúan kézbentarthatja. Az első lehetséges megoldás egy publikus mező alkalmazása: public class Singleton { private Singleton () { ... } public static final Singleton INSTANCE = new Singleton (); }
Ekkor az egyedi példányhoz az alábbi módon férhetünk hozzá: Singleton s = Singleton.INSTANCE;
© 2002-2005 Wagner Ambrus
4
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 2
A másik lehetőség, hogy egy statikus metódust alkalmazunk a példány "kinyerésére". Ennek a megoldásnak az az előnye, hogy az API megváltoztatása nélkül később megszüntethetjük az osztály singleton tulajdonságát. public class Singleton { private Singleton () { ... } public static getInstance () { return INSTANCE; } private static final Singleton INSTANCE = new Singleton (); }
© 2002-2005 Wagner Ambrus
5
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 2
•
Az utóbbi megoldás esetén a getInstance metódust használhatjuk a példány lekérésére:
Singleton s = Singleton.getInstance ();
•
Ha a példány létrehozása időigényes, és nem biztos, hogy a program futása során bárkinek is szüksége lesz rá, érdemes a "lusta példányosítás" módszerét alkalmazni.
public class Singleton { private Singleton () { ... } public static getInstance () { if (instance == null) instance = new Singleton (); return instance; } private static Singleton instance; } © 2002-2005 Wagner Ambrus
6
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 3
Enforce noninstantiability with a private constructor. Előfordul, hogy olyan osztályt készítünk, amelyet nem arra szánunk, hogy példányokat hozzunk létre belőle. Az ilyen osztályok tipikusan egy-egy adattípushoz vagy objektumhoz tartozó műveletek csoportosítására valók, mint a java.util.Arrays, vagy a java.util.Collections. Az ilyen osztályok példányosítását célszerű megakadályozni úgy, hogy elhelyezünk benne egy privát konstruktort.
public class NonInstantiable { private NonInstantiable () {} ... } © 2002-2005 Wagner Ambrus
7
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 3
A konstruktornak természetesen nem kell csinálnia semmit, célja csak az, hogy megakadályozzuk a fordító által automatikusan generált publikus default konstruktor létrejöttét. Bár az abstract módosítóval is elérhetnénk azt, hogy az osztályt ne lehessen példányosítani, ez nem célszerű, mert ha valaki egy leszármazottat készít, az már példányosítható lesz. Következmény: az egyedüli privát konstruktor léte megakadályozza az osztály leszármaztatását, hiszen a leszármazottak konstruktora mindig meghívja az ős konstruktorát, ebben az esetben azonban nincs hozzáférhető konstruktor az ősben.
© 2002-2005 Wagner Ambrus
8
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14
Favor composition over inheritance. A leszármaztatás célja általában az, hogy egy meglévő osztályt új tulajdonságokkal, funkciókkal bővítsünk, illetve a meglévő funkciók viselkedését megváltoztassuk. A leszármaztatással azonban az a probléma, hogy megsérti az encapsulation elvét. Tegyük fel, hogy egy olyan HashSet variánst akarunk készíteni, amely nyilvántartja, hogy hány elemet adtak hozzá.
© 2002-2005 Wagner Ambrus
9
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14
•
A naív megoldás az alábbi:
public class CounterHashSet extends HashSet { public public public public
CounterHashSet CounterHashSet CounterHashSet CounterHashSet
() {} (Collection c) { super (c); } (int ic) { super (ic); } (int ic,float lf) { super (ic,lf); }
public boolean add (Object o) { counter++; return super.add (o); } public boolean addAll (Collection c) { counter += c.size (); return super.addAll (c); } public int getCounter () { return counter; } private int counter = 0; } © 2002-2005 Wagner Ambrus
10
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14
A kisebb probléma az, hogy a HashSet négy konstruktorának megfelelő konstruktorokat el kellett készíteni.
A nagyobbik probléma az, hogy ha az alábbi kódot lefuttatjuk:
CounterHashSet chs = new CounterHashSet (); chs.addAll (Arrays.asList (new String[] { "alma","szolo","korte" })); System.out.println (chs.getCounter ());
A várt 3 helyett 6-ot kapunk. Miért?
Azért, mert a HashSet megvalósításában az addAll nem csinál mást, mint a paraméterként átadott Collection minden elemére meghívja az add metódust.
A CounterHashSet megoldás tehát azért rossz, mert függ a HashSet belső megvalósításától.
© 2002-2005 Wagner Ambrus
11
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14
Jobb megoldás, ha kompozíciót (composition) alkalmazunk. A kompozíció azt jelenti, hogy a kiegészítendő osztály egy példányát beburkoljuk egy új osztályba, ez a burkoló osztály (wrapper class). Ezt a megoldást dekorátornak (decorator) is hívják, mert a burkoló osztály új funkciókkal "dekorálja" a másik osztályt. A kiegészítendő osztály metódusait továbbító metódusokon keresztül (forwarding method) továbbítjuk a burkoló osztályba. Ezt a megoldást gyakran (tévesen) delegációnak (delgation) nevezik.
© 2002-2005 Wagner Ambrus
12
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14 public class CounterSet implements Set { public CounterSet (Set set) { this.set = set; } public boolean add (Object o) { counter++; return set.add (o); } public boolean addAll (Collection c) { counter += c.size (); return set.addAll (c); } public void clear () { set.clear (); } public boolean contains (Object o) { return set.contains (o); } ... // a Set interface többi metódusa public int getCounter () { return counter; } private int counter = 0; private Set set; }
© 2002-2005 Wagner Ambrus
13
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14
•
A fenti megoldás legfőbb előnye, hogy jól működik, mert az adott Set implementációjától független:
CounterSet cs = new CounterSet (new HashSet ()); cs.addAll (Arrays.asList (new String[] { "alma","szolo","korte" })); System.out.println (cs.getCounter ()); •
További előny, hogy tetszőleges Setre alkalmazható, mert a Set interface-en alapul. Ebből kifolyólag a konkrét megvalósítás konstruktorainak megfelelő konstruktorokat sem kell megvalósítani.
Mivel csak burkolóként működik, akár ideiglenesen is beburkolhatunk vele egy Setet.
Hátrányai, hogy visszahívó (callback) helyzetekben nem működik, mert a burkolt objektum nem tud a burkolóról, illetve hogy a továbbító metódusokat meg kell valósítani.
© 2002-2005 Wagner Ambrus
14
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 14
Öröklődést általában csak akkor szabad használni, ha az A osztályból leszármaztatott B osztályra igaz az, hogy "a B az egy A". A fenti kitétel tipikusan igaz a Barack és a Kajszi osztályokra, de nem igaz a Körte és a Villanykörte osztályokra. Akkor is érdemes kompozíciót használni, ha a kibővítendő osztálynak olyan API hiányosságai vannak, amelyeket el szeretnénk fedni a felhasználók elől.
© 2002-2005 Wagner Ambrus
15
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 27
Return zero-length arrays, not nulls. Gyakori, ám helytelen megoldás, hogy az olyan metódusok, amelyek egy tömböt adnak vissza, nulla méretű tömb helyett nullt adnak vissza: public Object[] getObjects () { if (!objectsAvailable) return null; ... }
•
Ez egyrészt azért nem szerencsés, mert a hívót meglepi ez a viselkedés, másrészt azért, mert a visszatérési érték korrekt kezeléséhez egy plusz feltételvizsgálat szükséges:
© 2002-2005 Wagner Ambrus
16
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 27
... Object[] objects = getObjects (); if (objects == null) System.out.println ("Object count: 0"); else System.out.println ("Object count: " + objects.length); ...
•
Ha azonban a metódus null helyett nulla méretű tömböt ad vissza, ez a probléma nem jelentkezik:
public Object[] getObjects () { if (!objectsAvailable) return new Object [0]; ... }
© 2002-2005 Wagner Ambrus
17
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 27
... Object[] objects = getObjects (); System.out.println ("Object count: " + objects.length); ...
•
Mivel egy nulla méretű tömb immutábilis, elég egyetlen példányt használni belőle, így nem kell minden alkalommal újat létrehozni:
private static final Object[] EMPTY_ARRAY = new Object [0]; ... public Object[] getObjects () { if (!objectsAvailable) return EMPTY_ARRAY; ... }
© 2002-2005 Wagner Ambrus
18
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 31
Avoid float and double if exact answers are required. A float és a double lebegőpontos típusok. Céljuk az hogy, széles értéktartományon jelenítsenek meg törtszámokat, de pontos számításokra nem alkalmasak. Vannak olyan számok, amelyeket nem lehet pontosan megjeleníteni floatban vagy double-ben:
System.out.println (0.1 – 0.42);
Ez -0.32 helyett -0.31999999999999995-öt ír ki. Ez a viselkedés például pénzügyi számítások esetén teljesen elfogadhatatlan.
© 2002-2005 Wagner Ambrus
19
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 31
•
A következő program 1 Forintból von ki 10 filléreket:
double money = 1; for (int i = 0;i < 10;i++) { money -= 0.1; System.out.println ("marad: " + money); }
Az eredmény szomorú: marad: marad: marad: marad: marad: marad: marad: marad: marad: marad:
0.9 0.8 0.7000000000000001 0.6000000000000001 0.5000000000000001 0.40000000000000013 0.30000000000000016 0.20000000000000015 0.10000000000000014 1.3877787807814457E-16 © 2002-2005 Wagner Ambrus
20
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 31
•
A megoldás: ne használjuk a float, illetve double típusokat, hanem az int, long, illetve BigDecimal típusok valamelyikét:
BigDecimal TIZ_FILLER = new BigDecimal ("0.1"); BigDecimal money = new BigDecimal ("1"); for (int i = 0;i < 10;i++) { money = money.subtract (TIZ_FILLER); System.out.println ("marad: " + money); }
•
Most már jobb a helyzet:
marad: marad: ... marad: marad:
0.9 0.8 0.1 0.0
© 2002-2005 Wagner Ambrus
21
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 31
A BigDecimal egy immutábilis, tetszőleges pontosságú decimális értéket reprezentál. Egy skálázatlan értékből, és egy skálatényezőből áll. Használatának előnye az intként, illetve longként ábrázolt fixpontos számokkal szemben, hogy a skálatényező követésére nincs szükség, illetve a BigDecimal osztály megvalósítja az összes szükséges műveletet. Hátránya, hogy operator overloading híján némileg kényelmetlen a használata.
© 2002-2005 Wagner Ambrus
22
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 33
Beware the performance of string concatenation. A stringek összefűzésére gyakori módszer a + operátor használata. Ez a megoldás teljesen jó akkor, ha csak néhány stringet kell összefűzni, de nagyon rosszul skálázható. Ennek oka az, hogy a String immutábilis, így két string összefűzésekor mindkettő tartalma átmásolódik egy harmadikba. A string összefűzés futásideje így az összefűzendő stringek számában o(n2).
© 2002-2005 Wagner Ambrus
23
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 33
•
Az alábbi metódus egy borzasztóan hatékonytalan megoldás stringek összefűzésére:
public String concatenate (String[] strings) { String result = ""; for (int i = 0;i < strings.length;i++) result = result + strings [i]; return result; }
•
A megoldás a StringBuffer osztály alkalmazása. A fenti metódus lényegesen hatékonyabb változata tehát:
public String concatenate (String[] strings) { StringBuffer result = new StringBuffer (); for (int i = 0;i < strings.length;i++) result.append (strings [i]); return result.toString (); } © 2002-2005 Wagner Ambrus
24
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 34
Refer to objects by their interfaces. Az alábbi példa a szokásos megoldást mutatja: ArrayList list = new ArrayList ();
A fenti megoldás hátránya, hogy az ArrayList típus túlságosan megköti a list változót: nem használhatunk más List implementációt az ArrayList helyett, ha mégis mást akarunk használni, át kell írni a list típusát. Sokkal szerencsésebb tehát azt az interface-t megadni típusként, amely alapján a változót használjuk: List list = new ArrayList ();
© 2002-2005 Wagner Ambrus
25
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 34
•
Ebben az esetben könnyű áttérni másik List megvalósításra:
List list = new Vector ();
•
Ez az elv metódusparaméterek esetén még fontosabb, hogy ne nyírbáljuk meg a felhasználó szabadságát:
public void method (ArrayList list) { ... }
•
helyett:
public void method (List list) { ... }
•
A fentiek alól kivétel lehet, ha nincs értelmesen használható interface, vagy egy konkrét implementáció speciális funkcióit akarjuk használni, mint például a LinkedList esetében.
© 2002-2005 Wagner Ambrus
26
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 47
Don't ignore exceptions. Az alábbihoz hasonló kódrészleteket, sajnos, nagyon gyakran láthatunk: ... try { ... } catch (Exception e) {} ...
Nyilvánvaló, hogy a fenti kódrészlet nagyon veszélyes. A kivételek lenyelése miatt a program végrehajtása a hiba ellenére is folytatódik, így a probléma később, a program egy teljesen más részében jelenik meg, rendkívül nehezen felderíthető helyzetet idézve elő.
© 2002-2005 Wagner Ambrus
27
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 47
A kivételek lenyelésénél még az is jobb, ha ellenőrzetlen kivételeket hagyunk terjedni felfelé a hívási lánc mentén, mert akkor legalább leáll a program, és a stack trace alapján (általában) azonosítható a hiba forrása.
Kivételek lenyelése kivételes esetekben mégis megengedhető, például real-time alkalmazásokban (video lejátszás), ahol egy-egy ütem kiesése megengedhető.
A "legális lenyelés" még egy jellegzetes esete:
try { Thread.sleep (500); } catch (InterruptedException e) {} •
Fontos, hogy ilyenkor a lehető legkonkrétabb kivételtípust adjuk meg a catch blokkban (mint a fenti példában is).
© 2002-2005 Wagner Ambrus
28
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 50
Never invoke wait outside a loop. A wait metódust egy megadott feltétel bekövetkeztére való várakozásra használjuk. Naív (és rossz) használata:
... synchronized (object) { object.wait (); } ...
•
Elvileg egy másik szál ébreszti fel ezt a szálat egy notify vagy notifyAll hívással, ha a várt feltétel bekövetkezett.
© 2002-2005 Wagner Ambrus
29
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 50
A fenti megoldás azonban önmagában kevés. Ha a feltétel már korábban bekövetkezett, mint ahogy a szál elaludt, a notify hatástalan marad, és a szál soha nem ébred fel többé. Emiatt szükség van a feltétel előzetes vizsgálatára elalvás előtt: ... synchronized (object) { if (!condition) object.wait (); } ...
© 2002-2005 Wagner Ambrus
30
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 50
Még mindig baj van akkor, ha: ) a feltétel hamissá vált a notify meghívása, és a szál felébredése között, ) egy szál meghívta a notify-t, noha a feltétel nem következett be (például ha több szál várakozik különböző feltételekre, az ébresztés pedig a notifyAll hívással történik), ) hamis ébredés (spurious wakeup) történik, vagyis a szál notify nélkül is felébred (ez a VM-en kívüli jelenség). A fentiek megelőzésére az ébredés után is meg kell vizsgálni a feltétel fennállását.
© 2002-2005 Wagner Ambrus
31
Java technológia Fejezetek az "Effective Java" c. könyvből
Item 50
... synchronized (object) { while (!condition) object.wait (); } ...
A fenti megoldás tökéletes, mert mind ébredés előtt, mind ébredés után megvizsgálja a feltételt, és ha szükséges, "visszaalszik". A wait metódust tehát kizárólag ciklusban szabad használni.
© 2002-2005 Wagner Ambrus
32