Methods and Fields: Adding Behavior and State
Introduction
Methods and fields are the core components that bring classes to life. Fields store the state (data) of an object, while methods define the behavior (actions) that objects can perform. Understanding how to design and implement these effectively is crucial for building robust, maintainable object-oriented applications.
In this lesson, you'll learn advanced techniques for defining methods, working with different types of fields, understanding method overloading and overriding, and creating clean APIs for your classes.
Fields: Storing Object State
Field Types and Visibility
class Person(name: String, age: Int) {
// Public mutable field
var email: String = ""
// Public immutable field
val id: String = java.util.UUID.randomUUID().toString
// Private mutable field
private var _password: String = ""
// Private immutable field
private val createdAt: java.time.LocalDateTime = java.time.LocalDateTime.now()
// Protected field (accessible in subclasses)
protected var lastLoginAt: Option[java.time.LocalDateTime] = None
// Computed field (no storage, computed each time)
def isMinor: Boolean = age < 18
// Lazy field (computed once, when first accessed)
lazy val hashedPassword: String = computeHash(_password)
private def computeHash(password: String): String = {
// Simplified hash function for example
s"hash_${password.hashCode}"
}
}
val person = new Person("Alice", 25)
println(person.id) // OK - public val
person.email = "alice@example.com" // OK - public var
// person._password = "secret" // Error - private field
println(person.isMinor) // OK - computed field
Custom Getters and Setters
class Temperature {
private var _celsius: Double = 0.0
// Custom getter
def celsius: Double = _celsius
// Custom setter with validation
def celsius_=(value: Double): Unit = {
require(value >= -273.15, "Temperature cannot be below absolute zero")
println(s"Setting temperature to ${value}°C")
_celsius = value
}
// Read-only computed properties
def fahrenheit: Double = _celsius * 9.0 / 5.0 + 32.0
def kelvin: Double = _celsius + 273.15
// Write-only setter for fahrenheit
def fahrenheit_=(value: Double): Unit = {
celsius = (value - 32.0) * 5.0 / 9.0
}
// Property with side effects
def kelvin_=(value: Double): Unit = {
println(s"Converting ${value}K to Celsius")
celsius = value - 273.15
}
}
val temp = new Temperature()
temp.celsius = 25.0 // Calls custom setter
println(temp.fahrenheit) // Calls computed getter
temp.fahrenheit = 100.0 // Calls fahrenheit setter, which calls celsius setter
Field Initialization and Lazy Evaluation
class DataProcessor(val dataSource: String) {
// Eager initialization (computed during object creation)
val connectionId: String = {
println("Establishing connection...")
s"conn_${System.currentTimeMillis()}"
}
// Lazy initialization (computed when first accessed)
lazy val expensiveData: List[String] = {
println("Loading expensive data...")
Thread.sleep(1000) // Simulate expensive operation
List("data1", "data2", "data3")
}
// Method that uses lazy field
def processData(): Unit = {
println(s"Processing ${expensiveData.length} items")
expensiveData.foreach(println)
}
}
val processor = new DataProcessor("database")
println("Object created") // connectionId is already computed
println("About to process...")
processor.processData() // expensiveData is computed here
Methods: Defining Object Behavior
Method Definitions and Parameters
class Calculator {
// Simple method
def add(a: Int, b: Int): Int = a + b
// Method with default parameters
def multiply(a: Int, b: Int = 1): Int = a * b
// Method with named parameters
def divide(dividend: Double, divisor: Double): Double = {
require(divisor != 0, "Division by zero")
dividend / divisor
}
// Variable arguments (varargs)
def sum(numbers: Int*): Int = numbers.sum
// Multiple parameter lists
def power(base: Double)(exponent: Int): Double = {
math.pow(base, exponent)
}
// Method with implicit parameter (covered in advanced lessons)
def format(value: Double)(implicit precision: Int): String = {
f"$value%.${precision}f"
}
}
val calc = new Calculator()
println(calc.add(5, 3)) // 8
println(calc.multiply(4)) // 4 (uses default)
println(calc.divide(dividend = 10, divisor = 3)) // Named parameters
println(calc.sum(1, 2, 3, 4, 5)) // 15
println(calc.power(2)(3)) // 8.0
implicit val precision: Int = 2
println(calc.format(3.14159)) // "3.14"
Method Overloading
class Printer {
// Overloaded methods with different parameter types
def print(value: String): Unit = println(s"String: $value")
def print(value: Int): Unit = println(s"Integer: $value")
def print(value: Double): Unit = println(s"Double: $value")
def print(value: Boolean): Unit = println(s"Boolean: $value")
// Overloaded methods with different number of parameters
def print(name: String, value: Any): Unit = println(s"$name: $value")
def print(prefix: String, name: String, value: Any): Unit =
println(s"$prefix - $name: $value")
// Overloaded methods with different parameter order
def format(value: Double, precision: Int): String = f"$value%.${precision}f"
def format(precision: Int, value: Double): String = f"$value%.${precision}f"
}
val printer = new Printer()
printer.print("Hello") // String version
printer.print(42) // Int version
printer.print(3.14) // Double version
printer.print("Score", 95) // Two parameter version
Methods with Side Effects vs Pure Methods
class Counter {
private var _count: Int = 0
// Impure method (has side effects)
def increment(): Unit = {
_count += 1
println(s"Count incremented to ${_count}")
}
// Pure method (no side effects)
def count: Int = _count
// Pure method returning new state
def incrementedBy(amount: Int): Int = _count + amount
// Method that returns this for chaining
def reset(): Counter = {
_count = 0
this
}
// Method chaining
def add(amount: Int): Counter = {
_count += amount
this
}
}
val counter = new Counter()
counter.increment() // Side effect: prints and modifies state
println(counter.count) // Pure: just returns current state
println(counter.incrementedBy(5)) // Pure: doesn't modify state
// Method chaining
counter.reset().add(10).add(5)
println(counter.count) // 15
Advanced Method Patterns
Factory Methods
class User private(val username: String, val email: String, val hashedPassword: String) {
override def toString: String = s"User($username, $email)"
}
object User {
// Factory method with validation
def create(username: String, email: String, password: String): Either[String, User] = {
if (username.length < 3) {
Left("Username must be at least 3 characters")
} else if (!email.contains("@")) {
Left("Invalid email format")
} else if (password.length < 8) {
Left("Password must be at least 8 characters")
} else {
val hashedPassword = hashPassword(password)
Right(new User(username, email, hashedPassword))
}
}
// Alternative factory method from data
def fromCsv(csvLine: String): Option[User] = {
csvLine.split(",") match {
case Array(username, email, hashedPass) if username.nonEmpty && email.nonEmpty =>
Some(new User(username.trim, email.trim, hashedPass.trim))
case _ => None
}
}
private def hashPassword(password: String): String = {
s"hashed_${password.hashCode}"
}
}
// Usage
User.create("alice", "alice@example.com", "secretpassword") match {
case Right(user) => println(s"Created: $user")
case Left(error) => println(s"Error: $error")
}
Template Method Pattern
abstract class DataProcessor {
// Template method - defines the algorithm structure
final def processData(input: String): String = {
val validated = validateInput(input)
val transformed = transformData(validated)
val processed = processCore(transformed)
val formatted = formatOutput(processed)
logResult(formatted)
formatted
}
// Abstract methods to be implemented by subclasses
protected def processCore(data: String): String
// Methods with default implementation (can be overridden)
protected def validateInput(input: String): String = {
require(input.nonEmpty, "Input cannot be empty")
input.trim
}
protected def transformData(data: String): String = data.toLowerCase
protected def formatOutput(data: String): String = s"Result: $data"
protected def logResult(result: String): Unit = {
println(s"Processing completed: ${result.take(50)}...")
}
}
class UpperCaseProcessor extends DataProcessor {
protected def processCore(data: String): String = data.toUpperCase
}
class ReverseProcessor extends DataProcessor {
protected def processCore(data: String): String = data.reverse
// Override default formatting
protected override def formatOutput(data: String): String = s"Reversed: $data"
}
val upperProcessor = new UpperCaseProcessor()
val reverseProcessor = new ReverseProcessor()
println(upperProcessor.processData(" Hello World "))
println(reverseProcessor.processData(" Hello World "))
Fluent Interface (Method Chaining)
class QueryBuilder {
private var table: String = ""
private var columns: List[String] = List("*")
private var whereConditions: List[String] = List()
private var orderBy: List[String] = List()
private var limitCount: Option[Int] = None
def from(tableName: String): QueryBuilder = {
table = tableName
this
}
def select(cols: String*): QueryBuilder = {
columns = cols.toList
this
}
def where(condition: String): QueryBuilder = {
whereConditions = whereConditions :+ condition
this
}
def orderBy(column: String, direction: String = "ASC"): QueryBuilder = {
this.orderBy = this.orderBy :+ s"$column $direction"
this
}
def limit(count: Int): QueryBuilder = {
limitCount = Some(count)
this
}
def build(): String = {
require(table.nonEmpty, "Table name is required")
val selectClause = s"SELECT ${columns.mkString(", ")}"
val fromClause = s"FROM $table"
val parts = List(selectClause, fromClause)
val whereClause = if (whereConditions.nonEmpty) {
Some(s"WHERE ${whereConditions.mkString(" AND ")}")
} else None
val orderClause = if (orderBy.nonEmpty) {
Some(s"ORDER BY ${orderBy.mkString(", ")}")
} else None
val limitClause = limitCount.map(count => s"LIMIT $count")
(parts ++ List(whereClause, orderClause, limitClause).flatten).mkString(" ")
}
}
// Usage with method chaining
val query = new QueryBuilder()
.select("name", "email", "age")
.from("users")
.where("age >= 18")
.where("active = true")
.orderBy("name")
.limit(10)
.build()
println(query)
// SELECT name, email, age FROM users WHERE age >= 18 AND active = true ORDER BY name LIMIT 10
Practical Examples
Example 1: Shopping Cart System
case class Product(id: String, name: String, price: Double, category: String)
case class CartItem(product: Product, quantity: Int) {
require(quantity > 0, "Quantity must be positive")
def total: Double = product.price * quantity
def withQuantity(newQuantity: Int): CartItem = copy(quantity = newQuantity)
}
class ShoppingCart {
private var items: Map[String, CartItem] = Map()
private var _discountCode: Option[String] = None
def addItem(product: Product, quantity: Int = 1): ShoppingCart = {
val productId = product.id
items.get(productId) match {
case Some(existingItem) =>
items = items + (productId -> existingItem.withQuantity(existingItem.quantity + quantity))
case None =>
items = items + (productId -> CartItem(product, quantity))
}
this
}
def removeItem(productId: String): ShoppingCart = {
items = items - productId
this
}
def updateQuantity(productId: String, quantity: Int): ShoppingCart = {
if (quantity <= 0) {
removeItem(productId)
} else {
items.get(productId) match {
case Some(item) => items = items + (productId -> item.withQuantity(quantity))
case None => // Product not in cart, do nothing
}
}
this
}
def applyDiscountCode(code: String): Boolean = {
// Simple validation - in real app, this would check against a database
if (code == "SAVE10" || code == "WELCOME") {
_discountCode = Some(code)
true
} else {
false
}
}
def removeDiscountCode(): ShoppingCart = {
_discountCode = None
this
}
// Computed properties
def subtotal: Double = items.values.map(_.total).sum
def discountAmount: Double = _discountCode match {
case Some("SAVE10") => subtotal * 0.1
case Some("WELCOME") => math.min(subtotal * 0.05, 50.0) // 5% up to $50
case _ => 0.0
}
def total: Double = subtotal - discountAmount
def itemCount: Int = items.values.map(_.quantity).sum
def isEmpty: Boolean = items.isEmpty
def getItems: List[CartItem] = items.values.toList.sortBy(_.product.name)
def summary(): String = {
if (isEmpty) {
"Cart is empty"
} else {
val itemsList = getItems.map { item =>
f" ${item.product.name}%-20s x${item.quantity}%2d = $$${item.total}%6.2f"
}.mkString("\n")
val discountLine = if (discountAmount > 0) {
f"\nDiscount (${_discountCode.get}): -$$${discountAmount}%.2f"
} else ""
s"""Shopping Cart Summary:
|$itemsList
|${"-" * 40}
|Subtotal: $$${subtotal}%.2f$discountLine
|Total: $$${total}%.2f
|Items: $itemCount""".stripMargin
}
}
def clear(): ShoppingCart = {
items = Map()
_discountCode = None
this
}
}
// Usage
val laptop = Product("P001", "Gaming Laptop", 1299.99, "Electronics")
val mouse = Product("P002", "Wireless Mouse", 49.99, "Electronics")
val keyboard = Product("P003", "Mechanical Keyboard", 129.99, "Electronics")
val cart = new ShoppingCart()
.addItem(laptop)
.addItem(mouse, 2)
.addItem(keyboard)
println(cart.summary())
cart.applyDiscountCode("SAVE10")
println("\nAfter applying discount:")
println(cart.summary())
cart.updateQuantity("P002", 1) // Reduce mouse quantity
println("\nAfter updating mouse quantity:")
println(cart.summary())
Example 2: File Manager with Metadata
import java.time.LocalDateTime
import java.nio.file.Path
case class FileMetadata(
size: Long,
createdAt: LocalDateTime,
modifiedAt: LocalDateTime,
isDirectory: Boolean,
permissions: String
)
class ManagedFile(val path: Path, val metadata: FileMetadata) {
// Computed properties
def name: String = path.getFileName.toString
def extension: String = {
val name = this.name
val lastDot = name.lastIndexOf('.')
if (lastDot > 0) name.substring(lastDot + 1) else ""
}
def sizeInKB: Double = metadata.size / 1024.0
def sizeInMB: Double = sizeInKB / 1024.0
def sizeInGB: Double = sizeInMB / 1024.0
def isTextFile: Boolean = {
val textExtensions = Set("txt", "md", "csv", "json", "xml", "yml", "yaml")
textExtensions.contains(extension.toLowerCase)
}
def isImageFile: Boolean = {
val imageExtensions = Set("jpg", "jpeg", "png", "gif", "bmp", "svg")
imageExtensions.contains(extension.toLowerCase)
}
def isCodeFile: Boolean = {
val codeExtensions = Set("scala", "java", "py", "js", "ts", "cpp", "c", "h")
codeExtensions.contains(extension.toLowerCase)
}
// Methods for file operations
def formatSize(): String = {
if (metadata.size < 1024) s"${metadata.size} B"
else if (sizeInKB < 1024) f"${sizeInKB}%.1f KB"
else if (sizeInMB < 1024) f"${sizeInMB}%.1f MB"
else f"${sizeInGB}%.2f GB"
}
def getFileType(): String = {
if (metadata.isDirectory) "Directory"
else if (isTextFile) "Text File"
else if (isImageFile) "Image File"
else if (isCodeFile) "Code File"
else extension.toUpperCase + " File"
}
def info(): String = {
s"""File: $name
|Path: $path
|Type: ${getFileType()}
|Size: ${formatSize()}
|Created: ${metadata.createdAt}
|Modified: ${metadata.modifiedAt}
|Permissions: ${metadata.permissions}""".stripMargin
}
override def toString: String = s"$name (${formatSize()})"
}
class FileManager {
private var files: List[ManagedFile] = List()
def addFile(file: ManagedFile): FileManager = {
files = files :+ file
this
}
def removeFile(path: Path): FileManager = {
files = files.filterNot(_.path == path)
this
}
// Query methods
def findByName(name: String): Option[ManagedFile] = {
files.find(_.name.toLowerCase.contains(name.toLowerCase))
}
def findByExtension(extension: String): List[ManagedFile] = {
files.filter(_.extension.toLowerCase == extension.toLowerCase)
}
def findLargerThan(sizeInMB: Double): List[ManagedFile] = {
files.filter(_.sizeInMB > sizeInMB)
}
def findCreatedAfter(date: LocalDateTime): List[ManagedFile] = {
files.filter(_.metadata.createdAt.isAfter(date))
}
// Statistics methods
def totalSize: Long = files.map(_.metadata.size).sum
def totalSizeFormatted: String = {
val totalMB = totalSize / (1024.0 * 1024.0)
if (totalMB < 1024) f"${totalMB}%.1f MB"
else f"${totalMB / 1024.0}%.2f GB"
}
def fileTypeStats: Map[String, Int] = {
files.groupBy(_.getFileType()).view.mapValues(_.length).toMap
}
def largestFiles(count: Int = 10): List[ManagedFile] = {
files.sortBy(-_.metadata.size).take(count)
}
def generateReport(): String = {
val fileCount = files.length
val dirCount = files.count(_.metadata.isDirectory)
val regularFileCount = fileCount - dirCount
val typeStats = fileTypeStats.toSeq.sortBy(-_._2).map { case (fileType, count) =>
f" $fileType%-15s: $count%4d files"
}.mkString("\n")
val largest = largestFiles(5)
val largestList = largest.map(f => f" ${f.name}%-30s: ${f.formatSize()}").mkString("\n")
s"""File Manager Report
|==================
|Total Files: $fileCount ($regularFileCount files, $dirCount directories)
|Total Size: $totalSizeFormatted
|
|File Types:
|$typeStats
|
|Largest Files:
|$largestList
|""".stripMargin
}
def listFiles(): Unit = {
files.sortBy(_.name).foreach { file =>
val typeIcon = if (file.metadata.isDirectory) "📁" else "📄"
println(f"$typeIcon ${file.name}%-30s ${file.formatSize()}%10s ${file.getFileType()}")
}
}
}
// Usage example (with mock data)
val manager = new FileManager()
// Create some sample files
val files = List(
(java.nio.file.Paths.get("/documents/report.pdf"), FileMetadata(2500000, LocalDateTime.now().minusDays(5), LocalDateTime.now().minusDays(2), false, "rw-r--r--")),
(java.nio.file.Paths.get("/images/photo.jpg"), FileMetadata(5500000, LocalDateTime.now().minusDays(10), LocalDateTime.now().minusDays(3), false, "rw-r--r--")),
(java.nio.file.Paths.get("/code/app.scala"), FileMetadata(45000, LocalDateTime.now().minusDays(1), LocalDateTime.now(), false, "rw-r--r--")),
(java.nio.file.Paths.get("/data/logs.txt"), FileMetadata(125000, LocalDateTime.now().minusDays(7), LocalDateTime.now().minusHours(2), false, "rw-r--r--")),
(java.nio.file.Paths.get("/projects"), FileMetadata(0, LocalDateTime.now().minusDays(30), LocalDateTime.now().minusDays(1), true, "rwxr-xr-x"))
)
files.foreach { case (path, metadata) =>
manager.addFile(new ManagedFile(path, metadata))
}
println(manager.generateReport())
println("\nFile Listing:")
manager.listFiles()
Summary
In this lesson, you've learned advanced techniques for working with methods and fields:
✅ Field Types: Public, private, protected, lazy, and computed fields
✅ Custom Accessors: Getters and setters with validation and side effects
✅ Method Patterns: Overloading, chaining, factory methods, and templates
✅ API Design: Creating fluent interfaces and clean class APIs
✅ Practical Applications: Shopping cart, file management, and data processing
✅ Best Practices: Pure vs impure methods, encapsulation, and validation
Understanding how to effectively use methods and fields is crucial for creating well-designed, maintainable classes that provide clear interfaces and robust functionality.
What's Next
In the next lesson, we'll explore "Singleton Objects: When You Only Need One." You'll learn about Scala's object
keyword for creating singleton instances, perfect for utility methods, constants, and application entry points.
This will introduce you to one of Scala's unique features that bridges object-oriented and functional programming paradigms.
Ready to explore singleton objects? Let's continue!
Comments
Be the first to comment on this lesson!