Classes & Constructors
|
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). Creator applications (3.0) and the deprecation of
private[this] (3.4) are available on the 3.9 LTS and 3.3 LTS lines as well, apart from the wording of some diagnostics.
A class is a template for objects: it bundles state (fields) and behaviour (methods) and is created with a constructor.
Scala differs from Java in one structural way that shapes everything below: the primary constructor is the class body.
The parameter list after the class name is the constructor’s parameter list, and every statement in the body is
constructor code. The related topics have their own pages: Objects & Companions
for apply factories and private access to companions, Case Classes
for classes with generated members, Traits & Mixins and
Inheritance, Abstract, Sealed, Open & Final
for extending classes, and Annotations & Visibility
for the full access-modifier rules.
The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.
Class parameters and fields
A class is declared with class, a name and an optional parameter list. A parameter written without a modifier is only a
constructor parameter: it is visible inside the class body but is not a member that clients can read. Writing val
(or var) in front of it also makes it a field with a public accessor.
//> using scala 3.10.0
class Trail(val name: String, val lengthKm: Double, ascentM: Int):
// ascentM is a plain constructor parameter: visible in the body, no member
val gradient: Double = ascentM / (lengthKm * 1000)
println(s"building $name") // statements run on every construction
def isSteep: Boolean = gradient > 0.1
def summary: String = s"$name: $lengthKm km, ascent $ascentM m" // captures ascentM as a private field
@main def e1(): Unit =
val t = Trail("Aguja", 7.5, 640)
println(t.name)
println(t.lengthKm)
println(t.gradient)
println(t.isSteep)
println(t.summary)
// t.ascentM // does not compile
building Aguja
Aguja
7.5
0.08533333333333333
false
Aguja: 7.5 km, ascent 640 m
How the three kinds of members relate:
-
valparameter (name,lengthKm): a read-only field plus a public getter, the Scala counterpart of a Javaprivate finalfield with a getter. -
varparameter: a mutable field plus a public getter and setter (see the accessors section). -
Plain parameter (
ascentM): no member, unless a method of the class uses it, in which case the compiler quietly stores it in a private field.summaryabove usesascentM, soTrailcarries it even though clients cannot see it. A plain parameter that nothing uses is not stored at all, and with-Wunused:allthe compiler says so (see the pitfalls). -
Body
valandvar(gradient): fields initialized by the body, in order, each time the class is instantiated. -
Body statements (
println(s"building $name")) run on every construction, in source order, interleaved with the field initializers. Java would put them in a constructor or an initializer block.
Using a plain parameter from outside, or assigning to a val, is rejected at compile time; both messages below are the real
ones from 3.10.0:
//> using scala 3.10.0
class Trail(val name: String, ascentM: Int)
@main def e1b(): Unit =
val t = Trail("Aguja", 640)
println(t.ascentM)
t.name = "Other"
-- [E173] Reference Error: e1b.scala:7:12
7 | println(t.ascentM)
| ^^^^^^^^^
|value ascentM cannot be accessed as a member of (t : Trail) from the top-level definitions in package <empty>.
| private value ascentM can only be accessed from class Trail.
-- [E052] Type Error: e1b.scala:8:9
8 | t.name = "Other"
| ^^^^^^^^^^^^^^^^
| Reassignment to val name
A class with no parameters and no body, such as class Marker, is complete. Braces are optional and, in the braceless style used
here, the body follows a colon on indented lines. The parameter list can be omitted entirely (class Thermometer:) or written
empty (class Counter():); both define a constructor taking no arguments. Type parameters come between the name and the value
parameters, as in class Box[T](val content: T); see Generics & Bounds.
Primary and auxiliary constructors
Every class has exactly one primary constructor: the parameter list plus the body. Auxiliary constructors are extra
entry points declared as def this(...). Each one must start by calling another constructor of the same class (the primary
or an earlier auxiliary one) as its first statement, so that every path ends in the primary constructor and the fields are
always initialized the same way. Combine auxiliary constructors with default parameters, which usually remove the need
for them.
//> using scala 3.10.0
class Hiker(val name: String, var weightKg: Double = 70.0, val pace: Int = 12):
def this(name: String, pace: Int) = this(name, 70.0, pace) // auxiliary constructor
def this() = this("anonymous") // must call another constructor first
override def toString = s"Hiker($name, $weightKg kg, $pace min/km)"
@main def e2(): Unit =
println(Hiker("Ana"))
println(Hiker("Ben", 82.5))
println(Hiker("Cleo", pace = 10))
println(Hiker("Dan", 9))
println(Hiker())
val h = Hiker("Eli")
h.weightKg = 68.0
println(h)
Hiker(Ana, 70.0 kg, 12 min/km)
Hiker(Ben, 82.5 kg, 12 min/km)
Hiker(Cleo, 70.0 kg, 10 min/km)
Hiker(Dan, 70.0 kg, 9 min/km)
Hiker(anonymous, 70.0 kg, 12 min/km)
Hiker(Eli, 68.0 kg, 12 min/km)
Notes on the example:
-
Default parameters (
weightKg: Double = 70.0) are evaluated at every call that omits the argument, and named arguments select a later default (Hiker("Cleo", pace = 10)). The rules are those of methods, see Methods & Parameters. -
The auxiliary
this(name: String, pace: Int)has a different signature from the primary one (String, Double, Int), so overload resolution picks it forHiker("Dan", 9). -
An auxiliary constructor can have statements after its first call, which run after the primary constructor has finished:
//> using scala 3.10.0
class Point(val x: Int, val y: Int)
class Base(val id: Int):
println(s"Base init, describe = $describe")
def describe: String = "base"
class Derived(id: Int) extends Base(id):
val label = "derived-" + id
override def describe: String = s"label=$label"
class Circle(val r: Double):
def this() =
this(1.0)
println("auxiliary body runs after the primary constructor")
@main def e3(): Unit =
val p = Point(1, 2) // creator application: no `new`
val q = new Point(3, 4) // still valid
println((p.x, p.y, q.x, q.y))
Derived(7)
Circle()
(1,2,3,4)
Base init, describe = label=null
auxiliary body runs after the primary constructor
The first statement rule is enforced by the parser. Putting any other statement first produces two errors, the second of which
is misleading because the compiler then reads the constructor as a plain method named this:
//> using scala 3.10.0
class Hiker(val name: String, val pace: Int):
def this(name: String) =
println("too early")
this(name, 12)
@main def e2b(): Unit = println(Hiker("x").pace)
-- [E040] Syntax Error: e2b.scala:5:4
5 | println("too early")
| ^^^^^^^
| 'this' expected, but identifier found
-- [E050] Type Error: e2b.scala:6:4
6 | this(name, 12)
| ^^^^
| class Hiker does not take parameters
Compared with other languages:
-
Java has several equal constructors, each declared with the class name and each free to run code before delegating with
this(...)/super(...)(only as the first statement). Scala has one primary constructor and delegating auxiliaries. -
Kotlin is closest: a primary constructor in the class header,
initblocks for body code andconstructor(...)secondary constructors that must delegate with: this(...). Kotlin’sval/varparameters correspond to Scala’s. -
Python and JavaScript have one
__init__/constructor; default arguments take the role of overloads, as in Scala.
Creator applications: no new
Scala 3 lets you create an instance by calling the class like a function. This is called a creator application
(also universal apply). Point(1, 2) and new Point(3, 4) are both valid, and the first is the idiomatic form.
The compiler rewrites C(args) to new C(args) when C is a class and there is no apply method to call. It works for
Scala classes, Java classes and the standard library alike. If the companion object (or the class itself) defines an
apply, that method is called instead, which is what makes custom factories possible.
//> using scala 3.10.0
class Route(val name: String)
object Route:
def apply(name: String): Route = new Route(name.toUpperCase) // explicit apply in the companion wins
class Plain(val name: String)
@main def e11(): Unit =
println(Route("aguja").name) // calls Route.apply
println(new Route("aguja").name) // bypasses it
println(Plain("aguja").name) // creator application
val sb = StringBuilder("abc") // Java and Scala library classes too
val f = java.io.File("/tmp")
println((sb.length, f.getName))
val build: String => Plain = Plain.apply // the companion proxy has an apply method
println(build("x").name)
AGUJA
aguja
aguja
(3,tmp)
x
What to remember:
-
The synthesized
applyis a constructor proxy: it forwards to the constructors that are accessible at the call site. When a constructor isprivate, the proxy is not generated, soApi("y")fails (see the next section). -
Scala 2 had
applyonly for case classes and for classes with a hand-written companionapply. The Scala 2.13 compiler rejects the same call (verified with Scala 2.13.18:error: not found: value P). -
newis still required to create an anonymous subclass of a class with a body (new Base { ... }), and it is how you bypass a custom companionapply, as the second line above shows. Coding style guides prefer the form withoutnew.
Getters, setters and uniform access
Scala has no getFoo()/setFoo() convention. A public val or var is read as obj.field and a var is assigned as
obj.field = value, both without parentheses, and the compiler generates the accessor methods behind them (field and,
for a var, field_=). The uniform access principle means a client cannot tell whether obj.field reads a stored
value or runs a method. You may therefore start with a public field and later replace it with a computed or validated
member without breaking callers, at the source level (a library still has to consider binary compatibility, because the bytecode changes from a field to a method).
To write your own setter, define a method named name_= next to the getter name; the pair then supports assignment syntax
and even +=:
//> using scala 3.10.0
class Thermometer:
private var celsiusValue = 0.0
def celsius: Double = celsiusValue // getter
def celsius_=(c: Double): Unit = // setter, validated
require(c >= -273.15, s"below absolute zero: $c")
celsiusValue = c
def fahrenheit: Double = celsiusValue * 9 / 5 + 32 // computed, same call syntax as a field
class Counter:
var count = 0 // public var: compiler-generated getter count and setter count_=
@main def e4(): Unit =
val t = Thermometer()
t.celsius = 21.5
println(t.celsius)
println(t.fahrenheit)
t.celsius += 1
println(t.celsius)
val c = Counter()
c.count += 5
println(c.count)
try t.celsius = -300 catch case e: IllegalArgumentException => println(e.getMessage)
21.5
70.7
22.5
5
requirement failed: below absolute zero: -300.0
The rules for custom accessors:
-
The setter must be named
name_=and the getternamemust be defined in the same class, as adefwithout a parameter list. -
A getter without a setter makes the member read-only, and assigning to it is the same
Reassignment to valerror:
//> using scala 3.10.0
class OnlyGetter:
def value: Int = 1
@main def e4b(): Unit =
val o = OnlyGetter()
o.value = 2
-- [E052] Type Error: e4b.scala:8:10
8 | o.value = 2
| ^^^^^^^^^^^
| Reassignment to val value
-
def fahrenheitversusval milesis the same choice as for any member: adefruns each time, avalruns once in the constructor, alazy valruns once on first use. The example below prints the values and thelazy valmessage only once.
//> using scala 3.10.0
class Distance(val metres: Double):
def km: Double = metres / 1000 // def: recomputed on each call
val miles: Double = metres / 1609.344 // val: computed once, stored in a field
lazy val label: String =
println("computing label")
f"$metres%.0f m"
@main def e12(): Unit =
val d = Distance(5000)
println(d.km)
println(d.miles)
println(d.label)
println(d.label)
5.0
3.1068559611866697
computing label
5000 m
5000 m
Private members and constructor visibility
private restricts a member to its class and its companion object; protected additionally allows subclasses. Unlike in Java,
private is checked per class, not per package, and the default (no modifier) is public. A private member is still
visible on other instances of the same class (as in Java), which is why same below can read that.owner:
//> using scala 3.10.0
class Cache:
private val store = scala.collection.mutable.Map.empty[String, Int]
private[this] val hits = 1
def put(k: String, v: Int): Unit = store(k) = v
def get(k: String): Option[Int] = store.get(k)
class Account(private val owner: String, private var balance: Int):
def same(that: Account): Boolean = this.owner == that.owner // other instances' privates are visible
@main def e5(): Unit =
val c = Cache()
c.put("a", 1)
println(c.get("a"))
println(Account("x", 1).same(Account("x", 2)))
-- Warning: e5.scala:5:9
5 | private[this] val hits = 1
| ^^^^^^
|Ignoring [this] qualifier.
|The syntax `private[this]` will be deprecated in the future; just write `private` instead.
|See: https://docs.scala-lang.org/scala3/reference/dropped-features/this-qualifier.html
|This construct can be rewritten automatically under -rewrite -source 3.4-migration.
1 warning found
Some(1)
true
Source: Scala 3 Reference: Dropped: private[this and protected[this]]
The warning shows the 3.4 change: private[this] (object-private, "only this very instance") is on the way out. In Scala 3.4 and
later the compiler treats it as plain private and warns, and the rewrite option -rewrite -source 3.4-migration removes
it. Where the old modifier had a meaningful effect (variance checking), the compiler now infers the same restriction for a
private member that is only accessed through this. The older books still teach private[this].
A constructor can have its own modifier, written between the class name (and type parameters) and the parameter list.
class Api private (val name: String) hides the primary constructor, leaving new Api(...) available only to the class and
its companion. This is the standard way to force clients through a factory; the creator-application proxy is then not
generated either:
//> using scala 3.10.0
class Api private (val name: String):
def this() = this("default")
override def toString = s"Api($name)"
object Api:
def named(n: String): Api = new Api(n)
@main def e10(): Unit =
println(Api.named("x"))
println(Api())
Api(x)
Api(default)
Source: Scala 3 Language Specification: Classes and Objects, private constructor example and Scala 3 Reference: Creator Applications
//> using scala 3.10.0
class Api private (val name: String)
@main def e10b(): Unit = println(Api("y").name)
-- [E050] Type Error: e10b.scala:5:33
5 |@main def e10b(): Unit = println(Api("y").name)
| ^^^
| object Api does not take parameters
In the first example Api.named is allowed to call the private constructor because it is in the companion, and Api() works
because the auxiliary constructor is public. Writing class Api private[trail] (...) instead limits it to the enclosing
package trail. A parameter can also be marked private val or protected val to restrict its generated accessor.
Initialization order
The body runs top to bottom in the order of the source, and a superclass constructor always runs before the subclass’s.
That produces the classic initialization trap shared with Java: calling an overridable member from the superclass body
reads a subclass field that has not been assigned yet. The line Base init, describe = label=null in the earlier output is the
trap: describe was called while Base was still being constructed, before Derived initialized label.
The same applies to abstract `val`s read by a parent or by a trait body:
//> using scala 3.10.0
trait Greeter:
val greeting: String
println(s"[Greeter] greeting = $greeting")
class Early extends Greeter:
val greeting = "hello"
abstract class Base:
val size: Int
val doubled = size * 2
class Impl extends Base:
val size = 21
@main def e7(): Unit =
Early()
println(Impl().doubled)
[Greeter] greeting = null
0
Ways to avoid it, from best to worst:
-
Pass the value as a constructor or trait parameter (Scala 3). A parent that receives the value as a parameter sees it during its own initialization.
-
Use
lazy valin the parent for the dependent value, so that it is evaluated on first use rather than during the parent’s construction. -
Use a
deffor the abstract member (avalcan implement it), and avoid reading it during construction.
The first two, applied to the example above:
//> using scala 3.10.0
trait Greeter(val greeting: String):
println(s"[Greeter] $greeting")
class Early extends Greeter("hello")
abstract class Base:
lazy val size: Int = 0
val doubled = size * 2
class Impl extends Base:
override lazy val size: Int = 21
@main def e15(): Unit =
Early()
println(Impl().doubled)
[Greeter] hello
42
Dropped and deprecated forms
Scala 3 removed several class features from Scala 2. The compiler rejects them; the older books and the Scala 2 code you meet still use them.
Early initializers. Scala 2’s class C extends { val x = 1 } with T initialized fields before the trait body ran. It was
dropped in 3.0 in favour of trait parameters (shown above) and the compiler says so:
//> using scala 3.10.0
trait Greeter:
val greeting: String
println(greeting)
class Early extends { val greeting = "hello" } with Greeter
@main def e14(): Unit = Early()
-- Error: e14.scala:7:20
7 |class Early extends { val greeting = "hello" } with Greeter
| ^
| `extends` must be followed by at least one parent
-- [E009] Syntax Error: e14.scala:7:47
7 |class Early extends { val greeting = "hello" } with Greeter
| ^^^^
| Early definitions are not supported; use trait parameters instead
Class shadowing. Scala 2 let a subclass define an inner class with the same name as one in its parent, which then hid (shadowed) it. Scala 3 forbids the override of class definitions, and so avoids the unsound type behaviour that shadowing allowed:
//> using scala 3.10.0
class A:
class B
class C extends A:
class B
@main def e6(): Unit = println(C())
-- [E069] Naming Error: e6.scala:6:8
6 | class B
| ^
|class B cannot have the same name as class B in class A -- class definitions cannot be overridden
private[this] and protected[this]. Deprecated by 3.4 as shown in the previous section; use private and protected.
Two other members to know are not dropped but absent: a plain class has no generated equals, hashCode, toString or
unapply. Two classes with equal fields are different objects, and a class cannot be used as a pattern without an extractor.
//> using scala 3.10.0
class Waypoint(val name: String, var elevationM: Int):
override def toString = s"Waypoint($name, $elevationM)"
@main def e13(): Unit =
val a = Waypoint("Col", 2100)
val b = Waypoint("Col", 2100)
println(a == b) // reference equality: no equals override
println(a.equals(a))
println(a)
false
true
Waypoint(Col, 2100)
Adding val Waypoint(n, _) = a to the example fails to compile with [E127] Pattern Match Error ... Waypoint cannot be used as
an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature. Case classes add
these members for you; see Case Classes and
Extractors & Pattern Bindings.
Classes with type parameters and parent parameters
Type parameters are written after the class name and are inferred from the constructor arguments, as for methods. A class
can pass arguments to its superclass constructor in the extends clause, and a Scala 3 trait can take parameters too:
//> using scala 3.10.0
trait Named(val name: String)
class Peak(name: String, val metres: Int) extends Named(name)
class Box[T](val content: T)
class Pair[A, B](val first: A, val second: B):
def swap: Pair[B, A] = Pair(second, first)
@main def e9(): Unit =
val p = Peak("Aneto", 3404)
println(s"${p.name} ${p.metres}")
val b = Box(42) // type arguments inferred
println(b.content + 1)
val s = Pair("one", 1).swap
println((s.first, s.second))
Aneto 3404
43
(1,one)
Note that Peak declares name as a plain parameter: the val is inherited from Named, so repeating val would be a
duplicate definition. Trait parameters have their own page, Traits & Mixins.
Pitfalls
-
Unused constructor parameters are silent by default. A plain parameter nobody uses is a classic typo (the author meant
val). Enable-Wunused:all(or-Wunused:params) to be told. Real output forclass Gen(seed: Int, unused: Int)://> using scala 3.10.0 //> using option -Wunused:all class Gen(seed: Int, unused: Int): def next: Int = seed + 1 @main def e8(): Unit = println(Gen(1, 2).next)
-- [E198] Unused Symbol Warning: e8.scala:4:21 4 |class Gen(seed: Int, unused: Int): | ^^^^^^ | unused explicit parameter 1 warning found 2
-
Forgetting
valand then reading the member from outside givesE173, shown in the first section. -
Calling overridable members from a constructor reads
nullor0in a subclass, as the initialization section shows. -
varparameters are public mutable state. Prefervaland copy-on-change, or keep thevarprivatebehind a validated setter as inThermometer. -
A
valthat depends on an abstractvalis initialized in declaration order; reorder, uselazy val, or pass a parameter. -
Mixing
newand creator applications on a class with a custom companionapplysilently skips the factory, asnew Route("aguja")showed. -
Return type of a setter must be
Unitand its name must end in_=; otherwisex = vis not translated.
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 classes and constructors, treat these points as superseded by the pages cited here:
-
newis no longer required (3.0). Older code writesnew Point(1, 2)everywhere, and it still compiles. -
Early initializers (
extends { ... } with T) do not exist in Scala 3; use trait parameters. -
Class shadowing is an error.
-
private[this]andprotected[this]are deprecated since 3.4 and produce a warning on 3.10.0. -
Braces and the
class C(...) {layout can be replaced by the braceless form used here. Both are valid. -
Primary constructor, auxiliary constructors,
val/varparameters, default arguments and uniform access are unchanged since Scala 2 and the explanations in the books 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.
References
-
Scala 3 Reference: Dropped: private[this and protected[this]]