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.
override
keyword, the compiler will throw an error when the new base-class member is introduced.
Java has an optional @Override
annotation for methods. It helps catch errors of the first type (mispellings), 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 section called “The Scala Type Hierarchy” in Chapter 7, The Scala Object System).
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 ui3 abstract 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
.
// code-examples/AdvOOP/ui3/button.scala package ui3 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() + ")" }
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 the section called “Linearization of an Object’s Hierarchy” in Chapter 7, The Scala Object System). 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 afterwards? 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 [GOF1995].
Most OO languages allow you to override mutable fields (var
). Fewer object-oriented 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.
allow you to override
Thanks. will fix.
We’ll discuss overriding fields in traits and classes separately, as traits have some particular issues.
Recall our VetoableClicks
trait in the section called “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 that is supported in version 2.8. The -Xcheckinit
option will analyze your code and report if 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 inner class that implicitly extends the trait. This script shows what happens when inverse
is calculated
why is the new type definition of AbstractT2 an "anonymous inner class" and not just an "anonymous class"?
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 the section called “Lazy Vals” in Chapter 8, Functional Programming in Scala, 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 above, 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 ui3 import 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 ui3 import org.specs._ import observer._ import ui.ButtonCountObserver object 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 mustEqual 2 val buttonClickCountObserver = new ButtonCountObserver observableButton.addObserver(buttonClickCountObserver) for (i <- 1 to 3) observableButton.click() buttonClickCountObserver.count mustEqual 2 } } }
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, Functional Programming in Scala, 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 section called “The Problems of Shared, Synchronized State” in Chapter 9, Robust, Scalable Concurrency with Actors).
Avoid var
fields when possible (in classes as well as traits). Consider public var
fields especially risky.
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 the section called “Abstract Types And Parameterized Types” in Chapter 2, Type Less, Do More. 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 the section called “Understanding Parameterized Types” in Chapter 12, The Scala Type System. 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, The Scala Type System.
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 the section called “Traits as Mixins” in Chapter 4, Traits. 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 observer trait AbstractSubject { type Observer private var observers = List[Observer]() def addObserver(observer:Observer) = observers ::= observer def 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” where user-supplied functions. (See the section called “Introducing Traits” in Chapter 4, Traits and a revisited version in the section called “Constructors in Scala” in Chapter 5, Basic Object-Oriented Programming 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 ui import 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 to 3) observableButton.click() buttonObserver.count mustEqual 3 } } "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 to 3) observableButton.click() count mustEqual 3 } } }
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 the section called “Scalable Abstractions” in Chapter 13, Application Design.
// 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 case 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 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 * i val 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, then 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, Functional Programming in Scala. 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 is no way for Scala to guarantee that the method will 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 and the last chapter. 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 [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 some how creating an 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 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 } }
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 combinator are an alternative. They are discussed in the section called “External DSLs with Parser Combinators” in Chapter 11, Domain-Specific Languages in Scala.)
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, the section called “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 objects import 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) => w match { 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) => w match { 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)") mustEqual None } } }
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 hard-coded 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 counter examples, such as the use of apply
in Domain-Specific Languages (see Chapter 11, Domain-Specific Languages in Scala).
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 than any other method. You can overload it, you can invoke it directly, etc.
The name unapply
suggests that it does the “opposite” operation that apply
does. 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 it is used to extract the field values from instances of the corresponding companion types. For this reason, unapply
methods are called extractors.
// code-examples/AdvOOP/objects/button.scala 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) }
Perhaps it would be interesting to point out that unapply can also return a Boolean, in which case it just matches, but does not extract anything?
object isInt { def unapply(s: String): Boolean = s matches "\d+" }
"54" match { case isInt() => println("Yep.") case _ => println("Oops!") }
Good point. Thanks. Need to decide on a good place for this.
// code-examples/AdvOOP/objects/button-unapply-spec.scala package objects import org.specs._ object ButtonUnapplySpec extends Specification { "Button.unapply" should { "match a Button object" in { val b = new Button("click me") b match { case Button(label) => case _ => fail() } } "match a RadioButton object" in { val b = new RadioButton(false, "click me") b match { case Button(label) => case _ => fail() } } "not match a non-Button object" in { val tf = new TextField("hello world!") tf match { case Button(label) => fail() case _ => } } "extract the Button's label" in { val b = new Button("click me") b match { case Button(label) => label mustEqual "click me" case _ => fail() } } "extract the RadioButton's label" in { val rb = new RadioButton(false, "click me, too") rb match { 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 section called “The Predef Object” in Chapter 7, The Scala Object System, 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 objects import org.specs._ object RadioButtonUnapplySpec extends Specification { "RadioButton.unapply" should { "should match a RadioButton object" in { val b = new RadioButton(true, "click me") b match { case RadioButton(on, label) => case _ => fail() } } "not match a Button (parent class) object" in { val b = new Button("click me") b match { case RadioButton(on, label) => fail() case _ => } } "not match a non-RadioButton object" in { val tf = new TextField("hello world!") tf match { case RadioButton(on, label) => fail() case _ => } } "extract the RadioButton's on/off state and label" in { val b = new RadioButton(true, "click me") b match { case RadioButton(on, label) => { label mustEqual "click me" on mustEqual true } 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.toList def 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 the section called “Understanding Parameterized Types” in Chapter 12, The Scala Type System 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) list match { 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.
The output is the following.
x = 1, y = 2.2
We’ll have much more to say about List
and pattern matching in the section called “Lists in Functional Programming” in Chapter 8, Functional Programming in Scala.
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) [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 objects class 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.
We like putting our main methods in a Main object. Seems redundant but there's never confusion about which method to call.
Yea, I think the typical "production" main would end up in its own object. The gotcha is most likely to happen when you put a "test" main in a class for convenience, but it doesn't work!
java.lang.NoSuchMethodException: objects.Person.main([Ljava.lang.String;)
The objects/Person.class
file exists. If you decompile it with javap -classpath ... objects.Person
(see the section called “The scalap, javap, and jad Command Line Tools” in Chapter 14, Scala Tools, Libraries and IDE Support), 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 objects object 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 the section called “Matching on Case Classes” in Chapter 3, Rounding Out the Essentials, 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 the section called “A Taste of Concurrency” in Chapter 1, Zero to Sixty: Introducing Scala 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 the section called “Equality of Objects” below, 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) = shape match { 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 the section called “Matching on Sequences” in Chapter 3, Rounding Out the Essentials when we discussed matching on lists? We wrote this case
expression.
def processList(l: List[Any]): Unit = l match { 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 above this way.
def matchOn(shape: Shape) = shape match { case center Circle radius => ... case ll Rectangle (h, w) => ... case p1 Triangle (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 arg Foo () => ...
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 the section called “Method Default and Named Arguments (Scala Version 2.8)” in Chapter 2, Type Less, Do More. The generated implementation of Circle.copy
looks roughly like the following.
The code should be "circle1 copy (radius = 4.0)".
Doh! Thanks.
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 only specifies by name 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 the section called “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 the section called “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 below in the section called “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\n", (fc == c)) format("Circle == FancyCircle? %b\n", (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.
Avoid inheriting a case class from another case class.
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 [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
.
In Java, C++, and C# the ==
operator tests for reference, not value equality. In contrast, Ruby’s ==
operator tests for value equality. Whatever language you’re used to, make sure to remember that in Scala, ==
is testing for value equality.
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
behave 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 Arrays 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 Arrays 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.
No comments yet
Add a comment