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. Myös suomenkielisessä materiaalissa käytetään ohjelmaprojektien koodissa englanninkielisiä nimiä kurssin alkupään johdantoesimerkkejä lukuunottamatta.
Voit vaihtaa kieltä A+:n valikon yläreunassa olevasta 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 Projects: 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 project, you’ll notice
that it differs slightly from the one in the above animation. That’s
because the given project 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. }
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
}
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.)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 = new Thingy(this.number, this.another)
}
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
}
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.)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 = new Category("Wine", "bottle")wineCategory.addExperience(new 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 = new 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)
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 attribute several different but more-or-less related meanings to the word “vector”. Even within programming, 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 the same way that you’ve created buffers (without new
):
val vectorOfWords = Vector(4, 10, -100, 10, 15)vectorOfWords: Vector[Int] = Vector(4, 10, -100, 10, 15) val vectorOfNumbers = Vector("first", "second", "third", "fourth")vectorOfNumbers: 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:
vectorOfWords(3)res3: Int = 10 vectorOfNumbers(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:
vectorOfWords += 999<console>:9: error: value += is not a member of scala.collection.immutable.Vector[Int] vectorOfWords += 999 ^ vectorOfWords(2) = 999<console>:9: error: value update is not a member of scala.collection.immutable.Vector[Int] vectorOfWords(2) = 999 ^
Vectors — and buffers — as objects
You can ask a vector to report the number of elements it contains by calling its size
method:
vectorOfNumbers.sizeres5: Int = 4 vectorOfWords.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: toVector
, toBuffer
It’s fairly 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:
val firstVector = Vector(10, 50, 5)firstVector: Vector[Int] = Vector(10, 50, 5) val contentsCopiedIntoBuffer = firstVector.toBuffercontentsCopiedIntoBuffer: Buffer[Int] = ArrayBuffer(10, 50, 5)
To clarify the above: the toBuffer
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.toVectorsecondVector: Vector[Int] = Vector(10, 50, 5, 100)
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
}
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 still 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 a collection’s contents never change at any point
during a program run. Among other benefits, this helps human readers who try to find
out how the program works or hunt for bugs. A vector can also help us define better
interfaces and prevent some errors, as we did with Category
above.
Immutable data is a cornerstone of the functional programming paradigm, which we’ll discuss in Chapter 10.2.
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 aval
, unless you have a good reason right now to make it avar
.
We can now also state:
Make every collection immutable (likeVector
), 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 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.
o1.play("cccedddfeeddc---&cdefg-g-gfedc-c-")
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:
import o1.misc._import o1.misc._ 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 o1.play(performance)
o1.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 ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc cc" )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) o1.play(together(voices, 20))
Add the function to misc.scala
in project 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 vectorOfWords = Vector(4, 10, -100, 10, 15)vectorOfWords: Vector[Int] = Vector(4, 10, -100, 10, 15) vectorOfWords.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.
vectorOfWordsres15: 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)
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 project, 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 winner
, 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 can 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.
Submission form
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 }
fave
much like a most-recent holder (Chapter 2.6) 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
null
means “a value that is no value at all” or “a
reference to nowhere”. It can be assigned to a variable as shown.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
}
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
. An error message pops up in the
text console:
Exception in thread "main" java.lang.NullPointerException
at o1.goodstuff.Experience.isBetterThan(Experience.scala:23)
at o1.goodstuff.Experience.chooseBetter(Experience.scala:29)
at o1.goodstuff.Category.addExperience(Category.scala:35)
at o1.goodstuff.CategoryTest$delayedInit$body.apply(CategoryTest.scala:7)
at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.collection.immutable.List.foreach(List.scala:309)
at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32)
at scala.App$class.main(App.scala:71)
at o1.goodstuff.CategoryTest$.main(CategoryTest.scala:3)
at o1.goodstuff.CategoryTest.main(CategoryTest.scala)
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:
NullPointerException
, which already
tells us something about the nature of the problem. More on
that below.isBetterThan
, which had been invoked on line 29 of the
same class within chooseBetter
, which had been invoked on line
35 of the category class within addExperience
.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 prgorammer 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 that has contributed to this ebook’s design. Thank you!
Weeks 1 to 13 of the ebook, including the assignments and weekly bulletins, have been written in Finnish and translated into English by Juha Sorva.
Weeks 14 to 20 are by Otto Seppälä. That part of the ebook isn’t available during the fall term, but we’ll publish it when it’s time.
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, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 have done 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 behind O1Library’s tools 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+ has been created by Aalto’s LeTech research group and is largely developed by students. The current lead developer is Jaakko Kantojärvi; many other students of computer science and information networks are also active on the project.
For O1’s current teaching staff, please see Chapter 1.1.
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.)