Scala Classes and Objects

Classes and objects are core concepts of object-oriented programming. This chapter will详细介绍(详细介绍 - 详细介绍)Scala中类的定义、对象的创建、继承、多态等面向对象编程的基本概念。

Class Definition

Basic Class Definition

// Basic class definition
class Person {
  // Class body
}

// Create object
val person = new Person()

// Class with parameters
class Student(name: String, age: Int) {
  // Primary constructor parameters automatically become class fields
  def introduce(): String = s"Hi, I'm $name and I'm $age years old"
}

val student = new Student("Alice", 20)
println(student.introduce())

Constructor Parameters

// val parameter: immutable field
class Person(val name: String, val age: Int) {
  // name and age automatically become public immutable fields
}

val person = new Person("Bob", 25)
println(person.name)  // Can access
println(person.age)   // Can access
// person.age = 26    // Compile error: cannot modify

// var parameter: mutable field
class Counter(var count: Int) {
  def increment(): Unit = count += 1
  def decrement(): Unit = count -= 1
}

val counter = new Counter(0)
counter.increment()
println(counter.count)  // 1
counter.count = 10      // Can directly modify

Private Parameters

// Private parameter: does not generate field
class BankAccount(private var balance: Double) {

  def deposit(amount: Double): Unit = {
    if (amount > 0) balance += amount
  }

  def withdraw(amount: Double): Boolean = {
    if (amount > 0 && amount <= balance) {
      balance -= amount
      true
    } else {
      false
    }
  }

  def getBalance: Double = balance  // Read-only access
}

val account = new BankAccount(1000.0)
account.deposit(500.0)
println(account.getBalance)  // 1500.0
// println(account.balance)  // Compile error: private field

Methods and Fields

Instance Methods

class Calculator {
  def add(x: Int, y: Int): Int = x + y

  def multiply(x: Int, y: Int): Int = x * y

  // Methods can access class fields
  private var history: List[String] = List()

  def addWithHistory(x: Int, y: Int): Int = {
    val result = x + y
    history = s"$x + $y = $result" :: history
    result
  }

  def getHistory: List[String] = history.reverse
}

val calc = new Calculator()
calc.addWithHistory(3, 4)
calc.addWithHistory(5, 6)
println(calc.getHistory)  // List("3 + 4 = 7", "5 + 6 = 11")

Field Initialization

class Person(firstName: String, lastName: String) {
  // Computed field
  val fullName: String = s"$firstName $lastName"

  // Lazy initialization field
  lazy val initials: String = {
    println("Computing initials...")
    s"${firstName.head}.${lastName.head}."
  }

  // Mutable field
  var nickname: String = firstName

  // Private field
  private var id: Int = Person.nextId()

  def getId: Int = id
}

object Person {
  private var currentId = 0

  private def nextId(): Int = {
    currentId += 1
    currentId
  }
}

val person = new Person("John", "Doe")
println(person.fullName)   // "John Doe"
println(person.initials)   // Computed now
println(person.getId)      // 1

Auxiliary Constructors

class Person(val name: String, val age: Int) {

  // Auxiliary constructor must call primary constructor or another auxiliary constructor
  def this(name: String) = {
    this(name, 0)  // Call primary constructor
  }

  def this() = {
    this("Unknown")  // Call the auxiliary constructor above
  }

  override def toString: String = s"Person($name, $age)"
}

// Use different constructors
val person1 = new Person("Alice", 25)
val person2 = new Person("Bob")
val person3 = new Person()

println(person1)  // Person(Alice, 25)
println(person2)  // Person(Bob, 0)
println(person3)  // Person(Unknown, 0)

Objects (Object)

Singleton Object

// Singleton object
object MathUtils {
  val PI: Double = 3.14159

  def square(x: Double): Double = x * x

  def circleArea(radius: Double): Double = PI * square(radius)

  private var callCount = 0

  def getCallCount: Int = {
    callCount += 1
    callCount
  }
}

