Chapter 5. Classes

Topics in This Chapter A1

In this chapter, you will learn how to implement classes in Scala. If you know classes in Java, Python, or C++, you won’t find this difficult, and you will enjoy the much more concise notation of Scala.

The key points of this chapter are:

  • An accessor method with no parameters does not need parentheses.

  • Fields in classes automatically come with getters and setters.

  • You can replace a field with a custom getter/setter without changing the client of a class—that is the “uniform access principle.”

  • Every class has a primary constructor that is “interwoven” with the class definition. Its parameters turn into the fields of the class. The primary constructor executes all statements in the body of the class.

  • Auxiliary constructors are optional. They are called this.

  • Classes can be nested. Each outer class object has its own version of the inner class.

5.1 Simple Classes and Parameterless Methods

In its simplest form, a Scala class looks very much like its equivalent in Java or C++:

class Counter :
  private var value = 0 // You must initialize the field
  def increment() = // Methods are public by default
    value += 1
  def current = value

Note the colon after the class name. For greater clarity, I always add a space before such a colon, but that is not a requirement.

Images Note

If the name of a class consists of symbolic characters, enclose them in backticks to avoid a compiler warning:

class `Images` :
  def now() = java.time.LocalTime.now()
  ...

When you use the class, no backticks are necessary: Images().now().

You can also use the traditional syntax with braces:

class Counter {
  private var value = 0
  def increment() = {
    value += 1
  }
  def current = value
}

Images Note

You can initialize a field with the special value uninitialized, defined in the scala.compiletime package. This causes initialization in the Java virtual machine with the default value (zero, false, or null). For clarity and brevity, I will always provide an explicit initial value.

In Scala, a class is not declared as public. A Scala source file can contain multiple top-level classes, and all of them have public visibility unless they are explicitly declared private.

To use this class, you construct objects and invoke methods in the usual way:

val myCounter = Counter() // Or new Counter()
myCounter.increment()
println(myCounter.current)

Note that the increment method has been defined with an empty parameter list, and current has been defined without parentheses. The methods must be called in the way they were declared.

Which form should you use? It is considered good style to declare parameterless methods that change the object state with (). Parameterless methods that do not change the object state should be declared without (). Some programmers refer to the former as mutator methods, and the latter as accessor methods.

That’s what we did in our example.

myCounter.increment() // A mutator method
println(myCounter.current) // An accessor method

5.2 Properties with Getters and Setters

When writing a Java class, we don’t like to use public fields:

public class Person { // This is Java
  public int age; // Frowned upon in Java
}

With a public field, anyone could write to fred.age, making Fred younger or older. That’s why we prefer to use getter and setter methods:

public class Person { // This is Java
  private int age;
  public int getAge() { return age; }
  public void setAge(int age) { this.age = age; }
}

A getter/setter pair such as this one is often called a property. We say that the class Person has an age property.

Why is this any better? By itself, it isn’t. Anyone can call fred.setAge(21), keeping him forever twenty-one.

But if that becomes a problem, we can guard against it:

public void setAge(int newValue) { // This is Java
  if (newValue > age) age = newValue;
  // Can’t get younger
}

Getters and setters are better than public fields because they let you start with simple get/set semantics and evolve them as needed.

Images Note

Just because getters and setters are better than public fields doesn’t mean they are always good. Often, it is plainly bad if every client can get or set bits and pieces of an object’s state. In this section, I show you how to implement properties in Scala. It is up to you to choose wisely when a gettable/settable property is an appropriate design.

Scala provides getter and setter methods for every public field. Consider this example:

class Person :
  var age = 0

Scala generates a class for the JVM with a private age field and getter and public setter methods.

In Scala, the getter and setter methods are called age and age_=. For example,

val fred = Person()
fred.age = 21 // Calls fred.age_=(21)
println(fred.age) // Calls the method fred.age()

In Scala, the getters and setters are not named getXxx and setXxx, but they fulfill the same purpose. If you need getXxx and setXxx methods for interoperability with Java, use the @BeanProperty annotation. See Chapter 15 for details.

Images Note

To see these methods with your own eyes, compile the file containing the Person class and then look at the bytecode with javap:

$ scala3-compiler Person.scala
$ javap -private Person
Compiled from "Person.scala"
public class Person {
  private int age;
  public Person();
  public int age();
  public void age_$eq(int);
}

As you can see, the compiler created methods age and age_$eq. (The = symbol is translated to $eq because the JVM does not allow an = in a method name.)

You can redefine the getter and setter methods. For example,

class Person :
  private var privateAge = 0 // Make private and rename

  def age = privateAge
  def age_=(newValue: Int) =
    if newValue > privateAge then privateAge = newValue // Can't get younger

The user of your class still accesses fred.age, but now Fred can’t get younger:

val fred = Person()
fred.age = 30
fred.age = 21
println(fred.age) // Prints 30

Images Note

Bertrand Meyer, the inventor of the influential Eiffel language, formulated the Uniform Access Principle that states: “All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.” In Scala, the caller of fred.age doesn’t know whether age is implemented through a field or a method. (Of course, in the JVM, the service is always implemented through a method, either synthesized or programmer-supplied.)

5.3 Properties with Only Getters

Sometimes you want a read-only property with a getter but no setter. If the value of the property never changes after the object has been constructed, use a val field:

class Message :
  val timeStamp = java.time.Instant.now
  ...

The Scala compiler produces a Java class with a private final field and a public getter method, but no setter.

Sometimes, however, you want a property that a client can’t set at will, but that is mutated in some other way. The Counter class from Section 5.1, “Simple Classes and Parameterless Methods,” on page 63 is a good example. Conceptually, the counter has a current property that is updated when the increment method is called, but there is no setter for the property.

You can’t implement such a property with a val—a val never changes. Instead, provide a private field and a property getter. That is what you saw in the Counter example:

class Counter :
  private var value = 0
  def increment() = value += 1
  def current = value // No () in declaration

Note that there are no () in the definition of the getter method. Therefore, you must call the method without parentheses:

val n = myCounter.current // Calling myCounter.current() is a syntax error

To summarize, you have four choices for implementing properties:

  1. var foo: Scala synthesizes a getter and a setter.

  2. val foo: Scala synthesizes a getter.

  3. You define methods foo and foo_=.

  4. You define a method foo.

Images Note

In some programming languages, you can declare write-only properties. Such a property has a setter but no getter. That is not possible in Scala.

Images Tip

When you see a public field in a Scala class, remember that it is not the same as a field in Java or C++. It is a private field together with a public getter (for a val field) or a getter and a setter (for a var field).

5.4 Private Fields

If you define a private field, Scala does not normally produce getters and setters. But there are two exceptions.

A method can access the private fields of all objects of its class. For example,

class Counter :
  private var value = 0
  def increment() = value += 1
  def isLess(other: Counter) = value < other.value
    // Can access private field of other object

Accessing other.value is legal because other is also a Counter object.

When a private field is accessed through an object other than this, private getters and setters are generated.

Moreover, there is a (rarely used) syntax for granting access rights to specific classes or packages. The private[name] qualifier states that only the methods of the given enclosing class or package can access the given field.

The compiler will generate auxiliary getter and setter methods that allow the enclosing class or package to access the field. These methods will be public because the JVM does not have a fine-grained access control system, and they will have implementation-dependent names.

Table 5–1 lists all field declarations that we discussed, and shows which methods are generated.

Table 5–1 Generated Methods for Fields

Scala field

Generated methods

When to use

val/var name

Public name
name_= (var only)

To implement a property that is publicly accessible and backed by a field.

@BeanProperty val/var name

Public name
getName()
name_= (var only)
setName(...) (var only)

To interoperate with JavaBeans.

private val/var name

Private getters/setters generated if needed

To confine the field to the methods of this class, just like in Java. Use private unless you really want a public property.

private[name] val/var name

Implementation-dependent

To grant access to an enclosing class or package. Not commonly used.

5.5 Auxiliary Constructors

A Scala class can have as many constructors as you like. However, one constructor is more important than all the others, called the primary constructor. In addition, a class may have any number of auxiliary constructors.

We discuss auxiliary constructors first because they are similar to constructors in Java, C++, or Python.

The auxiliary constructors are called this. (In Java or C++, constructors have the same name as the class—which is not so convenient if you rename the class.)

Each auxiliary constructor must start with a call to another auxiliary constructor or the primary constructor.

Here is a class with two auxiliary constructors:

class Person :
  private var name = ""
  private var age = 0

def this(name: String) = // An auxiliary constructor
  this() // Calls primary constructor
  this.name = name

def this(name: String, age: Int) = // Another auxiliary constructor
  this(name) // Calls previous auxiliary constructor
  this.age = age

We will look at the primary constructor in the next section. For now, it is sufficient to know that a class for which you don’t define a primary constructor has a primary constructor with no arguments.

You can construct objects of this class in three ways:

val p1 = Person() // Primary constructor
val p2 = Person("Fred") // First auxiliary constructor
val p3 = Person("Fred", 42) // Second auxiliary constructor

You can optionally use the new keyword to invoke a constructor, but you don’t have to.

5.6 The Primary Constructor

In Scala, every class has a primary constructor. The primary constructor is not defined with a this method. Instead, it is interwoven with the class definition.

The parameters of the primary constructor are placed immediately after the class name.

class Person(val name: String, val age: Int) :
  // Parameters of primary constructor in parentheses after class name
  ...

If a primary constructor parameter is declared with val or var, it turns into a field that is initialized with the construction parameter. In our example, name and age become fields of the Person class. A constructor call such as Person("Fred", 42) sets the name and age fields.

Half a line of Scala is the equivalent of seven lines of Java:

public class Person { // This is Java
  private String name;
  private int age;
  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
  public String name() { return this.name; }
  public int age() { return this.age; }
  ...
}

The primary constructor executes all statements in the class definition. For example, in the following class

class Person(val name: String, val age: Int) :
  println("Just constructed another person")
  def description = s"$name is $age years old"

the println statement is a part of the primary constructor. It is executed whenever an object is constructed.

This is useful when you need to configure a field during construction. For example:

class MyProg :
  private val props = java.util.Properties()
  props.load(new FileReader("myprog.properties"))
    // The statement above is a part of the primary constructor
  ...

Images Note

If there are no parameters after the class name, then the class has a primary constructor with no parameters. That constructor simply executes all statements in the body of the class.

Images Tip

You can often eliminate auxiliary constructors by using default arguments in the primary constructor. For example:

class Person(val name: String = "", val age: Int = 0) :
  ...

Primary constructor parameters can have any of the forms in Table 5–1. For example,

class Person(val name: String, private var age: Int)

declares and initializes fields

val name: String
private var age: Int

Construction parameters can also be regular method parameters, without val or var. They don’t give rise to getters. How these parameters are processed depends on their usage inside the class.

class Person(firstName: String, lastName: String, age: Int) :
  val name = firstName + " " + lastName
  def description = s"$name is $age years old"

Since the age parameter is used inside a method, it becomes an object-private field.

However, the firstName and lastName parameters are not used in any method. After the primary constructor has completed, they are no longer required. Therefore, there is no need to turn them into fields.

Table 5–2 summarizes the fields and methods that are generated for different kinds of primary constructor parameters.

Table 5–2 Fields and Methods Generated for Primary Constructor Parameters

Primary constructor parameter

Generated field/methods

name: String

Object-private field, or no field if no method uses name

private val/var name: String

Private field, private getter/setter

val/var name: String

Private field, public getter/setter

@BeanProperty val/var name: String

Private field, public Scala and JavaBeans getters/setters

If you find the primary constructor notation confusing, you don’t need to use it. Just provide one or more auxiliary constructors in the usual way, but remember to call this() if you don’t chain to another auxiliary constructor.

However, many programmers like the concise syntax. Martin Odersky suggests to think about it this way: In Scala, classes have parameters, just like methods do.

Images Note

When you think of the primary constructor’s parameters as class parameters, parameters without val or var become easier to understand. The scope of such a parameter is the entire class. Therefore, you can use the parameter in methods. If you do, it is the compiler’s job to save it in a field.

Images Tip

The Scala designers think that every keystroke is precious, so they let you combine a class with its primary constructor. When reading a Scala class, you need to disentangle the two. For example, when you see

class Person(val name: String) :
  var age = 0
  def description = s"$name is $age years old"

mentally take this definition apart into a class definition:

class Person(val name: String) :
  var age = 0
  def description = s"$name is $age years old"

and a constructor definition:

