Scala is an object-oriented language like Java, Python, Ruby, Smalltalk, and others. If you’re coming from the Java world, you’ll notice some notable improvements over the limitations of Java’s object model.
We assume you have some prior experience with object-oriented programming (OOP), so we will not discuss the basic principles here, although some common terms and concepts are discussed in the Glossary. See [Meyer1997] for a detailed introduction to OOP; see [Martin2003] for a recent treatment of OOP principles in the context of “agile software development”; see [GOF1995] to learn about design patterns; and see [WirfsBrock2003] for a discussion of object-oriented design concepts.
Let’s review the terminology of OOP in Scala.
We saw previously that Scala has the concept of a declared
object
, which we’ll
dig into in Classes and Objects: Where Are the Statics?. We’ll use the
term instance to refer to a class instance
generically, meaning either an object
or an instance
of a class
, to avoid the potential for confusion
between these two concepts.
Classes are declared with
the keyword class
. We will see later that additional
keywords can also be used, like final
to prevent
creation of derived classes and
abstract
to indicate that the class can’t be
instantiated, usually because it contains or inherits member declarations
without providing concrete definitions for them.
An instance can refer to
itself using the this
keyword, just as in Java and
similar languages.
Following Scala’s
convention, we use the term method for a function
that is tied to an instance. Some other object-oriented languages use the
term “member function.” Method definitions start with the
def
keyword.
Like Java, but unlike Ruby and Python, Scala allows overloaded methods. Two or more methods can have the same name as long as their full signatures are unique. The signature includes the type name, the list of parameters with types, and the method’s return value.
There is an exception to
this rule due to type erasure, which is a feature of
the JVM only, but is used by Scala on both the JVM and .NET platforms, to
minimize incompatibilities. Suppose two methods are identical except that
one takes a parameter of type List[String]
while the
other takes a parameter of type List[Int]
, as
follows:
// code-examples/BasicOOP/type-erasure-wont-compile.scala
// WON'T COMPILE
object
Foo
{def
bar
(list:List[String]
) = list.toStringdef
bar
(list:List[Int]
) = list.size.toString }
You’ll get a compilation error on the second method because the two methods will have an identical signature after type erasure.
The scala
interpreter will let you type in both
methods. It simply drops the first version. However, if you try to load
the previous example using the :load file
command,
you’ll get the same error scalac
raises.
We’ll discuss type erasure in more detail in Chapter 12.
Also by convention, we use the term field for a variable that is tied to an instance. The term attribute is often used in other languages (like Ruby). Note that the state of an instance is the union of all the values currently represented by the instance’s fields.
As we discussed in Variable Declarations, read-only (“value”)
fields are declared using the val
keyword, and
read-write fields are declared using the var
keyword.
Scala also allows types to be declared in classes, as we saw in Abstract Types And Parameterized Types.
We use the term member to refer to a field, method, or type in a generic way. Note that field and method members (but not type members) share the same namespace, unlike Java. We’ll discuss this more in When Accessor Methods and Fields Are Indistinguishable: The Uniform Access Principle.
Finally, new instances of
reference types are created from a class using the
new
keyword, as in languages like Java and C#. Note
that you can drop the parentheses when using a
default constructor (i.e., one that takes no
arguments). In some cases, literal values can be used instead, e.g.,
val name = "Programming Scala"
is equivalent to
val name = new String("Programming Scala")
.
Instances of value
types (Int
, Double
,
etc.), which correspond to the primitives in languages like Java, are
always created using literal values, e.g., 1
,
3.14
. In fact, there are no public constructors for
these types, so an expression like val i = new Int(1)
won’t compile.
We’ll discuss the difference between reference and value types in The Scala Type Hierarchy.
Scala supports single
inheritance, not multiple inheritance. A child (or derived) class can have
one and only one parent (or base) class. The sole exception is the root of
the Scala class hierarchy, Any
, which has no
parent.
We’ve seen several examples of parent and child classes. Here are snippets of one of the first we saw, in Abstract Types And Parameterized Types:
// code-examples/TypeLessDoMore/abstract-types-script.scala
import
java.io._abstract
class
BulkReader
{// ...
}class
StringBulkReader
(val
source:String
)extends
BulkReader
{// ...
}class
FileBulkReader
(val
source:File
)extends
BulkReader
{// ...
}
As in Java, the keyword extends
indicates the
parent class, in this case BulkReader
. In Scala,
extends
is also used when a class inherits a trait as
its parent (even when it mixes in other traits using the
with
keyword). Also, extends
is used
when one trait is the child of another trait or class. Yes, traits can
inherit classes.
If you don’t
extend
a parent class, the default parent is
AnyRef
, a direct child class of Any
.
(We discuss the difference between Any
and
AnyRef
when we discuss the Scala type hierarchy in
The Scala Type Hierarchy.)
Scala distinguishes between
a primary constructor and zero or more
auxiliary constructors. In Scala, the primary
constructor is the entire body of the class. Any parameters that the
constructor requires are listed after the class name. We’ve seen many
examples of this already, as in the ButtonWithCallbacks
example we used in Chapter 4:
// code-examples/Traits/ui/button-callbacks.scala
package
uiclass
ButtonWithCallbacks
(val
label:String
,val
clickedCallbacks:List[() => Unit]
)extends
Widget
{ require(clickedCallbacks !=null
,"Callback list can't be null!"
)def
this
(label:String
, clickedCallback: ()=>
Unit
) =this
(label,List
(clickedCallback))def
this
(label:String
) = {this
(label,Nil
) println("Warning: button has no click callbacks!"
) }def
click
() = {// ... logic to give the appearance of clicking a physical button ...
clickedCallbacks.foreach(f=>
f()) } }
The
ButtonWithCallbacks
class represents a button on a
graphical user interface. It has a label and a list of callback functions
that are invoked if the button is clicked. Each callback function takes no
arguments and returns Unit
. The
click
method iterates through the list of callbacks and
invokes each one.
ButtonWithCallbacks
defines three constructors. The primary constructor, which is the body of
the entire class, has a parameter list that takes a label string and a
list of callback functions. Because each parameter is declared as a
val
, the compiler generates a private field
corresponding to each parameter (a different internal name is used), along
with a public reader method that has the same name as the parameter.
“Private” and “public” have the same meaning here as in most
object-oriented languages. We’ll discuss the various visibility rules and
the keywords that control them in Visibility Rules.
If a parameter has the
var
keyword, a public writer method is also generated
with the parameter’s name as a prefix, followed by _=
.
For example, if label
were declared as a
var
, the writer method would be named
label_=
and it would take a single argument of type
String
.
There are times when you don’t want the
accessor methods to be generated automatically. In other words, you want
the field to be private. Add the
private
keyword before the val
or
var
keyword, and the accessor methods won’t be
generated. (See Visibility Rules for more
details.)
For you Java programmers, Scala doesn’t follow the JavaBeans [JavaBeansSpec]
convention that field reader and writer methods begin with
get
and set
, respectively,
followed by the field name with the first character capitalized. We’ll
see why when we discuss the Uniform Access
Principle in When Accessor Methods and Fields Are Indistinguishable: The
Uniform Access Principle.
However, you can get JavaBeans-style getters and
setters when you need them using the
scala.reflect.BeanProperty
annotation, as we’ll
discuss in JavaBean Properties.
When an instance of the class is created, each field corresponding to a parameter in the parameter list will be initialized with the parameter automatically. No constructor logic is required to initialize these fields, in contrast to most other object-oriented languages.
The first statement in the
ButtonWithCallbacks
class (i.e., the constructor) body
is a test to ensure that a non-null
list has been
passed to the constructor. (It does allow an empty Nil
list, however.) It uses the convenient require
function
that is imported automatically into the current scope (as we’ll discuss in
The Predef Object). If the list is null,
require
will throw an exception. The
require
function and its companion
assume
are very useful for Design by
Contract programming, as discussed in Better Design with Design By Contract.
Here is part of a full
specification for ButtonWithCallbacks
that demonstrates
the require
statement in use:
// code-examples/Traits/ui/button-callbacks-spec.scala
package
uiimport
org.specs._object
ButtonWithCallbacksSpec
extends
Specification
{"A ButtonWithCallbacks"
should {// ...
"not be constructable with a null callback list"
in {val
nullList:List[() => Unit]
=null
val
errorMessage ="requirement failed: Callback list can't be null!"
(new
ButtonWithCallbacks
("button1"
, nullList)) must throwA(new
IllegalArgumentException
(errorMessage)) } } }
Scala even makes it difficult to pass
null
as the second parameter to the constructor; it
won’t type check when you compile it. However, you can assign
null
to a value, as shown. If we didn’t have the
must throwA(...)
clause, we would see the following
exception thrown:
java.lang.IllegalArgumentException: requirement failed: Callback list can't be null! at scala.Predef$.require(Predef.scala:112) at ui.ButtonWithCallbacks.<init>(button-callbacks.scala:7) ...
ButtonWithCallbacks
defines two auxiliary constructors for the user’s convenience. The first
auxiliary constructor accepts a label and a single callback. It calls the
primary constructor, passing the label and a new List
to wrap the single callback.
The second auxiliary
constructor accepts just a label. It calls the primary constructor with
Nil
(which represents an empty List
object). The constructor then prints a warning message that there are no
callbacks, since lists are immutable and there is no way to replace the
callback list val
with a new one.
To avoid infinite recursion, Scala requires each auxiliary constructor to invoke another constructor defined before it (see [ScalaSpec2009]). The constructor invoked may be either another auxiliary constructor or the primary constructor, and it must be the first statement in the auxiliary constructor’s body. Additional processing can occur after this call, such as the warning message printed in our example.
Because all auxiliary constructors eventually invoke the primary constructor, logic checks and other initializations done in the body will be performed consistently for all instances created.
There are a few advantages of Scala’s constraints on constructors:
Because auxiliary constructors invoke the primary constructor, potential duplication of construction logic is largely eliminated.
As shown in the examples, when one or more of the primary
constructor parameters is declared as a val
or a
var
, Scala automatically generates a field, the
appropriate accessor methods (unless they are declared
private
), and the initialization logic for when
instances are created.
There is also at least one disadvantage of Scala’s constraints on constructors:
The primary constructor
in a derived class must invoke one of the parent class constructors,
either the primary constructor or an auxiliary constructor. In the
following example, a class derived from
ButtonWithCallbacks
, called
RadioButtonWithCallbacks
, invokes the primary
ButtonWithCallbacks
constructor. “Radio” buttons can
be either on or off:
// code-examples/BasicOOP/ui/radio-button-callbacks.scala
package
ui/**
* Button with two states, on or off, like an old-style,
* channel-selection button on a radio.
*/
class
RadioButtonWithCallbacks
(var
on:Boolean
, label:String
, clickedCallbacks:List[() => Unit]
)extends
ButtonWithCallbacks
(label, clickedCallbacks) {def
this
(on:Boolean
, label:String
, clickedCallback: ()=>
Unit
) =this
(on, label,List
(clickedCallback))def
this
(on:Boolean
, label:String
) =this
(on, label,Nil
) }
The primary constructor
for RadioButtonWithCallbacks
takes three parameters:
an on
state (true
or
false
), a label, and a list of callbacks. It passes
the label and list of callbacks to its parent class,
ButtonWithCallbacks
. The on
parameter is declared as a var
, so it is mutable.
on
is also the one constructor parameter unique to a
radio button, so it is kept as an attribute of
RadioButtonWithCallbacks
.
For consistency with its
parent class, RadioButtonWithCallbacks
also declares
two auxiliary constructors. Note that they must invoke a preceding
constructor in RadioButtonWithCallbacks
, as before.
They can’t invoke a ButtonWithCallbacks
constructor
directly. Declaring all these constructors in each class could get
tedious after a while, but we explored techniques in Chapter 4 that can eliminate repetition.
Scala lets you nest class
declarations, like many object-oriented languages. Suppose we want all
Widgets
to have a map of properties. These properties
could be size, color, whether or not the widget is visible, etc. We might
use a simple map
to hold the properties, but let’s assume that we also
want to control access to the properties, and to perform other operations
when they change.
Here is one way we might
expand our original Widget
example from Traits As Mixins to add this feature:
// code-examples/BasicOOP/ui/widget.scala
package
uiabstract
class
Widget
{class
Properties
{import
scala.collection.immutable.HashMapprivate
var
values:Map[String, Any]
=new
HashMap
def
size
= values.sizedef
get
(key:String
) = values.get(key)def
update
(key:String
, value:Any
) = {// Do some preprocessing, e.g., filtering.
values = values.update(key, value)// Do some postprocessing.
} }val
properties =new
Properties
}
We added a
Properties
class that has a private, mutable reference
to an immutable HashMap
. We also added three public
methods that retrieve the size (i.e., the number of properties defined),
retrieve a single element in the map, and update the map with a new
element, respectively. We might need to do additional work in the
update
method, and we’ve indicated as much with
comments.
You can see from the previous example that Scala allows classes to be declared inside one another, or “nested.” A nested class make sense when you have enough related functionality to lump together in a class, but the functionality is only ever going to be used by its “outer” class.
So far, we’ve covered how to declare classes, how to instantiate them, and some of the basics of inheritance. In the next section, we’ll discuss visibility rules within classes and objects.
For convenience, we’ll use the word “type” in this section to
refer to classes and traits generically, as opposed to referring to
member type
declarations. We’ll include those when we
use the term “member” generically,
unless otherwise indicated.
Most object-oriented languages have constructs to constrain the visibility (or scope) of type and type-member declarations. These constructs support the object-oriented form of encapsulation, where only the essential public abstraction of a class or trait is exposed and implementation information is hidden from view.
You’ll want to use public visibility for anything that users of your classes and objects should see and use. Keep in mind that the set of publicly visible members form the abstraction exposed by the type, along with the type’s name itself.
The conventional wisdom in object-oriented design is that fields should be private or protected. If access is required, it should happen through methods, but not everything should be accessible by default. The virtue of the Uniform Access Principle (see When Accessor Methods and Fields Are Indistinguishable: The Uniform Access Principle) is that we can give the user the semantics of public field access via either a method or direct access to a field, whichever is appropriate for the task.
The art of good object-oriented design includes defining minimal, clear, and cohesive public abstractions.
There are two kinds of “users” of a type: derived types, and code that works with instances of the type. Derived types usually need more access to the members of their parent types than users of instances do.
Scala’s visibility rules
are similar to Java’s, but tend to be both more consistently applied and
more flexible. For example, in Java, if an inner class has a
private
member, the enclosing class can see it. In
Scala, the enclosing class can’t see a private
member,
but Scala provides another way to declare it visible to the enclosing
class.
As in Java and C#, the
keywords that modify visibility, such as private
and
protected
, appear at the beginning
of declarations. You’ll find them before the class
or
trait
keywords for types, before the
val
or var
for fields, and before
the def
for methods.
You can also use an access modifier keyword on the primary
constructor of a class. Put it after the type name and type parameters,
if any, and before the argument list, as in this example: class
Restricted[+A] private (name: String) {...}
Table 5-1 summarizes the visibility scopes.
Name | Keyword | Description |
public | none | Public members and types are visible everywhere, across all boundaries. |
protected |
| Protected members are visible to the defining type, to derived types, and to nested types. Protected types are visible only within the same package and subpackages. |
private |
| Private members are visible only within the defining type and nested types. Private types are visible only within the same package. |
scoped protected |
| Visibility is limited to |
scoped private |
| Synonymous with scoped protected visibility, except under inheritance (discussed below). |
Let’s explore these visibility options in more detail. To keep things simple, we’ll use fields for member examples. Method and type declarations behave the same way.
Unfortunately, you can’t apply any of the visibility modifiers to packages. Therefore, a package is always public, even when it contains no publicly visible types.
Any declaration without a
visibility keyword is “public,” meaning it is visible everywhere. There
is no public
keyword in Scala. This is in contrast to
Java, which defaults to public visibility only within the enclosing
package (i.e., “package private”). Other object-oriented languages, like
Ruby, also default to public visibility:
// code-examples/BasicOOP/scoping/public.scala
package
scopeA {class
PublicClass1
{val
publicField =1
class
Nested
{val
nestedField =1
}val
nested =new
Nested
}class
PublicClass2
extends
PublicClass1
{val
field2 = publicField +1
val
nField2 =new
Nested
().nestedField } }package
scopeB {class
PublicClass1B
extends
scopeA.PublicClass1
class
UsingClass
(val
publicClass:scopeA.PublicClass1
) {def
method
="UsingClass:"
+" field: "
+ publicClass.publicField +" nested field: "
+ publicClass.nested.nestedField } }
You can compile this file
with scalac
. It should compile without error.
Everything is public in
these packages and classes. Note that
scopeB.UsingClass
can access
scopeA.PublicClass1
and its members, including the
instance of Nested
and its public field.
Protected visibility is
for the benefit of implementers of derived types, who need a little more
access to the details of their parent types. Any member declared with
the protected
keyword is visible
only to the defining type, including other instances of the same type
and any derived types. When applied to a type,
protected
limits visibility to the enclosing
package.
Java, in contrast, makes protected members visible throughout the enclosing package. Scala handles this case with scoped private and protected access:
// code-examples/BasicOOP/scoping/protected-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
ProtectedClass1
(protected
val
protectedField1:Int
) {protected
val
protectedField2 =1
def
equalFields
(other:ProtectedClass1
) = (protectedField1 == other.protectedField1) && (protectedField1 == other.protectedField1) && (nested == other.nested)class
Nested
{protected
val
nestedField =1
}protected
val
nested =new
Nested
}class
ProtectedClass2
extends
ProtectedClass1
(1
) {val
field1 = protectedField1val
field2 = protectedField2val
nField =new
Nested
().nestedField// ERROR
}class
ProtectedClass3
{val
protectedClass1 =new
ProtectedClass1
(1
)val
protectedField1 = protectedClass1.protectedField1// ERROR
val
protectedField2 = protectedClass1.protectedField2// ERROR
val
protectedNField = protectedClass1.nested.nestedField// ERROR
}protected
class
ProtectedClass4
class
ProtectedClass5
extends
ProtectedClass4
protected
class
ProtectedClass6
extends
ProtectedClass4
}package
scopeB {class
ProtectedClass4B
extends
scopeA.ProtectedClass4
// ERROR
}
When you compile this
file with scalac
, you get the following output. (The
file names before the N:
line numbers have been
removed from the output to better fit the space.)
16: error: value nestedField cannot be accessed in ProtectedClass2.this.Nested val nField = new Nested().nestedField ^ 20: error: value protectedField1 cannot be accessed in scopeA.ProtectedClass1 val protectedField1 = protectedClass1.protectedField1 ^ 21: error: value protectedField2 cannot be accessed in scopeA.ProtectedClass1 val protectedField2 = protectedClass1.protectedField2 ^ 22: error: value nested cannot be accessed in scopeA.ProtectedClass1 val protectedNField = protectedClass1.nested.nestedField ^ 32: error: class ProtectedClass4 cannot be accessed in package scopeA class ProtectedClass4B extends scopeA.ProtectedClass4 ^ 5 errors found
The // ERROR
comments in the listing mark the lines that fail to parse.
ProtectedClass2
can access protected members of ProtectedClass1
,
since it derives from it. However, it can’t access the protected
nestedField
in
protectedClass1.nested
. Also,
ProtectedClass3
can’t access protected members of the
ProtectedClass1
instance it uses.
Finally, because
ProtectedClass4
is declared
protected
, it is not visible in the
scopeB
package.
Private visibility
completely hides implementation details, even from the implementers of
derived classes. Any member declared with the private
keyword is visible only to the defining type, including other instances
of the same type. When applied to a type, private
limits visibility to the enclosing package:
// code-examples/BasicOOP/scoping/private-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
PrivateClass1
(private
val
privateField1:Int
) {private
val
privateField2 =1
def
equalFields
(other:PrivateClass1
) = (privateField1 == other.privateField1) && (privateField2 == other.privateField2) && (nested == other.nested)class
Nested
{private
val
nestedField =1
}private
val
nested =new
Nested
}class
PrivateClass2
extends
PrivateClass1
(1
) {val
field1 = privateField1// ERROR
val
field2 = privateField2// ERROR
val
nField =new
Nested
().nestedField// ERROR
}class
PrivateClass3
{val
privateClass1 =new
PrivateClass1
(1
)val
privateField1 = privateClass1.privateField1// ERROR
val
privateField2 = privateClass1.privateField2// ERROR
val
privateNField = privateClass1.nested.nestedField// ERROR
}private
class
PrivateClass4
class
PrivateClass5
extends
PrivateClass4
// ERROR
protected
class
PrivateClass6
extends
PrivateClass4
// ERROR
private
class
PrivateClass7
extends
PrivateClass4
}package
scopeB {class
PrivateClass4B
extends
scopeA.PrivateClass4
// ERROR
}
Compiling this file yields the following output:
14: error: not found: value privateField1 val field1 = privateField1 ^ 15: error: not found: value privateField2 val field2 = privateField2 ^ 16: error: value nestedField cannot be accessed in PrivateClass2.this.Nested val nField = new Nested().nestedField ^ 20: error: value privateField1 cannot be accessed in scopeA.PrivateClass1 val privateField1 = privateClass1.privateField1 ^ 21: error: value privateField2 cannot be accessed in scopeA.PrivateClass1 val privateField2 = privateClass1.privateField2 ^ 22: error: value nested cannot be accessed in scopeA.PrivateClass1 val privateNField = privateClass1.nested.nestedField ^ 27: error: private class PrivateClass4 escapes its defining scope as part of type scopeA.PrivateClass4 class PrivateClass5 extends PrivateClass4 ^ 28: error: private class PrivateClass4 escapes its defining scope as part of type scopeA.PrivateClass4 protected class PrivateClass6 extends PrivateClass4 ^ 33: error: class PrivateClass4 cannot be accessed in package scopeA class PrivateClass4B extends scopeA.PrivateClass4 ^ 9 errors found
Now,
PrivateClass2
can’t access private members of its
parent class PrivateClass1
. They are completely
invisible to the subclass, as indicated by the error messages. Nor can
it access a private field in a Nested
class.
Just as for the case of
protected
access, PrivateClass3
can’t access private members of the PrivateClass1
instance it is using. Note, however, that the
equalFields
method can access private members of the
other
instance.
The declarations of
PrivateClass5
and PrivateClass6
fail because, if allowed, they would enable
PrivateClass4
to “escape its defining scope.”
However, the declaration of PrivateClass7
succeeds
because it is also declared to be private. Curiously, our previous
example was able to declare a public class that subclassed a protected
class without a similar error.
Finally, just as for
protected
type declarations, the
private
types can’t be subclassed outside the same
package.
Scala allows you to
fine-tune the scope of visibility with the scoped
private
and protected
visibility declarations. Note
that using private
or protected
in
a scoped declaration is interchangeable, as they behave identically,
except under inheritance when applied to members.
While either choice behaves the same in most scenarios, it is
more common to see private[X]
rather than
protected[X]
used in code. In the core libraries
included with Scala, the ratio is roughly five to one.
Let’s begin by considering the only differences in behavior between scoped private and scoped protected—how they behave under inheritance when members have these scopes:
// code-examples/BasicOOP/scoping/scope-inheritance-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
Class1
{private
[scopeA]val
scopeA_privateField =1
protected
[scopeA]val
scopeA_protectedField =2
private
[Class1
]val
class1_privateField =3
protected
[Class1
]val
class1_protectedField =4
private
[this
]val
this_privateField =5
protected
[this
]val
this_protectedField =6
}class
Class2
extends
Class1
{val
field1 = scopeA_privateFieldval
field2 = scopeA_protectedFieldval
field3 = class1_privateField// ERROR
val
field4 = class1_protectedFieldval
field5 = this_privateField// ERROR
val
field6 = this_protectedField } }package
scopeB {class
Class2B
extends
scopeA.Class1
{val
field1 = scopeA_privateField// ERROR
val
field2 = scopeA_protectedFieldval
field3 = class1_privateField// ERROR
val
field4 = class1_protectedFieldval
field5 = this_privateField// ERROR
val
field6 = this_protectedField } }
Compiling this file yields the following output:
17: error: not found: value class1_privateField val field3 = class1_privateField // ERROR ^ 19: error: not found: value this_privateField val field5 = this_privateField // ERROR ^ 26: error: not found: value scopeA_privateField val field1 = scopeA_privateField // ERROR ^ 28: error: not found: value class1_privateField val field3 = class1_privateField // ERROR ^ 30: error: not found: value this_privateField val field5 = this_privateField // ERROR ^ 5 errors found
The first two errors,
inside Class2
, show us that a derived class inside
the same package can’t reference a member that is scoped private to the
parent class or this
, but it can reference a private
member scoped to the package (or type) that encloses both
Class1
and Class2
.
In contrast, for a derived
class outside the same package, it has no access to any of the scoped
private members of Class1
.
However, all the scoped protected members are visible in both derived classes.
We’ll use scoped private declarations for the rest of our examples and discussion, since use of scoped private is a little more common in the Scala library than scoped protected, when the previous inheritance scenarios aren’t a factor.
First, let’s start with
the most restrictive visibility, private[this]
, as it
affects type members:
// code-examples/BasicOOP/scoping/private-this-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
PrivateClass1
(private
[this
]val
privateField1:Int
) {private
[this
]val
privateField2 =1
def
equalFields
(other:PrivateClass1
) = (privateField1 == other.privateField1) &&// ERROR
(privateField2 == other.privateField2) && (nested == other.nested)class
Nested
{private
[this
]val
nestedField =1
}private
[this
]val
nested =new
Nested
}class
PrivateClass2
extends
PrivateClass1
(1
) {val
field1 = privateField1// ERROR
val
field2 = privateField2// ERROR
val
nField =new
Nested
().nestedField// ERROR
}class
PrivateClass3
{val
privateClass1 =new
PrivateClass1
(1
)val
privateField1 = privateClass1.privateField1// ERROR
val
privateField2 = privateClass1.privateField2// ERROR
val
privateNField = privateClass1.nested.nestedField// ERROR
} }
Compiling this file yields the following output:
5: error: value privateField1 is not a member of scopeA.PrivateClass1 (privateField1 == other.privateField1) && ^ 14: error: not found: value privateField1 val field1 = privateField1 ^ 15: error: not found: value privateField2 val field2 = privateField2 ^ 16: error: value nestedField is not a member of PrivateClass2.this.Nested val nField = new Nested().nestedField ^ 20: error: value privateField1 is not a member of scopeA.PrivateClass1 val privateField1 = privateClass1.privateField1 ^ 21: error: value privateField2 is not a member of scopeA.PrivateClass1 val privateField2 = privateClass1.privateField2 ^ 22: error: value nested is not a member of scopeA.PrivateClass1 val privateNField = privateClass1.nested.nestedField ^ 7 errors found
Lines 6–8 also won’t parse. Since they are part of the expression that started on line 5, the compiler stopped after the first error.
The
private[this]
members are only visible to the same
instance. An instance of the same class can’t see
private[this]
members of another instance, so the
equalFields
method won’t parse.
Otherwise, the visibility
of class members is the same as private
without a
scope specifier.
When declaring a type with
private[this]
, use of this
effectively binds to the enclosing package, as shown here:
// code-examples/BasicOOP/scoping/private-this-pkg-wont-compile.scala
// WON'T COMPILE
package
scopeA {private
[this
]class
PrivateClass1
package
scopeA2 {private
[this
]class
PrivateClass2
}class
PrivateClass3
extends
PrivateClass1
// ERROR
protected
class
PrivateClass4
extends
PrivateClass1
// ERROR
private
class
PrivateClass5
extends
PrivateClass1
private
[this
]class
PrivateClass6
extends
PrivateClass1
private
[this
]class
PrivateClass7
extends
scopeA2.PrivateClass2
// ERROR
}package
scopeB {class
PrivateClass1B
extends
scopeA.PrivateClass1
// ERROR
}
Compiling this file yields the following output:
8: error: private class PrivateClass1 escapes its defining scope as part of type scopeA.PrivateClass1 class PrivateClass3 extends PrivateClass1 ^ 9: error: private class PrivateClass1 escapes its defining scope as part of type scopeA.PrivateClass1 protected class PrivateClass4 extends PrivateClass1 ^ 13: error: type PrivateClass2 is not a member of package scopeA.scopeA2 private[this] class PrivateClass7 extends scopeA2.PrivateClass2 ^ 17: error: type PrivateClass1 is not a member of package scopeA class PrivateClass1B extends scopeA.PrivateClass1 ^ four errors found
In the same package,
attempting to declare a public
or
protected
subclass fails. Only
private
and private[this]
subclasses are allowed. Also, PrivateClass2
is scoped
to scopeA2
, so you can’t declare it outside
scopeA2
. Similarly, an attempt to declare a class in
unrelated scopeB
using
PrivateClass1
also fails.
Hence, when applied to
types, private[this]
is equivalent to Java’s
package private
visibility.
Next, let’s examine
type-level visibility, private[T]
, where
T
is a type:
// code-examples/BasicOOP/scoping/private-type-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
PrivateClass1
(private
[PrivateClass1
]val
privateField1:Int
) {private
[PrivateClass1
]val
privateField2 =1
def
equalFields
(other:PrivateClass1
) = (privateField1 == other.privateField1) && (privateField2 == other.privateField2) && (nested == other.nested)class
Nested
{private
[Nested
]val
nestedField =1
}private
[PrivateClass1
]val
nested =new
Nested
val
nestedNested = nested.nestedField// ERROR
}class
PrivateClass2
extends
PrivateClass1
(1
) {val
field1 = privateField1// ERROR
val
field2 = privateField2// ERROR
val
nField =new
Nested
().nestedField// ERROR
}class
PrivateClass3
{val
privateClass1 =new
PrivateClass1
(1
)val
privateField1 = privateClass1.privateField1// ERROR
val
privateField2 = privateClass1.privateField2// ERROR
val
privateNField = privateClass1.nested.nestedField// ERROR
} }
Compiling this file yields the following output:
12: error: value nestedField cannot be accessed in PrivateClass1.this.Nested val nestedNested = nested.nestedField ^ 15: error: not found: value privateField1 val field1 = privateField1 ^ 16: error: not found: value privateField2 val field2 = privateField2 ^ 17: error: value nestedField cannot be accessed in PrivateClass2.this.Nested val nField = new Nested().nestedField ^ 21: error: value privateField1 cannot be accessed in scopeA.PrivateClass1 val privateField1 = privateClass1.privateField1 ^ 22: error: value privateField2 cannot be accessed in scopeA.PrivateClass1 val privateField2 = privateClass1.privateField2 ^ 23: error: value nested cannot be accessed in scopeA.PrivateClass1 val privateNField = privateClass1.nested.nestedField ^ 7 errors found
A
private[PrivateClass1]
member is visible to other
instances, so the equalFields
method now parses.
Hence, private[T]
is not as restrictive as
private[this]
. Note that
PrivateClass1
can’t see
Nested.nestedField
because that field is declared
private[Nested]
.
When members of T
are declared
private[T]
the behavior is equivalent to
private
. It is not equivalent to
private[this]
, which is more restrictive.
What if we change the
scope of Nested.nestedField
to be
private[PrivateClass1]
? Let’s see how
private[T]
affects nested types:
// code-examples/BasicOOP/scoping/private-type-nested-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
PrivateClass1
{class
Nested
{private
[PrivateClass1
]val
nestedField =1
}private
[PrivateClass1
]val
nested =new
Nested
val
nestedNested = nested.nestedField }class
PrivateClass2
extends
PrivateClass1
{val
nField =new
Nested
().nestedField// ERROR
}class
PrivateClass3
{val
privateClass1 =new
PrivateClass1
val
privateNField = privateClass1.nested.nestedField// ERROR
} }
Compiling this file yields the following output:
10: error: value nestedField cannot be accessed in PrivateClass2.this.Nested def nField = new Nested().nestedField ^ 14: error: value nested cannot be accessed in scopeA.PrivateClass1 val privateNField = privateClass1.nested.nestedField ^ two errors found
Now
nestedField
is visible to
PrivateClass1
, but it is still invisible outside of
PrivateClass1
. This is how private
works in Java.
Let’s examine scoping using a package name:
// code-examples/BasicOOP/scoping/private-pkg-type-wont-compile.scala
// WON'T COMPILE
package
scopeA {private
[scopeA]class
PrivateClass1
package
scopeA2 {private
[scopeA2]class
PrivateClass2
private
[scopeA]class
PrivateClass3
}class
PrivateClass4
extends
PrivateClass1
protected
class
PrivateClass5
extends
PrivateClass1
private
class
PrivateClass6
extends
PrivateClass1
private
[this
]class
PrivateClass7
extends
PrivateClass1
private
[this
]class
PrivateClass8
extends
scopeA2.PrivateClass2
// ERROR
private
[this
]class
PrivateClass9
extends
scopeA2.PrivateClass3
}package
scopeB {class
PrivateClass1B
extends
scopeA.PrivateClass1
// ERROR
}
Compiling this file yields the following output:
14: error: class PrivateClass2 cannot be accessed in package scopeA.scopeA2 private[this] class PrivateClass8 extends scopeA2.PrivateClass2 ^ 19: error: class PrivateClass1 cannot be accessed in package scopeA class PrivateClass1B extends scopeA.PrivateClass1 ^ two errors found
Note that
PrivateClass2
can’t be subclassed outside of
scopeA2
, but PrivateClass3
can be
subclassed in scopeA
, because it is declared
private[scopeA]
.
Finally, let’s look at the effect of package-level scoping of type members:
// code-examples/BasicOOP/scoping/private-pkg-wont-compile.scala
// WON'T COMPILE
package
scopeA {class
PrivateClass1
{private
[scopeA]val
privateField =1
class
Nested
{private
[scopeA]val
nestedField =1
}private
[scopeA]val
nested =new
Nested
}class
PrivateClass2
extends
PrivateClass1
{val
field = privateFieldval
nField =new
Nested
().nestedField }class
PrivateClass3
{val
privateClass1 =new
PrivateClass1
val
privateField = privateClass1.privateFieldval
privateNField = privateClass1.nested.nestedField }package
scopeA2 {class
PrivateClass4
{private
[scopeA2]val
field1 =1
private
[scopeA]val
field2 =2
} }class
PrivateClass5
{val
privateClass4 =new
scopeA2.PrivateClass4
val
field1 = privateClass4.field1// ERROR
val
field2 = privateClass4.field2 } }package
scopeB {class
PrivateClass1B
extends
scopeA.PrivateClass1
{val
field1 = privateField// ERROR
val
privateClass1 =new
scopeA.PrivateClass1
val
field2 = privateClass1.privateField// ERROR
} }
Compiling this file yields the following output:
28: error: value field1 cannot be accessed in scopeA.scopeA2.PrivateClass4 val field1 = privateClass4.field1 ^ 35: error: not found: value privateField val field1 = privateField ^ 37: error: value privateField cannot be accessed in scopeA.PrivateClass1 val field2 = privateClass1.privateField ^ three errors found
The only errors are when
we attempt to access members scoped to scopeA
from
the unrelated package scopeB
and when we attempt to
access a member from a nested package scopeA2
that is
scoped to that package.
Scala visibility
declarations are very flexible, and they behave consistently. They
provide fine-grained control over visibility at all possible scopes,
from the instance level (private[this]
) up to package-level
visibility (private[P]
, for a package
P
). For example, they make it easier to create
“components” with types exposed outside of the component’s top-level
package, while hiding implementation types and type members within the
“component’s” packages.
Finally, we have observed a potential “gotcha” with hidden members of traits.
We introduced the basics of Scala’s object model, including constructors, inheritance, nesting of classes, and rules for visibility.
In the next chapter we’ll explore Scala’s more advanced OOP features, including overriding, companion objects, case classes, and rules for equality between objects.