Lesson 7 / 25

Classes, Objects and Companions

Define classes, singleton objects and companion objects.

Object-oriented building blocks

A class declares its primary constructor in the header: class Account(val owner: String, private var balance: BigDecimal). Parameters marked val become public read-only fields, var makes them mutable, and unmarked ones are private to the class. In Scala 3, new is optional: Account("Asha", 0) works (universal apply). Methods are public by default, with private and protected modifiers available. An object is a singleton: a single instance created lazily on first use, used for utilities, constants and entry points. An object with the same name as a class in the same file is its companion object; class and companion can access each other's private members, and the companion is the place for factory methods (apply), constants and validation (def fromString(s: String): Option[Account]). Override toString, equals and hashCode when needed, though case classes (next topics) generate them for you. Use require(cond, msg) to validate constructor arguments.

Class and companion object

The class describes instances; the companion object holds factories and shared members.

Two connected boxes side by side, one showing several small instance copies and the other a single star-marked singleton.
Figure 3.1 — A class with its companion object.

A class with a companion factory

Validation, private state and a smart constructor.

class Account private (val owner: String, private var balancePaise: Long):
  require(owner.nonEmpty, "owner is required")

  def balance: Long = balancePaise

  def deposit(amountPaise: Long): Unit =
    require(amountPaise > 0, "amount must be positive")
    balancePaise += amountPaise

  def withdraw(amountPaise: Long): Either[String, Long] =
    if amountPaise > balancePaise then Left(s"Insufficient funds for $owner")
    else
      balancePaise -= amountPaise
      Right(balancePaise)

  override def toString = s"Account($owner, $balancePaise paise)"

object Account:
  val MinimumOpeningPaise = 50000L

  def open(owner: String, initialPaise: Long): Either[String, Account] =
    if initialPaise < MinimumOpeningPaise then Left("Opening balance too low")
    else Right(new Account(owner, initialPaise))   // companion can call the private constructor

@main def bank(): Unit =
  Account.open("Asha", 100000) match
    case Right(acc) => println(acc.withdraw(30000))  // Right(70000)
    case Left(err)  => println(err)

Smart constructors

Making the constructor private and exposing a companion method that returns Either or Option guarantees that every Account in your program is valid, without throwing exceptions.

Quick check: What is a companion object?

  • An object with the same name as a class in the same file, which can access its private members
  • A subclass
  • A copy of every instance
  • A Java interface
Answer

An object with the same name as a class in the same file, which can access its private members — Companions share privacy with their class and typically hold factories and constants.