Objects & Companions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI and Scala 3.10.0 on JDK 26 on 2026-10-15 (the examples were not re-run on 3.9 or 3.3). None of the features used is newer than 3.3, so they are available on the 3.9 LTS and 3.3 LTS lines as well; only the wording of some diagnostics depends on the compiler version. Scala has no static keyword. Everything that Java puts in static members lives instead in an object: a class that has exactly one instance, created by the language. An object that shares its name and its source file with a class or trait is that type’s companion, and the pair gets special access rights to each other. This page covers the singleton object, the companion pair and the apply factory idiom; the class side of the pair is described in Classes & Constructors, and the objects that come with enum and case class are in Enums and Case Classes.

The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.

The singleton object

An object declaration defines a class and creates its only instance in one step. The name of the object refers to that instance, so its members are called as Name.member, which is how Java calls static methods.

//> using scala 3.10.0

object TrailMath:
  private val MinutesPerKm = 12
  def walkingMinutes(km: Double, ascentM: Int): Int =
    math.round(km * MinutesPerKm + ascentM / 100.0 * 10).toInt

object Registry:
  println("Registry initialised")
  private var names = List.empty[String]
  def add(n: String): Unit = names = n :: names
  def all: List[String] = names.reverse

@main def o1(): Unit =
  println("start of main")
  println(TrailMath.walkingMinutes(7.5, 640))
  println("before first use of Registry")
  Registry.add("Aguja")
  Registry.add("Pico")
  println(Registry.all)
  println(Registry.getClass.getName)
start of main
154
before first use of Registry
Registry initialised
List(Aguja, Pico)
Registry$

What the output shows:

  • An object is created lazily, on first access. "Registry initialised" appears after "before first use of Registry", not at program start. The first touch of the name runs the body of the object (its constructor) exactly once. The JVM guarantees that, even with several threads touching the object at once, the body runs once and every thread sees the fully built instance.

  • An object body can hold val, var, def, nested classes and statements. Statements in the body run at initialisation, as the println shows. State kept in an object is global to the JVM (more precisely to the class loader), so a mutable one like names is shared by every caller and must be thread-safe when threads are used.

  • The JVM class is Registry$. The compiler generates a class named after the object plus a $, and the single instance is held in a static field MODULE$. This is why Java code sees Registry$.MODULE$.add("x"), and why the compiler also emits static forwarders on a companion class (see below) for use from Java.

  • An object is a value. It can be passed around, extended and stored: val r = Registry is the same instance as Registry.

Objects that extend types

An object can extend one class and any number of traits, so a singleton can implement an interface and be passed wherever the interface is expected.

//> using scala 3.10.0

trait Greeter:
  def greet(n: String): String

object Polite extends Greeter:
  def greet(n: String) = s"Good day, $n"

object Hike:
  def main(args: Array[String]): Unit =
    println(Polite.greet("Ana"))
    val g: Greeter = Polite
    println(g.greet("Luis"))
    println(Polite.isInstanceOf[Greeter])
Good day, Ana
Good day, Luis
true

This is what replaces both Java’s static utility classes and its one-instance classes (the singleton pattern). Unlike a Java singleton, the object needs no private constructor, no getInstance() and no double-checked locking, and unlike a Java static member it can implement an interface and be handed to code that expects one.

The Hike object above also shows the other common use of an object: a public static void main(String[]) entry point. A method def main(args: Array[String]): Unit in a top-level object is a JVM entry point, and Scala 3 adds @main methods as a shorter spelling; see Hello World & @main Methods.

Utility modules

A group of related functions, constants and helper types with no state of their own is a module. In Scala 2 and in much Scala 3 code it is an object, like TrailMath above, and the standard library is full of them: scala.math, scala.util.Using, scala.collection.mutable.ArrayBuffer (the companion object holds its factory methods). Since Scala 3.0 a function or value can also sit directly at the top level of a file, with no object around it, and the compiler wraps it in a synthetic object named after the file (o1$package for o1.scala). Reach for a named object when the module needs a name of its own, extends a trait, holds state or has to be imported selectively; use top-level definitions for plain helpers. Both forms are compared in Packages, Imports & Top-Level Definitions.

Companion classes and objects

A class (or trait) and an object with the same name, declared in the same source file, are companions. Each can read the other’s private members, which makes the companion the natural home for everything that would be static in Java, for constructors that must be controlled, and for helpers that need access to the class internals.

//> using scala 3.10.0

class Trail private (val name: String, private val km: Double):
  def describe: String = s"$name ($km km)"
  def longerThan(other: Trail): Boolean = km > other.km

object Trail:
  private var created = 0
  def apply(name: String, km: Double): Trail =
    created += 1
    new Trail(name.trim, km)
  def count: Int = created
  def fromCsv(line: String): Trail =
    val Array(n, k) = line.split(",")
    Trail(n, k.toDouble)

