ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 02.01.2026
Просмотров: 3504
Скачиваний: 0
Section 6.2 |
Chapter 6 · Functional Objects |
141 |
Immutable object trade-offs
Immutable objects offer several advantages over mutable objects, and one potential disadvantage. First, immutable objects are often easier to reason about than mutable ones, because they do not have complex state spaces that change over time. Second, you can pass immutable objects around quite freely, whereas you may need to make defensive copies
of mutable objects before passing them to other code. Third, there is no way for two threads concurrently accessing an immutable to corrupt its state once it has been properly constructed, because no thread can change the state of an immutable. Fourth, immutable objects make safe hash table keys. If a mutable object is mutated after it is placed into a HashSet, for example, that object may not be found the next time you look into the HashSet.
The main disadvantage of immutable objects is that they sometimes require that a large object graph be copied where otherwise an update could be done in place. In some cases this can be awkward to express and might also cause a performance bottleneck. As a result, it is not uncommon for libraries to provide mutable alternatives to immutable classes. For example, class StringBuilder is a mutable alternative to the immutable String. We’ll give you more information on designing mutable objects in Scala in Chapter 18.
Note
This initial Rational example highlights a difference between Java and Scala. In Java, classes have constructors, which can take parameters, whereas in Scala, classes can take parameters directly. The Scala notation is more concise—class parameters can be used directly in the body of the class; there’s no need to define fields and write assignments that copy constructor parameters into fields. This can yield substantial savings in boilerplate code, especially for small classes.
The Scala compiler will compile any code you place in the class body, which isn’t part of a field or a method definition, into the primary constructor. For example, you could print a debug message like this:
class Rational(n: Int, d: Int) { println("Created "+ n +"/"+ d)
}
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.3 |
Chapter 6 · Functional Objects |
142 |
Given this code, the Scala compiler would place the call to println into Rational’s primary constructor. The println call will, therefore, print its debug message whenever you create a new Rational instance:
scala> new Rational(1, 2) Created 1/2
res0: Rational = Rational@90110a
6.3Reimplementing the toString method
When we created an instance of Rational in the previous example, the interpreter printed “Rational@90110a”. The interpreter obtained this somewhat funny looking string by calling toString on the Rational object. By default, class Rational inherits the implementation of toString defined in class java.lang.Object, which just prints the class name, an @ sign, and a hexadecimal number. The result of toString is primarily intended to help programmers by providing information that can be used in debug print statements, log messages, test failure reports, and interpreter and debugger output. The result currently provided by toString is not especially helpful, because it doesn’t give any clue about the rational number’s value. A more useful implementation of toString would print out the values of the Rational’s numerator and denominator. You can override the default implementation by adding a method toString to class Rational, like this:
class Rational(n: Int, d: Int) { override def toString = n +"/"+ d
}
The override modifier in front of a method definition signals that a previous method definition is overridden; more on this in Chapter 10. Since Rational numbers will display nicely now, we removed the debug println statement we put into the body of previous version of class Rational. You can test the new behavior of Rational in the interpreter:
scala> val x = new Rational(1, 3) x: Rational = 1/3
scala> val y = new Rational(5, 7) y: Rational = 5/7
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.4 |
Chapter 6 · Functional Objects |
143 |
6.4Checking preconditions
As a next step, we will turn our attention to a problem with the current behavior of the primary constructor. As mentioned at the beginning of this chapter, rational numbers may not have a zero in the denominator. Currently, however, the primary constructor accepts a zero passed as d:
scala> new Rational(5, 0) res1: Rational = 5/0
One of the benefits of object-oriented programming is that it allows you to encapsulate data inside objects so that you can ensure the data is valid throughout its lifetime. In the case of an immutable object such as Rational, this means that you should ensure the data is valid when the object is constructed. Given that a zero denominator is an invalid state for a Rational number, you should not let a Rational be constructed if a zero is passed in the d parameter.
The best way to approach this problem is to define as a precondition of the primary constructor that d must be non-zero. A precondition is a constraint on values passed into a method or constructor, a requirement which callers must fulfill. One way to do that is to use require,1 like this:
class Rational(n: Int, d: Int) { require(d != 0)
override def toString = n +"/"+ d
}
The require method takes one boolean parameter. If the passed value is true, require will return normally. Otherwise, require will prevent the object from being constructed by throwing an IllegalArgumentException.
6.5Adding fields
Now that the primary constructor is properly enforcing its precondition, we will turn our attention to supporting addition. To do so, we’ll define a public add method on class Rational that takes another Rational as a parameter. To keep Rational immutable, the add method must not add the passed
1The require method is defined in standalone object, Predef. As mentioned in Section 4.4, Predef’s members are imported automatically into every Scala source file.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.5 |
Chapter 6 · Functional Objects |
144 |
rational number to itself. Rather, it must create and return a new Rational that holds the sum. You might think you could write add this way:
class Rational(n: Int, d: Int) { // This won’t compile require(d != 0)
override def toString = n +"/"+ d def add(that: Rational): Rational =
new Rational(n * that.d + that.n * d, d * that.d)
}
However, given this code the compiler will complain:
<console>:11: error: value d is not a member of Rational new Rational(n * that.d + that.n * d, d * that.d)
ˆ
<console>:11: error: value d is not a member of Rational new Rational(n * that.d + that.n * d, d * that.d)
ˆ
Although class parameters n and d are in scope in the code of your add method, you can only access their value on the object on which add was invoked. Thus, when you say n or d in add’s implementation, the compiler is happy to provide you with the values for these class parameters. But it won’t let you say that.n or that.d, because that does not refer to the Rational object on which add was invoked.2 To access the numerator and denominator on that, you’ll need to make them into fields. Listing 6.1 shows how you could add these fields to class Rational.3
In the version of Rational shown in Listing 6.1, we added two fields named numer and denom, and initialized them with the values of class parameters n and d.4 We also changed the implementation of toString and add so that they use the fields, not the class parameters. This version of class Rational compiles. You can test it by adding some rational numbers:
2Actually, you could add a Rational to itself, in which case that would refer to the object on which add was invoked. But because you can pass any Rational object to add, the compiler still won’t let you say that.n.
3In Section 10.6 you’ll find out about parametric fields, which provide a shorthand for writing the same code.
4Even though n and d are used in the body of the class, given they are only used inside constructors, the Scala compiler will not emit fields for them. Thus, given this code the Scala compiler will generate a class with two Int fields, one for numer and one for denom.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.6 |
Chapter 6 · Functional Objects |
145 |
class Rational(n: Int, d: Int) { require(d != 0)
val numer: Int = n val denom: Int = d
override def toString = numer +"/"+ denom def add(that: Rational): Rational =
new Rational(
numer * that.denom + that.numer * denom, denom * that.denom
)
}
Listing 6.1 · Rational with fields.
scala> val oneHalf = new Rational(1, 2) oneHalf: Rational = 1/2
scala> val twoThirds = new Rational(2, 3) twoThirds: Rational = 2/3
scala> oneHalf add twoThirds res3: Rational = 7/6
One other thing you can do now that you couldn’t do before is access the numerator and denominator values from outside the object. Simply access the public numer and denom fields, like this:
scala> val r = new Rational(1, 2) r: Rational = 1/2
scala> r.numer res4: Int = 1
scala> r.denom res5: Int = 2
6.6Self references
The keyword this refers to the object instance on which the currently executing method was invoked, or if used in a constructor, the object instance
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.7 |
Chapter 6 · Functional Objects |
146 |
being constructed. As an example, consider adding a method, lessThan, which tests whether the given Rational is smaller than a parameter:
def lessThan(that: Rational) =
this.numer * that.denom < that.numer * this.denom
Here, this.numer refers to the numerator of the object on which lessThan was invoked. You can also leave off the this prefix and write just numer; the two notations are equivalent.
As an example where you can’t do without this, consider adding a max method to class Rational that returns the greater of the given rational number and an argument:
def max(that: Rational) =
if (this.lessThan(that)) that else this
Here, the first this is redundant. You could have equally well left it off and written: lessThan(that). But the second this represents the result of the method in the case where the test returns false; were you to omit it, there would be nothing left to return!
6.7Auxiliary constructors
Sometimes you need multiple constructors in a class. In Scala, constructors other than the primary constructor are called auxiliary constructors. For example, a rational number with a denominator of 1 can be written more succinctly as simply the numerator. Instead of 51 , for example, you can just write 5. It might be nice, therefore, if instead of writing new Rational(5, 1), client programmers could simply write new Rational(5). This would require adding an auxiliary constructor to Rational that takes only one argument, the numerator, with the denominator predefined to be 1. Listing 6.2 shows what that would look like.
Auxiliary constructors in Scala start with def this(...). The body of Rational’s auxiliary constructor merely invokes the primary constructor, passing along its lone argument, n, as the numerator and 1 as the denominator. You can see the auxiliary constructor in action by typing the following into the interpreter:
scala> val y = new Rational(3) y: Rational = 3/1
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.7 |
Chapter 6 · Functional Objects |
147 |
class Rational(n: Int, d: Int) {
require(d != 0)
val numer: Int = n val denom: Int = d
def this(n: Int) = this(n, 1) // auxiliary constructor
override def toString = numer +"/"+ denom
def add(that: Rational): Rational = new Rational(
numer * that.denom + that.numer * denom, denom * that.denom
)
}
Listing 6.2 · Rational with an auxiliary constructor.
In Scala, every auxiliary constructor must invoke another constructor of the same class as its first action. In other words, the first statement in every auxiliary constructor in every Scala class will have the form “this(. . . )”. The invoked constructor is either the primary constructor (as in the Rational example), or another auxiliary constructor that comes textually before the calling constructor. The net effect of this rule is that every constructor invocation in Scala will end up eventually calling the primary constructor of the class. The primary constructor is thus the single point of entry of a class.
Note
If you’re familiar with Java, you may wonder why Scala’s rules for constructors are a bit more restrictive than Java’s. In Java, a constructor must either invoke another constructor of the same class, or directly invoke a constructor of the superclass, as its first action. In a Scala class, only the primary constructor can invoke a superclass constructor. The increased restriction in Scala is really a design trade-off that needed to be paid in exchange for the greater conciseness and simplicity of Scala’s constructors compared to Java’s. Superclasses and the details of how constructor invocation and inheritance interact will be explained in Chapter 10.
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index
Section 6.8 |
Chapter 6 · Functional Objects |
148 |
6.8Private fields and methods
In the previous version of Rational, we simply initialized numer with n and denom with d. As a result, the numerator and denominator of a Rational can be larger than needed. For example, the fraction 6642 could be normalized to an equivalent reduced form, 117 , but Rational’s primary constructor doesn’t currently do this:
scala> new Rational(66, 42) res6: Rational = 66/42
To normalize in this way, you need to divide the numerator and denominator by their greatest common divisor. For example, the greatest common divisor of 66 and 42 is 6. (In other words, 6 is the largest integer that divides evenly into both 66 and 42.) Dividing both the numerator and denominator of 6642 by 6 yields its reduced form, 117 . Listing 6.3 shows one way to do this:
class Rational(n: Int, d: Int) {
require(d != 0)
private val g = gcd(n.abs, d.abs) val numer = n / g
val denom = d / g
def this(n: Int) = this(n, 1)
def add(that: Rational): Rational = new Rational(
numer * that.denom + that.numer * denom, denom * that.denom
)
override def toString = numer +"/"+ denom
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
}
Listing 6.3 · Rational with a private field and method.
In this version of Rational, we added a private field, g, and modified the initializers for numer and denom. (An initializer is the code that initializes
Cover · Overview · Contents · Discuss · Suggest · Glossary · Index