Scala - Classes and Objects

Scala classes are more concise than Java equivalents while offering greater flexibility. Constructor parameters become fields automatically when declared with `val` or `var`.

Key Insights

  • Scala unifies object-oriented and functional programming through a sophisticated class system where everything is an object, including primitive types and functions
  • Companion objects provide a clean alternative to static members, enabling factory patterns and implicit conversions while maintaining type safety
  • Case classes eliminate boilerplate for immutable data structures with built-in pattern matching, making them the preferred choice for domain modeling

Class Fundamentals

Scala classes are more concise than Java equivalents while offering greater flexibility. Constructor parameters become fields automatically when declared with val or var.

class Person(val name: String, var age: Int) {
  def greet(): String = s"Hello, I'm $name"
  
  def haveBirthday(): Unit = {
    age += 1
  }
}

val person = new Person("Alice", 30)
println(person.name)  // Alice
person.haveBirthday()
println(person.age)   // 31

Parameters without val or var remain private to the constructor:

class Employee(name: String, val salary: Double) {
  // name is not accessible outside constructor
  def description: String = s"Employee earning $salary"
}

val emp = new Employee("Bob", 75000)
// emp.name  // Compilation error
println(emp.salary)  // 75000

Auxiliary Constructors

Multiple constructors enable flexible object creation. Each auxiliary constructor must call another constructor as its first action.

class Rectangle(val width: Double, val height: Double) {
  def this(side: Double) = this(side, side)  // Square
  
  def this() = this(1.0)  // Unit square
  
  def area: Double = width * height
}

val rect1 = new Rectangle(5, 3)
val square = new Rectangle(4)
val unit = new Rectangle()

println(rect1.area)   // 15.0
println(square.area)  // 16.0
println(unit.area)    // 1.0

Objects: Scala’s Singleton Pattern

Objects are single-instance classes. They replace Java’s static members and provide a cleaner approach to singleton patterns.

object DatabaseConnection {
  private var connectionCount = 0
  
  def connect(): String = {
    connectionCount += 1
    s"Connection #$connectionCount established"
  }
  
  def getConnectionCount: Int = connectionCount
}

println(DatabaseConnection.connect())  // Connection #1 established
println(DatabaseConnection.connect())  // Connection #2 established
println(DatabaseConnection.getConnectionCount)  // 2

Companion Objects

When an object shares the same name and file as a class, they become companions. Companions can access each other’s private members, enabling elegant factory patterns.

class User private(val username: String, val email: String) {
  def displayInfo: String = s"$username <$email>"
}

object User {
  private val emailRegex = """^[\w.-]+@[\w.-]+\.\w+$""".r
  
  def apply(username: String, email: String): Option[User] = {
    if (isValidEmail(email)) Some(new User(username, email))
    else None
  }
  
  private def isValidEmail(email: String): Boolean = {
    emailRegex.matches(email)
  }
}

val validUser = User("alice", "alice@example.com")
val invalidUser = User("bob", "invalid-email")

println(validUser)    // Some(User@...)
println(invalidUser)  // None

The apply method enables constructor-like syntax without the new keyword, making object creation more natural.

Case Classes

Case classes are Scala’s killer feature for immutable data modeling. They automatically generate apply, unapply, equals, hashCode, toString, and copy methods.

case class Product(id: Int, name: String, price: Double)

val laptop = Product(1, "MacBook Pro", 2499.99)
val phone = Product(2, "iPhone", 999.99)

println(laptop)  // Product(1,MacBook Pro,2499.99)

// Structural equality
val laptop2 = Product(1, "MacBook Pro", 2499.99)
println(laptop == laptop2)  // true

// Pattern matching
def categorize(product: Product): String = product match {
  case Product(_, _, price) if price > 2000 => "Premium"
  case Product(_, _, price) if price > 500 => "Mid-range"
  case _ => "Budget"
}

println(categorize(laptop))  // Premium
println(categorize(phone))   // Mid-range

The copy method creates modified copies while maintaining immutability:

val discountedLaptop = laptop.copy(price = 1999.99)
println(discountedLaptop)  // Product(1,MacBook Pro,1999.99)
println(laptop)            // Product(1,MacBook Pro,2499.99) - unchanged

Inheritance and Traits

Scala supports single inheritance but multiple trait composition. Traits are similar to Java interfaces but can contain concrete implementations.

trait Auditable {
  def log(message: String): Unit = {
    println(s"[LOG] $message")
  }
}

trait Timestamped {
  val createdAt: Long = System.currentTimeMillis()
}

class BankAccount(val accountNumber: String, private var balance: Double) 
  extends Auditable with Timestamped {
  
  def deposit(amount: Double): Unit = {
    balance += amount
    log(s"Deposited $amount to $accountNumber")
  }
  
  def withdraw(amount: Double): Boolean = {
    if (balance >= amount) {
      balance -= amount
      log(s"Withdrew $amount from $accountNumber")
      true
    } else {
      log(s"Insufficient funds in $accountNumber")
      false
    }
  }
  
  def getBalance: Double = balance
}

val account = new BankAccount("ACC-001", 1000.0)
account.deposit(500.0)   // [LOG] Deposited 500.0 to ACC-001
account.withdraw(2000.0) // [LOG] Insufficient funds in ACC-001
println(account.createdAt)

Abstract Classes vs Traits

Use abstract classes when you need constructor parameters or want to ensure Java interoperability. Use traits for composable behavior.

abstract class Vehicle(val wheels: Int) {
  def maxSpeed: Double
  def description: String = s"Vehicle with $wheels wheels"
}

trait Electric {
  def batteryCapacity: Double
  def range: Double = batteryCapacity * 3.5  // miles per kWh
}

class Tesla(wheels: Int, val batteryCapacity: Double) 
  extends Vehicle(wheels) with Electric {
  def maxSpeed: Double = 155.0
}

val modelS = new Tesla(4, 100.0)
println(modelS.description)      // Vehicle with 4 wheels
println(modelS.range)            // 350.0
println(modelS.maxSpeed)         // 155.0

Sealed Classes for ADTs

Sealed classes restrict inheritance to the same file, enabling exhaustive pattern matching. This is crucial for algebraic data types.

sealed trait Result[+T]
case class Success[T](value: T) extends Result[T]
case class Failure(error: String) extends Result[Nothing]

def divide(a: Int, b: Int): Result[Double] = {
  if (b == 0) Failure("Division by zero")
  else Success(a.toDouble / b)
}

def handleResult(result: Result[Double]): String = result match {
  case Success(value) => s"Result: $value"
  case Failure(error) => s"Error: $error"
  // Compiler ensures all cases are covered
}

println(handleResult(divide(10, 2)))  // Result: 5.0
println(handleResult(divide(10, 0)))  // Error: Division by zero

Value Classes for Type Safety

Value classes provide type safety without runtime overhead. They must extend AnyVal and have exactly one val parameter.

case class UserId(value: Int) extends AnyVal
case class OrderId(value: Int) extends AnyVal

def processOrder(orderId: OrderId, userId: UserId): String = {
  s"Processing order ${orderId.value} for user ${userId.value}"
}

val order = OrderId(12345)
val user = UserId(67890)

println(processOrder(order, user))
// println(processOrder(user, order))  // Compilation error - type safety

Scala’s class system provides powerful abstractions while maintaining performance and type safety. Case classes and companion objects eliminate boilerplate, traits enable flexible composition, and sealed hierarchies ensure exhaustive pattern matching. Master these patterns to write idiomatic, maintainable Scala code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.