// Use singleton object
println(MathUtils.PI)
println(MathUtils.circleArea(5.0))
println(MathUtils.getCallCount)  // 1
println(MathUtils.getCallCount)  // 2

Companion Object

// Class and its companion object can access each other's private members
class BankAccount private (private var balance: Double) {

  def deposit(amount: Double): Unit = {
    balance += amount
  }

  def getBalance: Double = balance

  // Access companion object's private members
  def getNextAccountNumber: String = BankAccount.nextAccountNumber()
}

object BankAccount {
  private var accountCounter = 0

  // Factory method
  def apply(initialBalance: Double): BankAccount = {
    new BankAccount(initialBalance)
  }

  def apply(): BankAccount = {
    new BankAccount(0.0)
  }

  private def nextAccountNumber(): String = {
    accountCounter += 1
    f"ACC$accountCounter%06d"
  }

  // Access class's private members
  def transfer(from: BankAccount, to: BankAccount, amount: Double): Boolean = {
    if (from.balance >= amount) {
      from.balance -= amount
      to.balance += amount
      true
    } else {
      false
    }
  }
}

// Use companion object
val account1 = BankAccount(1000.0)  // Use apply method
val account2 = BankAccount()        // Use no-argument apply method

account1.deposit(500.0)
BankAccount.transfer(account1, account2, 200.0)

println(account1.getBalance)  // 1300.0
println(account2.getBalance)  // 200.0

Case Classes

Basic Case Classes

// Case classes automatically generate many useful methods
case class Person(name: String, age: Int)

val person1 = Person("Alice", 25)  // No need for new keyword
val person2 = Person("Alice", 25)

// Automatically generated methods
println(person1)                    // Person(Alice,25) - toString
println(person1 == person2)         // true - equals
println(person1.hashCode)           // Auto-generated hashCode

// Pattern matching support
person1 match {
  case Person(name, age) => println(s"Name: $name, Age: $age")
}

// copy method
val person3 = person1.copy(age = 26)
println(person3)  // Person(Alice,26)

// Field access
println(person1.name)  // Alice
println(person1.age)   // 25

Advanced Case Class Features

case class Address(street: String, city: String, zipCode: String)

case class Person(
  name: String,
  age: Int,
  email: Option[String] = None,
  address: Option[Address] = None
) {
  // Can add custom methods
  def isAdult: Boolean = age >= 18

  def hasEmail: Boolean = email.isDefined

  def fullAddress: String = address match {
    case Some(addr) => s"${addr.street}, ${addr.city} ${addr.zipCode}"
    case None => "No address"
  }
}

val address = Address("123 Main St", "New York", "10001")
val person = Person(
  name = "John Doe",
  age = 30,
  email = Some("john@example.com"),
  address = Some(address)
)

println(person.isAdult)      // true
println(person.hasEmail)     // true
println(person.fullAddress)  // 123 Main St, New York 10001

// Nested copy
val movedPerson = person.copy(
  address = person.address.map(_.copy(city = "Boston"))
)

Inheritance

Basic Inheritance

// Base class
class Animal(val name: String) {
  def speak(): String = "Some sound"

  def move(): String = "Moving"

  // Methods that can be overridden
  def describe(): String = s"This is $name"
}

// Derived class
class Dog(name: String, val breed: String) extends Animal(name) {

  // Override method
  override def speak(): String = "Woof!"

  override def describe(): String = s"This is $name, a $breed dog"

  // New method
  def wagTail(): String = s"$name is wagging tail"
}

class Cat(name: String) extends Animal(name) {
  override def speak(): String = "Meow!"

  def purr(): String = s"$name is purring"
}

// Use inheritance
val dog = new Dog("Buddy", "Golden Retriever")
val cat = new Cat("Whiskers")

println(dog.speak())      // Woof!
println(dog.describe())   // This is Buddy, a Golden Retriever dog
println(dog.wagTail())    // Buddy is wagging tail

println(cat.speak())      // Meow!
println(cat.purr())       // Whiskers is purring

Abstract Classes

// Abstract class
abstract class Shape {
  // Abstract methods
  def area(): Double
  def perimeter(): Double

  // Concrete methods
  def describe(): String = s"A shape with area ${area()} and perimeter ${perimeter()}"
}

class Rectangle(val width: Double, val height: Double) extends Shape {
  def area(): Double = width * height
  def perimeter(): Double = 2 * (width + height)
}

class Circle(val radius: Double) extends Shape {
  def area(): Double = math.Pi * radius * radius
  def perimeter(): Double = 2 * math.Pi * radius
}

val rectangle = new Rectangle(5.0, 3.0)
val circle = new Circle(2.0)

println(rectangle.describe())
println(circle.describe())

// Polymorphism
val shapes: List[Shape] = List(rectangle, circle)
shapes.foreach(shape => println(s"Area: ${shape.area()}"))

Traits

Basic Traits

// Trait definition
trait Drawable {
  def draw(): String
}

trait Movable {
  def move(x: Int, y: Int): String
}

// Classes can mix in multiple traits
class Circle(val radius: Double) extends Drawable with Movable {
  def draw(): String = s"Drawing a circle with radius $radius"

  def move(x: Int, y: Int): String = s"Moving circle to ($x, $y)"
}

class Rectangle(val width: Double, val height: Double) extends Drawable with Movable {
  def draw(): String = s"Drawing a rectangle ${width}x${height}"

  def move(x: Int, y: Int): String = s"Moving rectangle to ($x, $y)"
}

val circle = new Circle(5.0)
val rectangle = new Rectangle(10.0, 8.0)

println(circle.draw())
println(rectangle.move(10, 20))

Traits with Default Implementations

trait Logger {
  // Abstract method
  def log(message: String): Unit

  // Concrete methods
  def info(message: String): Unit = log(s"INFO: $message")
  def error(message: String): Unit = log(s"ERROR: $message")
  def debug(message: String): Unit = log(s"DEBUG: $message")
}

class ConsoleLogger extends Logger {
  def log(message: String): Unit = println(message)
}

class FileLogger(filename: String) extends Logger {
  def log(message: String): Unit = {
    // Simplified file writing
    println(s"Writing to $filename: $message")
  }
}

val consoleLogger = new ConsoleLogger()
val fileLogger = new FileLogger("app.log")

consoleLogger.info("Application started")
fileLogger.error("Database connection failed")

Access Modifiers

Access Levels

class AccessExample {
  // Public field (default)
  val publicField = "public"

  // Private field
  private val privateField = "private"

  // Protected field
  protected val protectedField = "protected"

  // Package private
  private[scala] val packagePrivateField = "package private"

  def publicMethod(): String = "public method"

  private def privateMethod(): String = "private method"

  protected def protectedMethod(): String = "protected method"

  // Private members can only be accessed within the class
  def accessPrivate(): String = privateMethod()
}

class SubClass extends AccessExample {
  // Can access protected members
  def accessProtected(): String = protectedMethod()

  // Cannot access private members
  // def accessPrivate(): String = privateMethod()  // Compile error
}

Practical Application Examples

Bank Account System

// Basic account class
abstract class Account(val accountNumber: String, protected var balance: Double) {

  def getBalance: Double = balance

  def deposit(amount: Double): Boolean = {
    if (amount > 0) {
      balance += amount
      true
    } else {
      false
    }
  }

  // Abstract method, implemented by subclasses
  def withdraw(amount: Double): Boolean

  def transfer(to: Account, amount: Double): Boolean = {
    if (withdraw(amount)) {
      to.deposit(amount)
      true
    } else {
      false
    }
  }
}

