Mike Slinn

Classes Part 1

— Draft —

Published 2024-08-20.
Time to read: 8 minutes.

This lecture introduces Scala classes, a major contributor to its object-oriented capability, and explores the topic in depth.

The sample code for this lecture can be found in courseNotes/src/main/scala/animals/Animals.scala.

Scala’s object-oriented design is similar to Java. The same concept of classes and abstract classes exist. Java’s interfaces have been made more powerful by Scala’s traits, discussed in the Scala Traits lecture. Java 8’s interfaces are more powerful but somewhat similar. Scala replaces Java’s static fields and methods with the concept of a companion object.

Scala also has a convenient type of class definition, called a case class, which is well-suited for modeling domain objects. A case class is a regular class with a lot of methods added by default, and an automatically defined companion object that is constructed with a standard set of methods. The Objects lecture discusses Scala objects, and the Case Classes lecture discusses case classes.

Scala Classes

A class is a logical building block that can accept parameters for constructing instances of the class definition.

Form of a Scala Class
class MyClass(param1: String, param2: Int) {
  // body of class goes here, including the constructor
}

Classes can contain methods and properties, as well as any other type of Scala artifact.

While classes are not required to accept constructor parameters, or to have methods or properties, a properly designed class should have a constructor that accepts parameters for creating an instance of the class. A class that does not accept constructor parameters should instead be defined as a trait, because traits are more flexible than classes without constructor parameters, or it should be defined as an singleton object. More on that later in this lecture.

A property is a public variable that is associated with an instance of a class; properties are either mutable or immutable, and might be read-only (so the property only has a getter – this is the default) or write-only (so the property only has a setter).

A method is a subroutine associated with an instance of a class that can be invoked in order to perform a computation.

Regular Classes Need a Body

Let’s start by defining a class called UselessAnimal that accepts two parameters to the constructor.

Class UselessAnimal
class UselessAnimal(numLegs: Int, breathesAir: Boolean)

This class is unusual and in fact useless because the parameters for the class constructor (numLegs and breathesAir) are not used, and then they are forgotten once the class is constructed. Parameters to the class constructor are available throughout the class as private variables. Since no body is defined for this class it has no constructor, no properties and no methods, so the constructor parameters serve no purpose.

Case Classes use a similar syntax to what is shown above. They are often used for data models.

Useful Class

Here is a class definition that accepts the same constructor parameters as UselessAnimal, but also provides a class body that defines a public property (property1) and two methods (walk and sing). Public properties default to being immutable, and act as if they had been declared as vals. Immutable public properties are just defined with a setter, and have no getter.

Scala class definition
class UsefulAnimal(numLegs: Int, breathesAir: Boolean) {
  val property1 = s"I have $numLegs legs"
def walk(n: Int) = (if (numLegs==0) "slither " else if (numLegs==1) "hop " else if (numLegs==2) "stomp " else if (numLegs==4) "gallop " else "crawl ") * n
def sing = if (breathesAir) "La di da!" else "You must be joking!" }

We will learn how to write a multiway branch in the Pattern Matching lecture of the Intermediate Scala course. For now, the series of if statements above will suffice.

Using the REPL

Let’s explore how Scala classes work by using one of the Scala REPLs. The Scala compiler driver can create a REPL for us, just by mentioning its name:

Shell
$ scala
Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_25).
Type in expressions to have them evaluated.
Type :help for more information.
scala>

Now we can create an instance of the UsefulAnimal class and play with it.

Like Java classes, Scala classes have an automatically generated toString method, which just prints out the name of the class and the memory address of the class instance.

The Scala REPL automatically invokes toString whenever a new class instance is created:

Scala REPL
scala> val monkey = new UsefulAnimal(2, true)
monkey: UsefulAnimal = UsefulAnimal@31190526
scala>
monkey.property1 res0: String = I have 2 legs
scala>
monkey.walk(3) res2: String = "stomp stomp stomp"
scala>
monkey.walk 3 res2: String = "stomp stomp stomp"
scala>
monkey.sing res3: String = La di da!

Notice that the first time monkey.walk(3) was computed, dot notation was used, but the second time monkey.walk 3 was computed, infix notation was used. The choice of the type of notation used is up to the programmer.

That was so much fun, let’s create another instance of UsefulAnimal and play with it also. This instance will have zero legs, so it will slither instead of stomping. Since breathesAir is assigned the value false, we are not surprised to learn that fish cannot sing.

Scala REPL
scala> val fish = new UsefulAnimal(0, false)
fish: UsefulAnimal = UsefulAnimal@31ea9581
scala>
fish.property1 res4: String = I have 0 legs
scala>
fish.walk(4)
res5: String = "slither slither slither slither"
scala>
fish.sing res6: String = You must be joking!

Notice that the two UsefulAnimal instances are quite separate. Their characteristics differ as a result of the values of the parameters pass to the class constructor. Class instances share behavior as defined in the class’s methods, however class instances do not share data between themselves. We will discuss how individual class instances might have a special relationship in the Deceptively Familiar: Access Modifiers lecture.

Missing Parameters

This is what happens if you forget to specify one of the constructor parameter values.

Scala REPL
scala> val oops = new UsefulAnimal(0)
-- [E171] Type Error: ----------------------------------------------------------
1 |val oops = new UsefulAnimal(0)
  |           ^^^^^^^^^^^^^^^^^^^
  |missing argument for parameter breathesAir of constructor UsefulAnimal in class UsefulAnimal: (numLegs: Int, breathesAir: Boolean): UsefulAnimal
1 error found 

Primary Constructor

In Scala, all of the code in a class that is not part of a method definition or an inner class definition is part of the primary constructor. The primary constructor is not enclosed in a single method, as is the case with Java. The following shows a bad practice – spreading primary constructor code throughout a class:

Scala class with scattered primary constructor
class BadAnimal(numLegs: Int, breathesAir: Boolean) {
  val x = 3 // part of primary constructor
  println(breathesAir) // part of primary constructor

  // This is an example of a method definition, a getter, that uses string interpolation:
  def whatAmI = s"$breathesAir; $numLegs legs"

  val y = 3 // part of primary constructor
}

The problem with the above is that you are never really sure if you have seen all of the primary constructor code unless you read every line of the class definition. Here is a better way of writing the same class:

Scala class with clearly distinct primary constructor
class GoodAnimal(numLegs: Int, breathesAir: Boolean) {
  // The primary constructor consists of these 3 lines:
  val x = 3
  println(breathesAir)
  val y = 3

  // method definition, a getter:
  def whatAmI = s"$breathesAir;$numLegs legs" // Example of string interpolation
}

The Auxiliary Constructors lecture is dedicated to the above topic.

Subclasses

We can create a specialized version of a class by defining a subclass. Subclasses inherit the properties and methods of their superclass.

We can derive another class from UsefulAnimal that extends the definition in order to add methods and properties; the resulting extended class definition is called a subclass, and the act of creating a class that extends another class is called subclassing.

Forwarding All Class Constructor Parameters to the Superclass Constructor

Notice how the parameters for the Kaiju subclass constructor are forwarded to the primary constructor of the superclass, which is UsefulAnimal. This is common, but we could provide other values to the superclass constructor as we will see shortly.

Scala code
object Monster extends App {
  class Kaiju(numLegs: Int, breathesAir: Boolean)
        extends ConcreteClass.UsefulAnimal(numLegs, breathesAir) {
    var totalSteps = 0

    override def walk(steps: Int): String = {
      totalSteps = steps + totalSteps
      "stomp " * steps
    }

    override def sing = "Roaarr!"
  }

  val gojira = new Kaiju(2, true)
  println("gojira.sing=${gojira.sing}")
  println("gojira.walk(10)=${gojira.walk(10)}")
  println("gojira.walk(5)=${gojira.walk(5)}")
  println("gojira.totalSteps=${gojira.totalSteps}")
}

To run this code, type:

Shell
$ sbt "runMain Monster"

The Kaiju class redefines, or overrides the walk and sing methods defined in the UsefulAnimal superclass.

The Kaiju class also defins a mutable public property called totalSteps of type Int that was not present in the superclass.

Now let’s create and exercise an instance of Kaiju.

Scala REPL
scala> val gojira = new Kaiju(2, true)
gojira: Kaiju = Kaiju@6a400542
scala>
gojira.sing res7: String = Roaarr!
scala>
gojira.walk(10) stomp stomp stomp stomp stomp stomp stomp stomp stomp stomp
scala>
gojira.walk(5) stomp stomp stomp stomp stomp
scala>
gojira.totalSteps res8: Int = 15

You can query or set the value of the Kaiju instance’s totalSteps property easily:

Scala REPL
scala> gojira.totalSteps = 0
gojira.totalSteps: Int = 0
scala>
gojira.totalSteps res9: Int = 0

Forwarding Some Class Constructor Parameters to the Superclass Constructor

We can forward the values of selected parameters from a subclass’s primary constructor to its superclass constructor, like this:

Scala REPL
scala> class KingGhidorah(numLegs: Int, numHeads: Int) extends Kaiju(numLegs, true)
defined class KingGhidorah 

Notice that the numLegs parameter for the KingGhidorah subclass is forwarded to the Kaiju superclass’s constructor, while the breathesAir parameter for the Kaiju superclass is always set true. All the other behavior and properties of the Kaiju superclass is inherited by the KingGhidorah subclass:

Scala REPL
scala> val kingGhidorah = new KingGhidorah(2, 3)
kingGhidorah: KingGhidorah = KingGhidorah@336f1079
scala>
kingGhidorah.walk(7) res10: String = "stomp stomp stomp stomp stomp stomp stomp"
scala>
kingGhidorah.sing res11: String = Roaarr!
scala>
kingGhidorah.totalSteps res12: Int = 7

Identical Instances Versus a Singleton

Let’s define a subclass of Kaiju called Mothra which does not forward any parameters to the superclass constructor:

Scala REPL
scala> class Mothra extends Kaiju(6, true)
defined class Mothra 

Notice that the extends keyword is followed by an invocation of the Kaiju superclass constructor, and constant values are passed in for all of the Kaiju constructor parameters. The Mothra subclass constructor does not accept any parameters, so each time you create an instance of Mothra you get an identical but distinct instance.

If you actually intended to define multiple identical instances (which would be unusual), then defining a class that does not accept any parameters for its constructor is fine. If, however, you really wanted to define the one and only Mothra (a singleton) then you should define a Scala object, which we will discuss in the Objects lecture.

Let’s create an instance of Mothra and exercise it. Behavior is as we expect:

Scala REPL
scala> val mothra = new Mothra
mothra: Mothra = Mothra@4d6025c5
scala>
mothra.walk(5) stomp stomp stomp stomp stomp
scala>
mothra.totalSteps res8: Int = 5

Abstract Classes

The above classes are called concrete classes because every declared method and property is defined.

An abstract class can contain declarations but not definitions for some or all properties or methods. An abstract class cannot be instantiated, instead, a concrete subclass that is derived from an abstract class can be instantiated.

DomesticAnimal is an abstract class that declares a method (eat) but does not define it, and declares a public property called favoriteFood that is also not defined. Because the method signature for eat is declared but no implementation for the method is defined, this an abstract method.

Similarly, because only the favoriteFood property’s name and type are declared, and no value is provided, it is an abstract property. Note that the toString method must decorated with the override keyword because this method definition replaces the default toString method that Scala automatically creates for every class.

Scala abstract class definition
abstract class DomesticAnimal(name: String, numLegs: Int, breathesAir: Boolean)
  extends UsefulAnimal(numLegs, breathesAir) {
  val favoriteFood: String
def eat(n: Int): String
override def toString = s"I am an animal called $name that likes to eat $favoriteFood like this: ${eat(3)}" }

We can define a concrete subclass of DomesticAnimal by extending it and providing definitions for the abstract members in the subclass.

Scala concrete subclass definition
class Horse(name: String) extends DomesticAnimal(name, 4, true) {
  val favoriteFood: String = "oats"
def eat(n: Int) = "Munch " * n + "!" }

When To Decorate With override

Notice that neither the favoriteFood property nor the eat method were decorated with the override keyword. This is because they merely provided initial definitions for the abstract class members, and did not redefine a pre-existing definition. We could preface these definitions with the override keyword if we wished, but that is a choice of personal style. My own style is not to type any more characters than necessary.

Let’s create an instance of Horse:

Scala REPL
scala>  new Horse("Bucephalus")
// defined class Horse 

Marking a Class Abstract

Abstract classes can define some or all properties and methods as abstract; if all properties and methods are defined then only the abstract keyword prevents the class from being able to be instantiated.

Let’s define an abstract class called Animal that uses the parameters to the constructor to compute an immutable public property called msg. By decorating the class with the abstract keyword, only subclasses of Animal can be instantiated. Every declared method and property in this class is defined, however, the programmer for some reason wished to prevent instances of Animal from being created and required that only subclasses could be defined and instantiated.

Scala REPL
scala> abstract class Animal(numLegs: Int, breathesAir: Boolean) {
     |   private val breatheMsg = if (breathesAir) "" else " do not"
     |   val msg = s"I have $numLegs legs and I$breatheMsg breathe air"
     | }
defined class Animal 

Up to now, all classes have been defined in the top-level, or root package. That is not desirable because you cannot access those definitions from other packages. In the sample code, I placed the definition of Animal within the animals package so we can refer to it with the following import statement:

Scala code
import animals.Animal

Class Properties

Let’s define a concrete class called Frog1 which extends the abstract Animal class.

Decorating a Scala class constructor parameter with var or val causes it to also become a public property of the class.

Decorating with val makes the property immutable, while decorating with var makes it a mutable property.

Note how two of the new Frog1 class’s constructor parameters are forwarded to the abstract superclass’s constructor. Frog1 also defines an immutable public property called canSwim.

Scala REPL
scala> class Frog1(var canSwim: Boolean, val numLegs: Int, breathesAir: Boolean)
extends Animal(numLegs, breathesAir)
defined class Frog1 

You can create an instance of a Frog1 by passing in values for each constructor parameter.

Scala REPL
scala> val frog1 = new Frog1(false, 2, true)
frog1: Frog1 = Frog1@3b9e714c 

You can query a property like this:

Scala REPL
scala> frog1.canSwim
res6: Boolean = true 

Public properties declared as vars are mutable, so they are automatically defined with a setter and a getter. Let’s teach this frog to swim:

Scala REPL
scala> frog1.canSwim = true

Digging Deeper

Unless decorated with val or var, class constructor parameters are only accessible from within the class. Constructors are special methods, and method parameters behave as if they are private variables within the enclosing scope. For the primary constructor of a Scala class, the enclosing scope is the entire body of the class. For a method, the enclosing scope is the body of the method.

In the previous section I said that decorating a Scala class constructor argument with var or val causes it to become a public property of the class. This is true whether or not the parameter is used within the body of the class. If a constructor parameter is turned into a public property with the val or var keywords, and it is not used, it is still available outside the class. To make this clear, try the following in a Scala REPL.

In class Macaw, the size and weight constructor parameters are only accessible from inside Macaw class instances, while the age is available inside and outside class instances:

Scala REPL
scala> class Macaw(size: Double, weight: Double) {
     |   val age: Int = 42
     |   println(s"Macaw: size=$size inches; weight=$weight pounds; age=$age years")
     | }
defined class Macaw
scala>
val macaw = new Macaw(32, 2.8) Macaw: size=32 inches; weight=2.8 pounds; age=42 years
scala>
macaw.age res1: Int = 42 years
scala>
macaw.size // does not compile <console>:10: error: value size is not a member of Macaw macaw.size ^

Even though the Toucan class has no body, it is still useful for modeling domain objects because its constructor parameters are decorated with the val keyword so they become properties of the class:

Scala REPL
scala> class Toucan(val size: Double, val weight: Double, val billLength: Double)
defined class Toucan
scala>
val toucan = new Toucan(27, 3.7, 9.3) toucan: Toucan = Toucan2@4ed5eb72
scala>
toucan.size res3: Double = 27
scala>
toucan.weight res8: Double = 3.7
scala>
toucan.billLength res9: Double: 9.3

Notice what happens if we forget to decorate one of the parameters with the val keyword:

Scala REPL
scala> class Toucan2(val size: Double, val weight: Double, <>billLength: Double)
defined class Toucan
scala>
val toucan2 = new Toucan2(27, 3.7, 9.3) toucan: Toucan2 = Toucan2@4ed5eb72
scala>
toucan2.size res3: Double = 27
scala>
toucan2.weight res8: Double = 3.7
scala>
toucan2.billLength // this parameter was not decorated with val so it is not a property <console>:11: error: value billLength is not a member of Toucan toucan.billLength

Exercise – Subclassing Animal

Define a new Animal subclass called Bird. Give it a Boolean constructor property called canFly, and another Double property called topSpeed. Create a few Bird instances with different top speeds.

Hint: If the class you defined for your solution resides in a different package than the package that Animal was defined in, don’t forget to import animals.Animal:

Scala code
import animals.Animal

Solution

This solution provides the constants 2 and true to the Animal superclass constructor.

Scala code
package solutions
import animals.Animal
class Bird(val canFly: Boolean, val topSpeed: Double) extends Animal2(2, true) { override def toString = s"canFly=$canFly; topSpeed=$topSpeed" }
object Birds1 extends App { val crane = new Bird(true, 55) val emu = new Bird(false, 25) println(s"Crane $crane") println(s"Emu $emu") }

This solution is provided in src/main/scala/solutions/Bird1.scala. Don’t look at Bird2.scala – that is for a later exercise. You can run this solution by typing:

Shell
$ sbt "runMain solutions.Birds1"
Crane canFly=true; topSpeed=55.0
Emu canFly=false; topSpeed=25.0

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.