This course has already ended.

The latest instance of the course can be found at: O1: 2024

Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.

Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.

Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.


Chapter 4.2: Containers — and a Program that Crashes

About This Page

Questions Answered: How do I represent a relationship between one object and multiple other objects? Are there other kinds of collections than buffers? What can I do with a buffer or a similar collection of elements? What is a good way to represent a collection whose contents never change?

Topics: Buffers as objects. The Vector collection type. Methods on buffers and vectors. Using a collection as part of a class definition. New roles for variables: containers and most-wanted holders. Run-time error messages.

What Will I Do? Read, experiment with library methods in the REPL, and program.

Rough Estimate of Workload:? Three or four hours.

Points Available: A115.

Related Modules: GoodStuff, Miscellaneous, Football2 (new).

../_images/sound_icon.png

Notes: Speakers or headphones will be useful at one point. They aren’t strictly necessary though.

../_images/person08.png

Back to GoodStuff: Using Category

In this chapter, we return to the GoodStuff application as we begin to work on an implementation for class Category. We’ll encounter several new concepts along the way.

Need a refresher?

The beginning of Chapter 3.3 provided an overview of GoodStuff’s key classes. If your memories are fuzzy, you might want to review it now.

First, let’s see what we want the class to do: that is explained in the following animation. The animation doesn’t feature any new programming concepts, and you can step through it quickly if the code feels straightforward to you. But do make sure you understand the example before you continue.

The animation features a simple app object named CategoryTest, which calls some of the methods in class Category.

Versions of Category

Just so you know: if you happen to experiment on your own with the Category class that comes with the GoodStuff module, you’ll notice that it differs slightly from the one in the above animation. That’s because the given module contains the final version that we’ll eventually come up with at the end of Chapter 4.3.

A First Draft of Class Category

Let’s sketch out an implementation for the class. Here’s a first draft as pseudocode:

class Category(val name: String, val unit: String):

  in a private instance variable experiences list all the experiences added in the category, initially none
  in a private instance variable fave store a reference to the experience with the highest rating

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences refers to a collection of experiences; add the given experience there.
    Replace the value of fave in case the newly added experience is the best one.

  def allExperiences = Return a listing of all the experiences added so far.

end Category

To turn this pseudocode into Scala, we need to solve two problems:

  1. How to keep track of multiple experience objects that are associated with a single category object?

  2. How to update the favorite only when the new experience is better than the previous favorite?

We’ll begin with the first problem. So: How do we use the variable experiences as a container that gives access to multiple other objects? What should the variable’s type be and how can we add new experiences?

Role: container

../_images/container.png

Multiple data items are available via a countainer. Each has its own “shelf”.

A container (säiliö) is a variable that provides access to multiple data items. This is one of the common roles of variables.

A single variable can store just a single value, so a container needs to store a reference to a collection. There are many types of collections; you already know one of them, the buffer. We can use this type in our Category implementation:

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  // We still need to add the variable fave here.

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience
    // We still need to update fave here.

  def allExperiences = this.experiences

end Category

For each category object, we create an auxiliary buffer. Each category object’s experiences variable contains a reference to such a buffer, and each category object is associated with a buffer of its own, initially an empty one. (Remember: this line of code is executed whenever a new category object is created.)

As explained in Chapter 1.5, when you create an empty buffer, you record the type of the buffer’s elements as a type parameter in square brackets.

Each category object uses its experiences variable as a container. It adds a new experience to the referenced buffer whenever addExperience is called. (The method uses the += operator from Chapter 1.5.)

The class’s users may call allExperiences in order to receive a collection of all the experiences that have been previously added to the category. This implementation of allExperiences simply returns a reference to the buffer used by the category object, which works but is unsatisfactory nevertheless. We’ll return to that in a bit.

You can also study this class’s behavior in the following animation.

Check your understanding

If the category class is defined as above, and we create ten category objects, how many buffers will also be created as a result?

If the category class is defined as above, and we create ten category objects, how many Experience objects will also be created as a result?

Code-reading task: cloning sheep

Study the following class and the associated test app. Consider what happens when the app runs.

import scala.collection.mutable.Buffer

