Scala Classes, Case Classes, Objects, and Traits
Edited by Dev Kilaru, Senior Data Analyst. How we make our content
Introduction
Scala is fully object-oriented, but it removes much of the ceremony found in Java. A class can declare its fields in the constructor, case classes give you equality, printing, and copying for free, objects are built-in singletons, and traits let you mix shared behavior into several classes. This lesson shows how to model a small domain with these tools.
Prerequisites
- Functions and collections from the previous lessons
Step-by-Step Guide
Step 1: Declare a class with constructor fields
The parameters in a class header become the constructor, and marking one with val makes it a public read-only field, while var makes it a public mutable field. Without either keyword the parameter is private to the class. The body can add methods and extra fields. Create instances by calling the class name, because Scala 3 allows creating an instance without the new keyword. Keep fields as vals wherever possible so an object cannot change after it is created.
class Account(val owner: String, private var balance: Double):
def deposit(amount: Double): Unit =
require(amount > 0, "Deposit must be positive")
balance += amount
def currentBalance: Double = balance
val acct = Account("Asha", 100.0)
acct.deposit(50)
println(acct.currentBalance) // 150.0
println(acct.owner) // AshaStep 2: Model data with case classes
A case class is for data. By adding the case keyword, you get several useful members automatically: fields are public vals, equality compares contents rather than references, toString prints the values, hashCode is consistent with equality, and a copy method makes a modified duplicate. This replaces dozens of lines of Java boilerplate. Because case class instances are immutable, you update them by copying with changes, which makes code easier to reason about, especially in concurrent programs.
case class Employee(name: String, dept: String, salary: Double)
val e1 = Employee("Asha", "Eng", 85000)
val e2 = Employee("Asha", "Eng", 85000)
println(e1) // Employee(Asha,Eng,85000.0)
println(e1 == e2) // true (same contents)
val raised = e1.copy(salary = e1.salary * 1.1)
println(raised.salary) // about 93500Step 3: Use objects and companion objects
An object declaration creates a single instance, a singleton, which replaces static members in Scala: it is the natural home for constants and utility methods. A companion object is an object with the same name as a class in the same file, and it holds factory methods and constants related to the class. Companions can access each other's private members. A very common use is a validated factory method that returns an Option, so invalid data never becomes an instance.
case class Email(value: String)
object Email:
def parse(text: String): Option[Email] =
if text.contains("@") then Some(Email(text.trim.toLowerCase))
else None
println(Email.parse("Asha@Example.com")) // Some(Email(asha@example.com))
println(Email.parse("not-an-email")) // NoneStep 4: Share behavior with traits
A trait defines methods and fields that classes can include. It is similar to a Java interface that may also contain implementations, and a class can extend a class and mix in several traits. An abstract method has no body, and each class must supply it, while a concrete method is inherited as is. Use a trait to describe a capability, such as being printable or being auditable, and keep traits small and focused so they combine cleanly.
trait Describable:
def name: String
def describe: String = s"I am $name"
trait Greets:
def greet(other: String): String = s"Hello, $other!"
class Robot(val name: String) extends Describable with Greets
val r = Robot("R2")
println(r.describe) // I am R2
println(r.greet("Asha")) // Hello, Asha!Step 5: Extend a class and override members
A subclass extends a parent and can replace a method by marking it override, which the compiler requires so you cannot override by accident. Fields in the parent that are not final can be given new values through the constructor. Inheritance is useful, but Scala code leans on composition and traits more than deep class hierarchies, because long inheritance chains become hard to follow. Mark a class final to prevent extension, or use sealed to restrict subclasses to the same file, which matters for pattern matching in the next lesson.
class Shape(val name: String):
def area: Double = 0.0
override def toString: String = s"$name with area $area"
class Square(side: Double) extends Shape("Square"):
override def area: Double = side * side
println(Square(3)) // Square with area 9.0Step 6: Organize with packages and imports
A package groups related code and prevents name clashes. A file starts with a package clause, and other files bring names into scope with import, using a wildcard to bring in everything, which Scala 3 writes as an asterisk (the underscore still works). You can import several names at once from one package, and rename an import to avoid a clash. Directory layout conventionally mirrors the package name, which build tools such as sbt expect. Even for small programs, organizing code into packages from the start makes it much easier to grow.
package com.calibertek.hr
import scala.collection.mutable.ArrayBuffer
import java.time.{LocalDate, Month}
import scala.util.*
val today: LocalDate = LocalDate.of(2026, Month.NOVEMBER, 8)FAQs
- When should I use a case class instead of a regular class?
- Use a case class for data whose identity is its contents, such as records, messages, and configuration. Use a regular class for objects with behavior and hidden state, such as a connection or an account with changing balance.
- What is the difference between a trait and an abstract class?
- A class can extend only one parent class but mix in many traits. Traits cannot take constructor parameters in Scala 2 and have a more flexible role in Scala 3. Use a trait for a capability, and an abstract class when you need a shared base with constructor arguments.
- Do I need the new keyword?
- Not in Scala 3. Calling Employee("Asha", "Eng", 85000) creates an instance for classes in general, so new is optional, and you will still see it in older code.
Summary
You can now define classes with constructor fields, model data with case classes, group helpers in companion objects, share behavior through traits, extend and override members, and organize code into packages. Case classes combined with sealed hierarchies are the heart of the next lesson, where pattern matching lets you take such data apart safely.
Related tutorials
- Schedule and Debug Shell Scripts: cron, bash -x, and Portability
- Shell Scripting Project: A Backup Script with Rotation and a Log Report
- Writing Safe Bash Scripts: set -euo pipefail, getopts, and trap
- Shell Pipes, Redirection, grep, sed, and awk for Text Processing
- Bash Loops, Arrays, and Functions Explained
Comments
Log in or create a free account to join the discussion.