Classes and Objects
Classes and Objects in Scala
Scala is a pure object-oriented language where every value is an object. It provides powerful features for object-oriented programming while maintaining functional programming capabilities.
Classes
Basic Class Definition
class Person(firstName: String, lastName: String) {
// Class body
def fullName: String = s"$firstName $lastName"
def greet(): Unit = {
println(s"Hello, I'm $fullName")
}
}
// Creating instances
val person = new Person("John", "Doe")
person.greet() // Hello, I'm John Doe
Class Parameters and Fields
// Parameters without val/var are private
class Student(name: String, grade: Int)
// Parameters with val become public immutable fields
class Employee(val name: String, val id: Int)
// Parameters with var become public mutable fields
class Counter(var count: Int) {
def increment(): Unit = count += 1
}
// Mixed visibility
class BankAccount(val accountNumber: String, private var balance: Double) {
def deposit(amount: Double): Unit = {
if (amount > 0) balance += amount
}
def getBalance: Double = balance
}
Constructors
class Rectangle(val width: Double, val height: Double) {
// Primary constructor body
println(s"Creating rectangle ${width}x${height}")
// Auxiliary constructors must call primary constructor
def this(size: Double) = {
this(size, size) // Square
}
def this() = {
this(1.0) // Unit square
}
def area: Double = width * height
}
val rect1 = new Rectangle(3, 4)
val square = new Rectangle(5)
val unit = new Rectangle()
Methods and Properties
class Circle(radius: Double) {
// Computed property (no parentheses)
def area: Double = math.Pi * radius * radius
// Method with side effects (use parentheses)
def printInfo(): Unit = {
println(s"Circle with radius $radius")
}
// Method with parameters
def scale(factor: Double): Circle = {
new Circle(radius * factor)
}
// Operator as method
def +(other: Circle): Circle = {
new Circle(radius + other.radius)
}
}
val c1 = new Circle(5)
val c2 = new Circle(3)
val c3 = c1 + c2 // Using + operator
Objects (Singletons)
Singleton Objects
object MathConstants {
val Pi = 3.14159
val E = 2.71828
def goldenRatio: Double = (1 + math.sqrt(5)) / 2
}
// Usage
println(MathConstants.Pi)
println(MathConstants.goldenRatio)
Companion Objects
class User private(val id: Int, val name: String) {
override def toString: String = s"User($id, $name)"
}
object User {
// Factory methods
def apply(name: String): User = new User(generateId(), name)
def fromDatabase(id: Int): Option[User] = {
// Simulate database lookup
if (id > 0) Some(new User(id, s"User$id"))
else None
}
private var nextId = 1
private def generateId(): Int = {
val id = nextId
nextId += 1
id
}
}
// Usage
val user1 = User("Alice") // Calls apply method
val user2 = User.fromDatabase(42)
Case Classes
Case classes are immutable data containers with built-in features:
case class Point(x: Double, y: Double) {
def distanceToOrigin: Double = math.sqrt(x * x + y * y)
def move(dx: Double, dy: Double): Point = {
Point(x + dx, y + dy)
}
}
// Automatic features:
val p1 = Point(3, 4) // No 'new' keyword needed
val p2 = p1.copy(y = 5) // Copy with modifications
println(p1) // Point(3.0,4.0) - toString implemented
val Point(x, y) = p1 // Pattern matching extraction
// Equality based on values
val p3 = Point(3, 4)
println(p1 == p3) // true (structural equality)
Case Classes vs Regular Classes
// Case class
case class Book(title: String, author: String, year: Int)
// Equivalent regular class (much more verbose!)
class RegularBook(val title: String, val author: String, val year: Int) {
override def equals(obj: Any): Boolean = obj match {
case that: RegularBook =>
this.title == that.title &&
this.author == that.author &&
this.year == that.year
case _ => false
}
override def hashCode(): Int = {
val state = Seq(title, author, year)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}
override def toString: String = s"RegularBook($title,$author,$year)"
def copy(title: String = this.title,
author: String = this.author,
year: Int = this.year): RegularBook = {
new RegularBook(title, author, year)
}
}
Inheritance
Class Inheritance
// Base class
open class Animal(val name: String) {
def speak(): String = s"$name makes a sound"
def move(): String = s"$name moves"
}
// Derived class
class Dog(name: String, val breed: String) extends Animal(name) {
override def speak(): String = s"$name barks"
def wagTail(): String = s"$name wags tail"
}
class Cat(name: String) extends Animal(name) {
override def speak(): String = s"$name meows"
override def move(): String = s"$name prowls"
}
val dog = new Dog("Rex", "Labrador")
println(dog.speak()) // Rex barks
println(dog.move()) // Rex moves (inherited)
Abstract Classes
abstract class Shape {
def area: Double // Abstract method
def perimeter: Double // Abstract method
// Concrete method
def description: String = s"Shape with area $area and perimeter $perimeter"
}
class Rectangle(width: Double, height: Double) extends Shape {
def area: Double = width * height
def perimeter: Double = 2 * (width + height)
}
class Circle(radius: Double) extends Shape {
def area: Double = math.Pi * radius * radius
def perimeter: Double = 2 * math.Pi * radius
}
Traits
Traits are like interfaces with implementation:
trait Greetable {
def name: String // Abstract
def greet(): String = s"Hello, I'm $name" // Concrete
}
trait Timestamped {
val createdAt: Long = System.currentTimeMillis()
def age: Long = System.currentTimeMillis() - createdAt
}
// Multiple trait inheritance
class Person(val name: String) extends Greetable with Timestamped {
override def greet(): String = super.greet() + s" (created ${age}ms ago)"
}
// Traits with parameters (Scala 3)
trait Logger(prefix: String) {
def log(message: String): Unit = println(s"$prefix: $message")
}
Mixing Traits
trait Flying {
def fly(): String = "Flying through the air"
}
trait Swimming {
def swim(): String = "Swimming in water"
}
trait Running {
def run(): String = "Running on ground"
}
class Duck extends Animal("Duck") with Flying with Swimming {
override def speak(): String = "Quack!"
}
class Penguin extends Animal("Penguin") with Swimming with Running
val duck = new Duck
println(duck.fly()) // Flying through the air
println(duck.swim()) // Swimming in water
Access Modifiers
class AccessExample {
// Public (default)
val publicField = "Everyone can see this"
def publicMethod(): Unit = println("Public method")
// Private - only this instance
private val privateField = "Only this class"
private def privateMethod(): Unit = println("Private method")
// Protected - this class and subclasses
protected val protectedField = "Subclasses can see"
protected def protectedMethod(): Unit = println("Protected method")
// Private to package
private[mypackage] val packagePrivate = "Package visible"
// Private to enclosing class
class Inner {
private[AccessExample] val outerPrivate = "Outer class can see"
}
}
Type Members
class Graph {
// Type members
type Node = Int
type Edge = (Node, Node)
private var nodes: Set[Node] = Set()
private var edges: Set[Edge] = Set()
def addNode(n: Node): Unit = nodes += n
def addEdge(e: Edge): Unit = edges += e
}
// Path-dependent types
val g1 = new Graph
val g2 = new Graph
// g1.Node and g2.Node are considered the same type
Inner Classes
class Outer(name: String) {
outer => // Self reference
class Inner(innerName: String) {
def fullName: String = s"${outer.name}.$innerName"
}
def createInner(innerName: String): Inner = new Inner(innerName)
}
val outer1 = new Outer("First")
val outer2 = new Outer("Second")
val inner1 = outer1.createInner("A")
val inner2 = outer2.createInner("B")
// Path-dependent types: inner1 and inner2 have different types!
// outer1.Inner != outer2.Inner
Enumerations
Scala 2 Style
object Color extends Enumeration {
val Red, Green, Blue = Value
val Yellow = Value(10, "YELLOW") // Custom id and name
}
// Usage
val color = Color.Red
println(color.id) // 0
println(Color.values) // Color.ValueSet(Red, Green, Blue, Yellow)
Scala 3 Enums
enum Color {
case Red, Green, Blue
case Custom(hex: String)
}
// With methods
enum Planet(mass: Double, radius: Double) {
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
case Jupiter extends Planet(1.9e+27, 7.1492e7)
def surfaceGravity: Double = 6.67300E-11 * mass / (radius * radius)
}
Variance
// Covariance (+T) - if A <: B then Container[A] <: Container[B]
class Box[+T](val content: T)
// Contravariance (-T) - if A <: B then Container[B] <: Container[A]
trait Printer[-T] {
def print(value: T): Unit
}
// Invariance (T) - no subtype relationship
class MutableBox[T](var content: T)
// Example
class Animal
class Dog extends Animal
val dogBox: Box[Dog] = new Box(new Dog)
val animalBox: Box[Animal] = dogBox // OK because Box is covariant
Best Practices
- Prefer immutability: Use
val
and case classes - Use meaningful names: Classes should be nouns, methods should be verbs
- Keep classes focused: Single Responsibility Principle
- Favor composition: Use traits for reusable behavior
- Make classes final: Unless designed for inheritance
- Use factory methods: Companion object's apply method
- Override toString: For better debugging
Common Patterns
Builder Pattern
case class Pizza(size: String = "medium",
cheese: Boolean = true,
toppings: List[String] = Nil) {
def withSize(s: String): Pizza = copy(size = s)
def withCheese(c: Boolean): Pizza = copy(cheese = c)
def withTopping(t: String): Pizza = copy(toppings = t :: toppings)
}
val myPizza = Pizza()
.withSize("large")
.withTopping("mushrooms")
.withTopping("pepperoni")
Factory Pattern
trait Animal {
def speak(): String
}
object Animal {
private class Dog extends Animal {
def speak(): String = "Woof"
}
private class Cat extends Animal {
def speak(): String = "Meow"
}
def apply(animalType: String): Option[Animal] = animalType.toLowerCase match {
case "dog" => Some(new Dog)
case "cat" => Some(new Cat)
case _ => None
}
}
Animal("dog").foreach(_.speak()) // Woof