// In this toy program, we use a buffer of integers to represent the virtual genes of sheep.
class Sheep(val name: String, val genes: Buffer[Int]):

  // Causes a particular sort of "mutation" in a sheep's genes.
  def mutate() =
    this.genes(0) += 1
    this.genes(1) += 5

  /* Creates a clone of the sheep. The clone is given a new name; its genes
     are identical to those of the original sheep. Later mutations of the
     original sheep don't impact on the clone, and vice versa. */
  def clone(nameOfClone: String) = Sheep(nameOfClone, this.genes)

end Sheep


object SheepTest extends App:
  val first = Sheep("Polly", Buffer(2, 5))
  val second = first.clone("Dolly")
  second.mutate()

Only one of the following claims about the clone method is correct. Which one?

More on cloning

It isn’t always as easy as you might first think to creating an initially identical copy that changes independently of the original object. What you need to do depends on the sort of object you’re cloning and the sort of cloning you’re after.

Here’s a simple approach for simple needs:

class Thingy(var number: Int, var another: Int):

  // Creates another identical but distinct object and returns a reference to it.
  // The original and the copy can be changed independently of each other.
  def copy = Thingy(this.number, this.another)

end Thingy

A few things to consider:

  • That method returns a shallow copy of the original object: it simply takes the values of the original’s instance variables and puts copies of those two values into a new object’s variables. (The cloning method in the Sheep class similarly created a shallow copy.)

  • What if the instance variables didn’t store Ints but references to compound objects? You might or might not like to create copies of those referenced objects at the same time. And what if the referenced objects contain further references to still other objects? Do you want a deep copy?

  • The object-oriented technique known as inheritance, which we’ll discuss in Week 7, further complicates the issue.

  • Immutability, on the other hand, makes things simple. If your object’s state never changes, do you even need to make a copy of it? Consider, for instance, the String, Pos, and Odds classes, whose instances are immutable.

A Flaw in Our Category Implementation

These musings on Category’s public interface aren’t mandatory reading, but you may find them thought-provoking. Moreover, they explain the reasoning behind introducing the Vector collection type below. If you’re in a hurry, you can skip this part and jump there.

A buffer in Category’s interface

Let’s take as given that we wish to prevent erroneous use of the classes that we write. To this end, we hope to prune each class’s public interface so that it doesn’t allow unneeded and error-prone operations (Chapter 3.2).

Take another look at this implementation:

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  // We still need to add the variable fave here.

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience
    // We still need to update fave here.

  def allExperiences = this.experiences

end Category

The purpose of allExperiences is to return a listing of all the experiences added in the category so far. (The GoodStuff GUI needs this method in order to display all the experiences onscreen.)

As implemented here, the category object shares the buffer reference that it uses privately with the method’s caller. Can you think of errors that might result from this? For example, what if we call someCategory.allExperiences, receive a reference to a buffer, and then add something to that buffer? Alternatively: what kinds of errors might arise if experiences was a public instance variable?

Answer: if we implement the method as above, the class’s user can obtain a reference to the buffer that is internally used by a category. The user can then modify that buffer without invoking addExperience and updating fave as appropriate:

val wineCategory = Category("Wine", "bottle")wineCategory: Category = o1.goodstuff.Category@31ab4859
wineCategory.addExperience(Experience("Il Barco 2001", "okay", 6.69, 6))wineCategory.favorite.nameres0: String = Il Barco 2001
val bufferUsedByCategory = wineCategory.allExperiencesbufferUsedByCategory: Buffer[Experience] = ArrayBuffer(o1.goodstuff.Experience@2a863ca0)
val greatExperience = Experience("Super Awesome", "woohoo", 10, 10)greatExperience: Experience = o1.goodstuff.Experience@4985e5b6
bufferUsedByCategory += greatExperiencewineCategory.favorite.nameres1: String = Il Barco 2001
wineCategory.allExperiencesres2: Buffer[Experience] = ArrayBuffer(o1.goodstuff.Experience@2a863ca0, o1.goodstuff.Experience@4985e5b6)

At the end of the example, the favorite is still the experience that was added first...

... even though the category now contains another experience whose rating is better.

The root of the problem is that a buffer’s contents can change. By giving Category’s user a reference to a mutable collection, we not only enable them to do “silly things”, we might even encourage them to do so: a return value of type Buffer suggests that it makes sense to modify the returned collection, even though we’ve actually intended that to be the sole responsibility of the Category object.

A better version of allExperiences would return a collection that contains exactly the experiences in the category right now, and that’s it. The collection would be immutable and the method’s return type would make this clear to whoever uses the class.

