This course has already ended.

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

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

Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.

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


Chapter 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.

Related Modules: IntroOOP, Odds (new).

../_images/person06.png

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 as this.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"
}
The definition of a singleton object begins with object. A class definition begins with class.
After the class name, we define the constructor parameters that need to be provided when instantiating the class. A singleton object doesn’t have this bit.
We can use the parameter variables to initialize the object. Here, we store the parameter values in the attributes of the new Employee instance. (More on that below.)
The methods of our singleton and class are exactly the same!
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

class Pet(nameParameter: String, speciesParameter: String) {

  val species = speciesParameter
  var name = nameParameter

  // methods go here

}

Examine the simple class above. In particular, consider the line val species = speciesParameter. Also bring to mind the earlier animation of creating employee objects. Which of the following claims are correct?

Suppose we have already executed the following commands:

import o1.classes.Employee
val a = new Employee("Eugenia Enkeli", 1965, 5000)
val b = new Employee("Teija Tonkeli", a.yearOfBirth, a.monthlySalary - 1000)

Which of the following claims are correct?

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

}
We can write 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.
In the more verbose version above, we had to name the parameter variables separately. Those names don’t feature at all in the compact version of the class. In the shorter code, the constructor parameters have the same names as the instance variables; their values are copied into the instance variables without an explicit command to do so.
Our example class has a fourth instance variable, which doesn’t receive its value from a constructor parameter. Its definition is identical in both versions.
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:

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

Let’s add a method dimensions in the rectangle class. This method returns a textual description of the form “X by Y”.

There are four attempts to implement the method below. Which of them work?

If you want, you try out any of the implementations by adding it in your rectangle class.

def dimensions = this.side1 + " by " + this.side2
def dimensions = s"$this.side1 by $this.side2"
def dimensions = s"$side1 by $side2"
def dimensions = s"${this.side1} by ${this.side2}"

Four of the following six statements are correct. Which four?

Assignment: An Account Class

Locate the code of the account object that you created in Chapter 2.2. What would it take to turn that singleton object into an account class that you can instantiate to produce multiple distinct account objects? Each of the instances would work just like the singleton you wrote.

Which of the following suggestions are sensible? If you want, go ahead and implement the chosen suggestions as code.

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 to side1, whose height is equal to side2, and whose color is determined by makePic’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)
We create an instance of the class, a monster object. The constructor parameters specify the type of this monster and its health when it’s at full strength.
We can ask any monster object to tell us its kind as well as its maximum health and current health. A monster starts at full health, so the two numbers are identical for now.
We can also ask the monster for a string that describes the monster. This description includes information about the monster’s current and maximum health.

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)
We pass a parameter to the 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.
We can observe the change by asking the monster for its updated health or description.

Now let’s see about implementing such a class:

class Monster(val kind: String, val healthMax: Int) {

  var healthNow = healthMax

  val description = this.kind + " (" + this.healthNow + "/" + this.healthMax + ")"

  def sufferDamage(healthLost: Int) = {
    this.healthNow = this.healthNow - healthLost
  }

}

That code is available in the IntroOOP module. You can try it in the REPL.

Study the class carefully. Which of the following claims are correct?

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 and will. 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.
    • Notice the multiplication by 1.0 to produce a decimal number; without it, the method would almost always return zero (cf. Chapter 1.3). If this feels like an unpleasant bit of trickery, console yourself with the knowledge that Chapter 5.2 will introduce a more elegant solution.
  • 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, return 6/2, not 3/1.
    • If you run into trouble constructing the string, return to check the description method in the rectangle assignment.
  • Can you implement decimal and winnings 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.")

}
Each person has a method that commands the person to say something. The method returns a string that contains the person’s name and the sentence they utter.
A person can also be commanded to react to certain things. The person reacts by saying stuff, and therefore...
... it’s convenient to implement these methods by calling the persons’s own 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
We create a person as per usual, except that...
... we add a method on the fly, to this one instance alone.

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!
We redefine a method already defined in class Person, for this one instance alone.
The redefinition overrides (korvata) the definition in the class. In order to prevent us from accidentally doing something like this, Scala sensibly requires us to mark the fact that we actually want to override an existing definition. (Had we omitted the 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.

a drop of ink
Posting submission...