Scala is a multi-paradigm language. The OOP side is small but powerful:
class,object, andcase classcover almost everything. This chapter walks through every form of declaration with examples.
In this chapter:
class: the basic shape- Primary constructor
- Auxiliary constructors
- Fields:
valandvar - Methods, override,
final object: singletons- Companion objects
apply,unapply,updatecase classcase object- Visibility modifiers
class Person(val name: String, var age: Int)
val p = new Person("Bhaskar", 30)
p.name // "Bhaskar" — read-only because `val`
p.age = 31 // OK — `var`
// p.name = "X" // compile errorval name exposes the parameter as an immutable field. var age exposes it as a mutable field. With neither, the parameter is private to the constructor.
class Foo(name: String) // name not exposed; only constructor sees it
class Bar(val name: String) // exposed as immutable field
class Baz(var name: String) // exposed as mutable field
class Qux(private val name: String) // visible in class but not from outsideScala 3 note: Same syntax.
newis also optional in Scala 3 for class instantiation:// scala 3 val p = Person("Bhaskar", 30) // no `new`
The primary constructor is the class signature itself. The class body is the constructor body:
class Counter(start: Int) {
println(s"Counter created with start=$start") // runs at construction
private var n = start
def value: Int = n
def incr(): Unit = n += 1
}
val c = new Counter(10)
// "Counter created with start=10"
c.value // 10
c.incr()
c.value // 11Anything you put in the class body — field initializations, side effects, helper methods — runs at construction time.
Defined as def this(...). Every auxiliary constructor must, on its first line, call another constructor (auxiliary or primary):
class Person(val name: String, val age: Int) {
def this(name: String) = this(name, 0) // age defaults to 0
def this() = this("anonymous") // chains through above
}
new Person("Bhaskar", 30)
new Person("Bhaskar")
new Person()In practice, default arguments on the primary constructor often replace auxiliary constructors:
class Person(val name: String = "anonymous", val age: Int = 0)
new Person()
new Person("Bhaskar")
new Person("Bhaskar", 30)
new Person(age = 30)Inside a class body:
class Account {
val openedAt: java.time.Instant = java.time.Instant.now() // immutable
var balance: Double = 0.0 // mutable
private val auditLog: List[String] = Nil // hidden
protected var lastTouched: Long = 0L // visible to subclasses
}val fields produce a getter only. var fields produce a getter and a setter named field_=:
class Box {
var contents: String = ""
}
val b = new Box
b.contents = "hello" // calls b.contents_= ("hello")
println(b.contents) // calls b.contentsYou can override the implicit getter/setter for custom logic:
class Bounded {
private var _n: Int = 0
def n: Int = _n
def n_=(v: Int): Unit = _n = math.max(0, math.min(100, v))
}
val b = new Bounded
b.n = 150 // calls n_=, clamps to 100
b.n // 100class Animal {
def speak(): String = "(silence)"
def name: String = "animal"
}
class Dog extends Animal {
override def speak(): String = "woof" // override required
override val name: String = "dog" // can override def with val
}
new Dog().speak() // "woof"
new Dog().name // "dog"The override keyword is required when overriding non-abstract members. It catches typos at compile time.
final prevents further override:
class Animal {
final def species: String = "Animalia"
}
class Dog extends Animal {
// override def species: String = "Dog" // compile error: cannot override final
}A final class cannot be extended at all.
object declares a singleton — a class with exactly one instance, accessed by name:
object Logger {
private var counter = 0
def log(msg: String): Unit = {
counter += 1
println(s"[$counter] $msg")
}
}
Logger.log("hello")
Logger.log("world")
// [1] hello
// [2] worldCommon uses:
- Utility / helper functions without a class to attach them to.
- Companion objects for classes (next section).
- Module-style namespacing for related constants and methods.
- Type classes instances (later chapters).
objects are lazy: they're initialized on first access, not at JVM startup.
When an object and a class (or trait) share a name and live in the same file, they are companions. Companions can see each other's private members.
class Account private (val id: String, var balance: Double) {
override def toString = s"Account($id, $$$balance)"
}
object Account {
def open(id: String): Account = new Account(id, 0.0) // factory
def transfer(from: Account, to: Account, amount: Double): Unit = {
from.balance -= amount
to.balance += amount
}
}
val a = Account.open("A-1") // factory call
val b = Account.open("A-2")
Account.transfer(a, b, 50.0)Notice class Account private — the constructor is private. Callers must go through the companion's open factory. This is a common idiom for constructing values with validation or invariants.
A companion can hold:
- Factory methods.
- Extractor methods (
unapply). - Constants and shared resources.
- Type class instances (in FP idioms).
- Implicit/given values defined for the type.
Three method names with special syntactic sugar.
X(args) calls X.apply(args). This works for both classes/instances and objects:
class Adder(by: Int) {
def apply(x: Int): Int = x + by
}
val plus5 = new Adder(5)
plus5(10) // 15 — sugar for plus5.apply(10)Most commonly used in companion objects to enable MyClass(args) factory syntax without new:
class Vec(val x: Double, val y: Double)
object Vec {
def apply(x: Double, y: Double): Vec = new Vec(x, y)
}
Vec(1.0, 2.0) // sugar for Vec.apply(1.0, 2.0) — returns new Veccase class synthesizes apply for you (next section).
The companion to apply for pattern matching, covered in Chapter 5. case class synthesizes this too.
x(args) = value calls x.update(args, value):
class Grid(rows: Int, cols: Int) {
private val data = Array.ofDim[Int](rows, cols)
def apply(r: Int, c: Int): Int = data(r)(c)
def update(r: Int, c: Int, v: Int): Unit = data(r)(c) = v
}
val g = new Grid(3, 3)
g(1, 1) = 42 // sugar for g.update(1, 1, 42)
g(1, 1) // 42This is what makes array(i) = x and map("key") = value work.
A case class is a regular class with a bunch of useful machinery synthesized:
case class Person(name: String, age: Int)You get for free:
- A factory method (
apply), soPerson(...)works withoutnew. - An extractor (
unapply), so it works in pattern matches. - Field accessors as
valby default. - Sensible
equals,hashCode, andtoString. - A
copymethod for non-destructive updates. productArity,productIterator,productElement— useful for generic code.
val p1 = Person("Bhaskar", 30)
val p2 = Person("Bhaskar", 30)
p1 == p2 // true — value equality
p1.toString // "Person(Bhaskar,30)"
val p3 = p1.copy(age = 31) // new Person with age changed
p3 // Person(Bhaskar,31)
p1 match {
case Person(name, _) => println(s"matched $name")
}The copy method takes named parameters that default to the current values — perfect for "change one thing" updates of immutable data.
case class is the canonical way to define data in Scala. Use it whenever you want a value object: requests, responses, events, configuration, ADT variants.
A singleton with case-class behavior — proper toString, equals, hashCode. Used heavily in ADTs:
sealed trait Day
case object Monday extends Day
case object Tuesday extends Day
case object Wednesday extends Day
case object Thursday extends Day
case object Friday extends Day
case object Saturday extends Day
case object Sunday extends Day
def isWeekend(d: Day): Boolean = d match {
case Saturday | Sunday => true
case _ => false
}Scala 3 note: Scala 3 prefers
enumfor closed sets like this:// scala 3 enum Day: case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
class Bank {
val publicId: String = "..." // visible everywhere
private val secret: String = "..." // visible only inside this Bank instance
private[this] val verySecret: String // visible only to *this* instance, not other Bank instances
protected val familySecret: String // visible to subclasses too
private[bank] val pkgSecret: String // visible inside the `bank` package
def operation(other: Bank): Unit = {
println(this.secret) // OK
// println(other.verySecret) // compile error: instance-private
println(other.secret) // OK — `private` allows other Bank instances
}
}The four scopes:
private— same class (any instance).private[this]— just this instance; even another Bank can't see it.protected— class + subclasses (no other instances unless they're subclasses).private[some.pkg]— visible inside the named package.- (default) — public.
Most code uses private and the implicit public default. private[this] is rare but useful for lock-style scenarios. private[somePkg] is good for "publish to the rest of the module, hide from outside callers."
Scala 3 note:
private[this]is deprecated in Scala 3 — Scala 3'sprivateis already this-instance by default in the cases that matter.
- Every form of class declaration: primary constructor params, auxiliary constructors, defaulted args.
- The difference between
valparameter,varparameter, plain parameter, and class-body field. - How
overrideandfinalinteract with inheritance. - That
objectis a singleton and what makes a companion special. - The three magic method names:
apply,unapply,update, and the syntax they enable. - What
case classsynthesizes for you, and why it's the default for data types. - The full set of visibility modifiers.
The next chapter (Chapter 7 — Traits and Inheritance) covers Scala's most distinctive OOP feature: traits, mixin composition, and the linearization that makes them all work.
← Previous: Chapter 5 — Pattern Matching | Back to README | Next: Chapter 7 — Traits →