Improving the class in this respect is easy. Moreover, it gives us reason to get to know a new collection type, Vector, which will turn out to be generally useful and which we’ll be using frequently in O1.

A New Type of Collection: Vectors

Mathematicians and programmers have given several different but more-or-less related meanings to the word “vector”. Even within programming alone, what the word means depends on context.

In Scala, a vector (vektori) is a type of collection in the same sense as a buffer is.

Class Vector

The class Vector is readily available in all Scala programs just like String and Int are; no import required. Each vector is a collection of elements that share a type. You can create a vector like you’ve created buffers:

val vectorOfNumbers = Vector(4, 10, -100, 10, 15)vectorOfNumbers: Vector[Int] = Vector(4, 10, -100, 10, 15)
val vectorOfWords = Vector("first", "second", "third", "fourth")vectorOfWords: Vector[String] = Vector(first, second, third, fourth)

Again just like a buffer, a vector associates each element with a zero-based index that you can use to select an individual element:

vectorOfNumbers(3)res3: Int = 10
vectorOfWords(2)res4: String = third

How is a vector different from a buffer, then?

The primary difference between Buffer and Vector is that while a buffer’s contents can change after the buffer’s been created, a vector is completely immutable. You can’t add elements to a vector, remove them, or swap an element for another:

vectorOfNumbers += 999-- Not Found Error:
  |vectorOfNumbers += 999
  |^^^^^^^^^^^^^^^^^^
  |value += is not a member of Vector[Int]
vectorOfNumbers(2) = 999-- Not Found Error:
  |vectorOfNumbers(2) = 999
  |^^^^^^^^^^^^^^^
  |value update is not a member of Vector[Int]

Vectors — and buffers — as objects

You can ask a vector to report the number of elements it contains by calling its size method:

vectorOfWords.sizeres5: Int = 4
vectorOfNumbers.sizeres6: Int = 5

This example reveals something that you already may have surmised: Scala’s vectors have methods, which means that vectors are objects. So are buffers, for that matter.

A vector’s size is always the same, but a buffer object’s size method may return different values at different times:

val bufferOfWords = Buffer[String]("first")bufferOfWords: Buffer[String] = ArrayBuffer(first)
bufferOfWords.sizeres7: Int = 1
bufferOfWords += "second"bufferOfWords += "third"bufferOfWords.sizeres8: Int = 3

Converting between collection types: to, toVector, toBuffer

It’s common that we need our program to place the contents of one collection in another collection, usually so that the source and target collections have different types. This is easy, because there are methods for it. Here’s the most versatile of the bunch:

val firstVector = Vector(10, 50, 5)firstVector: Vector[Int] = Vector(10, 50, 5)
val contentsCopiedIntoBuffer = firstVector.to(Buffer)contentsCopiedIntoBuffer: Buffer[Int] = ArrayBuffer(10, 50, 5)

The to method created a new buffer, copied the vector’s contents into the buffer, and returned a reference that points to the buffer. We can now modify the buffer as per usual:

contentsCopiedIntoBuffer += 100res9: Buffer[String] = ArrayBuffer(10, 50, 5, 100)

Copying a buffer’s contents into a vector is equally easy:

val secondVector = contentsCopiedIntoBuffer.to(Vector)secondVector: Vector[Int] = Vector(10, 50, 5, 100)

When you call to, you pass in a parameter that specifies which kind of collection you want, as shown with Buffer and Vector above. For a handful of commonly used collection types, Scala additionally provides a bespoke method, such as toBuffer and toVector. The following commands accomplish the same thing:

val copiedIntoABuffer = someCollection.to(Buffer)
val copiedIntoAVector = someCollection.to(Vector)
val copiedIntoABuffer = someCollection.toBuffer
val copiedIntoAVector = someCollection.toVector

In practice, this is a way to leave out a pair of round brackets, if you prefer.

Using toVector to improve class Category

Let’s patch up the leak in Category’s public interface:

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  // We still need to add the variable fave here.

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience
    // We still need to update fave here.

  def allExperiences = this.experiences.toVector

end Category

Now allExperiences creates a vector that contains exactly those experiences that belong to the category at the time. The method’s return type is Vector[Experience] rather than Buffer[Experience], which makes sense, since modifying the return value isn’t a meaningful operation.

But still, why not always Buffer?