@main def o2(): Unit =
  val a = Trail("  Aguja ", 7.5)
  val b = Trail.fromCsv("Pico,12.0")
  println(a.describe)
  println(b.longerThan(a))
  println(Trail.count)
Aguja (7.5 km)
true
2

Three things happen in the example:

  • The constructor is private, so only the class and its companion can write new Trail(...). The companion’s apply is the single public way to build a Trail, and it is where validation or normalisation (here trim) is put.

  • The class reads other.km, a private member of another instance. private in Scala means "private to the class and its companion", and other.km on a different instance is allowed, the same as in Java.

  • The companion keeps class-level state (created) and class-level behaviour (fromCsv), which is what a Java static field and static factory method would hold. They are hidden from instances unless an instance calls Trail.count explicitly.

Outside the companion pair, none of that is reachable. The compiler’s messages for the three attempts below were produced by running the program:

//> using scala 3.10.0

class Trail private (val name: String, private val km: Double)
object Trail:
  private def internalCount = 0
  def apply(name: String, km: Double): Trail = new Trail(name, km)

@main def o3(): Unit =
  val a = new Trail("A", 1.0)
  val b = Trail("B", 2.0)
  println(b.km)
  println(Trail.internalCount)
[error] ./o3.scala:9:15
[error] constructor Trail cannot be accessed as a member of Trail from the top-level definitions in package <empty>.
[error]   private constructor Trail can only be accessed from class Trail.
[error]   val a = new Trail("A", 1.0)
[error]               ^^^^^
[error] ./o3.scala:11:11
[error] value km cannot be accessed as a member of (b : Trail) from the top-level definitions in package <empty>.
[error]   private value km can only be accessed from class Trail.
[error]   println(b.km)
[error]           ^^^^
[error] ./o3.scala:12:11
[error] method internalCount cannot be accessed as a member of Trail.type from the top-level definitions in package <empty>.
[error]   private method internalCount can only be accessed from object Trail.
[error]   println(Trail.internalCount)
[error]           ^^^^^^^^^^^^^^^^^^^
Error compiling project (Scala 3.10.0, JVM (26))

The same-file rule

The class and the object must be defined together, in the same file; two files do not make a companion pair, and the compiler reports the clash. The test compiled class Box private (val v: Int) in one file and an object Box with a make method in another:

[error] ./o6e.scala:1:1
[error] Box clashes with class Box in o6d.scala; the two must be defined together
[error] object Box:
[error] ^

(The path of the other file was shortened in the message above; the compiler prints the absolute one.) The same rule makes a companion for a trait, an enum and a case class, and it is why helper code for a type is conventionally written directly under the type in its file. In a REPL or worksheet, where each line is its own unit, the companion pair has to be pasted as one block (:paste) for the same reason.

The this qualifier and companion access

Scala 2 also had private[this], which restricted a member to the current instance and hid it even from the companion. Scala 3 ignores the qualifier: on 3.10.0 the compiler warns "Ignoring [this] qualifier" and suggests writing plain private, and -rewrite -source 3.4-migration rewrites it. See Scala 3 Reference: Dropped: The this qualifier and Annotations & Visibility.

apply and creator applications

Trail(" Aguja ", 7.5) has no new and is not a constructor call. It is the shorthand for Trail.apply(" Aguja ", 7.5): a call on an object that is written like a call on a function is rewritten to the object’s apply method. That is the rule behind List(1, 2, 3), Map("a" -> 1) and Some(3), which are all apply methods on companions.

The apply factory idiom has three benefits over new:

  • the factory may return a subtype or a cached instance, because the caller cannot tell (Trail could return a different Trail subclass depending on the length);

  • it can return Option or Either on invalid input, which a constructor cannot do (see Option & Null Safety);

  • the constructor can be made private, which forces every client through the checks.

Since Scala 3.0 there is also the universal apply or creator application: for a class that has no apply of its own in a companion, Hut("A") works as new Hut("A"), so you can drop new for any class, including Java classes (StringBuilder(), java.io.File("x")). Defining an apply in the companion switches the rule off for that class, as the example shows:

//> using scala 3.10.0

class Hut(val name: String)               // no companion: Hut("A") is a creator application

class Peak(val name: String)
object Peak:
  def apply(name: String): Peak = new Peak(name.toUpperCase)

@main def o7(): Unit =
  println(Hut("A").name)
  println(Peak("aneto").name)
  println(new Peak("aneto").name)
  println(Peak.apply("x").name)
A
ANETO
aneto
X

Peak("aneto") goes through the companion apply and so is upper-cased; new Peak("aneto") bypasses it, which is why a factory that enforces an invariant must also make the constructor private, as Trail does. case class generates an apply automatically, see Case Classes; a private constructor on a case class makes the generated apply and copy private too (checked on 3.10.0: P(2) is rejected outside the companion with "object P does not take parameters" and copy with "private method copy can only be accessed from class P"). A companion’s apply and unapply are the pair that makes a type constructible and deconstructible without new, see Extractors & Pattern Bindings.

