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:

  • val parameter (name, lengthKm): a read-only field plus a public getter, the Scala counterpart of a Java private final field with a getter.

  • var parameter: 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. summary above uses ascentM, so Trail carries it even though clients cannot see it. A plain parameter that nothing uses is not stored at all, and with -Wunused:all the compiler says so (see the pitfalls).

  • Body val and var (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 for Hiker("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, init blocks for body code and constructor(...) secondary constructors that must delegate with : this(...). Kotlin’s val/var parameters 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 apply is a constructor proxy: it forwards to the constructors that are accessible at the call site. When a constructor is private, the proxy is not generated, so Api("y") fails (see the next section).

  • Scala 2 had apply only for case classes and for classes with a hand-written companion apply. The Scala 2.13 compiler rejects the same call (verified with Scala 2.13.18: error: not found: value P).

  • new is still required to create an anonymous subclass of a class with a body (new Base { ... }), and it is how you bypass a custom companion apply, as the second line above shows. Coding style guides prefer the form without new.

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 getter name must be defined in the same class, as a def without a parameter list.

  • A getter without a setter makes the member read-only, and assigning to it is the same Reassignment to val error:

//> 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 fahrenheit versus val miles is the same choice as for any member: a def runs each time, a val runs once in the constructor, a lazy val runs once on first use. The example below prints the values and the lazy val message 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)
//> 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:

  1. 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.

  2. Use lazy val in the parent for the dependent value, so that it is evaluated on first use rather than during the parent’s construction.

  3. Use a def for the abstract member (a val can 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 for class 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 val and then reading the member from outside gives E173, shown in the first section.

  • Calling overridable members from a constructor reads null or 0 in a subclass, as the initialization section shows.

  • var parameters are public mutable state. Prefer val and copy-on-change, or keep the var private behind a validated setter as in Thermometer.

  • A val that depends on an abstract val is initialized in declaration order; reorder, use lazy val, or pass a parameter.

  • Mixing new and creator applications on a class with a custom companion apply silently skips the factory, as new Route("aguja") showed.

  • Return type of a setter must be Unit and its name must end in _=; otherwise x = v is 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:

  • new is no longer required (3.0). Older code writes new 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] and protected[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/var parameters, 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.