You may be wondering if having a separate Vector type makes sense, since a vector has fewer operations than a buffer.

It’s not always good to have more operations available. By choosing an immutable collection, we can guarantee that the collection’s contents never change at any point during a program run.

Among other benefits, this helps human readers who are trying to find out how the program works or hunting for bugs. A vector can also help us define better interfaces and prevent some errors, as we did with Category above.

Immutable data is the cornerstone of the functional programming paradigm and a key design principle behind a lot of high-quality software. We’ll return to that in Chapter 11.2, and O1’s follow-on courses will elaborate further.

Vectors and buffers also differ in terms of their runtime efficiency. Which one is more efficient depends on specific circumstances and is something we won’t be looking into in O1.

The difference between an immutable collection and a mutable one is analogous to that between a val and a var. In Chapter 1.4, we established this rule of thumb:

Make every variable a val, unless you have a good reason right now to make it a var.

We can now also state:

Make every collection immutable (like Vector), unless you have a good reason right now to make it mutable (like Buffer).

Selected Methods of Vectors and Buffers

Before the longer programming assignment that ends this chapter, let’s take a look at some of the methods defined on vectors and buffers, which we now know to be objects. Among these methods, you’ll find tools for tackling that programming assignment, too.

You don’t need to memorize all the methods listed below, but you can make a mental note that they are described in this chapter (and in the Scala Reference). Later chapters will add to this selection of collection methods.

In the following examples, we’ll mostly use vectors, but you can apply the same methods to buffers as well (not to mention various other collection types).

For starters, let’s define a test vector that contains, say, strings:

val testVector = Vector("first", "second", "third", "fourth")testVector: Vector[String] = Vector(first, second, third, fourth)

Describing a collection: mkString

The mkString method (“make string”) is often convenient when you want to format a program’s output:

testVector.mkString(";")res10: String = first;second;third;fourth

As shown above, the method constructs and returns a string that contains a description of each element in the collection. Those descriptions are separated by copies of a string that you pass in as a parameter; in this example, that separator string consists of just a single semicolon.

Here’s another example where a line break (denoted as \n) separates the elements:

val numbers = Vector(10, 50, 30, 100, 0)numbers: Vector[Int] = Vector(10, 50, 30, 100, 0)
println(numbers.mkString("\n"))10
50
30
100
0

Practice on mkString

It’s been a while since we used the play function from Week 1. This function has a previously unexplored feature: it can play multiple melodies simultaneously if you place an ampersand (&) between the notes of each melody.

play("cccedddfeeddc---&cdefg-g-gfedc-c-")

The string contains an ampersand. play splits the string there and plays the melody on the left at the same time as the melody on the right.

We can use the same technique to build a virtual band that plays multiple instruments.

Your assignment is to write a function named together that constructs a piece of music with multiple melodies, or “voices”, playing in unison:

val melody = "cccedddfeeddc---"melody: String = cccedddfeeddc---
val base = "[33]<<c-c-d-d-e-d-cdc"base: String = [33]<<c-c-d-d-e-d-cdc
val performance = together(Vector(melody, base), 150)performance: String = cccedddfeeddc---&[33]<<c-c-d-d-e-d-cdc/150
play(performance)

As its first parameter, the function receives the notes that correspond to each voice. Each voice is a separate element in a vector.

As its second parameter, it receives the entire performance’s tempo as an integer.

The function returns a string that represents the entire multiply voiced performance, adding special characters as expected by play.

This second usage example has a more sophisticated input but follows the same principle:

val voices = Vector(
  "FGHbG(GHb>D<)--.(GHb>D<)--.(FAC)---- FGHbG(FAC)--.(FAC)--.(DGHb)--AG-FGHbG(FGHb)--->C-<(AF)--GF---F-(DF>C<)---.(DGHb)------",
  "<<    (<eb>eb)--.(<eb>eb)--.(<f>f)----     (<d>d)--.(<d>d)--.(<g>g)----     (<c>c)-----(<f>f)-----f---(da)---.(<g>g)------",
  "P:<<    " + "c cc" * 14)voices: Vector[String] = Vector(FGHbG(GHb>D<)--.(GHb>D<)--.(FAC)---- FGHbG(FAC)--.(FAC)--.(DGHb)--AG-FGHbG(FGHb)--->C-<(AF)--
GF---F-(DF>C<)---.(DGHb)------, <<    (<eb>eb)--.(<eb>eb)--.(<f>f)----     (<d>d)--.(<d>d)--.(<g>g)----     (<c>c)-----(<f>f)----
-f---(da)---.(<g>g)------, P:<<    c ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc cc)
play(together(voices, 20))