// Savings account
class SavingsAccount(accountNumber: String, balance: Double, val interestRate: Double)
  extends Account(accountNumber, balance) {

  def withdraw(amount: Double): Boolean = {
    if (amount > 0 && amount <= balance) {
      balance -= amount
      true
    } else {
      false
    }
  }

  def addInterest(): Unit = {
    balance += balance * interestRate
  }
}

// Checking account
class CheckingAccount(accountNumber: String, balance: Double, val overdraftLimit: Double)
  extends Account(accountNumber, balance) {

  def withdraw(amount: Double): Boolean = {
    if (amount > 0 && amount <= balance + overdraftLimit) {
      balance -= amount
      true
    } else {
      false
    }
  }

  def getAvailableBalance: Double = balance + overdraftLimit
}

// Usage example
val savings = new SavingsAccount("SAV001", 1000.0, 0.02)
val checking = new CheckingAccount("CHK001", 500.0, 200.0)

savings.deposit(500.0)
savings.addInterest()
println(s"Savings balance: ${savings.getBalance}")

checking.withdraw(600.0)  // Use overdraft
println(s"Checking balance: ${checking.getBalance}")
println(s"Available balance: ${checking.getAvailableBalance}")

// Transfer
savings.transfer(checking, 300.0)
println(s"After transfer - Savings: ${savings.getBalance}, Checking: ${checking.getBalance}")

Graphics System

// Trait definition
trait Drawable {
  def draw(): String
}

trait Scalable {
  def scale(factor: Double): Unit
}

trait Colorable {
  var color: String
  def setColor(newColor: String): Unit = color = newColor
}

// Abstract base class
abstract class Shape extends Drawable {
  def area(): Double
  def perimeter(): Double
}

// Concrete implementations
class Circle(var radius: Double, var color: String = "black")
  extends Shape with Scalable with Colorable {

  def area(): Double = math.Pi * radius * radius

  def perimeter(): Double = 2 * math.Pi * radius

  def draw(): String = s"Drawing a $color circle with radius $radius"

  def scale(factor: Double): Unit = radius *= factor
}

class Rectangle(var width: Double, var height: Double, var color: String = "black")
  extends Shape with Scalable with Colorable {

  def area(): Double = width * height

  def perimeter(): Double = 2 * (width + height)

  def draw(): String = s"Drawing a $color rectangle ${width}x${height}"

  def scale(factor: Double): Unit = {
    width *= factor
    height *= factor
  }
}

// Usage example
val shapes: List[Shape with Scalable with Colorable] = List(
  new Circle(5.0, "red"),
  new Rectangle(10.0, 8.0, "blue")
)

shapes.foreach { shape =>
  println(shape.draw())
  println(s"Area: ${shape.area()}")

  shape.scale(1.5)
  shape.setColor("green")

  println(s"After scaling and recoloring: ${shape.draw()}")
  println(s"New area: ${shape.area()}")
  println()
}

Exercises

Exercise 1: Student Management System

object StudentManagementSystem {

  case class Course(code: String, name: String, credits: Int)

  case class Grade(course: Course, score: Double) {
    def letterGrade: String = score match {
      case s if s >= 90 => "A"
      case s if s >= 80 => "B"
      case s if s >= 70 => "C"
      case s if s >= 60 => "D"
      case _ => "F"
    }

    def isPass: Boolean = score >= 60
  }

  class Student(val id: String, val name: String) {
    private var grades: List[Grade] = List()

    def addGrade(grade: Grade): Unit = {
      grades = grade :: grades
    }

    def getGrades: List[Grade] = grades.reverse

    def gpa: Double = {
      if (grades.isEmpty) 0.0
      else {
        val totalPoints = grades.map(g => g.score * g.course.credits).sum
        val totalCredits = grades.map(_.course.credits).sum
        totalPoints / totalCredits
      }
    }

    def totalCredits: Int = grades.filter(_.isPass).map(_.course.credits).sum

    override def toString: String = s"Student($id, $name, GPA: ${gpa})"
  }

