Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
12: Case Classes
In a lot of languages, you spend a huge chunk of your time writing "boilerplate" code—getters, setters, equals(), hashCode(), and toString()—just to create a simple object that holds some data. I used to do this in Java for years, and it's tedious. In Scala, we have case classes, which essentially tell the compiler: "This class is primarily a container for data, so just handle all that boring stuff for me."
The frustration of regular classes for data
To show you why case classes are a lifesaver, let's try to build a simple system to track coffee orders. I'll start by doing it the "wrong" way—using a standard class—so you can see the headache I'm talking about.
class Order(val id: Int, val item: String)
val order1 = Order(1, "Latte")
val order2 = Order(1, "Latte")
println(order1 == order2) // This prints: false
Wait, why false? The IDs are the same, the items are the same. But because Order is a regular class, Scala uses reference equality. It's checking if these are the exact same object in memory, not if they contain the same data. If I wanted this to return true, I'd have to manually override the equals and hashCode methods. It's a lot of typing for very little gain.
Switching to Case Classes
Now, let's fix this by simply adding the case keyword. This changes everything. A case class automatically implements value-based equality, a useful toString implementation, and it makes the constructor parameters public vals by default.
case class Order(id: Int, item: String)
val order1 = Order(1, "Latte")
val order2 = Order(1, "Latte")
println(order1 == order2) // This prints: true
println(order1) // This prints: Order(1,Latte)
Much better. I didn't have to write a single line of logic to make that work. Also, notice I didn't use the new keyword to create the objects. Case classes provide an apply method automatically, making the syntax cleaner.
Updating data without mutating it
In Scala, we prefer immutability. You shouldn't be changing the properties of an order once it's created; instead, you create a new version of that order with the change. You might think this is inefficient, but the copy method makes it trivial.
Let's say the customer decided they wanted a "Large Latte" instead of just a "Latte".
val originalOrder = Order(1, "Latte")
val updatedOrder = originalOrder.copy(item = "Large Latte")
println(originalOrder) // Order(1,Latte) - still intact!
println(updatedOrder) // Order(1,Large Latte)
The copy method creates a new instance, copying all existing fields except the ones you explicitly change. It's a pattern I use constantly to keep my state predictable and bug-free.
Making the data useful with Pattern Matching
The real "superpower" of case classes unlocks when you combine them with pattern matching. Because the compiler knows the structure of a case class, it can "deconstruct" it to get to the data inside. This is called unapply.
Imagine we want to print a custom message based on the order item:
def processOrder(order: Order): String = order match {
case Order(_, "Latte") => "Steaming the milk for a Latte!"
case Order(id, item) => s"Processing order #$id for a $item"
}
println(processOrder(Order(1, "Latte"))) // Steaming the milk for a Latte!
println(processOrder(Order(2, "Espresso"))) // Processing order #2 for a Espresso
In the first case, I used an underscore _ because I didn't care about the ID—I only cared if the item was a "Latte". In the second case, I captured both the ID and the item to use them in the string. This is infinitely more readable than a chain of if/else statements checking properties.
📋 Practical Task
Build a Digital Library Inventory System
You need to create a small system to manage books in a library. Your task is to implement the following using the concepts from this lesson:
- Create a case class called
Bookwith three properties:isbn: String,title: String, andisAvailable: Boolean. - Create a function called
checkoutBook(book: Book). This function should take aBookand return a newBookinstance whereisAvailableis set tofalseusing thecopymethod. - Create a function called
bookStatus(book: Book)that uses pattern matching to return a string:- If the book is available, return:
"The book [title] is ready for pickup." - If the book is not available, return:
"Sorry, [title] is currently checked out."
- If the book is available, return:
Test your code by creating a book, checking it out, and printing the status of both the original and the updated book objects.
There are no comments for now.