Egy példán keresztül mutatom be az osztályok főbb tulajdonságait:
Ez az osztály egy pontot definiál az x és y változókkal és a move és toString metódusokkal. A move két integer típusú változót vár paraméterül, de nincs visszatérési értéke. A toString ezzel szemben nem vár paramétert és String típussal tér vissza. Mivel az felüldefiniálja az előre-definiált toString metódust, ezért ki kell írnunk az override kulcsszót.
A Scala osztályai konstruktor argumentumokkal paraméterezhetőek. A példakódban kettő is szerepel, xc és yc. Mindkettő az osztály teljes törzsében látható és a példában x és y inicializálásában vesznek részt.
Osztályainkat a new kulcsszóval tudjuk példányosítani:
Lehetőség van osztályokat singletonként implicit példányosítani is ha a class kulcsszó helyett az objectet használjuk:
Az object kulcsszóval definiált singleton egy név nélküli osztállyá fordul; mivel a példányosítás is implicit történik, így nem lehet konstruktora.
Egy másik példa:
A példakód nagyon ismerős lehet a JAVA programozóknak. Egy nagyon fontos különbség azonban, hogy amint látható a main függvény nem statikusan lett definiálva (public static void main). Ennek az az oka, hogy statikus membereket nem definiálhatunk Scala-ban. Ezt megkerülendően az összes statikus membert singleton objektumokban, a fent látható módon hozzuk létre. A singleton object-ek olyan osztályok, melyek csak egyszer példányosíthatók.
A Scala-ban a racionális számoknak nincs beépített típusa, de könnyen definiálható egy ilyen osztály:
Eszerint a Rational osztály két konstruktor argumentummal rendelkezik. Az osztály mezőket biztosít a nevező és a számláló elérésére, valamint metódusokat az aritmetikai műveletek elvégzésére.
Privát tagok: A fenti implementációban egy privát metódus van, a gcd, mely a legnagyobb közös osztót határozza meg a számlálóra és a nevezőre vonatkozóan. A g is privát mező, mely tárolja a metódussal kiszámított értéket. Ezek a Rational osztályon kívülről nem érhetőek el.
Objektumok létrehozása és elérése: Példaként lássuk azt a programot, amely kiírja az 1/i számok összegét, ahol i 1-től 10-ig változik:
Öröklődés és felüldefiniálás: Minden osztálynak van egy ősosztálya, melyet kiterjeszt. Ez alól egyetlen kivétel van, az Object osztály, melynek nincs őse és, amely minden más osztálynak őse. Ha egy osztály nem határozza meg explicite az ősosztályát, akkor az az Object lesz automatikusan. Vagyis a fenti Rational osztály így is definiálható lett volna:
Egy osztály az őse minden tagját örökli, azonban némelyiket felüldefiniálhatja. Például az Object osztály biztosít egy toString metódust, ami egy string reprezentációját adja az objektumnak:
A Rational osztályban felüldefiniálhatjuk ezt:
Az átdefiniáló definíciókat mindig az override módosítóval kell ellátni.
Természetesen a következő definíció érvényes:
Paraméternélküli metódusok: A metódusoknak nem kell, hogy mindig legyen paraméterük. Erre egy példa a következő:
A paraméternélküli metódusok úgy használhatóak, mint ha egy mezőt érnénk el. Az értékek és paraméternélküli metódusok közötti különbség a definícióban van. Egy mező jobb oldala akkor kerül kiértékelésre, amikor az objektum létrejön, és az értéke később nem változik. Egy paraméternélküli metódus jobb oldala viszont minden hívásnál kiértékelődik.
Absztrakt metódusok: Az osztályok tagjainak definíciója el is hagyható. Tekintsük a következő példát:
Mivel nem tudjuk, hogy mely objektumokat kell összehasonlítanunk, így nem tudjuk megadni a < implementációját. Viszont amint sikerült ezt a metódust meghatározni, azonnal a másik három is megvalósult. == egyenlőség ellenőrzés az Object osztályban adott, referencia ellenőrzést végez.
Önreferenciák: A this név az aktuális objektumra hivatkozik. A this típusa szintén this. Ha típusként használjuk, akkor a this az aktuális objektum típusa.
Kevert kompozíció: Definiálhatjuk a racionális számok OrderedRational
osztályát, ami az összehasonlítást is támogatja:
Ez az osztály felüldefiniálja az == metódust, valamint megvalósítja a < metódust is. Ezenkívül örökli a Rational osztály összes és az Ord összes nem absztrakt tagját.
A Rational(n, d) with Ord a kevert kompozíció egy példánya. Ez azt jelenti, hogy az osztály kompatibilis a Rational és az Ord osztályokkal, és mindkettő tagjait tartalmazza. A Rational lesz a OrderedRational ősosztálya, az Ord pedig a keverék osztálya. Az osztály típusa egy összetett típus, a Rational with Ord.
A kevert kompozíció úgy tűnik, megegyezik a többszörös öröklődéssel. Többszörös öröklődésnél a Rational és az Ord is az Object leszármazottja, így meg kell határoznunk, hogy mi legyen az Objectből származó tagokkal. Kevert kompozíciónál nincs ilyen probléma. A Rational with Ord esetén a Rational az ősosztály.
Befejezett osztályok: A fenti példában az OrderedRational final módosítóval lett definiálva. Ez azt jelenti, hogy az OrderedRational osztályt kiterjesztő másik osztály nem definiálható sehol a programban.
Scala-ban minden osztálynak van egy implicit konstruktora, az úgynevezett elsődleges konstruktor. Ez az osztály teljes törzse, amely példányosításkor fut le, és az osztály neve utáni "(" és ")" között megadott konstruktor argumentumokkal paraméterezhető. A következő példában az elsődleges konstruktorban beállítjuk a számláló és nevező értékét, így nem kell explicit konstruktort írnunk az osztályhoz:
Lehetőség van azonban explicit konstruktor, úgynevezett segédkonstruktor (auxiliary constructor) megadására is. Ezt a def this(
Itt a segédkonstruktor törzsében az elsődleges konstruktor hívása történik a paraméterül kapott egésszel és egy konstans 1-essel.
Hasonlóan a legtöbb objektumorientált nyelvhez Scala-ban is van lehetőség beágyazott osztályok írására. A példányosítás "kívülről befelé" történik:
Beágyazott osztályokra akkor lehet szükség, ha egy osztálynak van egy jól elkülöníthető, külön egységbe zárható részfunkcionalitása, amelyet előreláthatólag csak az adott osztály fog használni.
Itt a segédkonstruktor törzsében az elsődleges konstruktor hívása történik a paraméterül kapott egésszel és egy konstans 1-essel.
A Scala az öröklődés velejárójaként támogatja a polimorfizmust és a dinamikus kötést. Ez azt jelenti, hogy egy származtatott osztály futási időben viselkedhet ősosztály objektumaként és származtatott osztály objektumaként is:
Ekkor a polimorfizmus miatt az alábbi értékadások közül mind a kettő helyes:
A demo() függvény meghívásakor ArrayElement típusú objektum esetén a felüldefiniált metódus fut le, a UniformElement esetében azonban a dinamikus kötés miatt az Element (absztrakt osztály) eredeti metódusa hívódik meg:
Időnként előfordul, hogy meg szeretnénk tiltani a szoftverünk felhasználóinak, hogy származtassanak egy adott osztályból. Erre Java-ban többféle megoldás is létezik. Az osztályt el lehet látni például a final módosítószóval, ekkor a teljesítménybeli előnyök mellett alosztály képzését is megtiltottuk. De nem csak a felhasználóknak, hanem saját magunknak is, amit nem feltétlenül szeretnénk.
Egy másik megoldás lehet, ha az ősosztály láthatóságát úgy állítjuk be, hogy a felhasználó ne férjen hozzá, és minden leszármazottat (amikhez már hozzáférhetnek) finallé teszünk. Ez jó megközelítés lehet, ha nincs szükség az ősosztályra, azonban jellemzően nem ez a helyzet. Sőt, ha valahol lefelejtjük a finalt, akkor máris hibára adunk lehetőséget.
A Scala beépített megoldást biztosít erre a problémára. A sealed módosítószóval ellátott osztályokból nem lehet származtatni, leszámítva ugyanazt a forrásfájlt, ahol az osztályt definiáltuk. A sealed nem tiltja leszármazottakra a további származtatást, tehát tulajdonképpen csak a közvetlen alosztályképzést korlátozza.
Ha mégis megpróbálunk sealed osztályból (nem ugyanabban a forrásfájlbna) közvetlenül származtatni, az alábbi hibaüzenetet kapjuk a fordítótól:
error: illegal inheritance from sealed class A
Előfordul azonban, hogy nem a teljes osztályra szeretnénk bevezetni ezt a korlátozást, hanem csak egy-egy metódusra akarjuk letiltani a felüldefiniálást. Ezt a metódus elé írt 'final' módosítóval tudjuk elérni:
Ekkor ha egy ebből az ArrayElement-ből származtatott osztályban szeretnénk felüldefiniálni a demo() metódust, a következő hibaüzenetet kapnánk: 'method demo cannot override final member override def demo()'.
A Scala-ban lehetőség van megadni típusparaméterekre és absztrakt típusokra egy úgynevezett felső típuskorlátot (upper type bound). Ez azt jelenti, hogy ezzel egy adott T típusról kikötjük, hogy kötelezően egy S típus altípusa kell legyen. Ennek jelölése például egy generikus metódus típusparaméterében:
Az alsó típuskorlátok (lower type bound) fogalma nagyon hasonló ez előző fejezetben említett felső típuskorláthoz, azonban annál valamivel nehezebben megérthető és ritkábban is használt.
Míg felső típuskorlát esetén egy típust felülről korlátoztunk, itt az öröklődési fán a másik irányból adunk egy megszorítást. T >: S azt jelenti, hogy T típus az S-nek szupertípusa, azaz S a T-nek altípusa.
A fogalom hasznosságának megértéséhez nézzük az alábbi példát:
Scala-ban egy generikus paraméter szerinti ko-, illetve kontavarianciát a paraméter elé írt + illetve - szimbólumokkal deklarálhatunk. Ez egyrészt szabályozza az altípusossági relációt, másrészt megszorítja a generikus paramétert, mely így csak ko-, illetve kontravariáns pozícióban lesz használható. A klasszikus példa erre a témakörre szintén a kollekciókhoz kapcsolódik. Ha adott egy bármilyen elemeket tartalmazó lista típusú változónk, List[Any], akkor ennek nem lehet értékül adni egy List[Int] típusú értéket, mert a változó a típusparaméterére nézve invariáns.
Kontravariáns esetben a helyzet épp fordított, ilyenkor A[Y] lesz A[X] altípusa. Vegyük a következő példát:
A Scalaban a csomagok olyan speciális objektumok amelyek tagosztályok, objektumok és más csomagok halmazát tartalmazzák. A csomagok tagjait „top level” definícióknak is hívják, mivel ez a legmagasabb absztrakciós szintje az objektumok szervezésének a nyelvben. A csomagok tagjaira a csomag nevével és ponttal hivatkozhatunk (mint más objektum adattagjaira). Viszont a nyelvben szereplő más objektumokkal ellentétben a csomagokat nem használhatjuk értékként. Amennyiben egy csomag tagja a private módosítószóval lett ellátva, akkor az csak a csomag tagjai számára lesz látható.
A lehetséges tagokat csomagba rendezhetjük a forrásfájlban a „package
Amennyiben egy forrásfájlban egy csomag tagjait használni szeretnénk importálnunk kell azokat vagy a teljes elérési útvonalukkal kell rájuk hivatkoznunk. Az importálásnál szabályozhatjuk, hogy pontosan a csomag mely részeit kívánjuk importálni. import p._ - p minden tagját importálja (hasonlóan a Java p.* -hoz). import p.x - a p x tagját import p.{x => a} - az x tagot a-ra átnevezve import p.{x, y} - a p x és y tagjait import p1.p2.z – a p1-ben lévő p2 csomag z tagját
Korábban láttuk, hogy Scala-ban lehetőség van singleton objektumok definiálására az "object" kulcsszóval (ez hasonló a Java statikus osztályához). Egy Scala objektumon belül definiált adattag / metódus szintén hasonló szerepet tölt be a Java statikus adattagjához / metódusához. Scala-ban ha egy osztály (objektum) ugyanabban a fájlban van definiálva, ugyanabban a csomagban szerepel és ugyanaz a neve, mint egy objektumnak (osztálynak), akkor társosztálynak (társobjektumnak) nevezzük. Névütközés azért nem léphet fel, mert a Scala az osztály nevét a típus névtérben, az objektum nevét pedig a term névtérben tárolja. A társobjektumban leggyakrabban definiált két metódus az "apply" és az "unapply".
Ellentétben a JAVA-val, a Scala-ban minden egy objektum, beleértve a számokat és függvényeket is.
Példa: 1 + 2 * 3 / x
A fenti kifejezés függvényhívásokból áll, mely ekvivalens az alábbi kifejezéssel:
(1).+(((2).*(3))./(x))
Ez ugye azt is jelenti, hogy Scalaban a *, +, /, stb. mind érvényes azonosítók. A függvények is objektumok, így lehetőség van arra, hogy függvényeket argumentumként használjunk, változókban tároljuk őket vagy visszatérjünk velük más függvényekben.
A fenti program main metódusa meghívja a oncePerSecond függvényt timeFlies paraméterrel. A oncePerSecond semmi mást nem csinál, csak időzíti, hogy a kapott callback függvény másodpercenként fusson le.
Az apply gyakorlatilag egy szintaktikus cukorka a példányosításra. Amikor egy osztályt példányosítunk, a fordító is ezt az apply metódust hívja meg. Objektumra alkalmazva az apply egy ún. factory metódus, amely egy új példánnyal tér vissza. A következő példában látjuk a (beépített) Pair típust:
A példányosítása pedig így történik:
Ebből úgy tűnhet, hogy létrehozunk egy Pair példányt a "new" használata nélkül. Ezzel szemben az történik, hogy a Pair konstruktorának közvetlen hívása helyett a Pair.apply-t hívjuk meg (tehát a társobjektum metódusát), amely aztán a Tuple2.apply metódust hívja meg (a Tuple2 társobjektumon).
Általánosságban ha több alternatíva is kínálkozik a konstruktor írására egy osztályhoz, amelynek van társobjektuma, célszerű kevesebb konstruktort írni az osztályhoz és helyette több túlterhelt apply metódust írni a társobjektumban.
Az apply nem korlátozódik arra, hogy a társosztályt példányosítja. Lehetőség van arra, hogy a társosztály egy leszármazottjának példányával tér vissza. A következő példában a társobjektum egy reguláris kifejezéssel ellenőrzött String-ben kapja meg a példányosítandó származtatott osztályt:
A Widget.apply egy "specification" string-et kap, amely megadja, hogy melyik osztályt kell példányosítania. Ez a string jöhet például egy konfigurációs fájlból, hogy indításkor milyen widget-eket hozzunk létre.
Az unapply metódus neve azt sugallja, hogy ez valamilyen formában az apply ellentéte, és valóban így van; arra használjuk, hogy kiszedjünk bizonyos alkotóeleme(ke)t egy példányból. Ebből következik, hogy ezt is társobjektumokban definiáljuk és ezzel nyerünk ki adattagokat a megfelelő társosztály példányából.
A Button.unapply metódus egy Button típusú argumentumot kap és visszatér a Button label mezőjének értékével, egy "Some" objektumba csomagolva.
Példányok közötti megbízható egyenlőségvizsgálatot implementálni nem könnyű feladat. A Scala számos különböző beépített módszert kínál arra, hogy eldönthessük, két objektum megegyezik-e. Ezek közül néhány nevében megegyezik más nyelvekben található metódussal, de szemantikában eltérhetnek.
Equals metódus: érték alapú vizsgálat. Ez alapján két objektum megegyezik, ha ugyanaz az értékük (tehát nem kell, hogy ugyanarra a példányra vonatkozzanak). Ez a vizsgálat megegyezik a Java equals és a Ruby eql? metódusával.
== és !=: a "==" sok programozási nyelvben egy operátor, Scala-ban azonban egy metódus, ami final-ként van definiálva az Any osztályban. Szemantikájában megegyezik az "equals" metódussal. Az Any osztályban a következő a jelentése: o == arg0 is the same as o.equals(arg0). Az AnyRef-ben viszont ez: o == arg0 is the same as if (o eq null) arg0 eq null else o.equals(arg0). Mivel a "==" final, nem lehet felüldefiniálni, viszont nincs is rá igazán szükség, mivel az equals-ra hivatkozik. A != (értelemszerűen) a == negáltja, azaz obj1 != obj2 megegyezik !(obj1 == obj2)-vel.
Érdekesség, hogy például Java-ban, C++-ban és C#-ban az == operátor referenciaegyenlőséget vizsgál, míg Ruby-ban a Scala-hoz hasonlóan értékegyenlőséget.
ne és eq: az "eq" referenciaegyenlőséget vizsgál. Két objektum akkor egyezik meg, ha ugyanarra a memóriaterületre mutatnak. Ez csak az AnyRef-hez van definiálva. Az "ne" az "eq" negáltja.
Tömbegyenlőség - sameElements: Két tömb egyenlőségét nem tudjuk az eddig felsorolt módokon megvizsgálni. A tömb elemeinek összehasonlítására Scala-ban külön metódus áll rendelkezésre: