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.
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 fielddef increment() = //
Methods are public by defaultvalue += 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.
Note
If the name of a class consists of symbolic characters, enclose them in backticks to avoid a compiler warning:
class `` :
def now() = java.time.LocalTime.now()
...
When you use the class, no backticks are necessary: ().now()
.
You can also use the traditional syntax with braces:
class Counter {
private var value = 0
def increment() = {
value += 1
}
def current = value
}
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 methodprintln(myCounter.current) //
An accessor method
When writing a Java class, we don’t like to use public fields:
public class Person { //
This is Javapublic 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 Javaprivate 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 Javaif (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.
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.
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
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.)
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:
var foo
: Scala synthesizes a getter and a setter.
val foo
: Scala synthesizes a getter.
You define methods foo
and foo_=
.
You define a method foo
.
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.
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).
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 |
---|---|---|
| Public | To implement a property that is publicly accessible and backed by a field. |
| Public | To interoperate with JavaBeans. |
| Private getters/setters generated if needed | To confine the field to the methods of this class, just like in Java. Use |
| Implementation-dependent | To grant access to an enclosing class or package. Not commonly used. |
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 constructorthis() //
Calls primary constructorthis.name = name
def this(name: String, age: Int) = //
Another auxiliary constructorthis(name) //
Calls previous auxiliary constructorthis.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 constructorval p2 = Person("Fred") //
First auxiliary constructorval p3 = Person("Fred", 42) //
Second auxiliary constructor
You can optionally use the new
keyword to invoke a constructor, but you don’t have to.
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 Javaprivate 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...
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.
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 |
---|---|
| Object-private field, or no field if no method uses |
| Private field, private getter/setter |
| Private field, public getter/setter |
| 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.
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.
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"
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.
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")
.
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 //
OKval 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]()
...
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}"
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.