Si consideri l’esempio della classe astratta IntSet
, con le due sottoclassi concrete che ne realizzano un’implementazione (basata su un BST):
abstract class IntSet {
def add(x: Int): IntSet
def contains(x: Int): Boolean
}
class Empty extends IntSet {
def contains(x: Int): Boolean = false
def add(x: Int): IntSet = new NonEmpty(x, new Empty(), new Empty())
}
class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
def contains(x: Int): Boolean =
if (x < elem) left contains x
else if (x > elem) right contains x
else true
def add(x: Int): IntSet =
if (x < elem) new NonEmpty(elem, left add x, right)
else if (x > elem) new NonEmpty(elem, left, right add x)
else this
}
Le definizioni di contains
e add
delle sottoclassi Empty
e NonEmpty
implementano i metodi astratti dichiarati nella superclasse IntSet
. Se invece si implementa un metodo che è già defnito in una classe base, allora si parla di overriding, sovrascrittura. Come già detto, Scala richiede di dichiarare esplicitamente la sovrascrittura dei metodi usando il modificatore override
. Si può mettere override
anche quando si implementa un metodo astratto della superclasse, ma non è obbligatorio.
toString
Siccome la classe Empty
non ha parametri e non fornisce operazioni di modifica, tutte le sue istanze sono identiche, dunque non vi è la necessità di avere istanze diverse. Allora, si potrebbero risparmiare il tempo e la memoria necessari a costruire le istanze di Empty
riutilizzando sempre un’unica istanza, ottenendo così alberi con una struttura del genere:
In Java, si può “forzare” la creazione di un’unica istanza di una classe usando un design pattern: il singleton. Scala internalizza tale pattern nel linguaggio tramite le object definition: usando la parola riservata object
invece di class
si definisce singleton object, cioè un oggetto che è l’unica istanza della sua classe ed è associato a un nome uguale a quello della classe.
Ad esempio, la definizione
object Empty extends IntSet {
def contains(x: Int): Boolean = false
def add(x: Int): IntSet = new NonEmpty(x, Empty, Empty)
override def toString: String = "."
}
crea un singleton object di nome Empty
. Più nel dettaglio, il nome Empty
fa riferimento all’unico oggetto di tipo Empty
, che viene automaticamente costruito dal compilatore, mentre non è possibile costruire esplicitamente istanze di Empty
(usando l’operatore new
).
Un singleton object è un valore, che nel modello di sostituzione viene rappresentato dal suo nome. Di conseguenza, tale nome (ad esempio Empty
) viene valutato in se stesso, è “già valutato”.
Quando si realizza un’applicazione in Java, il punto di ingresso (da cui inizia l’esecuzione del codice) è il metodo statico main
. Anche se i metodi statici sono scritti all’interno di una classe, essi non sono associati ad alcuna istanza, dunque la creazione del primo “vero” oggetto di un programma orientato agli oggetti spetta al programmatore.
Invece, Scala adotta un approccio orientato agli oggetti più puro: il main
è un metodo “tradizionale”, non statico ma appartenente a un singleton object che viene istanziato automaticamente dal compilatore (come tutti i singleton object). Quello che invece rimane uguale a Java è il tipo del metodo main
: esso deve avere un singolo parametro di tipo array di stringhe (gli argomenti passati al programma dalla riga di comando):
def main(args: Array[String]) = /* ... */
Ad esempio, il punto d’ingresso di un’applicazione Scala potrbbe essere definito da un file con i seguenti contenuti:
package lib.intset
object Main {
def main(args: Array[String]) = {
println("Some IntSets")
val set1 = Empty.add(1)
println(set1)
// ...
}
}
Una volta compilata l’applicazione, per eseguirla da riga di comando è necessario usare il comando scala
(analogo a java
), passando come argomento il nome completo dell’oggetto che contiene il metodo main
: