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.