Add the function to misc.scala in module Miscellaneous.

A+ presents the exercise submission form here.

Checking the contents of a collection: indexOf, contains, and isEmpty

The indexOf method determines the given element’s location within a collection.

val testVector = Vector("first", "second", "third", "fourth")res11: Vector[String] = Vector(first, second, third, fourth)
testVector.indexOf("second")res12: Int = 1
testVector.indexOf("first")res13: Int = 0

Experiment with indexOf. Which of the following claims correctly describe how the method behaves in case the given element occurs multiple times in the collection?

And what does indexOf return if there are no occurences of the given element at all?

You can also use the parameterless method isEmpty to examine a collection, as well as the contains method, which you can call just like indexOf. Try these methods on some vector or buffer.

Which of the following claims are correct? Select all that apply. Assume that the variable collection refers to some vector or buffer object.

Selecting elements: apply; head and tail; take and drop

Collections have the parameterless methods head and tail as well as an apply method that receives an index as a parameter.

Experiment with those three methods in the REPL. See if you can work out which of the following claims are correct. Select each correct answer.

Assume that collection is a variable that refers to a vector or buffer object that contains multiple elements.

The methods take and drop both take an integer parameter.

Experiment with the two methods in the REPL. See if you can work out which of the following claims are correct. Select each correct answer.

Again, assume that collection refers to a non-empty collection.

More claims for you to assess:

Methods that “change” a vector

It’s natural to describe a method like drop by saying that it “takes all the elements of the vector except the first”. This sounds like the method changes what remains in the vector. Other methods, too, appear to — but don’t actually — modify the contents of a vector. The method reverse, for instance, “inverts a vector’s contents”:

val vectorOfNumbers = Vector(4, 10, -100, 10, 15)vectorOfNumbers: Vector[Int] = Vector(4, 10, -100, 10, 15)
vectorOfNumbers.reverseres14: Vector[Int] = Vector(15, 10, -100, 10, 4)

What reverse really does is create an entirely new collection that contains the same elements in different order. The original remains intact.

vectorOfNumbersres15: Vector[Int] = Vector(4, 10, -100, 10, 15)

None of the methods introduced above is effectful. None of them change the original vector. The same applies to all of a vector’s methods. A programmer who uses a vector can rely on the fact that a vector, once created, never changes.

Methods that change a buffer

Buffers have the same effect-free methods as vectors do. In addition, they have some effectful methods that modify the buffer’s contents. Here are a few of them:

See if you can work out which of the following claims are correct. Select all that apply. Use the REPL to experiment with the methods mentioned below.

Assume that bufferOfNumbers is a variable of type Buffer[Int].

Combining collections with ++, +:, and diff

The example below shows a simple way to combine two collections. To be more precise, what this does is construct a new collection that contains the same elements as two existing collections:

val firstStrings = Vector("first", "second")firstStrings: Vector[String] = Vector(first, second)
val secondStrings = Vector("a", "b", "c")secondStrings: Vector[String] = Vector(a, b, c)
val combination = firstStrings ++ secondStringscombination: Vector[String] = Vector(first, second, a, b, c)
val theOtherWayAround = secondStrings ++ firstStringstheOtherWayAround: Vector[String] = Vector(a, b, c, first, second)

The operator is made up of two plus signs.

This works on buffers, too.

The operator ++ combines two collections, but if you want a new collection with just one more element at the front, you can use +: instead:

val firstStrings = Vector("first", "second")firstStrings: Vector[String] = Vector(first, second)
val bigger = "actually first" +: firstStringsbigger: Vector[String] = Vector(actually first, first, second)

diff “subtracts” a collection from another, again producing a new collection:

Vector("a", "b", "c", "d").diff(Vector("a", "c"))res16: Vector[String] = Vector(b, d)

Football2: A New Match Class

Introduction

The only constant is change.

—attributed to Heraclitus; actual origin unknown

Successful software always gets changed.

Fred Brooks

Software development is often iterative: The programmer or team first creates a simple version or prototype of a program and tests and assesses it. They then develop it further in cycles.

