TL;DR
- Classes: Write constructor parameters directly in the declaration to reduce boilerplate
- object: Singleton instances are supported at the language level
- Companion objects: Replace static members with same-named objects as classes
- Traits: Enjoy the benefits of multiple inheritance while avoiding the diamond problem
Target Audience: Developers with Java/OOP experience Prerequisites: Scala basic syntax, functions and methods
Scala supports both object-oriented and functional programming. Classes, objects, traits, and other OOP features can be used harmoniously with the functional paradigm. In particular, Scala’s object supports the singleton pattern at the language level, and traits maximize code reuse while solving multiple inheritance problems.
Classes#
Classes are the basic unit that encapsulates data and behavior. Scala classes write constructor parameters directly in the class declaration to reduce boilerplate code.
Basic Class Definition
// Basic class
class Person(name: String, age: Int) {
def greet(): String = s"Hello, I'm $name."
}
val person = new Person("John", 30)
println(person.greet()) // Hello, I'm John.Constructor Parameters
Adding val or var to constructor parameters automatically creates fields.
// name is not accessible from outside, age is a val field
class Person(name: String, val age: Int)
val p = new Person("John", 30)
// println(p.name) // Compile error
println(p.age) // 30
// var makes it mutable
class MutablePerson(var name: String, var age: Int)
val mp = new MutablePerson("John", 30)
mp.age = 31
println(mp.age) // 31Auxiliary Constructors
Auxiliary constructors are defined with the this keyword and must first call the primary constructor or another auxiliary constructor.
class Person(val name: String, val age: Int) {
// Auxiliary constructor
def this(name: String) = this(name, 0)
def this() = this("Unknown", 0)
}
val p1 = new Person("John", 30)
val p2 = new Person("Jane") // age = 0
val p3 = new Person() // name = "Unknown", age = 0Using Default Values (Recommended)
Using default parameter values is cleaner and easier to maintain than auxiliary constructors.
// Default values are cleaner than auxiliary constructors
class Person(val name: String = "Unknown", val age: Int = 0)
val p1 = new Person("John", 30)
val p2 = new Person("Jane")
val p3 = new Person()Scala 3 Syntax
In Scala 3, you can define the class body with a colon and indentation. You can write cleanly without braces.
class Person(val name: String, val age: Int):
def greet(): String = s"Hello, I'm $name."
def isAdult: Boolean = age >= 18
override def toString: String = s"Person($name, $age)"class Person(val name: String, val age: Int) {
def greet(): String = s"Hello, I'm $name."
def isAdult: Boolean = age >= 18
override def toString: String = s"Person($name, $age)"
}Key Points
- Adding
val/varto constructor parameters automatically creates fields- Using default parameter values is cleaner than auxiliary constructors
- In Scala 3, define class body with colon and indentation
Object#
object defines a singleton instance. Implementing the singleton pattern in Java requires complex code like private constructors, static fields, and synchronization, but Scala solves it with just the object keyword. Objects are lazily initialized on first access and are thread-safe.
Singleton Object
An object has only one instance throughout the program. Suitable for global state, utility methods, factories, etc.
object DatabaseConnection {
private var connection: String = _
def connect(url: String): Unit = {
connection = url
println(s"Connected to $url")
}
def getConnection: String = connection
}
// Use directly without new
DatabaseConnection.connect("jdbc:mysql://localhost/db")
println(DatabaseConnection.getConnection)Utility Methods
Using object for a collection of pure functions without state is similar to Java’s static methods.
object MathUtils {
def square(x: Int): Int = x * x
def cube(x: Int): Int = x * x * x
val PI: Double = 3.14159
}
println(MathUtils.square(5)) // 25
println(MathUtils.PI) // 3.14159Key Points
objectdefines a singleton instance (use without new)- Lazily initialized and thread-safe
- Suitable for utility methods, global state, factories
Companion Object#
An object with the same name as a class is called a companion object. Companion objects must be defined in the same file and can access each other’s private members with the class. Replaces Java’s static members and is mainly used to define factory methods or constants.
class Circle(val radius: Double) {
import Circle._ // Import companion object members
def area: Double = PI * radius * radius
def circumference: Double = 2 * PI * radius
}
object Circle {
val PI: Double = 3.14159
// Factory method
def apply(radius: Double): Circle = new Circle(radius)
def fromDiameter(diameter: Double): Circle = new Circle(diameter / 2)
}
// apply allows creation without new
val c1 = Circle(5)
val c2 = Circle.fromDiameter(10)
println(c1.area) // 78.53975Private Member Access
Companion objects and classes can access each other’s private members.
class Person private (val name: String, val age: Int)
object Person {
def create(name: String, age: Int): Option[Person] =
if (age >= 0) Some(new Person(name, age)) // Access private constructor
else None
}
val person = Person.create("John", 30) // Some(Person)
val invalid = Person.create("Error", -5) // NoneKey Points
- Companion objects are defined with the same name as a class in the same file
- Can access each other’s
privatemembersapplymethod allows creating instances without new
Trait#
Traits are similar to Java interfaces but can include implementations. Classes can mixin multiple traits to enjoy the benefits of multiple inheritance while avoiding the diamond problem. Traits play a key role in code reuse, separation of concerns, and modularization.
Basic Trait
Defining only abstract methods can be used like a Java interface.
trait Greeter {
def greet(name: String): String
}
class FormalGreeter extends Greeter {
def greet(name: String): String = s"Good day, $name."
}
class CasualGreeter extends Greeter {
def greet(name: String): String = s"Hey, $name!"
}Trait with Implementation
Providing default implementations in traits allows implementing classes to use them without redefinition.
trait Logger {
def log(message: String): Unit = println(s"[LOG] $message")
def info(message: String): Unit = log(s"[INFO] $message")
def error(message: String): Unit = log(s"[ERROR] $message")
}
class MyService extends Logger {
def doSomething(): Unit = {
info("Starting work")
// Do work
info("Work complete")
}
}Multiple Traits (Mixin)
You can combine multiple traits with the with keyword. The first supertype uses extends, the rest use with.
trait Swimmer {
def swim(): String = "Swimming..."
}
trait Flyer {
def fly(): String = "Flying..."
}
// Multiple trait mixin
class Duck extends Swimmer with Flyer {
def quack(): String = "Quack!"
}
val duck = new Duck
println(duck.swim()) // Swimming...
println(duck.fly()) // Flying...
println(duck.quack()) // Quack!Trait Stacking
When multiple traits override the same method, they’re called according to linearization order. super.process calls the next trait’s method in linearization order. This is called the stackable modification pattern.
trait Base {
def process(s: String): String = s
}
trait Uppercase extends Base {
override def process(s: String): String = super.process(s.toUpperCase)
}
trait Trim extends Base {
override def process(s: String): String = super.process(s.trim)
}
// Applied right to left
class TextProcessor extends Base with Trim with Uppercase
val processor = new TextProcessor
println(processor.process(" hello world ")) // HELLO WORLDKey Points
- Traits are interfaces that can include implementations
- Can mixin multiple traits with
withkeyword- Trait stacking can combine behaviors (linearization order)
Abstract Class#
Abstract classes cannot be instantiated and can define abstract members that must be implemented by subclasses. Unlike traits, they can have constructor parameters.
abstract class Animal(val name: String) {
// Abstract method
def speak(): String
// Implemented method
def describe(): String = s"$name says ${speak()}"
}
class Dog(name: String) extends Animal(name) {
def speak(): String = "Woof"
}
class Cat(name: String) extends Animal(name) {
def speak(): String = "Meow"
}
val dog = new Dog("Buddy")
println(dog.describe()) // Buddy says WoofAbstract Class vs Trait
The table below summarizes the main differences between abstract classes and traits. Generally, traits are preferred, but abstract classes are used when constructor parameters are needed or Java compatibility is important.
| Feature | Abstract Class | Trait |
|---|---|---|
| Constructor parameters | Yes | Only in Scala 3 |
| Multiple inheritance | No | Yes (mixin) |
| Java compatibility | Good | Limited |
Recommendation: Use traits unless there’s a specific reason.
Key Points
- Abstract classes can have constructor parameters
- Traits allow multiple inheritance, but abstract classes only single inheritance
- Generally prefer traits; use abstract classes when constructors are needed
Access Modifiers#
Scala’s access modifiers are more granular than Java’s. The default is public, and private and protected can be used with scope specifiers for precise access control.
class MyClass {
private val privateField = 1 // Only this class
protected val protectedField = 2 // This class and subclasses
val publicField = 3 // Anywhere
private[this] val strictPrivate = 4 // Only this instance
}
// Package level access
class PackageAccess {
private[mypackage] val packagePrivate = 5 // Only within mypackage
}Key Points
- Default is public; use
privateandprotectedprivate[this]only accessible from same instanceprivate[packagename]controls package-level access
Enum (Scala 3)#
In Scala 3, you can define enumerations with the enum keyword. Supports various forms from simple enumerations to enumerations with parameters and ADT (Algebraic Data Type) style. In Scala 2, enumerations were implemented with sealed trait and case object combinations.
// Simple enumeration
enum Color:
case Red, Green, Blue
val color = Color.Red
println(color) // Red
// Enumeration with parameters
enum Planet(val mass: Double, val radius: Double):
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Venus extends Planet(4.869e+24, 6.0518e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
println(Planet.Earth.mass) // 5.976E24
// ADT style
enum Shape:
case Circle(radius: Double)
case Rectangle(width: Double, height: Double)
case Triangle(base: Double, height: Double)
import Shape.*
val shapes = List(Circle(5), Rectangle(3, 4), Triangle(6, 4))// Implement enum with sealed trait + case object
sealed trait Color
object Color {
case object Red extends Color
case object Green extends Color
case object Blue extends Color
}
val color: Color = Color.Red
// ADT style
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
val shapes: List[Shape] = List(Circle(5), Rectangle(3, 4), Triangle(6, 4))Key Points
- Scala 3: Concisely define enumerations and ADTs with
enum- Scala 2: Use
sealed trait+case object/classcombination- Supports both enumerations with parameters and ADT style
Exercises#
Practice class and object concepts with these exercises.
1. Bank Account Class
Implement a BankAccount class that manages balance.
deposit(amount): Depositwithdraw(amount): Withdraw (return false if insufficient balance)balance: Current balance
Show Answer
class BankAccount(initialBalance: Double) {
private var _balance: Double = initialBalance
def balance: Double = _balance
def deposit(amount: Double): Unit =
if (amount > 0) _balance += amount
def withdraw(amount: Double): Boolean =
if (amount > 0 && amount <= _balance) {
_balance -= amount
true
} else false
}
val account = new BankAccount(1000)
account.deposit(500)
println(account.balance) // 1500.0
println(account.withdraw(200)) // true
println(account.balance) // 1300.0
println(account.withdraw(2000)) // false2. Trait Mixin
Define Printable and Comparable traits, and mixin into a Product class.
Show Answer
trait Printable {
def print(): String
}
trait Comparable[T] {
def compare(other: T): Int
}
case class Product(name: String, price: Double)
extends Printable with Comparable[Product] {
def print(): String = s"Product: $name, Price: $price"
def compare(other: Product): Int = this.price.compare(other.price)
}
val p1 = Product("Laptop", 1500)
val p2 = Product("Mouse", 50)
println(p1.print()) // Product: Laptop, Price: 1500.0
println(p1.compare(p2)) // 1 (p1 is more expensive)Next Steps#
- Case Classes — Immutable data modeling
- Pattern Matching — Advanced match expressions