1. Overview

In this tutorial, we’ll be focusing on Scala‘s take on two core OOP elements; classes and objects.

We’ll start by covering classes before delving into implicit classes and inner classes. Then we’ll take a look at Scala Objects.

To conclude, we’ll also learn about some of the differences between objects and classes in Scala.

2. Classes

Classes are blueprints for creating objects. When we define a class, we can then create new objects (instances) from the class.

We define a class using the class keyword followed by whatever name we give for that class.

Let’s see how we can create a simple class in the terminal with the Scala REPL:

scala> class Vehicle
defined class Vehicle

scala> var car = new Vehicle
car: Vehicle = [email protected]

scala>

What we’ve got now is a Vehicle class with a no-argument constructor.

In Scala, class constructors are generally much more concise and easier to read than in Java.

The language supports two types: primary and auxiliary.

2.1. Primary Constructor

By default, every Scala class has a primary constructor. The primary constructor consists of the constructor parameters, the methods called in the class body, and the statements executed in the body of the class.

Let’s define a class called Abc whose constructor takes a String and an Int:

class Abc(var a: String = "A", var b: Int) {
  println("Hello world from Abc")
}

This time, instead of a no-argument constructor provided by Scala, this class has a two-argument constructor we defined by listing them next to the class name. The parameters at the top and statements in the body make up the constructor.

Note also that for one of our parameters, a, we’ve specified a default; “A”.

In Java, this would have been a bit lengthier; we would have to create a special method Abc as a constructor to initialize the object.

When an instance of Abc is created, we see the output from the println statement:

scala> val abc = new Abc(b=3)
Hello world from Abc
abc: Abc = [email protected]

This is because all the statements and expressions in the class body are part of the constructor.

2.2. Auxiliary Constructor

To define auxiliary (or secondary) constructors, we define methods named this:

val constA = "A"
val constB = 4

class Abc(var a: String, var b: Int) {
  def this(a: String) {
    this(a, constB)
    this.a = a
  }

  def this(b: Int) {
    this(constA, b)
    this.b = b
  }

  def this() {
    this(constA, constB)
  }
}

Using these auxiliary constructors, we can create our class instances in several different ways:

new Abc("Some string")
new Abc(1)
new Abc()

There are two rules to bear in mind when defining auxiliary constructors:

    • Each constructor must have a unique signature; the parameter set must be different from the rest of the constructors
    • Each constructor must call one of the initial constructors or the base class constructor

2.3. Class Instance

We can think of classes more like templates for creating objects. A class instance is the actual object created using the class as a template. As we’ve already seen, we use the keyword new to create an instance of a class.

Let’s say we have a Vehicle template which we can use to produce different kinds of vehicles. Our Vehicle template represents the class which does not represent a real vehicle. When we create a car using our Vehicle template, the car is an instance of the Vehicle.

In our last example, we created an instance of the Vehicle class called car. In most cases, the class and the instance are not as simple as our Vehicle and car example.

Let’s build on our vehicle example by adding some new interesting features. To begin, we’ll create a new class called Car:

class Car (val manufacturer: String, brand: String, var model: String) {
  var speed: Double = 0;
  var gear: Any = 0;
  var isOn: Boolean = false;

  def start(keyType: String): Unit = {
    println(s"Car started using the $keyType")
  }

  def selectGear(gearNumber: Any): Unit = {
    gear = gearNumber
    println(s"Gear has been changed to $gearNumber")
  }

  def accelerate(rate: Double, seconds: Double): Unit = {
    speed += rate * seconds
    println(s"Car accelerates at $rate per second for $seconds seconds.")
  }

  def brake(rate: Double, seconds: Double): Unit = {
      speed -= rate * seconds
      println(s"Car slows down at $rate per second for $seconds seconds.")
  }

  def stop(): Unit = {
    speed = 0;
    gear = 0;
    isOn = false;
    println("Car has stopped.")
  }
}

Now with our Car class, we can create as many instances as we wish. Let’s load our code into the terminal:

scala> :load path/to/my/scala/File.scala
args: Array[String] = Array()
Loading path/to/my/scala/File.scala
defined class Car

and create an object, familyCar, from our Car class:

scala> var familyCar = new Car("Toyota", "SUV", "RAV4")
familyCar: Car = [email protected]

scala>

Our familyCar variable is an instance of the Car class, possessing all the attributes of Car.

Now we can try using our familyCar variable:

scala> familyCar.start("remote")
Car started using the remote

scala> familyCar.speed
res0: Double = 0.0

scala> familyCar.accelerate(2, 5)
Car accelerates at 2.0 per second for 5.0 seconds.

scala> familyCar.speed
res1: Double = 10.0

scala> familyCar.brake(1, 3)
Car slows down at 1.0 per second for 3.0 seconds.

scala> familyCar.speed
res2: Double = 7.0

2.4. Extending a Class

Extending a class gives us the ability to create a new class that inherits all the properties from the first class. We extend a class using the extends keyword.

Let’s create a new class called Toyota by extending our Car class:

class Toyota(transmission: String, brand: String, model: String) extends Car("Toyota", brand, model) { 
  override def start(keyType: String): Unit = { 
    if (isOn) {
      println(s"Car is already on.") 
      return
    } 
    if (transmission == "automatic") { 
      println(s"Car started using the $keyType") 
    } else { 
      println(s"Please ensure you're holding down the clutch.") 
      println(s"Car started using the $keyType") 
    } 
    isOn = true  
  } 
}

If we want to provide different behavior in one of the methods, we can override it to define our custom behavior. As we can see in our Toyota class, we’ve overridden the start method.

Now let’s see what our Toyota class has in common with the Car class and what has changed:

scala> val prado = new Toyota("Manual", "SUV", "Prado")
prado: Toyota = [email protected]

scala> prado.start("key")
Please ensure you're holding down the clutch.
Car started.

scala> prado.accelerate(5, 2)
Car accelerates at 5.0 per second for 2.0 seconds.

scala> prado.speed
res0: Double = 10.0

scala>

We now have a start method that reminds us to hold the clutch.

Since we’ve not overridden any other methods, the rest of the methods will work the same way as they do in Car.

2.5. Implicit Classes

Implicit classes (introduced in Scala 2.10) provide a way of adding new functionality to an existing object. This comes in handy, especially when we don’t have the option of modifying the source object.

We define an implicit class with the implicit keyword. For example, let’s create an implicit class that adds a method to the String class:

object Prediction {
  implicit class AgeFromName(name: String) {
    val r = new scala.util.Random
    def predictAge(): Int = 10 + r. nextInt(90)
  }
}

In this example, we created an implicit class AgeFromName with a predictAge method that returns a random integer between 10 and 100. Don’t worry about the object keyword, we’ll learn more about it in the coming section.

Provided we’ve got our implicit class within the scope, we can call the predictAge method on any string:

scala> import Prediction._
import Prediction._

scala> "Faith".predictAge()
res0: Any = 89

scala> "Faith".predictAge()
res1: Any = 74

It’s worth noting that creating an implicit class has some restrictions:

1. They must be defined inside of another trait/class/object. In our AgeFromName example, we placed it in an object.

2. They may only take one non-implicit argument in their constructor. We can do:

implicit class AgeFromName(name: String)
implicit class AgeFromName(name: String)(implicit val a: String, val b: Int)

but not:

implicit class AgeFromName(name: String, val a: Int)

3. They should be unique. In other words, there may not be any method, member, or object in scope with the same name as the implicit class.

4. An implicit class cannot be a case class.

2.6. Inner Classes

Scala offers us the ability to nest classes inside another class. Scala inner classes are bound to the outer object.

Let’s see a simple example:

class PlayList {
  var songs: List[Song] = Nil
  def addSong(song: Song): Unit = {
    songs = song :: songs
  }
  class Song(title: String, artist: String)
}