It’s also typical that new needs become apparent as the program develops. This has already happened to our FlappyBug program, for instance, and is about to happen to the football-statistics program of Chapter 3.5. (Not for the last time, either.)

Task description

Fetch Football2 and study its Scaladocs. Right now, we’ll focus on class Match. The classes Club and Player have been provided for you, and there’s no need for you to change them. The documentation also mentions a Season class, which you’ll get to implement in a later assignment (Chapter 4.4).

../_images/module_football2.png

A diagram of the main relationships between the classes.

Football2’s specification differs from the simpler Football1. The new Match class should keep track of not just the number of goals scored but also the scorers. The old methods addHomeGoal and addAwayGoal have been replaced by a single addGoal method that is expected to record a goal for either team, depending on who scored it. What’s more, our wish list now features a number of new methods. All this requires quite a makeover of class Match.

There is a partial implementation of Match in Match.scala. It’s a pretty good start: There are two instance variables for two buffers that let you keep track of the goalscorers. There is a sketch of the new addGoal method. Also given is the new method winnerName, which now begets an error only because the class is otherwise incomplete.

Your job is to complete the class by finishing addGoal and adding the missing methods.

Instructions and hints

  • Package o1.football2.gui contains FootballApp: a simple program for interactive testing. In order to work, it needs a Match class that has the specified methods. Once your class is ready for testing, you can run FootballApp and experiment with it in the window that pops up:

    ../_images/football2_gui.png
  • Use instance variables to refer to buffers and Player objects as suggested in the starter code.

  • See if you can find uses for some of the methods introduced in this chapter.

  • Do not edit Player or Club.

  • Some of the missing methods you already implemented in Chapter 3.5. You can copy those parts of your earlier solution into Football2. If you didn’t do the earlier assignment on Football1, start with that. (You may also see its example solution.)

  • FootballApp only adds goals and reports the scorer of the winning goal. You can additionally experiment with your class in the REPL or write a text-based test app of your own.

A+ presents the exercise submission form here.

Tracking the Favorite Experience (First Try)

We have unfinished business with Category: how should a category object ensure that it can determine the diarist’s favorite experience among the ones added so far?

Here are two alternative ways to go about it:

  1. Let’s have the category object keep track of the best experience added so far. This information must be updated whenever a newly added experience is better than the previous favorite.

  2. Let’s not add any additional bookkeeping to the category object; the buffer of all added experiences will suffice. Whenever the object needs to return the favorite, it goes through the entire buffer and finds the highest-rated experience currently stored there.

We’ll now adopt the first approach. In fact, we vaguely outlined it already at the beginning of the chapter: we planned to use a variable named fave that always refers to the highest-rated experience added so far.

What should this variable be like? How should we update it when addExperience is called?

A pseudocode draft

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  private var fave = initially there is no fave yet

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience
    Set this.fave to whichever is rated higher: newExperience or the previous this.fave.

  def allExperiences = this.experiences.toVector

end Category

A category with no experiences has no favorite, either. We need some way to mark that the variable initially has no value, or has a value that means “no favorite”. For now, we have no good tools for the job.

We use fave much like a most-recent holder, but this time we’re pickier: the most recent value replaces the old value only in case it’s better.

The null reference

Perhaps we could initialize fave like this?

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  private var fave: Experience = null

  def favorite = this.fave

The word null means “a value that is no value at all” or “a reference to nowhere”. It can be assigned to a variable as shown.

Many different types of variables can be given the null value, and so the value itself isn’t enough to indicate that we want fave have the type Experience. This is why we need to annotate this instance variable’s type just as we’ve routinely annotated parameter variables. (A type annotation is always allowed; see Chapters 1.8 and 3.5.)

Advance warning

The null reference is problematic and it’s generally advisable to avoid it. We’ll discuss this at some length soon. But first, let’s try and write an implementation for addExperience.

New role: most-wanted holder

../_images/most-wanted_holder.png

When you have values that want to be the Caliph instead of the Caliph, that’s a most-wanted holder. —Otto Seppälä

A most-wanted holder (sopivimman säilyttäjä) is a variable that keeps track of the “best” value encountered so far among a sequence of values. Any number of different criteria can define “best”: you might wish to track the largest or smallest number, the longest string, the best sports result, the youngest student object, or what not. In our program, fave is a most-wanted holder whose criterion is the experiences’ numerical ratings.

