Come Java, Scala è un linguaggio a ereditarietà singola (single inheritance language): una classe può avere solo una superclasse. Tramite le interfacce (interface
), Java permette di simulare l’ereditarità multipla sui tipi, cioè definire un tipo come sottotipo diretto di più supertipi, ma non permette di definire una classe che eredita codice da più superclassi diertte. Scala fornisce invece il meccanismo dei trait, che consentono di simulare l’ereditarietà multipla sui tipi che sulle classi:
Un trait
è dichiarato in modo simile a una classe astratta, ma usando la parola riservata trait
invece di abstract class
. La differenza sostanziale tra trait e classi astratte è che i trait non hanno costruttori, non ammettono parametri (questa restrizione è proprio ciò che “tiene in piedi” il meccanismo di ricerca dei metodi a runtime, evitando i problemi di ambiguità che tipicamente si hanno con l’ereditarietà multipla sulle classi).
Il seguente esempio mostra la dichiarazione di un traixt Planar
, che modella una superficie piana:
trait Planar {
def height: Int
def width: Int
def surface = height * width
}
Eso dichiara due metodi (senza parametri, ovvero campi) astratti, height
e width
, e un metodo concreto, surface
.
Una classe, un object o un trait può ereditare direttamente da al più una classe e da un numero arbitrario di trait. La classe o il primo trait da cui si eredita viene indicato con la parola riservata extends
, mentre ciascuno degli eventali altri trait va indicato con la parola riservata with
. Ad esempio:
class Square extends Shape with Planar with Movable
(dove Shape
potrebbe essere una classe o un trait, mentre Planar
e Movable
sono dei trait).
La gerarchia delle classi di Scala, che comprende classi, object e trait, ha la seguente struttura:
Come si può osservare, tale gerarchia è più complessa rispetto a quella di Java. Le parti interessanti sono, in particolare, i top types e i bottom types, cioè rispettivametne i tipi (le classi) in cima alla gerarchia e quelli in fondo (non presenti in Java).
Nel grafo della gerarchia appena mostrato sono indicate anche le conversioni implicite sui tipi base, che rispecchiano quelle sui tipi primitivi di Java. Esse non sono relazioni supertipo/sottotipo: quando un’istanza di un sottotipo viene interpretata come un’istanza di un supertipo l’oggetto rimane invariato, mentre le conversioni implicite tra i tipi base modificano gli oggetti a cui si applicano. Ad esempio, ogni valore del tipo Byte
è concettualmente un valore ammesso anche per Short
, dunque si potrebbe in un certo senso dire che Byte
è un sottotipo di Short
, ma per interpretare un valore di Byte
come valore di Short
è necessario trasformare la sua rappresentazione su un byte in una rappresentazione su due byte.
La radice della gerarchia è l classe scala.Any
, che è il tipo base, il supertipo di tutti i tipi. Essa definisce dei metodi universali, che vengono ereditati da tutti gli oggetti, tra cui:
def equals(that: Any): Boolean
final def ==(that: Any): Boolean
final def !=(that: Any): Boolean
def hashCode: Int
final def ##: Int
def toString: String
equals
, hashCode
e toString
sono analoghi a quelli definiti in Java da java.lang.Object
(ma questi ultimi sono disponibili solo per le istanze dei tipi riferimento, e non per i tipi primitivi, in quanto essi non sono oggetti, dunque non hanno metodi). Per questi metodi, Any
fornisce delle implementazioni di default, che però è speso opportuno sovrascrivere perchè nella maggior parte dei casi non realizzano i comportamenti desiderati.