class DemoPlayList {
  val funk = new PlayList
  val jazz = new PlayList
  val song1 = new funk.Song("We celebrate", "Laboriel")
  val song2 = new jazz.Song("Amazing grace", "Victor Wooten")
}

We can now go ahead and add the appropriate song to our funk and  jazz playlists in DemoPlayList:

scala> val demo = new DemoPlayList
demo: DemoPlayList = [email protected]

scala> demo.funk.addSong(demo.song1)

scala> demo.jazz.addSong(demo.song2)

scala> demo.funk.songs
res0: List[demo.funk.Song] = List([email protected])

scala> demo.jazz.songs
res1: List[demo.jazz.Song] = List([email protected])

scala>

Everything worked as expected because we added the right song to the right playlist: song1 to funk, song2 to jazz.

Looking at DemoPlayList again, we see that song1 and song2 are members of funk and jazz respectively. Though song1 and song2 are instances of Song, they don’t belong to the same PlayList instance and therefore are of different types.

What this implies is that we cannot add song1 to jazz:

scala> demo.jazz.addSong(demo.song1)
                              ^
       error: type mismatch;
        found   : demo.funk.Song
        required: demo.jazz.Song

scala>

The same applies to song2, we cannot add it to funk:

scala> demo.funk.addSong(demo.song2)
                              ^
       error: type mismatch;
        found   : demo.jazz.Song
        required: demo.funk.Song

scala>

This behavior is because Scala inner classes are bound to the outer object.

3. Objects

Remembering our car example from earlier, car is an object, capable of doing everything a Vehicle can do. While classes give us the template, objects are what we create from the template.

Generally speaking in OOP, it’s perfect to say that objects are instances of a class. However, Scala has an object keyword that we can use when defining a singleton object.

When we say singleton, we mean an object that can only be instantiated once. Creating an object requires just the object keyword and an identifier:

object SomeObject

Objects do not take any parameters, but we can define fields, methods, and classes just as in regular classes:

object Router {
  val baseUrl: String = "https://www.baeldung.com"
  
  case class Response(baseUrl: String, path: String, action: String)
  def get(path: String): Response = {
    println(s"This is a get method for ${path}")
    Response(baseUrl, path, "GET")
  }

  def post(path: String): Response = {
    println(s"This is a post method for ${path}")
    Response(baseUrl, path, "POST")
  }

  def patch(path: String): Response = {
    println(s"This is a patch method for ${path}")
    Response(baseUrl, path, "PATCH")
  }

  def put(path: String): Response = {
    println(s"This is a put method for ${path}")
    Response(baseUrl, path, "PUT")
  }

  def delete(path: String): Response = {
    println(s"This is a delete method for ${path}")
    Response(baseUrl, path, "DELETE")
  }
}

We can use any of the members of our Router object by importing them. Let’s import everything in Router:

scala> import Router._
import Router._

scala> Response("some url", "some path", "GET")
res1: Router.Response.type = Response(some url,some path,GET)

scala> baseUrl
Here we go about Routing!
res2: String = https://www.baeldung.com

