We’ve got the basics of OOP in Scala under our belt, but there’s plenty more to learn.
Classes and traits can declare abstract members: fields, methods, and types. These members must be defined by a derived class or trait before an instance can be created. Most object-oriented languages support abstract methods, and some also support abstract fields and types.
When overriding a concrete member, Scala requires the
override
keyword. It is optional when a subtype
defines (“overrides”) an abstract member. Conversely, don’t use
override
unless you are actually overriding a
member.
Requiring the
override
keyword has several benefits:
It catches misspelled members that were intended to be overrides. The compiler will throw an error that the member doesn’t override anything.
It catches a potentially subtle bug that can occur if a new
member is added to a base class where the member’s name collides with
an older derived class member that is unknown to the base class
developer. That is, the derived-class member was never intended to
override a base-class member. Because the derived class member won’t
have the override
keyword, the compiler will throw
an error when the new base-class member is introduced.
Having to add the keyword reminds you to consider what members should or should not be overridden.
Java has an optional
@Override
annotation for methods. It helps catch errors
of the first type (misspellings), but it can’t help with errors of the
second type, since using the annotation is optional.
However, if a declaration
includes the final
keyword, then overriding the
declaration is prohibited. In the following example, the
fixedMethod
is declared final
in
the parent class. Attempting to compile the example will result in a
compilation error:
// code-examples/AdvOOP/overrides/final-member-wont-compile.scala
// WON'T COMPILE.
class
NotFixed
{final
def
fixedMethod
="fixed"
}class
Changeable2
extends
NotFixed
{override
def
fixedMethod
="not fixed"
// ERROR
}
This constraint applies
to classes and traits as well as members. In this example, the class
Fixed
is declared final
, so an
attempt to derive a new type from it will also fail to compile:
// code-examples/AdvOOP/overrides/final-class-wont-compile.scala
// WON'T COMPILE.
final
class
Fixed
{def
doSomething
="Fixed did something!"
}class
Changeable1
extends
Fixed
// ERROR
Some of the types in the Scala library are final, including JDK
classes like String
and all the “value” types
derived from AnyVal
(see The Scala Type Hierarchy).
For declarations that aren’t final, let’s examine the rules and behaviors for overriding, starting with methods.
Let’s extend our familiar
Widget
base class with an abstract method
draw
, to support “rendering” the widget to a display,
web page, etc. We’ll also override a concrete method familiar to any
Java programmer, toString()
, using an ad hoc format.
As before, we will use a new package, ui3
.
Drawing is actually a cross-cutting
concern. The state of a Widget
is one
thing; how it is rendered on different platforms, thick clients, web
pages, mobile devices, etc., is a separate issue. So, drawing is a
very good candidate for a trait, especially if you want your GUI
abstractions to be portable. However, to keep things simple, we will
handle drawing in the Widget
hierarchy
itself.
Here is the revised
Widget
class, with draw
and
toString
methods:
// code-examples/AdvOOP/ui3/widget.scala
package
ui3abstract
class
Widget
{def
draw
():Unit
override
def
toString
() ="(widget)"
}
The draw
method is abstract because it has no body; that is, the method isn’t
followed by an equals sign (=
), nor any text after
it. Therefore, Widget
has to be declared
abstract
(it was optional before). Each concrete
subclass of Widget
will have to implement
draw
or rely on a parent class that implements it. We
don’t need to return anything from draw
, so its
return value is Unit
.
The
toString()
method is straightforward. Since
AnyRef
defines toString
, the
override
keyword is required for
Widget.toString
.
Here is the revised
Button
class, with draw
and
toString
methods:
// code-examples/AdvOOP/ui3/button.scala
package
ui3class
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() +")"
}
Button
implements the abstract method draw
. No
override
keyword is required. Button
also overrides
toString
, so the override
keyword
is required. Note that super.toString
is
called.
The
super
keyword is analogous to
this
, but it binds to the parent type, which is the
aggregation of the parent class and any mixed-in traits. The search for
super.toString
will find the “closest” parent type
toString
, as determined by the linearization process (see Linearization of an Object’s Hierarchy). In this case, since
Clickable
doesn’t define toString
,
Widget.toString
will be called.
Overriding a concrete method should be done rarely, because it is error-prone. Should you invoke the parent method? If so, when? Do you call it before doing anything else, or afterward? While the writer of the parent method might document the overriding constraints for the method, it’s difficult to ensure that the writer of a derived class will honor those constraints. A much more robust approach is the Template Method Pattern (see [GOF1995]).
Most object-oriented
languages allow you to override mutable fields (var
).
Fewer OO languages allow you to define abstract fields or override
concrete immutable fields (val
). For example, it’s
common for a base class constructor to initialize a mutable field and
for a derived class constructor to change its value.
We’ll discuss overriding fields in traits and classes separately, as traits have some particular issues.
Recall our
VetoableClicks
trait in Stackable Traits. It defines a val
named
maxAllowed
and initializes it to
1
. We would like the ability to override the value in
a class that mixes in this trait.
Unfortunately, in Scala
version 2.7.X, it is not possible to override a val
defined in a trait. However it is possible to
override a val
defined in a parent
class. Version 2.8 of Scala does support overriding
a val
in a trait.
Because the override behavior for a val
in a
trait is changing, you should avoid relying on the ability to override
it, if you are currently using Scala version 2.7.X. Use another
approach instead.
Unfortunately, the version
2.7 compiler accepts code that attempts to override a trait-defined
val
, but the override does not actually happen, as
illustrated by this example:
// code-examples/AdvOOP/overrides/trait-val-script.scala // DANGER! Silent failure to override a trait's "name" (V2.7.5 only). // Works as expected in V2.8.0. trait T1 { val name = "T1" } class Base class ClassWithT1 extends Base with T1 { override val name = "ClassWithT1" } val c = new ClassWithT1() println(c.name) class ClassExtendsT1 extends T1 { override val name = "ClassExtendsT1" } val c2 = new ClassExtendsT1() println(c2.name)
If you run this script with
scala
version 2.7.5, the output is the
following:
T1 T1
Reading the script, we
would have expected the two T1
strings to be
ClassWithT1
and ClassExtendsT1
,
respectively.
However, if you run this
script with scala
version 2.8.0, you get this
output:
ClassWithT1 ClassExtendsT1
Attempts to override a trait-defined val
will
be accepted by the compiler, but have no effect in Scala version
2.7.X.
There are three
workarounds you can use with Scala version 2.7. The first is to use some
advanced options for scala
and
scalac
. The -Xfuture
option will
enable the override behavior supported in version 2.8. The
-Xcheckinit
option will analyze your code and report
whether the behavior change will break it. The option
-Xexperimental
, which enables many experimental
changes, will also warn you that the val
override
behavior is different.
The second workaround is
to make the val
abstract in the trait. This forces an
instance using the trait to assign a value. Declaring a
val
in a trait abstract is a perfectly useful design
approach for both versions of Scala. In fact, this will be the best
design choice, when there is no appropriate default value to assign to
the val
in the trait:
// code-examples/AdvOOP/overrides/trait-abs-val-script.scala
trait
AbstractT1
{val
name:String
}class
Base
class
ClassWithAbstractT1
extends
Base
with
AbstractT1
{val
name ="ClassWithAbstractT1"
}val
c =new
ClassWithAbstractT1
() println(c.name)class
ClassExtendsAbstractT1
extends
AbstractT1
{val
name ="ClassExtendsAbstractT1"
}val
c2 =new
ClassExtendsAbstractT1
() println(c2.name)
This script produces the output that we would expect:
ClassWithAbstractT1 ClassExtendsAbstractT1
So, an abstract
val
works fine, unless the field
is used in the trait body in a way that will fail until the field is
properly initialized. Unfortunately, the proper initialization won’t
occur until after the trait’s body has executed. Consider the following
example:
// code-examples/AdvOOP/overrides/trait-invalid-init-val-script.scala
// ERROR: "value" read before initialized.
trait
AbstractT2
{ println("In AbstractT2:"
)val
value:Int
val
inverse =1.0
/value// ???
println("AbstractT2: value = "
+value+", inverse = "
+inverse) }val
c2b =new
AbstractT2
{ println("In c2b:"
)val
value =10
} println("c2b.value = "
+c2b.value+", inverse = "
+c2b.inverse)
While it appears that we
are creating an instance of the trait with new AbstractT2
...
, we are actually using an anonymous class that implicitly
extends the trait. This script shows what happens when
inverse
is calculated:
In AbstractT2: AbstractT2: value = 0, inverse = Infinity In c2b: c2b.value = 10, inverse = Infinity
As you might expect, the
inverse
is calculated too early. Note that a divide
by zero exception isn’t thrown; the compiler recognizes the value is
infinite, but it hasn’t actually “tried” the division yet!
The behavior of this
script is actually quite subtle. As an exercise, try selectively
removing (or commenting out) the different println
statements, one at a time. Observe what happens to the results.
Sometimes inverse
is initialized properly! (Hint:
remove the println("In c2b:")
statement. Then try
putting it back, but after the val value = 10
line.)
What this experiment
really shows is that side effects (i.e., from the
println
statements) can be unexpected and subtle,
especially during initialization. It’s best to avoid them.
Scala provides two solutions to this problem: lazy values, which we discuss in Lazy Vals, and pre-initialized fields, which is demonstrated in the following refinement to the previous example:
// code-examples/AdvOOP/overrides/trait-pre-init-val-script.scala
trait
AbstractT2
{ println("In AbstractT2:"
)val
value:Int
val
inverse =1.0
/value println("AbstractT2: value = "
+value+", inverse = "
+inverse) }val
c2c =new
{// Only initializations are allowed in pre-init. blocks.
// println("In c2c:")
val
value =10
}with
AbstractT2
println("c2c.value = "
+c2c.value+", inverse = "
+c2c.inverse)
We instantiate an
anonymous inner class, initializing the value
field
in the block, before the with AbstractT2
clause. This
guarantees that value
is initialized before the body
of AbstractT2
is executed, as shown when you run the
script:
In AbstractT2: AbstractT2: value = 10, inverse = 0.1 c2c.value = 10, inverse = 0.1
Also, if you selectively
remove any of the println
statements, you get the
same expected and now predictable results.
Now let’s consider the
second workaround we described earlier, changing the declaration to
var
. This solution is more suitable if a good default
value exists and you don’t want to require instances that use the trait
to always set the value. In this case, change the val
to a var
, either a public var
or a
private var
hidden behind reader and writer methods.
Either way, we can simply reassign the value in a derived trait or
class.
Returning to our
VetoableClicks
example, here is the modified
VetoableClicks
trait that uses a public
var
for maxAllowed
:
// code-examples/AdvOOP/ui3/vetoable-clicks.scala
package
ui3import
observer._trait
VetoableClicks
extends
Clickable
{var
maxAllowed =1
// default
private
var
count =0
abstract
override
def
click
() = { count +=1
if
(count <= maxAllowed)super
.click() } }
Here is a new
specs
object,
ButtonClickableObserverVetoableSpec2
, that
demonstrates changing the value of maxAllowed
:
// code-examples/AdvOOP/ui3/button-clickable-observer-vetoable2-spec.scala
package
ui3import
org.specs._import
observer._import
ui.ButtonCountObserverobject
ButtonClickableObserverVetoableSpec2
extends
Specification
{"A Button Observer with Vetoable Clicks"
should {"observe only the first 'maxAllowed' clicks"
in {val
observableButton =new
Button
("Okay"
)with
ObservableClicks
with
VetoableClicks
{ maxAllowed =2
} observableButton.maxAllowed mustEqual2
val
buttonClickCountObserver =new
ButtonCountObserver
observableButton.addObserver(buttonClickCountObserver)for
(i<-
1
to3
) observableButton.click() buttonClickCountObserver.count mustEqual2
} } }
No override
var
is required. We just assign a new value. Since the body of
the trait is executed before the body of the class using it, reassigning
the field value happens after the initial
assignment in the trait’s body. However, as we saw before, that
reassignment could happen too late if the field is used in the trait’s
body in some calculation that will become invalid by a reassignment
later! You can avoid this problem if you make the field private and
define a public writer method that redoes any dependent
calculations.
Another disadvantage of
using a var
declaration is that
maxAllowed
was not intended to be writable. As we
will see in Chapter 8, read-only values
have important benefits. We would prefer for
maxAllowed
to be read-only, at least after the
construction process completes.
We can see that the
simple act of changing the val
to a
var
causes potential problems for the maintainer of
VetoableClicks
. Control over that field is now lost.
The maintainer must carefully consider whether or not the value will
change and if a change will invalidate the state of the instance. This
issue is especially pernicious in multithreaded systems (see The Problems of Shared, Synchronized State).
In contrast to traits,
overriding a val
declared in a class works as
expected. Here is an example with both a val
override
and a var
reassignment in a derived class:
// code-examples/AdvOOP/overrides/class-field-script.scala
class
C1
{val
name ="C1"
var
count =0
}class
ClassWithC1
extends
C1
{override
val
name ="ClassWithC1"
count =1
}val
c =new
ClassWithC1
() println(c.name) println(c.count)
The
override
keyword is required for the
concrete val
field
name
, but not for the var
field
count
. This is because we are changing the
initialization of a constant (val
), which is a
“special” operation.
If you run this script, the output is the following:
ClassWithC1 1
Both fields are
overridden in the derived class, as expected. Here is the same example
modified so that both the val
and the
var
are abstract in the base class:
// code-examples/AdvOOP/overrides/class-abs-field-script.scala
abstract
class
AbstractC1
{val
name:String
var
count:Int
}class
ClassWithAbstractC1
extends
AbstractC1
{val
name ="ClassWithAbstractC1"
var
count =1
}val
c =new
ClassWithAbstractC1
() println(c.name) println(c.count)
The
override
keyword is not required for
name
in ClassWithAbstractC1
, since
the original declaration is abstract. The output of this script is the
following:
ClassWithAbstractC1 1
It’s important to
emphasize that name
and count
are
abstract fields, not concrete fields with default
values. A similar-looking declaration of name
in a
Java class, String name;
, would
declare a concrete field with the default value (null
in this case). Java doesn’t support abstract fields or types (as we’ll
discuss next), only methods.
We introduced abstract
type declarations in Abstract Types And Parameterized Types. Recall the
BulkReader
example from that section:
// code-examples/TypeLessDoMore/abstract-types-script.scala
import
java.io._abstract
class
BulkReader
{type
In
val
source:In
def
read
:String
}class
StringBulkReader
(val
source:String
)extends
BulkReader
{type
In
=String
def
read
= source }class
FileBulkReader
(val
source:File
)extends
BulkReader
{type
In
=File
def
read
= {val
in =new
BufferedInputStream
(new
FileInputStream
(source))val
numBytes = in.available()val
bytes =new
Array
[Byte]
(numBytes) in.read(bytes,0
, numBytes)new
String
(bytes) } } println(new
StringBulkReader
("Hello Scala!"
).read ) println(new
FileBulkReader
(new
File
("abstract-types-script.scala"
)).read )
Abstract types are an alternative to parameterized types, which we’ll explore in Understanding Parameterized Types. Like parameterized types, they provide an abstraction mechanism at the type level.
The example shows how to
declare an abstract type and how to define a concrete value in derived
classes. BulkReader
declares type
In
without initializing it. The concrete derived class
StringBulkReader
provides a concrete value using
type In = String
.
Unlike fields and methods,
it is not possible to override a concrete type
definition. However, the abstract declaration can constrain the allowed
concrete type values. We’ll learn how in Chapter 12.
Finally, you probably noticed that this example also demonstrates defining an abstract field, using a constructor parameter, and an abstract method.
For another example, let’s
revisit our Subject
trait from Traits As Mixins. The definition of the
Observer
type is a structural
type with a method named receiveUpdate
.
Observers must have this “structure.” Let’s generalize the
implementation now, using an abstract type:
// code-examples/AdvOOP/observer/observer2.scala
package
observertrait
AbstractSubject
{type
Observer
private
var
observers =List
[Observer]
()def
addObserver
(observer:Observer
) = observers ::= observerdef
notifyObservers
= observers foreach (notify(_
))def
notify
(observer:Observer
):Unit
}trait
SubjectForReceiveUpdateObservers
extends
AbstractSubject
{type
Observer
= {def
receiveUpdate
(subject:Any
) }def
notify
(observer:Observer
):Unit
= observer.receiveUpdate(this
) }trait
SubjectForFunctionalObservers
extends
AbstractSubject
{type
Observer
= (AbstractSubject
)=>
Unit
def
notify
(observer:Observer
):Unit
= observer(this
) }
Now,
AbstractSubject
declares type
Observer
as abstract (implicitly, because there is no
definition). Since the original structural type is gone, we don’t know
exactly how to notify an observer. So, we also added an abstract method
notify
, which a concrete class or trait will define
as appropriate.
The
SubjectForReceiveUpdateObservers
derived trait
defines Observer
with the same structural type we
used in the original example, and notify
simply calls
receiveUpdate
, as before.
The
SubjectForFunctionalObservers
derived trait defines
Observer
to be a function taking an instance of
AbstractSubject
and returning
Unit
. All notify
has to do is call
the observer function, passing the subject as the sole argument. Note
that this implementation is similar to the approach we used in our
original button implementation, ButtonWithCallbacks
,
where the “callbacks” were user-supplied functions. (See Introducing Traits and a revisited version in Constructors in Scala.)
Here is a specification that exercises these two variations, observing button clicks as before:
// code-examples/AdvOOP/observer/button-observer2-spec.scala
package
uiimport
org.specs._import
observer._object
ButtonObserver2Spec
extends
Specification
{"An Observer watching a SubjectForReceiveUpdateObservers button"
should {"observe button clicks"
in {val
observableButton =new
Button
(name)with
SubjectForReceiveUpdateObservers
{override
def
click
() = {super
.click() notifyObservers } }val
buttonObserver =new
ButtonCountObserver
observableButton.addObserver(buttonObserver)for
(i<-
1
to3
) observableButton.click() buttonObserver.count mustEqual3
} }"An Observer watching a SubjectForFunctionalObservers button"
should {"observe button clicks"
in {val
observableButton =new
Button
(name)with
SubjectForFunctionalObservers
{override
def
click
() = {super
.click() notifyObservers } }var
count =0
observableButton.addObserver((button)=>
count +=1
)for
(i<-
1
to3
) observableButton.click() count mustEqual3
} } }
First we exercise
SubjectForReceiveUpdateObservers
, which looks very
similar to our earlier examples. Next we exercise
SubjectForFunctionalObservers
. In this case, we don’t
need another “observer” instance at all. We just maintain a
count
variable and pass a function
literal to addObserver
to increment the
count (and ignore the button).
The main virtue of
SubjectForFunctionalObservers
is its minimalism. It
requires no special instances, no traits defining abstractions, etc. For
many cases, it is an ideal approach.
AbstractSubject
is more reusable than the original definition of
Subject
, because it imposes fewer constraints on
potential observers.
AbstractSubject
illustrates that an
abstraction with fewer concrete details is usually more
reusable.
But wait, there’s more! We’ll revisit the use of abstract types and the Observer Pattern in Scalable Abstractions.
Suppose a user of
ButtonCountObserver
from Traits As Mixins accesses the count
member:
// code-examples/Traits/ui/button-count-observer-script.scala
val
bco =new
ui.ButtonCountObserver
val
oldCount = bco.count bco.count =5
val
newCount = bco.count println(newCount +" == 5 and "
+ oldCount +" == 0?"
)
When the
count
field is read or written, as in this example,
are methods called or is the field accessed directly? As originally
declared in ButtonCountObserver
, the field is
accessed directly. However, the user doesn’t really care. In fact, the
following two definitions are functionally equivalent, from the
perspective of the user:
class
ButtonCountObserver
{var
count =0
// public field access (original definition)
// ...
}
class
ButtonCountObserver
{private
var
cnt =0
// private field
def
count
= cnt// reader method
def
count_
=(newCount:Int
) = cnt = newCount// writer method
// ...
}
This equivalence is an
example of the Uniform Access Principle. Clients
read and write field values as if they are publicly accessible, even
though in some cases they are actually calling methods. The maintainer
of ButtonCountObserver
has the freedom to change the
implementation without forcing users to make code changes.
The reader method in the second version does not have parentheses. Recall that consistency in the use of parentheses is required if a method definition omits parentheses. This is only possible if the method takes no arguments. For the Uniform Access Principle to work, we want to define field reader methods without parentheses. (Contrast that with Ruby, where method parentheses are always optional as long as the parse is unambiguous.)
The writer method has the
format count_=(...)
. As a bit of syntactic sugar, the
compiler allows invocations of methods with this format to be written in
either of the following ways:
obj.field_=(newValue)
// or
obj.field = newValue
We named the private
variable cnt
in the alternative definition. Scala
keeps field and method names in the same namespace,
which means we can’t name the field count
if a method
is named count
. Many languages, like Java, don’t have
this restriction because they keep field and method names in separate
namespaces. However, these languages can’t support the Uniform Access
Principle as a result, unless they build in ad hoc support in their
grammars or compilers.
Since member
object
definitions behave similar to fields from the
caller’s perspective, they are also in the same namespace as methods and
fields. Hence, the following class would not compile:
// code-examples/AdvOOP/overrides/member-namespace-wont-compile.scala
// WON'T COMPILE
class
IllegalMemberNameUse
{def
member
(i:Int
) =2
* ival
member =2
// ERROR
object
member
{// ERROR
def
apply
() =2
} }
There is one other
benefit of this namespace “unification.” If a parent class declares a
parameterless method, then a subclass can override that method with a
val
. If the parent’s method is concrete, then the
override
keyword is required:
// code-examples/AdvOOP/overrides/method-field-class-script.scala
class
Parent
{def
name
="Parent"
}class
Child
extends
Parent
{override
val
name ="Child"
} println(new
Child
().name)// => "Child"
If the parent’s method is
abstract, then the override
keyword is
optional:
// code-examples/AdvOOP/overrides/abs-method-field-class-script.scala
abstract
class
AbstractParent
{def
name
:String
}class
ConcreteChild
extends
AbstractParent
{val
name ="Child"
} println(new
ConcreteChild
().name)// => "Child"
This also works for traits. If the trait’s method is concrete, we have the following:
// code-examples/AdvOOP/overrides/method-field-trait-script.scala
trait
NameTrait
{def
name
="NameTrait"
}class
ConcreteNameClass
extends
NameTrait
{override
val
name ="ConcreteNameClass"
} println(new
ConcreteNameClass
().name)// => "ConcreteNameClass"
If the trait’s method is abstract, we have the following:
// code-examples/AdvOOP/overrides/abs-method-field-trait-script.scala
trait
AbstractNameTrait
{def
name
:String
}class
ConcreteNameClass
extends
AbstractNameTrait
{val
name ="ConcreteNameClass"
} println(new
ConcreteNameClass
().name)// => "ConcreteNameClass"
Why is this feature useful? It allows derived classes and traits to use a simple field access when that is sufficient, or a method call when more processing is required, such as lazy initialization. The same argument holds for the Uniform Access Principle, in general.
Overriding a
def
with a val
in a subclass can
also be handy when interoperating with Java code. Turn a getter into a
val
by placing it in the constructor. You’ll see this
in action in the following example, in which our Scala class
Person
implements a hypothetical
PersonInterface
from some legacy Java code:
class
Person
(val
getName:String
)extends
PersonInterface
If you only have a few accessors in the Java code you’re integrating with, this technique makes quick work of them.
What about overriding a
parameterless method with a var
, or overriding a
val
or var
with a method? These
are not permitted because they can’t match the behaviors of the things
they are overriding.
If you attempt to use a
var
to override a parameterless method, you get an
error that the writer method, override name_=
, is not
overriding anything. This would also be inconsistent with a
philosophical goal of functional programming, that a method that takes
no parameters should always return the same result. To do otherwise
would require side effects in the implementation, which functional
programming tries to avoid, for reasons we will examine in Chapter 8. Because a var
is
changeable, the no-parameter “method” defined in the parent type would
no longer return the same result consistently.
If you could override a
val
with a method, there would be no way for Scala to
guarantee that the method would always return the same value, consistent
with val
semantics. That issue doesn’t exist with a
var
, of course, but you would have to override the
var
with two methods, a reader and a writer. The
Scala compiler doesn’t support that substitution.
Recall that fields and
methods defined in objects
serve the role that class
“static” fields and methods serve in languages like Java. When
object
-based fields and methods are closely associated
with a particular class
, they are normally defined in a
companion object.
We mentioned companion
objects briefly in Chapter 1, and we discussed
the Pair
example from the Scala library in Chapter 2. Let’s fill in the remaining details
now.
First, recall that if a
class
(or a type
referring to a
class) and an object
are declared in the same file, in
the same package, and with the same name, they are called a
companion class (or companion
type) and a companion object,
respectively.
There is no namespace collision when the name is reused in this way, because Scala stores the class name in the type namespace, while it stores the object name in the term namespace (see [ScalaSpec2009]).
The two most interesting
methods frequently defined in a companion object are
apply
and unapply
.
Scala provides some
syntactic sugar in the form of the apply
method. When
an instance of a class is followed by parentheses with a list of zero or
more parameters, the compiler invokes the apply
method for that instance. This is true for an object
with a defined apply
method (such as a companion
object), as well as an instance of a class
that
defines an apply
method.
In the case of an
object
, apply
is conventionally
used as a factory method, returning a new instance.
This is what Pair.apply
does in the Scala library.
Here is Pair
from the standard library:
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) }
So, you can create a new
Pair
as follows:
val
p =Pair
(1
,"one"
)
It looks like we are
somehow creating a Pair
instance without a
new
. Rather than calling a Pair
constructor directly, we are actually calling
Pair.apply
(i.e., the companion object
Pair
), which then calls
Tuple2.apply
on the Tuple2
companion object!
If there are several alternative constructors for a class and it
also has a companion object, consider defining fewer constructors on
the class and defining several overloaded apply
methods on the companion object to handle the variations.
However,
apply
is not limited to instantiating the companion
class. It could instead return an instance of a subclass of the
companion class. Here is an example where we define a companion object
Widget
that uses regular expressions to parse a
string representing a Widget
subclass. When a match
occurs, the subclass is instantiated and the new instance is
returned:
// code-examples/AdvOOP/objects/widget.scala
package
objectsabstract
class
Widget
{def
draw
():Unit
override
def
toString
() ="(widget)"
}object
Widget
{val
ButtonExtractorRE ="""(button: label=([^,]+),s+(Widget))"""
.rval
TextFieldExtractorRE ="""(textfield: text=([^,]+),s+(Widget))"""
.rdef
apply
(specification:String
):Option[Widget]
= specificationmatch
{case
ButtonExtractorRE
(label)=>
new
Some
(new
Button
(label))case
TextFieldExtractorRE
(text)=>
new
Some
(new
TextField
(text))case
_
=>
None
} }
Widget.apply
receives a string “specification” that defines which class to
instantiate. The string might come from a configuration file with
widgets to create at startup, for example. The string format is the same
format used by toString()
. Regular expressions are
defined for each type. (Parser combinators are an
alternative. They are discussed in External DSLs with Parser Combinators.)
The
match
expression applies each regular expression to
the string. A case expression like:
case
ButtonExtractorRE
(label)=>
new
Some
(new
Button
(label))
means that the string is
matched against the ButtonExtractorRE
regular
expression. If successful, it extracts the substring in the first
capture group in the regular expression and assigns it to the variable
label
. Finally, a new Button
with
this label is created, wrapped in a Some
. We’ll learn
how this extraction process works in the next section, Unapply.
A similar case handles
TextField
creation. (TextField
is
not shown. See the online code examples.) Finally, if
apply
can’t match the string, it returns
None
.
Here is a specs
object
that exercises
Widget.apply
:
// code-examples/AdvOOP/objects/widget-apply-spec.scala
package
objectsimport
org.specs._object
WidgetApplySpec
extends
Specification
{"Widget.apply with a valid widget specification string"
should {"return a widget instance with the correct fields set"
in {Widget
("(button: label=click me, (Widget))"
)match
{case
Some
(w)=>
wmatch
{case
b:Button => b.label
mustEqual"click me"
case
x=>
fail(x.toString()) }case
None
=>
fail("None returned."
) }Widget
("(textfield: text=This is text, (Widget))"
)match
{case
Some
(w)=>
wmatch
{case
tf:TextField => tf.text
mustEqual"This is text"
case
x=>
fail(x.toString()) }case
None
=>
fail("None returned."
) } } }"Widget.apply with an invalid specification string"
should {"return None"
in {Widget
("(button: , (Widget)"
) mustEqualNone
} } }
The first match statement
implicitly invokes Widget.apply
with the string
"(button: label=click me, (Widget))"
. If a button
wrapped in a Some
is not returned with the label
"click me"
, this test will fail. Next, a similar test
for a TextField
widget is done. The final test uses
an invalid string and confirms that None
is
returned.
A drawback of this
particular implementation is that we have hardcoded a dependency on each
derived class of Widget
in Widget
itself, which breaks the Open-Closed Principle (see
[Meyer1997]
and [Martin2003]). A better implementation
would use a factory design pattern from [GOF1995]. Nevertheless, the example
illustrates how an apply
method
can be used as a real factory.
There is no requirement
for apply
in an object
to be used
as a factory. Neither is there any restriction on the argument list or
what apply
returns. However, because it is so common
to use apply
in an object
as a
factory, use caution when using apply
for other
purposes, as it could confuse users. However, there are good
counterexamples, such as the use of apply
in
Domain-Specific Languages (see Chapter 11).
The factory convention is
less commonly used for apply
defined in classes. For
example, in the Scala standard library, Array.apply(i:
int)
returns the element at index i
in the
array. Many of the other collections use apply
in a
similar way. So, users can write code like the following:
val
a =Array
(1
,2
,3
,4
) println(a(2
))// => 3
Finally, as a reminder,
although apply
is handled specially by the compiler,
it is otherwise no different from any other method. You can overload it,
you can invoke it directly, etc.
The name
unapply
suggests that it does the “opposite”
operation of apply
. Indeed, it is used to extract the
constituent parts of an instance. Pattern matching uses this feature
extensively. Hence, unapply
is often defined in
companion objects and is used to extract the field values from instances
of the corresponding companion types. For this reason,
unapply
methods are called
extractors.
Here is an expanded
button.scala
with a Button
object
that defines an unapply
extractor method:
// code-examples/AdvOOP/objects/button.scala
package
objectsimport
ui3.Clickableclass
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) }
Button.unapply
takes a single Button
argument and returns a
Some
wrapping the label
value.
This demonstrates the protocol for unapply
methods.
They return a Some
wrapping the extracted fields.
(We’ll see how to handle more than one field in a moment.)
Here is a specs
object
that exercises
Button.unapply
:
// code-examples/AdvOOP/objects/button-unapply-spec.scala
package
objectsimport
org.specs._object
ButtonUnapplySpec
extends
Specification
{"Button.unapply"
should {"match a Button object"
in {val
b =new
Button
("click me"
) bmatch
{case
Button
(label)=>
case
_
=>
fail() } }"match a RadioButton object"
in {val
b =new
RadioButton
(false
,"click me"
) bmatch
{case
Button
(label)=>
case
_
=>
fail() } }"not match a non-Button object"
in {val
tf =new
TextField
("hello world!"
) tfmatch
{case
Button
(label)=>
fail()case
_
=>
} }"extract the Button's label"
in {val
b =new
Button
("click me"
) bmatch
{case
Button
(label)=>
label mustEqual"click me"
case
_
=>
fail() } }"extract the RadioButton's label"
in {val
rb =new
RadioButton
(false
,"click me, too"
) rbmatch
{case
Button
(label)=>
label mustEqual"click me, too"
case
_
=>
fail() } } } }
The first three examples
(in
clauses) confirm that
Button.unapply
is only called for actual
Button
instances or instances of derived classes,
like RadioButton
.
Since
unapply
takes a Button
argument
(in this case), the Scala runtime type checks the instance being
matched. It then looks for a companion object with an
unapply
method and invokes that method, passing the
instance. The default case clause case _
is invoked
for the instances that don’t type check as compatible. The pattern
matching process is fully type-safe.
The remaining examples
(in
clauses) confirm that the correct values for the
label
are extracted. The Scala runtime automatically
extracts the item in the Some
.
What about extracting
multiple fields? For a fixed set of known fields, a
Some
wrapping a Tuple
is returned,
as shown in this updated version of
RadioButton
:
// code-examples/AdvOOP/objects/radio-button.scala
package
objects/**
* Button with two states, on or off, like an old-style,
* channel-selection botton on a radio.
*/
class
RadioButton
(val
on:Boolean
, label:String
)extends
Button
(label)object
RadioButton
{def
unapply
(button:RadioButton
) =Some
((button.on, button.label))// equivalent to: = Some(Pair(button.on, button.label))
}
A Some
wrapping a Pair(button.on, button.label)
is returned.
As we discuss in The Predef Object,
Pair
is a type defined to be
equal to Tuple2
. Here is the corresponding
specs
object
that tests
it:
// code-examples/AdvOOP/objects/radio-button-unapply-spec.scala
package
objectsimport
org.specs._object
RadioButtonUnapplySpec
extends
Specification
{"RadioButton.unapply"
should {"should match a RadioButton object"
in {val
b =new
RadioButton
(true
,"click me"
) bmatch
{case
RadioButton
(on, label)=>
case
_
=>
fail() } }"not match a Button (parent class) object"
in {val
b =new
Button
("click me"
) bmatch
{case
RadioButton
(on, label)=>
fail()case
_
=>
} }"not match a non-RadioButton object"
in {val
tf =new
TextField
("hello world!"
) tfmatch
{case
RadioButton
(on, label)=>
fail()case
_
=>
} }"extract the RadioButton's on/off state and label"
in {val
b =new
RadioButton
(true
,"click me"
) bmatch
{case
RadioButton
(on, label)=>
{ label mustEqual"click me"
on mustEqualtrue
}case
_
=>
fail() } } } }
What if you want to build
a collection from a variable argument list passed to
apply
? What if you want to extract the first few
elements from a collection and you don’t care about the rest of
it?
In this case, you define
apply
and unapplySeq
(“unapply
sequence”) methods. Here are those methods from Scala’s own
List
class:
def
apply
[A]
(xs:A
*):List[A]
= xs.toListdef
unapplySeq
[A]
(x:List[A]
):Some[List[A]]
=Some
(x)
The [A]
type parameterization on these methods allows the
List
object
, which is not
parameterized, to construct a new List[A]
. (See Understanding Parameterized Types for more details.) Most of the time, the
type parameter will be inferred based on the context.
The parameter list
xs: A*
is a variable argument list. Callers of
apply
can pass as many A
instances
as they want, including none. Internally, variable argument lists are
stored in an Array[A]
, which inherits the
toList
method from Iterable
that
we used here.
This is a handy idiom for API writers. Accepting variable
arguments to a function can be convenient for users, and converting
the arguments to a List
is often ideal for internal
management.
Here is an example script
that uses List.apply
implicitly:
// code-examples/AdvOOP/objects/list-apply-example-script.scala
val
list1 =List
()val
list2 =List
(1
,2.2
,"three"
,'four
)val
list3 =List
("1"
,"2.2"
,"three"
,"four"
) println("1: "
+list1) println("2: "
+list2) println("3: "
+list3)
The 'four
is a symbol, essentially an interned string.
Symbols are more commonly used in Ruby, for example, where the same
symbol would be written as :four
. Symbols are useful
for representing identities consistently.
This script yields the following output:
1: List() 2: List(1, 2.2, three, 'four) 3: List(1, 2.2, three, four)
The
unapplySeq
method is trivial; it returns the input
list wrapped in a Some
. However, this is sufficient
for pattern matching, as shown in this example:
// code-examples/AdvOOP/objects/list-unapply-example-script.scala
val
list =List
(1
,2.2
,"three"
,'four
) listmatch
{case
List
(x, y,_
*)=>
println("x = "
+x+", y = "
+y)case
_
=>
throw
new
Exception
("No match! "
+list) }
The List(x, y,
_*)
syntax means we will only match on a list with at least
two elements, and the first two elements will be assigned to
x
and y
. We don’t care about the
rest of the list. The _*
matches zero or more
remaining elements.
x = 1, y = 2.2
We’ll have much more to
say about List
and pattern matching in Lists in Functional Programming.
There is one more thing
to know about companion objects. Whenever you define a
main
method to use as the entry point for an
application, Scala requires you to put it in an object. However, at the
time of this writing, main
methods cannot be defined
in a companion object. Because of implementation details in the
generated code, the JVM won’t find the main
method.
This issue may be resolved in a future release. For now, you must define
any main
method in a singleton
object (i.e., a “non-companion” object; see [ScalaTips]). Consider the following
example of a simple Person
class and companion object
that attempts to define main
:
// code-examples/AdvOOP/objects/person.scala
package
objectsclass
Person
(val
name:String
,val
age:Int
) {override
def
toString
="name: "
+ name +", age: "
+ age }object
Person
{def
apply
(name:String
, age:Int
) =new
Person
(name, age)def
unapply
(person:Person
) =Some
((person.name, person.age))def
main
(args:Array[String]
) = {// Test the constructor...
val
person =new
Person
("Buck Trends"
,18
) assert(person.name =="Buck Trends"
) assert(person.age ==21
) } }object
PersonTest
{def
main
(args:Array[String]
) = Person.main(args) }
This code compiles fine,
but if you attempt to invoke Person.main
, using
scala -cp ... objects.Person
, you get the following
error:
java.lang.NoSuchMethodException: objects.Person.main([Ljava.lang.String;)
The
objects/Person.class
file exists. If you decompile it
with javap -classpath ... objects.Person
(refer to
The scalap, javap, and jad Command-Line Tools), you can see that it
doesn’t contain a main
method. If you decompile
objects/Person$.class
, the file for the companion
object’s byte code, it has a main
method, but notice
that it isn’t declared static
. So, attempting to
invoke scala -cp ... objects.Person$
also fails to
find the “static” main
:
java.lang.NoSuchMethodException: objects.Person$.main is not static
The separate
singleton object PersonTest
defined in this example has to be used. Decompiling it with
javap -classpath ... objects.PersonTest
shows that it
has a static main
method. If you invoke it using
scala -cp ... objects.PersonTest
, the
PersonTest.main
method is invoked, which in turn
invokes Person.main
. You get an assertion error from
the second call to assert
, which is
intentional:
java.lang.AssertionError: assertion failed at scala.Predef$.assert(Predef.scala:87) at objects.Person$.test(person.scala:15) at objects.PersonTest$.main(person.scala:20) at objects.PersonTest.main(person.scala) ...
In fact,
this is a general issue with methods defined in companion objects that
need to be visible to Java code as static methods. They aren’t static in
the byte code. You have to put these methods in singleton objects
instead. Consider the following Java class that attempts to create a
user with Person.apply
:
// code-examples/AdvOOP/objects/PersonUserWontCompile.java
// WON'T COMPILE
package
objects;public
class
PersonUserWontCompile {public
static
void
main(String[] args) { Person buck = Person.apply("Buck Trends"
,100
);// ERROR
System.out.println(buck); } }
If we compile it (after
compiling Person.scala
), we get the following
error:
$ javac -classpath ... objects/PersonUserWontCompile.java objects/PersonUserWontCompile.java:5: cannot find symbol symbol : method apply(java.lang.String,int) location: class objects.Person Person buck = Person.apply("Buck Trends", 100); ^ 1 error
However, we can use the following singleton object:
// code-examples/AdvOOP/objects/person-factory.scala
package
objectsobject
PersonFactory
{def
make
(name:String
, age:Int
) =new
Person
(name, age) }
Now the following Java class will compile:
// code-examples/AdvOOP/objects/PersonUser.java
package
objects;public
class
PersonUser {public
static
void
main(String[] args) {// The following line won't compile.
// Person buck = Person.apply("Buck Trends", 100);
Person buck = PersonFactory.make("Buck Trends"
,100
); System.out.println(buck); } }
Do not define main
or any other method in a
companion object that needs to be visible to Java
code as a static
method. Define it in a
singleton object, instead.
If you have
no other choice but to call a method in a companion object from Java,
you can explicitly create an instance of the object with
new
, since the object is a “regular” Java class in
the byte code, and call the method on the instance.
In Matching on Case Classes, we briefly introduced you to case classes. Case classes have several useful features, but also some drawbacks.
Let’s rewrite the
Shape
example we used in A Taste of Concurrency to use case classes. Here is the original
implementation:
// code-examples/IntroducingScala/shapes.scala
package
shapes {class
Point
(val
x:Double
,val
y:Double
) {override
def
toString
() ="Point("
+ x +","
+ y +")"
}abstract
class
Shape
() {def
draw
():Unit
}class
Circle
(val
center:Point
,val
radius:Double
)extends
Shape
{def
draw
() = println("Circle.draw: "
+this
)override
def
toString
() ="Circle("
+ center +","
+ radius +")"
}class
Rectangle
(val
lowerLeft:Point
,val
height:Double
,val
width:Double
)extends
Shape
{def
draw
() = println("Rectangle.draw: "
+this
)override
def
toString
() ="Rectangle("
+ lowerLeft +","
+ height +","
+ width +")"
}class
Triangle
(val
point1:Point
,val
point2:Point
,val
point3:Point
)extends
Shape
() {def
draw
() = println("Triangle.draw: "
+this
)override
def
toString
() ="Triangle("
+ point1 +","
+ point2 +","
+ point3 +")"
} }
Here is the example
rewritten using the case
keyword:
// code-examples/AdvOOP/shapes/shapes-case.scala
package
shapes {case
class
Point
(x:Double
, y:Double
)abstract
class
Shape
() {def
draw
():Unit
}case
class
Circle
(center:Point
, radius:Double
)extends
Shape
() {def
draw
() = println("Circle.draw: "
+this
) }case
class
Rectangle
(lowerLeft:Point
, height:Double
, width:Double
)extends
Shape
() {def
draw
() = println("Rectangle.draw: "
+this
) }case
class
Triangle
(point1:Point
, point2:Point
, point3:Point
)extends
Shape
() {def
draw
() = println("Triangle.draw: "
+this
) } }
Adding the
case
keyword causes the compiler to add a number of
useful features automatically. The keyword suggests an association with
case
expressions in pattern matching. Indeed, they are
particularly well suited for that application, as we will see.
First, the compiler
automatically converts the constructor arguments into immutable fields
(val
s). The val
keyword is optional.
If you want mutable fields, use the var
keyword. So,
our constructor argument lists are now shorter.
Second, the compiler
automatically implements equals
,
hashCode
, and toString
methods to
the class, which use the fields specified as constructor arguments. So, we
no longer need our own toString
methods. In fact, the
generated toString
methods produce the same outputs as
the ones we implemented ourselves. Also, the body of
Point
is gone because there are no methods that we need
to define!
The following script uses these methods that are now in the shapes:
// code-examples/AdvOOP/shapes/shapes-usage-example1-script.scala
import
shapes._val
shapesList =List
(Circle
(Point
(0.0
,0.0
),1.0
),Circle
(Point
(5.0
,2.0
),3.0
),Rectangle
(Point
(0.0
,0.0
),2
,5
),Rectangle
(Point
(-2.0
, -1.0
),4
,3
),Triangle
(Point
(0.0
,0.0
),Point
(1.0
,0.0
),Point
(0.0
,1.0
)))val
shape1 = shapesList.head// grab the first one.
println("shape1: "
+shape1+". hash = "
+shape1.hashCode)for
(shape2<-
shapesList) { println("shape2: "
+shape2+". 1 == 2 ? "
+(shape1 == shape2)) }
This script outputs the following:
shape1: Circle(Point(0.0,0.0),1.0). hash = 2061963534 shape2: Circle(Point(0.0,0.0),1.0). 1 == 2 ? true shape2: Circle(Point(5.0,2.0),3.0). 1 == 2 ? false shape2: Rectangle(Point(0.0,0.0),2.0,5.0). 1 == 2 ? false shape2: Rectangle(Point(-2.0,-1.0),4.0,3.0). 1 == 2 ? false shape2: Triangle(Point(0.0,0.0),Point(1.0,0.0),Point(0.0,1.0)). 1 == 2 ? false
As we’ll see in Equality of Objects, the ==
method actually
invokes the equals
method.
Even outside of
case
expressions, automatic generation of these three
methods is very convenient for simple, “structural” classes, i.e., classes
that contain relatively simple fields and behaviors.
Third, when the
case
keyword is used, the compiler automatically
creates a companion object with an
apply
factory method that takes the same arguments as
the primary constructor. The previous example used
the appropriate apply
methods to create the
Points
, the different Shapes
, and
also the List
itself. That’s why we don’t need
new
; we’re actually calling
apply(x,y)
in the Point
companion
object, for example.
You can have secondary constructors in case
classes, but there will be no overloaded apply
method
generated that has the same argument list. You’ll have to use
new
to create instances with those
constructors.
The companion object also
gets an unapply
extractor method, which extracts all
the fields of an instance in an elegant fashion. The following script
demonstrates the extractors in pattern matching case
statements:
// code-examples/AdvOOP/shapes/shapes-usage-example2-script.scala
import
shapes._val
shapesList =List
(Circle
(Point
(0.0
,0.0
),1.0
),Circle
(Point
(5.0
,2.0
),3.0
),Rectangle
(Point
(0.0
,0.0
),2
,5
),Rectangle
(Point
(-2.0
, -1.0
),4
,3
),Triangle
(Point
(0.0
,0.0
),Point
(1.0
,0.0
),Point
(0.0
,1.0
)))def
matchOn
(shape:Shape
) = shapematch
{case
Circle
(center, radius)=>
println("Circle: center = "
+center+", radius = "
+radius)case
Rectangle
(ll, h, w)=>
println("Rectangle: lower-left = "
+ll+", height = "
+h+", width = "
+w)case
Triangle
(p1, p2, p3)=>
println("Triangle: point1 = "
+p1+", point2 = "
+p2+", point3 = "
+p3)case
_
=>
println("Unknown shape!"
+shape) } shapesList.foreach { shape=>
matchOn(shape) }
This script outputs the following:
Circle: center = Point(0.0,0.0), radius = 1.0 Circle: center = Point(5.0,2.0), radius = 3.0 Rectangle: lower-left = Point(0.0,0.0), height = 2.0, width = 5.0 Rectangle: lower-left = Point(-2.0,-1.0), height = 4.0, width = 3.0 Triangle: point1 = Point(0.0,0.0), point2 = Point(1.0,0.0), point3 = Point(0.0,1.0)
By the way, remember in
Matching on Sequences when we discussed matching on
lists? We wrote this case
expression:
def
processList
(l:List[Any]
):Unit
= lmatch
{case
head :: tail=>
... ... }
It turns out that the following expressions are identical:
case
head :: tail=>
...case
::(head, tail)=>
...
We are using the
companion object for the case class named ::
, which
is used for non-empty lists. When used in case
expressions, the compiler supports this special infix operator notation
for invocations of unapply
.
It works not only for
unapply
methods with two arguments, but also with one
or more arguments. We could rewrite our matchOn
method this way:
def
matchOn
(shape:Shape
) = shapematch
{case
centerCircle
radius=>
...case
llRectangle
(h, w)=>
...case
p1Triangle
(p2, p3)=>
...case
_
=>
... }
For an
unapply
that takes one argument, you would have to
insert an empty set of parentheses to avoid a parsing ambiguity:
case
argFoo
()=>
...
From the point of view of
clarity, this syntax is elegant for some cases when there are two
arguments. For lists, head :: tail
matches the
expressions for building up lists, so there is a beautiful symmetry when
the extraction process uses the same syntax. However, the merits of this
syntax are less clear for other examples, especially when there are N !=
2 arguments.
In Scala version 2.8,
another instance method is automatically generated, called
copy
. This method is useful when you want to make a
new instance of a case class that is identical to another instance with
a few fields changed. Consider the following example script:
// code-examples/AdvOOP/shapes/shapes-usage-example3-v28-script.scala
// Scala version 2.8 only.
import
shapes._val
circle1 =Circle
(Point
(0.0
,0.0
),2.0
)val
circle2 = circle1 copy (radius =4.0
) println(circle1) println(circle2)
The second circle is
created by copying the first and specifying a new radius. The
copy
method implementation that is generated by the
compiler exploits the new named and default parameters in Scala version
2.8, which we discussed in Method Default and Named Arguments (Scala Version 2.8). The generated
implementation of Circle.copy
looks roughly like the
following:
case
class
Circle
(center:Point
, radius:Double
)extends
Shape
() { ...def
copy
(center:Point
=this
.center, radius:Double
=this
.radius) =new
Circle
(center, radius) }
So, default values are
provided for all the arguments to the method (only two in this case).
When using the copy
method, the user specifies by
name only the fields that are changing. The values for the rest of the
fields are used without having to reference them explicitly.
Did you notice that the new
Shapes
code in Case Classes did not
put the case
keyword on the abstract
Shape
class? This is allowed by the compiler, but
there are reasons for not having one case class inherit another. First,
it can complicate field initialization. Suppose we make
Shape
a case class. Suppose we want to add a string
field to all shapes representing an id
that the user
wants to set. It makes sense to define this field in
Shape
. Let’s make these two changes to
Shape
:
abstract
case
class
Shape
(id:String
) {def
draw
():Unit
}
Now the derived shapes
need to pass the id
to the Shape
constructor. For example, Circle
would become the
following:
case
class
Circle
(id:String
, center:Point
, radius:Double
)extends
Shape
(id){def
draw
():Unit
}
However, if you compile this code, you’ll get errors like the following:
... error: error overriding value id in class Shape of type String; value id needs `override' modifier case class Circle(id: String, center: Point, radius: Double) extends Shape(id){ ^
Remember that both
definitions of id
, the one in
Shape
and the one in Circle
, are
considered val
field definitions! The error message
tells us the answer; use the override
keyword, as we discussed in
Overriding Members of Classes and Traits. So, the complete set of required
modifications are as follows:
// code-examples/AdvOOP/shapes/shapes-case-id.scala
package
shapesid {case
class
Point
(x:Double
, y:Double
)abstract
case
class
Shape
(id:String
) {def
draw
():Unit
}case
class
Circle
(override
val
id:String
, center:Point
, radius:Double
)extends
Shape
(id) {def
draw
() = println("Circle.draw: "
+this
) }case
class
Rectangle
(override
val
id:String
, lowerLeft:Point
, height:Double
, width:Double
)extends
Shape
(id) {def
draw
() = println("Rectangle.draw: "
+this
) }case
class
Triangle
(override
val
id:String
, point1:Point
, point2:Point
, point3:Point
)extends
Shape
(id) {def
draw
() = println("Triangle.draw: "
+this
) } }
Note that we also have to
add the val
keywords. This works, but it is somewhat
ugly.
A more ominous problem
involves the generated equals
methods. Under
inheritance, the equals
methods don’t obey all the
standard rules for robust object equality. We’ll discuss those rules in
Equality of Objects. For now, consider the following
example:
// code-examples/AdvOOP/shapes/shapes-case-equals-ambiguity-script.scala
import
shapesid._case
class
FancyCircle
(name:String
,override
val
id:String
,override
val
center:Point
,override
val
radius:Double
)extends
Circle
(id, center, radius) {override
def
draw
() = println("FancyCircle.draw: "
+this
) }val
fc =FancyCircle
("me"
,"circle"
,Point
(0.0
,0.0
),10.0
)val
c =Circle
("circle"
,Point
(0.0
,0.0
),10.0
) format("FancyCircle == Circle? %b
"
, (fc == c)) format("Circle == FancyCircle? %b
"
, (c == fc))
If you run this script, you get the following output:
FancyCircle == Circle? false Circle == FancyCircle? true
So,
Circle.equals
evaluates to true
when given a FancyCircle
with the same values for the
Circle
fields. The reverse case isn’t true. While you
might argue that, as far as Circle
is concerned, they
really are equal, most people would argue that this
is a risky, “relaxed” interpretation of equality. It’s true that a
future version of Scala could generate equals
methods
for case
classes that do exact type-equality
checking.
So, the conveniences provided by case classes sometimes lead to problems. It is best to avoid inheritance of one case class by another. Note that it’s fine for a case class to inherit from a non-case class or trait. It’s also fine for a non-case class or trait to inherit from a case class.
Because of these issues, it is possible that case class inheritance will be deprecated and removed in future versions of Scala.
Implementing a reliable
equality test for instances is difficult to do correctly.
Effective Java ([Bloch2008]) and the Scaladoc page for
AnyRef.equals
describe the requirements for a good
equality test. A very good description of the techniques for writing
correct equals
and hashCode
methods
can be found in [Odersky2009], which uses Java syntax,
but is adapted from Chapter 28 of Programming in
Scala ([Odersky2008]). Consult these references
when you need to implement your own equals
and
hashCode
methods. Recall that these methods are created
automatically for case
classes.
Here we focus on the
different equality methods available in Scala and their meanings. There
are some slight inconsistencies between the Scala specification (see [ScalaSpec2009]) and the Scaladoc pages
for the equality-related methods for Any
and AnyRef
, but the general behavior is
clear.
Some of the equality methods have the same names as equality methods in other languages, but the semantics are sometimes different!
The
equals
method tests for value
equality. That is, obj1 equals obj2
is true if both
obj1
and obj2
have the same value.
They do not need to refer to the same instance.
Hence,
equals
behaves like the equals
method in Java and the eql?
method in Ruby.
While ==
is
an operator in many languages, it is a method in Scala, defined as
final
in Any
. It tests for
value equality, like equals
.
That is, obj1 == obj2
is true if both
obj1
and obj2
have the same value.
In fact, ==
delegates to equals
.
Here is part of the Scaladoc entry for
Any.==
:
o == arg0 is the same as o.equals(arg0).
Here is the corresponding
part of the Scaladoc entry for AnyRef.==
:
o == arg0 is the same as if (o eq null) arg0 eq null else o.equals(arg0).
As you would expect,
!=
is the negation, i.e., it is equivalent to
!(obj1 == obj2)
.
Since ==
and
!=
are declared final
in
Any
, you can’t override them, but you don’t need to,
since they delegate to equals
.
The eq
method tests for reference equality. That is,
obj1 eq obj2
is true if both obj1
and obj2
point to the same location in memory. These
methods are only defined for AnyRef
.
Hence, eq
behaves like the ==
operator in Java, C++, and C#,
but not ==
in Ruby.
The ne
method is the negation of eq
, i.e., it is equivalent
to !(obj1 eq obj2)
.
Comparing the contents of
two Array
s doesn’t have an obvious result in
Scala:
scala> Array(1, 2) == Array(1, 2) res0: Boolean = false
That’s a surprise!
Thankfully, there’s a simple solution in the form of the sameElements
method:
scala> Array(1, 2).sameElements(Array(1, 2)) res1: Boolean = true
Much better. Remember to
use sameElements
when you want to test if two
Array
s contain the same elements.
While this may seem like an inconsistency, encouraging an explicit test of the equality of two mutable data structures is a conservative approach on the part of the language designers. In the long run, it should save you from unexpected results in your conditionals.
We explored the fine points of overriding members in derived classes. We learned about object equality, case classes, and companion classes and objects.
In the next
chapter, we’ll learn about the Scala type hierarchy—in particular, the
Predef
object that includes many
useful definitions. We’ll also learn about Scala’s alternative to Java’s
static
class members and the
linearization rules for method lookup.