class Person(val name: String) :
  var age = 0
  def description = s"$name is $age years old"

Images Note

To make the primary constructor private, place the keyword private like this:

class Person private(val id: Int) :
  ...

A class user must then use an auxiliary constructor to construct a Person object.

5.7 Nested Classes L1

In Scala, you can nest just about anything inside anything. You can define functions inside other functions, and classes inside other classes. Here is a simple example of the latter:

class Network :
  class Member(val name: String) :
    val contacts = ArrayBuffer[Member]()

  private val members = ArrayBuffer[Member]()

  def join(name: String) =
    val m = Member(name)
    members += m
    m

Consider two networks:

val chatter = Network()
val myFace = Network()

In Scala, each instance has its own class Member, just like each instance has its own field members. That is, chatter.Member and myFace.Member are different classes. To construct a new inner object, simply use the type name: chatter.Member("Fred").

Images Note

This is different from Java, where an inner class belongs to the outer class.

In Java, the objects chatter.new Member("Fred") and myFace.new Member("Wilma") are both instances of a single class Network$Member.

In our network example, you can add a member within its own network:

val fred = chatter.join("Fred")
val wilma = chatter.join("Wilma")
fred.contacts += wilma // OK
val barney = myFace.join("Barney") // Has type myFace.Member

However, trying to add across networks is a compile-time error:

fred.contacts += barney
  // No—can’t add a myFace.Member to a buffer of chatter.Member elements

For networks of people, this behavior can be plausible. And it is certainly interesting to have types that depend on objects.

If you want to express the type “Member of any Network”, use a type projection Network#Member. For example,

class Network :
  class Member(val name: String) :
    val contacts = ArrayBuffer[Network#Member]()
  ...

The type Network#Member corresponds to an inner class in Java.

But then again, perhaps you just want to nest the classes to limit their scope, as one commonly does with static inner classes in Java or nested classes in C++. Then you can move the Member class to the Network companion object. (Companion objects are described in Chapter 6.)

object Network :
  class Member(val name: String) :
    val contacts = ArrayBuffer[Member]()

class Network :
  private val members = ArrayBuffer[Network.Member]()
  ...

Images Note

In a method of a nested class, you can access the this reference of the enclosing class as EnclosingClass.this:

class Network(val name: String) :
  class Member(val name: String) :
    val contacts = ArrayBuffer[Member]()
    def description = s"$name inside ${Network.this.name}"

Exercises

1. Improve the Counter class in Section 5.1, “Simple Classes and Parameterless Methods,” on page 63 so that it doesn’t turn negative at Int.MaxValue.

2. Write a class BankAccount with methods deposit and withdraw, and a read-only property balance.

3. Write a class Time with read-only properties hours and minutes and a method before(other: Time): Boolean that checks whether this time comes before the other. A Time object should be constructed as Time(hrs, min), where hrs is in military time format (between 0 and 23).

4. Reimplement the Time class from the preceding exercise so that the internal representation is the number of minutes since midnight (between 0 and 24 × 60 − 1). Do not change the public interface. That is, client code should be unaffected by your change.

5. In the Person class of Section 5.2, “Properties with Getters and Setters,” on page 65, provide a primary constructor that turns negative ages to 0.

6. Write a class Person with a primary constructor that accepts a string containing a first name, a space, and a last name, such as Person("Fred Smith"). Supply read-only properties firstName and lastName. Should the primary constructor parameter be a var, a val, or a plain parameter? Why?

7. Make a class Car with read-only properties for manufacturer, model name, and model year, and a read-write property for the license plate. Supply four constructors. All require the manufacturer and model name. Optionally, model year and license plate can also be specified in the constructor. If not, the model year is set to -1 and the license plate to the empty string. Which constructor are you choosing as the primary constructor? Why?

8. Reimplement the class of the preceding exercise in Java, JavaScript, Python, C#, or C++ (your choice). How much shorter is the Scala class?

9. Consider the class

class Employee(val name: String, var salary: Double) :
  def this() = this("John Q. Public", 0.0)

Rewrite it to use explicit fields and a default primary constructor. Which form do you prefer? Why?

10. Implement the equals method for the Member class that is nested inside the Network class in Section 5.7, “Nested Classes,” on page 73. For two members to be equal, they need to be in the same network.

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

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