Scala - Constructors (Primary and Auxiliary)
The primary constructor in Scala is embedded directly in the class definition. Unlike Java, where constructors are separate methods, Scala's primary constructor parameters appear in the class...
Key Insights
- Scala classes have exactly one primary constructor defined in the class signature itself, with its parameters automatically becoming class fields when prefixed with
valorvar - Auxiliary constructors are defined using
def this()and must call either the primary constructor or another auxiliary constructor as their first action - Constructor overloading in Scala favors default parameters and apply methods over multiple auxiliary constructors, leading to more concise and maintainable code
Primary Constructor Fundamentals
The primary constructor in Scala is embedded directly in the class definition. Unlike Java, where constructors are separate methods, Scala’s primary constructor parameters appear in the class signature, and its body comprises the entire class body.
class Person(firstName: String, lastName: String, age: Int) {
// This is part of the primary constructor body
println(s"Creating person: $firstName $lastName")
val fullName = s"$firstName $lastName"
// Validation logic runs during construction
require(age >= 0, "Age cannot be negative")
}
val person = new Person("John", "Doe", 30)
// Output: Creating person: John Doe
Every statement in the class body that isn’t part of a method or field declaration executes as part of the primary constructor. This includes initialization code, validation, and side effects.
Class Parameters vs Fields
Constructor parameters without val or var are private to the constructor scope and don’t become class fields:
class Employee(name: String, val employeeId: Int, var salary: Double) {
// 'name' is only accessible within constructor
// 'employeeId' becomes an immutable field
// 'salary' becomes a mutable field
def displayInfo(): Unit = {
// println(name) // Compilation error: name is not accessible
println(s"ID: $employeeId, Salary: $salary")
}
def giveRaise(percentage: Double): Unit = {
salary = salary * (1 + percentage / 100)
}
}
val emp = new Employee("Alice", 1001, 75000)
// emp.name // Compilation error
println(emp.employeeId) // Works: 1001
emp.salary = 80000 // Works: salary is mutable
This distinction provides fine-grained control over encapsulation. Use plain parameters for values needed only during initialization, val for immutable public fields, and var for mutable fields.
Auxiliary Constructors
Auxiliary constructors provide alternative ways to instantiate a class. They’re defined using def this() and must call another constructor (primary or auxiliary) as their first statement:
class Rectangle(val width: Double, val height: Double) {
// Auxiliary constructor for squares
def this(side: Double) = {
this(side, side)
}
// Auxiliary constructor with default position
def this(width: Double, height: Double, x: Int, y: Int) = {
this(width, height)
this.x = x
this.y = y
}
var x: Int = 0
var y: Int = 0
def area: Double = width * height
}
val rect1 = new Rectangle(10, 20)
val square = new Rectangle(15) // Uses auxiliary constructor
val positioned = new Rectangle(10, 20, 5, 5)
The requirement that auxiliary constructors must call another constructor creates a chain that ultimately leads to the primary constructor. This ensures all initialization logic in the primary constructor executes regardless of which constructor is used.
Constructor Chaining
Complex initialization scenarios often require multiple auxiliary constructors:
class BankAccount(val accountNumber: String,
val accountHolder: String,
initialBalance: Double) {
private var balance: Double = initialBalance
require(initialBalance >= 0, "Initial balance cannot be negative")
// Auxiliary constructor with default balance
def this(accountNumber: String, accountHolder: String) = {
this(accountNumber, accountHolder, 0.0)
}
// Auxiliary constructor generating account number
def this(accountHolder: String) = {
this(java.util.UUID.randomUUID().toString, accountHolder, 0.0)
}
def deposit(amount: Double): Unit = {
require(amount > 0, "Deposit amount must be positive")
balance += amount
}
def getBalance: Double = balance
}
val account1 = new BankAccount("ACC001", "Bob Smith", 1000)
val account2 = new BankAccount("ACC002", "Jane Doe")
val account3 = new BankAccount("Charlie Brown")
println(account3.accountNumber) // Generated UUID
Default Parameters: The Modern Alternative
Modern Scala code typically favors default parameters over auxiliary constructors. This approach is more concise and equally powerful:
class Configuration(val host: String = "localhost",
val port: Int = 8080,
val timeout: Int = 30000,
val retries: Int = 3) {
def connectionString: String = s"$host:$port"
}
// Multiple instantiation options without auxiliary constructors
val config1 = new Configuration()
val config2 = new Configuration(host = "api.example.com")
val config3 = new Configuration(port = 9000, retries = 5)
val config4 = new Configuration("prod.example.com", 443, 60000, 10)
Named parameters combined with defaults provide more flexibility than auxiliary constructors. You can specify only the parameters you want to override, in any order.
Private Primary Constructor
For singleton patterns or factory methods, make the primary constructor private:
class DatabaseConnection private(val url: String, val username: String) {
private var connection: Option[java.sql.Connection] = None
def connect(): Unit = {
// Connection logic
println(s"Connecting to $url as $username")
}
}
object DatabaseConnection {
private var instance: Option[DatabaseConnection] = None
def getInstance(url: String, username: String): DatabaseConnection = {
instance match {
case Some(conn) => conn
case None =>
val conn = new DatabaseConnection(url, username)
instance = Some(conn)
conn
}
}
// Factory method with validation
def create(url: String, username: String, password: String): Option[DatabaseConnection] = {
if (password.length >= 8) {
Some(new DatabaseConnection(url, username))
} else {
None
}
}
}
// val direct = new DatabaseConnection("url", "user") // Compilation error
val conn = DatabaseConnection.getInstance("jdbc:postgresql://localhost/db", "admin")
Case Classes and Constructor Generation
Case classes automatically generate an optimized constructor implementation:
case class Product(id: Int, name: String, price: Double, category: String = "General")
// Compiler generates:
// - Primary constructor with val parameters
// - apply method in companion object
// - copy method for creating modified copies
// - equals, hashCode, toString
val product1 = Product(1, "Laptop", 999.99)
val product2 = product1.copy(price = 899.99)
val product3 = Product(2, "Mouse", 29.99, "Accessories")
// No 'new' keyword needed due to generated apply method
println(product2.price) // 899.99
Case classes eliminate boilerplate for data-centric classes. The generated apply method in the companion object acts as a factory, making instantiation cleaner.
Constructor Validation Patterns
Implement robust validation in constructors to maintain invariants:
class Email private(val address: String) {
require(address.contains("@"), "Invalid email format")
require(address.split("@").length == 2, "Invalid email format")
val (localPart, domain) = {
val parts = address.split("@")
(parts(0), parts(1))
}
override def toString: String = address
}
object Email {
def apply(address: String): Option[Email] = {
try {
Some(new Email(address))
} catch {
case _: IllegalArgumentException => None
}
}
}
val validEmail = Email("user@example.com") // Some(Email)
val invalidEmail = Email("invalid-email") // None
validEmail.foreach(e => println(s"Domain: ${e.domain}"))
This pattern combines a private constructor with a companion object factory method, providing safe instantiation with error handling rather than throwing exceptions.
Performance Considerations
Primary constructors execute all class-level initialization code. For expensive operations, consider lazy initialization:
class DataProcessor(val filePath: String) {
println("Constructor called")
// Eager initialization - runs immediately
val metadata: Map[String, String] = loadMetadata()
// Lazy initialization - runs on first access
lazy val data: List[String] = {
println("Loading data...")
scala.io.Source.fromFile(filePath).getLines().toList
}
private def loadMetadata(): Map[String, String] = {
println("Loading metadata...")
Map("format" -> "csv", "version" -> "1.0")
}
}
val processor = new DataProcessor("data.csv")
// Output: Constructor called
// Loading metadata...
// Data loads only when accessed
val records = processor.data
// Output: Loading data...
Use lazy val for expensive computations that might not be needed in every code path, deferring initialization until first access.