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).
Notes: Speakers or headphones will be useful at one point. They aren’t strictly necessary though.
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:
How to keep track of multiple experience objects that are associated with a single category object?
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
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
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
Code-reading task: cloning sheep
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
Int
s 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
, andOdds
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 avar
.
We can now also state:
Make every collection immutable (like
Vector
), unless you have a good reason right now to make it mutable (likeBuffer
).
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
Selecting elements: apply
; head
and tail
; take
and drop
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:
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.
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).
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
containsFootballApp
: a simple program for interactive testing. In order to work, it needs aMatch
class that has the specified methods. Once your class is ready for testing, you can runFootballApp
and experiment with it in the window that pops up: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
orClub
.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:
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.
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
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.
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 tonull
.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.
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.)