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$
Source: Scala 3 Book: Objects
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 theprintlnshows. State kept in an object is global to the JVM (more precisely to the class loader), so a mutable one likenamesis 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 fieldMODULE$. This is why Java code seesRegistry$.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 = Registryis the same instance asRegistry.
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
Source: Tour of Scala: Singleton Objects
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
Source: Scala 3 Book: Companion objects
Three things happen in the example:
-
The constructor is
private, so only the class and its companion can writenew Trail(...). The companion’sapplyis the single public way to build aTrail, and it is where validation or normalisation (heretrim) is put. -
The class reads
other.km, a private member of another instance.privatein Scala means "private to the class and its companion", andother.kmon 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 Javastaticfield andstaticfactory method would hold. They are hidden from instances unless an instance callsTrail.countexplicitly.
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))
Source: Scala 3 Book: Companion objects
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] ^
Source: Scala 3 Book: Companion objects
(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 (
Trailcould return a differentTrailsubclass depending on the length); -
it can return
OptionorEitheron 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$
Source: Scala 3 Book: Case objects
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
Source: Tour of Scala: Singleton Objects
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
nullfield (a circular dependency between two objects initialising each other). Keep object bodies free of cross-object dependencies, or move the shared state to alazy val. -
A
valin an object is initialised in the object body, in order of appearance. A forward reference to a latervalsees the default value (0,null), exactly as in a class. See Laziness forlazy valand how it differs fromdefandval. -
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 anobject. The initialisation-on-demand class holder idiom that Java programmers write by hand is what the JVM does for an object automatically. -
Kotlin
objectis the same idea and the same name. The difference is that Kotlin hascompanion objectas a member of a class (one per class, usable withName.memberand with an optional name), while Scala spells it as a separateobjectdeclaration 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
applyto a companion does not stop clients from writingnew Peak("aneto"); mark the constructorprivate(class Trail private (...)) when the factory must be the only way in. -
Creator applications can hide a factory.
Peak("x")callsapply, butHut("x")calls the constructor; if the companion later gains anapply, existingHut(...)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
varin 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 objectandobjectdiffer intoString. Usecase objectfor 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 Appis the Scala 2 way to write an entry point. It still compiles on Scala 3, but theDelayedInittrick behind it was dropped andtrait Appis deprecated since 3.8.0 (-deprecationprintstrait App in package scala is deprecated since 3.8.0); write an@mainmethod or an explicitdef main(args: Array[String]). -
newon 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,
applyfactories, 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.