Case objects

A case object is an object with the extras of a case class: a readable toString, equals and hashCode, Serializable, and the Product interface. It is the right form of a singleton value in an algebraic data type: one case without parameters.

//> using scala 3.10.0

sealed trait Difficulty
case object Easy extends Difficulty
case object Hard extends Difficulty

@main def o4(): Unit =
  println(Easy)
  println(Easy.hashCode == Easy.hashCode)
  println(Easy == Hard)
  println(Easy.isInstanceOf[Product])
  println(Easy.productPrefix)
  val d: Difficulty = Hard
  println(d match
    case Easy => "easy"
    case Hard => "hard")
  println(Easy.getClass.getName)
Easy
true
false
true
Easy
hard
Easy$

Compared with a plain object, a case object prints as its name (Easy, not Easy$@4fa5b3), compares as a value and takes part in exhaustivity checking of a match. A plain object still prints as Name$@ followed by a hash code, which is rarely wanted in logs or tests. Whether to model a set of fixed values as case objects of a sealed trait or as an enum is the subject of Enums and Algebraic Data Types & GADTs; in Scala 3 prefer enum for simple sets unless the cases need different supertypes or members.

Lazy initialization of objects

An object body runs on first access, not at program start, as Registry showed. That makes an object a convenient lazy holder for an expensive resource, and an easy source of surprise when the body has side effects. A lazy val inside an object defers a single member further still, until that member is read:

//> using scala 3.10.0

object Origin:
  lazy val big = { println("computing"); 42 }

object Lazy:
  println("Lazy init")
  val x = 1

@main def o4b(): Unit =
  println("before")
  println(Origin.big)
  println(Origin.big)
  println(Lazy.x)
  println(Lazy.x)
before
computing
42
42
Lazy init
1
1

The rules behind it:

  • Thread safety is provided by the JVM class initialisation lock, not by the code. The body runs once, but a body that touches another object which in turn touches the first can deadlock or see a null field (a circular dependency between two objects initialising each other). Keep object bodies free of cross-object dependencies, or move the shared state to a lazy val.

  • A val in an object is initialised in the object body, in order of appearance. A forward reference to a later val sees the default value (0, null), exactly as in a class. See Laziness for lazy val and how it differs from def and val.

  • There is no static initialiser block. A Java static { ... } block corresponds to the statements in the object body.

Compared with Java and Kotlin

  • Java static members become members of an object. A Scala class has no static members; a Java program calls the forwarders the compiler generates in the companion class, or Name$.MODULE$.member(). Scala code can use Java statics directly: java.lang.Math.abs(-3), java.time.Duration.ofMinutes(5).

  • A Java singleton (private constructor and a getInstance()) is replaced by an object. The initialisation-on-demand class holder idiom that Java programmers write by hand is what the JVM does for an object automatically.

  • Kotlin object is the same idea and the same name. The difference is that Kotlin has companion object as a member of a class (one per class, usable with Name.member and with an optional name), while Scala spells it as a separate object declaration with the same name. See Classes and Objects in Kotlin and Classes and Objects in Java.

Pitfalls

  • A companion needs the same name and the same file. A different file, a different package or a different spelling makes a stand-alone object, with no special access, as the "must be defined together" message above shows.

  • The constructor is not private unless you say so. Adding an apply to a companion does not stop clients from writing new Peak("aneto"); mark the constructor private (class Trail private (...)) when the factory must be the only way in.

  • Creator applications can hide a factory. Peak("x") calls apply, but Hut("x") calls the constructor; if the companion later gains an apply, existing Hut(...) calls silently change meaning to the factory.

  • An object body with side effects runs at an unpredictable time, the first access, so avoid printing, reading files or opening connections in an object body in production code.

  • Mutable state in an object is global. A var in an object is shared by the whole JVM and by every test in the same process; prefer passing state explicitly or wrapping it in a class.

  • case object and object differ in toString. Use case object for ADT values that appear in logs and test output.

Where the books are outdated

The books consulted for this section (see the section bibliography) were written for Scala 2 or for the first Scala 3 releases. For objects and companions, treat these points as superseded by the pages cited here:

  • object Main extends App is the Scala 2 way to write an entry point. It still compiles on Scala 3, but the DelayedInit trick behind it was dropped and trait App is deprecated since 3.8.0 (-deprecation prints trait App in package scala is deprecated since 3.8.0); write an @main method or an explicit def main(args: Array[String]).

  • new on every class is shown throughout the older books; creator applications make it optional on Scala 3.

  • Package objects as the home of top-level helpers are replaced by top-level definitions in Scala 3 (and the referenceable package objects preview of 3.10 revives a narrower form).

  • Companion access, apply factories, case objects and lazy initialisation are unchanged since Scala 2 and the explanations still hold.

The run results above are from 2026-10-15 on Scala 3.10.0 and were not re-run on the 3.9 LTS or 3.3 LTS releases.