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 2.4: Inside a Class
About This Page
Questions Answered: How do I implement a class of objects in Scala? What happens when I create a new instance of a class? How does a class’s program code apply to its individual instances? Can an instance’s behavior differ from that defined for its class?
Topics: Class definitions. Constructor parameters and instance variables. Tailoring instance-specific methods. Superman.
What Will I Do? Both read and program.
Rough Estimate of Workload:? One and a half hours. Or more. Or less. In this chapter, you’ll need to build on what you learned in practically all the earlier chapters; revisit them if necessary. In any case, the concepts of instance variable and constructor parameter are often troublesome at first. Don’t hesitate to ask for advice!
Points Available: A65.
Recap
In this chapter, we’ll explore the program code that defines a class. In doing so, we effectively combine what you know about defining a singleton object (Chapter 2.2) with what you just learned about classes and their relationship to objects (Chapter 2.3). Let’s begin with a quick review of some key facts that you’ll need in this chapter.
Things you already know about classes and objects:
- Classes define the types of data that a program operates on. A class represents a generic concept that can be instantiated; for example, a class may represent the generic concept of student.
- Instantiating a class means creating an instance, that is, an object of the type that the class defines. Such an object is a specific case of the generic concept, such as an individual student.
- As you instantiate a class (with
new
), you often pass in one or more constructor parameters. The class may use these parameters to initialize instance-specific attributes such as a student object’s name or student ID). - Different objects of the same class each have their own attributes (e.g., student objects may have different names). However, these attributes share the same data type (e.g., each student object’s name object is a string).
- Objects of the same type have the same methods. For example, if the student class defines an enrollment method, all student instances will have that method.
Things you already know about singleton objects:
- You define a singleton object without separately defining a class for it.
- Within a singleton object’s program code, you can define variables that store the object’s attributes. You can also implement methods on the object.
- Within a method implementation, the word
this
refers to the object itself. Within an object’s methods, you can use expressions such asthis.someVariable
to access the object’s own variables.
Class Definition vs. Singleton Definition
In Chapter 2.2, we implemented and used a singleton object that represents an individual
employee. In contrast, in Chapter 2.3 we used a class Employee
that represents
employees more generally and was therefore more useful than the original singleton.
The code of the singleton object from Chapter 2.2 is reproduced below. Below it, you’ll
find a previously unfamiliar piece of code that defines the Employee
class. Comparing the
two, it’s easy to see that they are in many respects identical. There are a few key
differences, however, as highlighted by the green boxes between the two code fragments.
object employee {
var name = "Edelweiss Fume"
val yearOfBirth = 1965
var monthlySalary = 5000.0
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) = {
this.monthlySalary = this.monthlySalary * multiplier
}
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
}
Employee
instance. (More on that below.)class EmployeeOriginal(nameParameter: String, yearParameter: Int, salaryParameter: Double) {
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) = {
this.monthlySalary = this.monthlySalary * multiplier
}
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
}
Now, let’s dig into how the class works.
Instance Variables
How does a class definition determine what happens when we create a new instance?
The answer is illustrated in the following animation. The animation also introduces the key concept of instance variable (ilmentymämuuttuja).
You can also consider the instantiation process in terms of the form metaphor that we used earlier. Here’s an update to the illustration from Chapter 2.3:
Instantiation and a class’s program code (a clarification)
It came up above that the commands inside a class definition run when a new instance is created. For example, the instance variables of a new employee object are assigned (initial) values at that point.
Note that this applies only to the commands that appeared directly within the curly brackets around the class body, not the commands located inside methods. A method’s body is executed only when we instruct the computer to do so by calling the method.
For example, when initializing an instance of class Employee
, none of the methods
ageInYear
, monthlyCost
, raiseSalary
, or description
are executed, even though
these methods are defined within the class’s program code. The class’s user can call
those methods after creating an instance with new
, as we’ve done in earlier examples.
Check your understanding: constructor parameters and instance variables
Constructor parameters vs. instance variables
A method’s parameter variables exist — and their values are held in memory — only as long as the method is being executed. Similarly, constructor parameters exist only while the new instance is being created. They are stored in a frame on the call stack; that memory is released for other use as soon as the object has been initialized.
Instance variables, on the other hand, are members of an object. Their values remain in memory after the object’s been created and persist between method calls.
An instruction such as val name = nameParameter
essentially takes a value received as
a parameter and stores it in a place where it can be accessed later.
In classes Employee
and Pet
, the only thing we did with the constructor parameters is
copy their values into instance variables. You may reasonably be wondering why we even
need a separate set of variables to store the constructor parameters, if those parameters
are there just to be copied into another set of variables.
It does make sense to be able to define constructor parameters as separate from instance variables, because copying their values isn’t the only thing we can do with the parameters; you’ll see examples of that later. But it’s true that the sort of copying that our example classes do is common, and it’s also the case that it should be possible to express these classes more succinctly. And indeed Scala does provide a way to tighten our code:
A Convenient Way to Write a Class
Below are two versions of class Employee
. The first version you saw already, the second
is more compact. Both accomplish the same thing.
class Employee(nameParameter: String, yearParameter: Int, salaryParameter: Double) {
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
// methods not shown
}
var
or val
at the top of the class definition,
as shown below. What this means is that we both require the
class’s user to pass in these three things as constructor
parameters and store the received values in the new object’s
instance variables.class Employee(var name: String, val yearOfBirth: Int, var monthlySalary: Double) {
var workingTime = 1.0
// methods not shown
}
If you want, you can view the following animation, which uses the compact version of
Employee
.
The compact version is shorter and, perhaps, easier to read. In any case, the compact style is extremely common in Scala programs and you should get accustomed to it. In O1, too, we’ll adopt this compact style of writing classes.
Methods on Instances
Methods operate on a class’s instances in essentially the same way as they operate on
singleton objects. As you saw in Chapter 2.2, we can use the word this
to refer to
the object whose method has been called. A class’s instance, too, can access its own
instance variables through expressions of the form this.variableName
.
If this seems perfectly clear to you, feel free to skip the following animation; it has no other new content. Otherwise, view the animation.
Assignment: A Compact Rectangle
Assume we have access to a class Rectangle
, which we can use as follows:
Side note
What we have here is an example of different programs adopting different perspectives on a particular concept. In this case, the concept is the rectangle.
In earlier chapters, we have drawn pictures of rectangles. In this assignment, we represent rectangles as geometrical entities with qualities such as area.
val test = new Rectangle(10.0, 15.5)test: o1.Rectangle = o1.Rectangle@152a308 test.side1res0: Double = 10.0 test.side2res1: Double = 15.5 test.areares2: Double = 155.0
As shown, we can ask a Rectangle
object for the lengths of its sides as well as its area.
Here’s one way to define a class that works as shown above:
class Rectangle(givenSideLength: Double, anotherGivenSideLength: Double) {
val side1 = givenSideLength
val side2 = anotherGivenSideLength
def area = this.side1 * this.side2
// Etc. You may write additional methods here.
}
This definition is unnecessarily wordy. Write a more compact class definition by defining
the instance variables in the class header, as demonstrated for class Employee
above.
Write your solution in Rectangle.scala
in the IntroOOP module. The given file contains the
above implementation for you to modify.
Use the REPL to confirm that your new implementation works as illustrated.
A+ presents the exercise submission form here.
Mini-Assignment: Describing a Rectangle
Assignment: An Account Class
Further practice: a Pic
-returning method
As noted, the class Rectangle
represents certain mathematical attributes of
rectangles. If we want to, it’s possible to extend the class with methods
that operate on rectangles as graphical elements.
Add a method makePic
in class Rectangle
. The method should:
- take in a single parameter of type
Color
; and - return an image (
Pic
) of a rectangle whose width is equal toside1
, whose height is equal toside2
, and whose color is determined bymakePic
’s parameter.
A+ presents the exercise submission form here.
In the practice task just above, we considered the drawing color to be an additional piece of information that’s passed to the rectangle object whenever we wish it to produce an illustration of itself.
Alternatively, we may choose to represent color as an inherent attribute of each rectangle.
Stay in Rectangle.scala
and add another class there, ColoredRectangle
.
As a starting point, you can copy and paste the definition of Rectangle
and rename it.
Then make the following changes to the new class:
- Add a third constructor parameter, of type
Color
. - See to it that the rectangle’s color is accessible through
an instance variable called
color
. - Edit
makePic
so that it takes no parameters and instead uses the rectangle object’s color attribute as it produces an image of the rectangle.
A+ presents the exercise submission form here.
Which of the two classes is better? As far as our toy example is concerned, there is little difference, and in any case the answer depends on what we might wish to use the class for.
Here is a more interesting general question that you may want to consider: if you have a class that represents an abstract concept (such as rectangle), should its definition include anything that pertains to the program’s outward appearance (such as colors and graphics)? We’ll return to that question later on.
A Monster Example
Let’s define a class that could represent monsters in a simple, otherwise imaginary game.
Monsters have a kind (e.g., "orc", "vampire") and a health score. We mean to use our monster class as illustrated in the example below.
val creature = new Monster("dragon", 200)creature: Monster = Monster@597f1753 creature.kindres3: String = dragon creature.healthMaxres4: Int = 200 creature.healthNowres5: Int = 200 creature.descriptionres6: String = dragon (200/200)
A monster’s state changes when it suffers damage (having been attacked by a hero or something):
creature.sufferDamage(30)creature.healthNowres7: Int = 170 creature.sufferDamage(40)creature.healthNowres8: Int = 130 creature.descriptionres9: String = dragon (130/200)
sufferDamage
method to indicate how
many health points the monster loses. This effectful method only
adjusts the monster’s state and does not return anything.Now let’s see about implementing such a class:
Assignment: Odds (Part 1 of 9)
There are different ways to express the likelihood, or odds, of an event happening. For example, to describe the odds of rolling a six on a six-sided die, we might say that the odds are “one out of six”. Another way to phrase this is “5/1”, which means that there are five ways to not roll a six and one way to roll it. In other words: it’s five times as likely to not roll a six as it is to roll it. The event’s likelihood may also be expressed as the percentage 16.67%, which can be alternatively written as 0.1667.
Let’s create a class for expressing the odds of events in different formats, and, eventually, computing the probabilities of combinations of events. In this chapter, we’ll begin our work on this class; in future chapters, we’ll return to improve on what we accomplish here.
Introduction: what we want from our Odds
class
The basic idea is that one instance of class Odds
stands for the likelihood of a single
event such as rolling a six or a bookmaker’s estimated odds of Norway winning the
next Eurovision Song Contest. We intend to create Odds
objects like this:
val rollingSix = new Odds(5, 1)rollingSix: Odds = o1.odds.Odds@1c60524
Now the variable rollingSix
stores a reference to an object of type Odds
. As
constructor parameters, we passed in first the number of non-occurrences (5) followed by
the number of occurrences (1); together, these two numbers specify the event’s odds.
We can now ask the object to describe these odds in a variety of formats. Here’s
the probability
of rolling a six as a Double
(i.e., the value of 1 / (5 + 1)):
rollingSix.probabilityres10: Double = 0.16666666666666666
The fractional
method describes the odds as a relation between non-occurrences and
occurrences:
rollingSix.fractionalres11: String = 5/1
As you can see, the method returns a string that connects the two constructor parameters with a slash.
The decimal
method returns the reciprocal of what probability
returns. That is,
the method’s return value describes the odds in “one-in-how-many” terms. Our example
event has a one-in-six chance of happening:
rollingSix.decimalres12: Double = 6.0
Consider another example. Many international betting agencies report the odds on offer in
fractional format (as returned by fractional
). We can use an Odds
object to represent
offered bets. For example, suppose that odds of 5/2 are on offer for the event of Norway
winning Eurovision; we’ll model this as an Odds
object:
val norwayWin = new Odds(5, 2)norwayWin: Odds = o1.odds.Odds@1e75d66 norwayWin.probabilityres13: Double = 0.2857142857142857 norwayWin.fractionalres14: String = 5/2 norwayWin.decimalres15: Double = 3.5
In a betting context, decimal
’s return value is the number that the bettor’s
investment multiplies by in case they win. For example, if the odds are 5/2, the
successful bettor will receive 3.5 times what they bet: their money back and 2.5 times
that much extra.
With that in mind, let’s sketch out one more method for our class. In this example, we
compute the winnings
of a bettor whose investment of 20 currency units on Norway has
paid off:
norwayWin.winnings(20.0)res16: Double = 70.0
(The result is the bet of 20 multiplied by a factor of 3.5, which factor is returned by
decimal
.)
Finally, here’s one more example that demonstrates that it’s perfectly possible for the first constructor parameter to be smaller than the second, if the event is likely:
val threeOutOfFive = new Odds(2, 3)threeOutOfFive: Odds = o1.odds.Odds@dfdd0c threeOutOfFive.probabilityres17: Double = 0.6 threeOutOfFive.fractionalres18: String = 2/3 threeOutOfFive.decimalres19: Double = 1.6666666666666667 threeOutOfFive.winnings(123.45)res20: Double = 205.75
Your assigment
Fetch the Odds module and locate the definition of class Odds
therein. (For now, pay no
mind to the module’s other contents.)
You’ll find the class lacking in functionality: probability
is there, but fractional
,
decimal
, and winnings
are missing. Implement those methods so that they work as
illustrated above.
Instructions and hints
- Two instance variables have already been defined for you:
wont
andwill
. They receive their values directly from constructor parameters (in the manner described above at A Convenient Way to Write a Class).- You can use these instance variables in the methods you write.
- You won’t need other instance variables.
- There’s no need to change
probability
; it works. But you can take a look at how it works. - As long as you make sure that you understand what each method is supposed to accomplish, actually implementing the methods with a combination of basic arithmetic operations shouldn’t be a lot of work.
- Use the REPL to test that your methods work as specified.
- Make sure to select the Odds module as you launch the REPL.
- Remember to reset the REPL after making changes.
- Submit your program only when you’re satisfied with how it works.
- When you implement
fractional
, don’t try to reduce the fraction. If the constructor parameters are, say, 6 and 2, return6/2
, not3/1
.- If you run into trouble constructing the
string, return to check the
description
method in the rectangle assignment.
- If you run into trouble constructing the
string, return to check the
- Can you implement
decimal
andwinnings
by calling another method of the same class in the method body? (This isn’t required but it is convenient.)
A+ presents the exercise submission form here.
Tailoring an Instance’s Methods
To conclude this chapter, let’s fool about with a silly little class. As we do so, we’ll pick up a few tricks that’ll come in handy in the next chapters.
class Person(val name: String) {
def say(sentence: String) = this.name + ": " + sentence
def reactToSriracha = this.say("What a nice sauce.")
def reactToKryptonite = this.say("What an odd mineral.")
}
say
method. You can think of this as the person
object sending itself a message: “When commanded to react to
something, I’ll command this
— which is me — to execute
the say
method with a specific parameter value.”An example of using the class:
val first = new Person("Jimmy")first: Person = Person@5cf6635a first.say("Super-Duper!")res21: String = Jimmy: Super-Duper! first.reactToSrirachares22: String = Jimmy: What a nice sauce. first.reactToKryptoniteres23: String = Jimmy: What an odd mineral. val second = new Person("Lois")second: Person = Person@645b797d second.reactToKryptoniteres24: String = Lois: What an odd mineral.
Every Person
likes sriracha and is puzzled by kryptonite, since that’s what the class
says people should do. That’s pretty much all a Person
can do; they can’t fly, for
example. But not everyone is bound by the same laws:
Adding an instance-specific method
val superman = new Person("Clark") { def fly = "WOOSH!" }superman: Person{def fly: String} = $anon$1@25ba32e0
Superman can now fly in addition to being able to state his name and react to things like a normal person.
superman.flyres25: String = WOOSH! superman.nameres26: String = Clark superman.reactToKryptoniteres27: String = Clark: What an odd mineral.
We managed to add a tailor-made method to a specific instance of a class. What about adapting a class’s behavior?
Overriding a method
Green kryptonite is Superman’s weakness. If we wish to represent Superman as an instance
of class Person
, can we still account for this fact? What we need is a way to make a
particular instance’s behavior deviate from the way the other instances behave.
val realisticSuperman = new Person("Clark") { def fly = "WOOSH!" override def reactToKryptonite = "GARRRRGH!" }realisticSuperman: Person{def fly: String} = $anon$1@47bd09ef realisticSuperman.reactToKryptoniteres28: String = GARRRRGH!
Person
,
for this one instance alone.override
keyword, we would have received an error message.)Summary of Key Points
- A class’s program code defines the instance variables and methods that the class’s instances (objects) have.
- When a class is instantiated, the new object gets its own copies of the instance variables, with values specific to that object.
- A class also defines which constructor parameters you must provide when you create a new instance. It’s common to store the values of constructor parameters in the new object’s instance variables.
- It’s possible, and occasionally useful, to attach additional methods to a specific instance of a class. It’s also possible to override an existing method definition with an instance-specific implementation.
- Links to the glossary: class, instance, instance variable,
to instantiate, constructor parameter;
this
; to override.
This is important stuff
Instance variables and their relationship to constructor parameters are among the trickier concepts in introductory object-oriented programming. Since they are absolutely crucial to class-based OOP and the rest of O1, we have devoted quite a few words to them in this chapter. We strongly suggest that you try to make sure you understand these concepts before advancing to the chapters that follow. Review the examples and animations above as needed and chat with the teaching assistants and your fellow students.
The term “constructor”
In this chapter, we said plenty about constructor parameters. This term, which refers to the parameters used when instantiating a class, comes from the concept of a constructor (konstruktori). A constructor is a subprogram that’s executed as soon as an object is created; its purpose is to initialize the object. Commands that assign values to instance variables are common in a constructor.
In Scala, the commands we write directly into a class definition, outside of its methods, serve as a constructor for the class. In many other programming languages, a constructor has a separate definition within a class much like methods do.
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!
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, 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 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 was created by Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi, and Paweł Stróżański with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.
object
. A class definition begins withclass
.