  def main(args: Array[String]): Unit = {
    val math = Course("MATH101", "Calculus I", 4)
    val cs = Course("CS101", "Introduction to Programming", 3)
    val english = Course("ENG101", "English Composition", 3)

    val student = new Student("S001", "Alice Johnson")

    student.addGrade(Grade(math, 85.0))
    student.addGrade(Grade(cs, 92.0))
    student.addGrade(Grade(english, 78.0))

    println(student)
    println(s"Total Credits: ${student.totalCredits}")

    student.getGrades.foreach { grade =>
      println(s"${grade.course.name}: ${grade.score} (${grade.letterGrade})")
    }
  }
}

Exercise 2: E-commerce System

object ECommerceSystem {

  trait Discountable {
    def applyDiscount(percentage: Double): Unit
  }

  abstract class Product(val id: String, val name: String, protected var price: Double) {
    def getPrice: Double = price
    override def toString: String = s"$name ($$${price})"
  }

  class Book(id: String, name: String, price: Double, val author: String, val isbn: String)
    extends Product(id, name, price) with Discountable {

    def applyDiscount(percentage: Double): Unit = {
      price = price * (1 - percentage / 100)
    }

    override def toString: String = s"Book: $name by $author ($$${price})"
  }

  class Electronics(id: String, name: String, price: Double, val brand: String, val warranty: Int)
    extends Product(id, name, price) with Discountable {

    def applyDiscount(percentage: Double): Unit = {
      price = price * (1 - percentage / 100)
    }

    override def toString: String = s"Electronics: $name by $brand ($$${price}, ${warranty}y warranty)"
  }

  case class CartItem(product: Product, quantity: Int) {
    def totalPrice: Double = product.getPrice * quantity
  }

  class ShoppingCart {
    private var items: List[CartItem] = List()

    def addItem(product: Product, quantity: Int): Unit = {
      items = CartItem(product, quantity) :: items
    }

    def removeItem(productId: String): Unit = {
      items = items.filterNot(_.product.id == productId)
    }

    def getItems: List[CartItem] = items.reverse

    def totalAmount: Double = items.map(_.totalPrice).sum

    def applyBulkDiscount(percentage: Double): Unit = {
      items.foreach { item =>
        item.product match {
          case discountable: Discountable => discountable.applyDiscount(percentage)
          case _ => // Products that don't support discounts
        }
      }
    }
  }

  def main(args: Array[String]): Unit = {
    val book = new Book("B001", "Scala Programming", 45.99, "Martin Odersky", "978-0981531687")
    val laptop = new Electronics("E001", "MacBook Pro", 1299.99, "Apple", 1)

    val cart = new ShoppingCart()
    cart.addItem(book, 2)
    cart.addItem(laptop, 1)

    println("Shopping Cart:")
    cart.getItems.foreach(item =>
      println(s"${item.product} x ${item.quantity} = $$${item.totalPrice}")
    )
    println(s"Total: $$${cart.totalAmount}")

    println("\nApplying 10% bulk discount...")
    cart.applyBulkDiscount(10.0)

    println("Updated Cart:")
    cart.getItems.foreach(item =>
      println(s"${item.product} x ${item.quantity} = $$${item.totalPrice}")
    )
    println(s"New Total: $$${cart.totalAmount}")
  }
}

Summary

This chapter introduced core concepts of Scala classes and objects:

  • Class Definition: Constructor, field, method definitions
  • Objects: Use of singleton objects and companion objects
  • Case Classes: Special classes that auto-generate useful methods
  • Inheritance: Class inheritance and method overriding
  • Traits: Multiple inheritance and mixin mechanism
  • Access Modifiers: Control member visibility
  • Practical Applications: Bank system, graphics system, etc.

Mastering these object-oriented programming concepts is the foundation for building complex Scala applications. In the next chapter, we will learn Scala Traits to deeply understand Scala's unique trait system and mixin mechanisms.