scala> get("/index")
This is a get method for /index
res4: Router.Response = Response(https://www.baeldung.com,/index,GET)

scala> put("/index")
This is a put method for /index
res5: Router.Response = Response(https://www.baeldung.com,/index,PUT)

scala> post("/scala-tutorials")
This is a post method for /scala-tutorials
res6: Router.Response = Response(https://www.baeldung.com,/scala-tutorials,POST)

scala>

We can see that when we accessed baseUrl, “Here we go about Routing!” was printed. This is because an object is instantiated lazily when we reference it. We can also see that next time we access baseUrl our message is not printed.

Java doesn’t have any direct equivalent to singleton object. For every Scala singleton object, the compiler creates a Java class (with a dollar sign added to the end) for the object and a static field named MODULE$ to hold the single instance of the class. So, to ensure there is only one instance of an object, Scala uses a static class holder.

3.1. Companion Objects

A companion object is an object with the same name and in the same file as a class; conversely, the class is the object’s companion class. We are going to modify our Router and create a companion class that will utilize it as a companion object:

object Router {
  //..
}

class Router(path: String) {
  import Router._
  def get(): Response = getAction(path)
  def post(): Response = postAction(path)
  def patch(): Response = patchAction(path)
  def put(): Response = putAction(path)
  def delete(): Response = deleteAction(path)
}

To see how this works, we’ll be using the Scala command-line :paste command to get our code on the terminal:

scala> :paste path/to/my/scala/File.scala
Pasting file path/to/my/scala/File.scala...
defined object Router
defined class Router

scala> val indexRouter = new Router("/index")
indexRouter: Router = [email protected]

scala> indexRouter.get()
Here we go about Routing!
This is a get method for /index
res0: Router.Response = Response(https://www.baeldung.com,/index,GET)

scala> indexRouter.post()
This is a post method for /index
res1: Router.Response = Response(https://www.baeldung.com,/index,POST)

scala> indexRouter.delete()
This is a delete method for /index
res2: Router.Response = Response(https://www.baeldung.com,/index,DELETE)

scala>

Though the methods in the Router object are private, the Router companion class can access them.

One other common use of the companion object is the creation of factory methods.

Let’s take a common use case where we have four environments in our application: test, int, staging, and production environments. We want a factory that will generate our current environment from a string. Also, we want the environment to be serializable so we can keep the state.

We can achieve this using a companion object and class:

sealed class BaeldungEnvironment extends Serializable {val name: String = "int"}

object BaeldungEnvironment {
  case class ProductionEnvironment() extends BaeldungEnvironment {override val name: String = "production"}
  case class StagingEnvironment() extends BaeldungEnvironment {override val name: String = "staging"}
  case class IntEnvironment() extends BaeldungEnvironment {override val name: String = "int"}
  case class TestEnvironment() extends BaeldungEnvironment {override val name: String = "test"}

  def fromEnvString(env: String): Option[BaeldungEnvironment] = {
    env.toLowerCase match {
      case "int" => Some(IntEnvironment())
      case "staging" => Some(StagingEnvironment())
      case "production" => Some(ProductionEnvironment())
      case "test" => Some(TestEnvironment())
      case e => println(s"Unhandled BaeldungEnvironment String: $e")
        None
    }
  }
}

We can verify the environments using a simple unit test:

@Test
def givenAppropriateString_whenFromEnvStringIsCalled_thenAppropriateEnvReturned(): Unit ={
  val test = BaeldungEnvironment.fromEnvString("test")
  val int = BaeldungEnvironment.fromEnvString("int")
  val stg = BaeldungEnvironment.fromEnvString("staging")
  val prod = BaeldungEnvironment.fromEnvString("production")

  assertEquals(test, Some(TestEnvironment()))
  assertEquals(int, Some(IntEnvironment()))
  assertEquals(stg, Some(StagingEnvironment()))
  assertEquals(prod, Some(ProductionEnvironment()))
}

4. Difference Between Scala Classes and Objects

Now that we have a better understanding of Scala classes and objects, let’s take a look at some of the differences:

  1. Definition: A class is defined with the class keyword while an object is defined using the object keyword. Also, whereas a class can take parameters, an object can’t take any parameter
  2. Instantiation: To instantiate a regular class, we use the new keyword. For an object, we don’t need the new keyword
  3. Singleton vs Multi-instance: While a class can have an unlimited number of instances, an object has just one instance created lazily when we first reference it
  4. Inheritance: Since an object is a singleton, it cannot be inherited/extended; doing so will result in creating more than an instance of the object – a class, on the other hand, can be extended

5. Conclusion

In this tutorial, we looked at Scala classes and objects using a series of simple examples. We learned about inner classes, implicit classes, companion objects, and their applications. Then, we concluded things by understanding some of the differences between classes and objects in Scala.

As always, the full source code of the article is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments