A Scala programozási nyelv

Osztályok és objektumok



Osztályok

Egy példán keresztül mutatom be az osztályok főbb tulajdonságait:

class Point(xc: Int, yc: Int) { var x: Int = xc var y: Int = yc def move(dx: Int, dy: Int) { x = x + dx y = y + dy } override def toString(): String = "(" + x + ", " + y + ")"; }

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:

val pt = new Point(1,2)

Lehetőség van osztályokat singletonként implicit példányosítani is ha a class kulcsszó helyett az objectet használjuk:

object Point extends App { var x: Int = 0; var y: Int = 0; def setXY(xp: Int, yp: Int) { x = xp; y = yp; } def move(dx: Int, dy: Int) { x = x + dx y = y + dy } override def toString(): String = "(" + x + ", " + y + ")"; setXY(2, 3); println(toString()); }

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:

object HelloWorld { def main(args: Array[String]) { println("Hello, world!") } }

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.

Racionális számok példája

A Scala-ban a racionális számoknak nincs beépített típusa, de könnyen definiálható egy ilyen osztály:

class Rational(n: Int, d: Int) with { private def gcd(x: Int, y: Int): Int = { if (x == 0) y else if (x < 0) gcd(-x, y) else if (y < 0) -gcd(x, -y) else gcd (y % x, x); } private val g = gcd(n, d); val numer: Int = n/g; val denom: Int = d/g; def +(that: Rational) = new Rational(numer * that.denom + that.numer * denom, denom * that.denom); def -(that: Rational) = new Rational(numer * that.denom - that.numer * denom, denom * that.denom); def *(that: Rational) = new Rational(numer * that.number, denom * that.denom); def /(that: Rational) = new Rational(numer * that.denom, denom * that.numer); }

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:

var i = 1; var x = Rational(0, 1); while (i <= 10) { x = x + Rational(1, i); i = i + 1; } System.out.println(x.number + "/" + x.denom);

Ö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:

class Rational(n: Int, d: Int) extends Object with { ... //as before }

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:

class Object { ... def toString(): String = ... }

A Rational osztályban felüldefiniálhatjuk ezt:

class Rational(n: Int, d: Int) extends Object with { ... // as before override def toString() = number + "/" + denom; }

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:

var x: Object = new Rational(1,2);

Paraméternélküli metódusok: A metódusoknak nem kell, hogy mindig legyen paraméterük. Erre egy példa a következő:

class Rational(n: Int, d: Int) extends Object with { ... // as before def square = Rational(numer*numer, denom*denom); } val r = new Rational(3,4); System.out.println(r.square); // prints "9/16"

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:

abstract class Ord with { def <(that: this): Boolean; def <=(that: this) = this < that || this == that; def >(that: this) = that < this; def >=((that: this) = that <= this;

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:

final class OrderedRational(n: Int, d: Int) extends Rational(n, d) with Ord with { override def ==(that: OrderedRational) = numer == that.numer && denom == that.denom; def <(that: OrderedRational): Boolean = numer * that.denom < that.numer * denom; }

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.

Elsődleges konstruktor és segédkonstruktorok

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:

class Rational(n: Int, d: Int) { val numer: Int = n val denom: Int = d }

Lehetőség van azonban explicit konstruktor, úgynevezett segédkonstruktor (auxiliary constructor) megadására is. Ezt a def this() = {} formában adhatjuk meg. Egy osztálynak tetszőleges számú (akár 0) segédkonstruktora lehet. A következő példában lehetőséget nyújtunk, hogy a Rational osztálynak csak egy Integert adva paraméterként egész számot hozzunk létre:

class Rational(n: Int, d: Int) { val numer: Int = n val denom: Int = d def this(n: Int) = this(n, 1) // auxiliary constructor }

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.

Beágyazott osztályok

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:

class Outer { class Inner } val o1 = new Outer val i1 = new o1.Inner

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.

abstract class Widget { class Properties { import scala.collection.immutable.HashMap private var values: Map[String, Any] = new HashMap def size = values.size def get(key: String) = values.get(key) def update(key: String, value: Any) = { // Do some preprocessing, e.g., filtering. values = values.update(key, value) // Do some postprocessing. } } val properties = new Properties }

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.

Polimorfizmus, dinamikus kötés

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:

abstract class Element { def contents: Array[String] val height = contents.length val width = if (height == 0) 0 else contents(0).length def demo() { println("Element's implementation invoked") } } class ArrayElement ( val contents: Array[String] ) extends Element { override def demo() { println("ArrayElement's implementation invoked") } } class UniformElement( ch: Char, override val width: Int, override val height: Int ) extends Element { private val line = ch.toString * width def contents = Array.make(height, line) }

Ekkor a polimorfizmus miatt az alábbi értékadások közül mind a kettő helyes:

val e1: Element = new ArrayElement(Array("hello", "world")) val e2: Element = new UniformElement('x', 2, 3)

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:

def invokeDemo(e: Element) { e.demo() } scala> invokeDemo(new ArrayElement) ArrayElement's implementation invoked scala> invokeDemo(new UniformElement) Element's implementation invoked

Sealed osztályok

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

Sealed metódusok

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:

class ArrayElement extends Element { final override def demo() { println("ArrayElement's implementation invoked") } }

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()'.

Felső típuskorlátok

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:

def methodName[T <: S](e: T) = { ... }
A következő kódrészlet például azt fejezi ki, hogy a C osztály generikus paramétere kötelezően a BigInt leszármazottja kell legyen.
class C[T <: BigInt] { ... }
T egyenlő is lehet BigInttel, mert felső (és alsó) típuskorlát esetén minden típus relációban áll önmagával is.

Alsó típuskorlátok

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:

class List[+T] { def append[U >: T](element: U) : List[U] = { ... } }
Az osztály egy listát reprezentál, ami T típusú elemek tárolására képes, T-re kovariáns. Ha a listához megpróbálunk hozzáfűzni egy elemet, akkor T típusú, vagy T altípusú esetben ez gond nélkül megy, az eredmény szintén List[T] lesz. Ha azonban egy bővebb típusból választunk hozzáfűzendő elemet (U-ból), akkor a lista nem maradhat List[T], hiszen van olyan eleme, amit nem lehet T-re konvertálni. Az append művelet visszatérési típusa List[U], hiszen U a legszűkebb olyan típus, amire az új lista minden eleme konvertálható.

Ko- és kontravariancia

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.

class List[T] { ... } def m() = { var x: List[Any] = new List[Any] x = new List[Int] }
Ez a kódrészlet a következő fordítási hibát fogja adni:
error: type mismatch; found : A[Int] required: A[Any] Note: Int <: Any, but class A is invariant in type T. You may wish to define T as +T instead. (SLS 4.5) x = new A[Int]
Azaz a fordító T szerinti kovarianciát javasol. Ez azt jelenti, hogy ha adott egy A[+T] osztályunk, és az X típus az Y-nak altípusa, akkor A[X] és A[Y] közötti viszony a következő: A[X] az A[Y] altípusa lesz, azaz az altípusossági irányt a kovariancia megtartja.

Kontravariáns esetben a helyzet épp fordított, ilyenkor A[Y] lesz A[X] altípusa. Vegyük a következő példát:

abstract class A[-T] { def processElement(t: T) }
Ha X extends Y fennáll, akkor A[Y] lesz A[X] altípusa, mivel A[Y] a "bővebb": minden elemet fel tud dolgozni, amit A[X], plusz még azokat is, amik Y-ok, de nem X-ek. A kontravariancia tehát az altípusossági relációra nézve fordított.

Csomagok

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 ” {} utasítással. Például:

package p1 { object test extends Application { println("p1.test") } } package p2 { object test extends Application { println("p2.test") } }

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

Objektumok, társobjektumok

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".

Minden objektum

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.

object Timer { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def timeFlies() { println("time flies like an arrow...") } def main(args: Array[String]) { oncePerSecond(timeFlies) } }

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 metódus

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:

type Pair[+A, +B] = Tuple2[A, B] object Pair { def apply[A, B](x: A, y: B) = Tuple2(x, y) def unapply[A, B](x: Tuple2[A, B]): Option[Tuple2[A, B]] = Some(x) }

A példányosítása pedig így történik:

val p = Pair(1, "one")

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:

package objects abstract class Widget { def draw(): Unit override def toString() = "(widget)" } object Widget { val ButtonExtractorRE = """\(button: label=([^,]+),\s+\(Widget\)\)""".r val TextFieldExtractorRE = """\(textfield: text=([^,]+),\s+\(Widget\)\)""".r def apply(specification: String): Option[Widget] = specification match { case ButtonExtractorRE(label) => new Some(new Button(label)) case TextFieldExtractorRE(text) => new Some(new TextField(text)) case _ => None } }

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

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.

package objects import ui3.Clickable class Button(val label: String) extends Widget with Clickable { def click() = { // Logic to give the appearance of clicking a button... } def draw() = { // Logic to draw the button on the display, web page, etc. } override def toString() = "(button: label="+label+", "+super.toString()+")" } object Button { def unapply(button: Button) = Some(button.label) }

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.

Objektumok egyenlősége

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:

scala> Array(1, 2) == Array(1, 2) res0: Boolean = false
scala> Array(1, 2).sameElements(Array(1, 2)) res1: Boolean = true