Chapter 5. Basic Object-Oriented Programming in Scala

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.

Class and Object Basics

Let’s review the terminology of OOP in Scala.

Note

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.toString
  def 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.

Warning

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.

Parent Classes

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.)

Constructors in Scala

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 ui

class 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.)

Note

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 ui
import 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.

Note

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:

Elimination of duplication

Because auxiliary constructors invoke the primary constructor, potential duplication of construction logic is largely eliminated.

Code size reduction

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:

Less flexibility

Sometimes it’s just not convenient to have one constructor body that all constructors are forced to use. However, we find these circumstances to be rare. In such cases, it may simply be that the class has too many responsibilities and it should be refactored into smaller classes.

Calling Parent Class 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.

Note

While super is used to invoke overridden methods, as in Java, it cannot be used to invoke a super class constructor.

Nested Classes

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 ui

abstract class Widget {
  class Properties {
    import scala.collection.immutable.HashMap

    private var values: Map[String, Any] = new HashMap

    def size = values.size

    def 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.

Note

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.

Visibility Rules

Note

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.

Tip

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.

Note

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.

Table 5-1. Visibility scopes
NameKeywordDescription

public

none

Public members and types are visible everywhere, across all boundaries.

protected

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

Private members are visible only within the defining type and nested types. Private types are visible only within the same package.

scoped protected

protected[scope]

Visibility is limited to scope, which can be a package, type, or this (meaning the same instance, when applied to members, or the enclosing package, when applied to types). See the text below for details.

scoped private

private[scope]

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.

Note

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.

Public Visibility

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

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 = protectedField1
    val field2 = protectedField2
    val 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

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.

Scoped Private and Protected Visibility

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.

Tip

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_privateField
    val field2 = scopeA_protectedField
    val field3 = class1_privateField     // ERROR
    val field4 = class1_protectedField
    val field5 = this_privateField       // ERROR
    val field6 = this_protectedField
  }
}

package scopeB {
  class Class2B extends scopeA.Class1 {
    val field1 = scopeA_privateField     // ERROR
    val field2 = scopeA_protectedField
    val field3 = class1_privateField     // ERROR
    val field4 = class1_protectedField
    val 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

Note

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].

Tip

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  = privateField
    val nField = new Nested().nestedField
  }

  class PrivateClass3 {
    val privateClass1 = new PrivateClass1
    val privateField  = privateClass1.privateField
    val 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.

Tip

When a type or member is declared private[P], where P is the enclosing package, then it is equivalent to Java’s package private visibility.

Final Thoughts on Visibility

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.

Tip

Be careful when choosing the names of members of traits. If two traits have a member of the same name and the traits are used in the same instance, a name collision will occur even if both members are private.

Fortunately, the compiler catches this problem.

Recap and What’s Next

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.37.120