As you update a most-wanted holder’s value, you need to check whether the new candidate value is “more wanted” than the variable’s current value. You can do that with an if or a suitable method call.

In our case, we have a suitable method on hand: chooseBetter from Chapter 3.4. Let’s use it:

class Category(val name: String, val unit: String):

  private val experiences = Buffer[Experience]()
  private var fave: Experience = null

  def favorite = this.fave

  def addExperience(newExperience: Experience) =
    this.experiences += newExperience
    this.fave = newExperience.chooseBetter(this.fave)

  def allExperiences = this.experiences.toVector

end Category

chooseBetter compares two experiences and returns the higher-rated one.

The basic idea here is exactly right. In principle, this code works just like you’ve seen in the “communicating objects” animations of GoodStuff: the category object consults the experience object to see which experience is better and uses the response to update the favorite.

However, this implementation has a serious defect. That much will be obvious as soon as we try to use it:

A Runtime Error

Suppose we take the above version of Category and the app object CategoryTest from the beginning of the chapter. We then run CategoryTest. The program does start, but an error message pops up in the text console:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at o1.goodstuff.gui.CategoryTest.main(CategoryTest.scala)
Caused by: java.lang.NullPointerException
    at o1.goodstuff.Experience.isBetterThan(Experience.scala:22)
    at o1.goodstuff.Experience.chooseBetter(Experience.scala:28)
    at o1.goodstuff.Category.addExperience(Category.scala:31)
    at o1.goodstuff.gui.CategoryTest$.<clinit>(CategoryTest.scala:7)

This is a runtime error, also known as an exception; such an error manifests itself only while the program is executing (Chapter 1.8). The lines of the error message list the frames that were on the call stack as the problem occurred. A report such as this is called a stack trace and it can help us locate the problem. Let’s take a closer look:

We’re told that an exception has occurred while running CategoryTest.

The cause is an error named NullPointerException. That name already tells us something about the nature of the problem. More on that below.

The problem arose on line 22 of the experience class within the method isBetterThan, which had been invoked on line 28 of the same class within chooseBetter, which had been invoked on line 31 of the category class within addExperience, which had been invoked from CategoryTest.scala.

The infamous NullPointerException

A NullPointerException is a symptom of an attempt to call a method or access an instance variable of an object that doesn’t exist. This error type tells us that we’ve somehow tried to access a feature of a null value (rather than a feature of an existing object). Such operations are meaningless and our programs should avoid them, just like we shouldn’t divide by zero when we do math.

Usually, a NullPointerException means that a programmer has made a mistake. That’s the case here, too.

Inspect how the problem arises in the following animation. Pay particular attention to the null value: how it’s first stored in a variable and subsequently used.

Assess the following claim: The error arises from the program’s attempt to determine the rating of an object that doesn’t exist, being null instead.

Claim: The error concerns the value that fave received earlier, when the category object was created.

Claim: When the error occurs, the currently active frame contains a this variable that stores a null reference rather than referring to an existing object.

The command newExperience.chooseBetter(this.fave) contains the expression this.fave. Claim: when that command is executed, the expression this.fave has the value null.

Claim: The error occurs as the program attempts to pass a null reference as a parameter to an Experience object’s method.

Is there some way to solve this?

Summary of Key Points

  • In order to represent a link from one object to multiple other objects, you can use an instance variable that refers to a collection of objects.

  • Collections come in different varieties. There are vectors and buffers, for example.

    • In Scala, collections are objects and have a wide range of methods.

    • A vector is an immutable collection while a buffer is mutable.

  • You can use a variable as a most-wanted holder: to track the value that so far best matches a particular criterion (e.g., the smallest number or the best experience).

    • The initial value of such a variable deserves special attention.

  • There is a value called null that is a “reference to nowhere”. It is unadvisable to use this value; it’s error-prone, for one thing. You will soon learn better alternatives to null.

  • Links to the glossary: collection, buffer, vector; container; immutable, mutable; most-wanted holder; null; runtime error, stack trace.

Feedback

Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.

Credits

Thousands of students have given feedback and so contributed to this ebook’s design. Thank you!

The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.

The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.

The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy of using O1Library for simple graphical programming (such as Pic) is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.

The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits for this page

Iznogoud is the creation of René Goscinny and Jean Tabary.

This chapter does injustice to music by Carl Bellman and someone else. Thank you and sorry.

a drop of ink
Posting submission...