Scala Classes: Constructors, Fields, and Auxiliary Constructors
A Scala class definition does in one line what Java typically needs five to ten lines for — a private field, a constructor parameter, and a public getter, all fused together in the class signature itself. This isn’t just less typing; it reflects a real design decision about what a class’s constructor is in Scala, and understanding that decision is the key to reading (and writing) Scala classes fluently instead of translating them mentally from Java.
The Primary Constructor Is the Class Body
class Person(val name: String, val age: Int) { def greet(): String = s"Hi, I'm $name, $age years old"}
val alice = new Person("Alice", 30)println(alice.name) // "Alice" — no getName() call neededprintln(alice.greet())// The equivalent in Java — for comparisonpublic class Person { private final String name; private final int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; } public int getAge() { return age; }
public String greet() { return "Hi, I'm " + name + ", " + age + " years old"; }}The parameter list directly after the class name is the primary constructor — there’s no separate constructor method to write. Adding val before a constructor parameter (val name: String) does three things simultaneously: declares a field, makes it publicly readable, and wires up the constructor assignment — all in one place. Leaving off val/var still allows the parameter to be used inside the class body, but doesn’t expose it as an externally-accessible field at all.
class Circle(radius: Double) { // no val — radius is usable inside, but not externally accessible def area: Double = math.Pi * radius * radius}
val c = new Circle(5.0)c.area // works — 78.53...// c.radius // Error: value radius is not a member of Circleclass Rectangle(val width: Double, val height: Double) { // val — width/height ARE externally accessible def area: Double = width * height}
val r = new Rectangle(4.0, 6.0)r.width // 4.0 — accessibler.areaThis gives you deliberate control over what’s part of a class’s public API versus purely an internal implementation detail — a distinction Java requires a separate private field plus a conditionally-present getter to express, and Scala expresses with the presence or absence of a single keyword.
Constructor Body Code Runs Directly in the Class Body
class BankAccount(val owner: String, initialBalance: Double) { require(initialBalance >= 0, "Initial balance cannot be negative") // runs during construction
private var balance: Double = initialBalance println(s"Account created for $owner with balance $initialBalance") // also runs during construction
def deposit(amount: Double): Unit = { balance += amount }
def getBalance: Double = balance}
val account = new BankAccount("Alice", 1000)// Prints: "Account created for Alice with balance 1000.0" — immediately, at construction timeAny code written directly in the class body (not inside a def) executes as part of object construction, top to bottom, in the order it’s written — this is why require(...) and the println above run the moment new BankAccount(...) is called, without being wrapped in an explicit constructor method. require is a standard library function that throws IllegalArgumentException if its condition is false — the idiomatic way to validate constructor arguments and fail fast on invalid state, rather than allowing a malformed object to exist.
Auxiliary Constructors
class Point(val x: Double, val y: Double) { def this() = this(0.0, 0.0) // auxiliary constructor: default to origin def this(x: Double) = this(x, 0.0) // auxiliary constructor: default y to 0
override def toString: String = s"($x, $y)"}
val origin = new Point() // (0.0, 0.0)val onXAxis = new Point(5.0) // (5.0, 0.0)val point = new Point(3.0, 4.0) // (3.0, 4.0)Auxiliary constructors are declared with def this(...), and — this is a real, enforced rule, not just convention — every auxiliary constructor must call the primary constructor (or another auxiliary constructor declared earlier) as its very first statement, either directly or transitively. This guarantees the primary constructor’s logic (field initialization, require validation) always runs regardless of which constructor a caller actually used — there’s no way to bypass it, unlike Java, where it’s possible (if usually discouraged) to write constructors that don’t delegate to each other at all.
Default and Named Parameters in Constructors
class ServerConfig( val host: String = "localhost", val port: Int = 8080, val timeout: Int = 30)
val defaultConfig = new ServerConfig()val customPort = new ServerConfig(port = 9090)val fullyCustom = new ServerConfig(host = "api.example.com", port = 443, timeout = 60)This is genuinely the more idiomatic Scala alternative to auxiliary constructors for the common case of “some fields have sensible defaults” — default parameter values combined with named arguments cover the vast majority of cases that would otherwise need several auxiliary constructor overloads in a Java-style design, with substantially less boilerplate.
Overriding toString, equals, and hashCode
class Point(val x: Double, val y: Double) { override def toString: String = s"($x, $y)"
override def equals(other: Any): Boolean = other match { case that: Point => this.x == that.x && this.y == that.y case _ => false }
override def hashCode: Int = (x, y).hashCode}
val p1 = new Point(1, 2)val p2 = new Point(1, 2)println(p1 == p2) // true — because equals was overriddenprintln(p1.toString) // "(1.0, 2.0)"override is a required keyword in Scala whenever you’re providing a new implementation of an inherited method — unlike Java’s optional @Override annotation (which is advisory, catching only some mismatches), Scala’s compiler enforces that override is present, and equally enforces that it’s absent when you’re not actually overriding anything. This is a real, compile-time-checked guard against two opposite mistakes: accidentally shadowing an inherited method without meaning to, and believing you’ve overridden something when a typo in the method signature means you’ve actually just defined an unrelated new method instead.
A shortcut worth knowing in advance: hand-writing equals, hashCode, and toString this way is exactly the boilerplate a case class (covered in its own dedicated guide) generates automatically — for immutable data-holding classes specifically, case class is almost always the better choice, and this section exists mainly so the manual mechanics are understood before that shortcut is introduced.
Private Fields and Encapsulation
class Thermostat { private var _temperature: Double = 20.0
def temperature: Double = _temperature
def temperature_=(value: Double): Unit = { if (value < -50 || value > 50) throw new IllegalArgumentException("Temperature out of range") _temperature = value }}
val t = new Thermostat()t.temperature = 25.0 // calls temperature_=, looks like plain field assignmentprintln(t.temperature) // calls temperature, looks like plain field accessThis pattern — a private backing field (_temperature), a getter method named without the underscore (temperature), and a setter method with a special _= suffix (temperature_=) — is how Scala implements custom getters/setters while keeping the call-site syntax looking exactly like plain field access (t.temperature = 25.0), even though real validation logic runs underneath. This is called the “uniform access principle”: callers can’t tell from the syntax alone whether they’re touching a plain field or a computed property, which means a class can start with a plain public val/var and later add validation logic without breaking any code that uses it.
Class Hierarchies: extends and super
class Animal(val name: String) { def speak(): String = s"$name makes a sound"}
class Dog(name: String, val breed: String) extends Animal(name) { override def speak(): String = s"${super.speak()}, specifically a bark from a $breed"}
val rex = new Dog("Rex", "Labrador")println(rex.speak())extends Animal(name) both establishes the inheritance relationship and calls the parent’s primary constructor with the required argument — Scala classes support single inheritance from another class (exactly like Java), with super.speak() calling the parent’s implementation explicitly, the same pattern covered from the JVM-interop angle in the Java material this series’ hybrid design draws on. Full coverage of abstract members, sealed hierarchies, and the deeper polymorphism story is covered from a different angle in the traits guide — traits, not single-class inheritance, are where most of Scala’s actual composition power lives.
Abstract Classes — A Quick Preview
abstract class Shape { def area: Double // abstract member — no implementation def describe: String = s"Area: $area" // concrete member — shared by every subclass}
class Circle(radius: Double) extends Shape { def area: Double = math.Pi * radius * radius}
val c = new Circle(5.0)println(c.describe) // uses the shared, concrete describe methodAn abstract class can hold both abstract members (declared but not implemented, like area) and concrete ones (fully implemented, like describe), and — like any class — supports constructor parameters and can be extended by exactly one subclass hierarchy. It cannot be instantiated directly (new Shape() would fail to compile). This is a genuinely useful tool, but in practice Scala developers reach for trait far more often for exactly this “shared behavior plus abstract members” role, because traits support the multiple-inheritance-style composition the next guide covers in depth, while a class can only extend one abstract class.
Common Pitfalls
| Symptom | Likely cause |
|---|---|
| ”value is not a member of” for a constructor parameter | Forgot val/var on the parameter — it’s usable inside the class but not externally |
| ”method overrides nothing” error | Missing override keyword is required, but so is getting the exact overridden signature right — a typo creates an unrelated new method instead of an override |
Two instances with identical fields aren’t == equal | Default equals is reference equality — override it manually, or (better) use a case class |
| Auxiliary constructor won’t compile | Its first statement must call the primary constructor or an earlier-declared auxiliary constructor — no other statement is allowed first |
| Getter/setter pattern doesn’t work as expected | The setter method name must be exactly the getter’s name plus _=, with no space — def temperature_=(...), not def temperature _= (...) |
Frequently Asked Questions
Why does adding val to a constructor parameter matter so much? Because it’s the difference between a value only usable inside the class (a plain constructor parameter with no val/var) and a genuine public field other code can read (val/var). This single keyword replaces the private-field-plus-getter boilerplate Java requires for the same distinction.
Is new required when creating an instance? For a plain class, yes (new Person(...)), though Scala 3 permits omitting new in many cases via “universal apply methods” if a companion object with a matching apply isn’t otherwise defined. For clarity, especially early on, writing new explicitly for classes avoids ambiguity about whether you’re calling a constructor or a companion object’s factory method (covered in its own guide).
When should I use an auxiliary constructor instead of default parameter values? Default parameters cover the vast majority of real cases now — reach for an auxiliary constructor specifically when the alternate construction path needs genuinely different logic, not just different default values for the same fields.
Do I need to write equals/hashCode/toString manually often in real Scala code? Rarely, once you know about case classes — for any class whose primary purpose is holding immutable data, a case class generates all three (correctly, including proper structural equality) for free. Manual classes with hand-written equals/hashCode are typically reserved for cases needing custom behavior a case class’s auto-generated versions don’t provide.
What does “uniform access principle” actually buy me in practice? It means you can start a class with a plain public val and, if validation or computed logic becomes necessary later, convert it to the getter/setter pattern shown above without changing a single line of code anywhere that already uses object.field syntax — the call site is unaffected by that internal change.
What’s Next
You now understand Scala’s constructor model — the biggest structural difference from Java’s class syntax — including how to control what’s genuinely public versus internal. Next: Traits in Scala, covering Scala’s answer to Java interfaces and abstract classes: a single, more powerful construct supporting multiple inheritance done safely.