Skip to main content
Scala: OOP
  1. Posts/

Scala: OOP

·4154 words·20 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

From last week I have started following this course Scala Essentials | Rock the JVM. I am now going through OOP.

Object-Oriented Programming
#

Object-Oriented Basics
#

  • Class: Defined with class.
  • Constructor: Parameters in class definition; use val/var to expose as fields.
  • Fields: Accessible via this.fieldName.
  • Methods: Defined with def.
  • Auxiliary Constructors: Use def this(...); rarely needed, prefer default parameters.
  • Overloading: Multiple methods with same name but different signatures.
// Can define classes in Scala without body, equivalent to Java's Record
class Animal(val name: String, val species: String)

// classes
class Person(val name: String, age: Int) { // constructor signature
  // Constructor argument is NOT a field by default
  // Only parameters with val/var become fields (e.g., name)
  // So you can't access age outside the class unless you use val/var

  // Fields - are accessible via this.allCaps
  val allCaps = name.toUpperCase()

  // Methods
  def greet(name: String): String =
    // this.name != name
    // this.name is the field in instance Person, name is the parameter
    s"${this.name} says: Hi, $name"

  // OVERLOADING
  def greet(): String =
    // there is no ambiguity here, because the method signature is different
    // this.name is the field in instance Person, no parameter name
    s"Hi, everyone, my name is $name"

  // Auxiliary constructor: must call another constructor in the first line
  def this(name: String) =
    // `this` refers to the primary constructor
    this(name, 0) // default age is 0

  // Another auxiliary constructor, chaining to previous one
  def this() = {
    // `this` refers to the other auxiliary constructor
    this("Jane Doe")
  }

  // Auxiliary constructors are rarely used,
  // Prefer default parameter values in class definition:
  // class Person(val name: String = "Jane Doe", val age: Int = 0) { }
}

val aPerson: Person = new Person("John", 26)
val john = aPerson.name // class parameter with val becomes field
val johnSayHiToDaniel = aPerson.greet("Daniel") // "John says: Hi, Daniel"
val johnSaysHi = aPerson.greet() // "Hi, everyone, my name is John"
val genericPerson = new Person() // Jane Doe, 0

Exercise 1
#

/**
  Exercise: imagine we're creating a backend for a book publishing house.
  Create a Novel and a Writer class.

  Writer: first name, surname, year
    - method fullname
  Novel: name, year of release, author
    - authorAge
    - isWrittenBy(author)
    - copy (new year of release) = new instance of Novel
*/

class Writer(firstName: String, lastName: String, val yearOfBirth: Int) {
  def fullName: String = s"$firstName $lastName"
}

class Novel(title: String, yearOfRelease: Int, val author: Writer) {
  def authorAge: Int = yearOfRelease - author.yearOfBirth
  def isWrittenBy(author: Writer): Boolean = this.author == author
  def copy(newYear: Int): Novel = new Novel(title, newYear, author)
}

val charlesDickens = new Writer("Charles", "Dickens", 1812)
val charlesDickensImpostor = new Writer("Charles", "Dickens", 2021)

val novel = new Novel("Great Expectations", 1861, charlesDickens)
val newEdition = novel.copy(1871)

println(charlesDickens.fullName)
println(novel.authorAge)
println(novel.isWrittenBy(charlesDickensImpostor)) // false, different instance
println(novel.isWrittenBy(charlesDickens)) // true, same instance
println(newEdition.authorAge)

Exercise 2
#

  • Immutability: Methods return new instances, not mutate state.
/**
 * Exercise #2: an immutable counter class
 * - constructed with an initial count
 * - increment/decrement => NEW instance of counter
 * - increment(n)/decrement(n) => NEW instance of counter
 * - print()
 *
 * Benefits:
 * + works well in distributed environments
 * + easier to read and understand code
 */
class Counter(count: Int = 0) {
  def increment(): Counter =
    new Counter(count + 1)

  def decrement(): Counter =
    if (count == 0) this
    else new Counter(count - 1)

  def increment(n: Int): Counter =
    if (n <= 0) this
    else increment().increment(n - 1) // recursive, not stack safe for large n

  def decrement(n: Int): Counter =
    if (n <= 0) this
    else decrement().decrement(n - 1)

  def print(): Unit =
    println(s"Current count: $count")
}

val counter = new Counter()
counter.print() // 0
counter.increment().print() // 1
counter.increment() // always returns new instances
counter.print() // 0

counter.increment(10).print() // 10
counter.increment(20000).print() // 20000
counter.increment(2000000).print() // this will crash because of stack overflow

Method Notations
#

  • Infix: obj method arg for single-arg methods, e.g. mary likes "Movie".
  • Prefix: Unary operators, e.g. -mary. Allowed for +, -, !, ~.
  • Postfix: obj method (discouraged, needs import).
  • Apply: obj() or obj(args) calls apply method.
  • Operator Overloading: Methods like +, !! can be defined for custom behaviour.
class Person(val name: String, val age: Int, favoriteMovie: String) {
  // Infix notation - for methods with ONE argument
  // Infix notation: obj method arg
  // makes it look like a natural language
  // Example: mary likes "Movie"
  infix def likes(movie: String): Boolean =
    movie == favoriteMovie

  infix def +(person: Person): String =
    s"${this.name} is hanging out with ${person.name}"

  infix def +(nickname: String): Person =
    new Person(s"$name ($nickname)", age, favoriteMovie)

  infix def !!(progLanguage: String): String =
    s"$name wonders how can $progLanguage be so cool!"

  // Prefix position
  // unary ops: -, +, ~, ! supported operators
  def unary_- : String =
    s"$name's alter ego"

  def unary_+ : Person =
    new Person(name, age + 1, favoriteMovie)

  def isAlive: Boolean = true

  // can just call the object with no arguments like a function
  // ex: mary()
  def apply(): String =
    s"Hi, my name is $name and I really enjoy $favoriteMovie"

  // can also take arguments
  // ex: mary(2)
  def apply(n: Int): String =
    s"$name watched $favoriteMovie $n times"
}

val mary = new Person("Mary", 34, "Inception")
val john = new Person("John", 36, "Fight Club")

val negativeOne = -1

println(mary.likes("Fight Club")) // true
// Infix notation - for methods with ONE argument
println(mary likes "Fight Club") // identical

// "operator" = plain method
println(mary + john) // "Mary is hanging out with John"
println(mary.+(john)) // identical
println(2 + 3)
println(2.+(3)) // same
println(mary !! "Scala") // "Mary wonders how can Scala be so cool!"

// Prefix notation
println(-mary) // "Mary's alter ego"

// Postfix notation
println(mary.isAlive) // true
println(mary isAlive) // discouraged, needs import scala.language.postfixOps

// Apply method
println(mary.apply()) // "Hi, my name is Mary and I really enjoy Inception"
println(mary()) // same

// exercises
val maryWithNickname = mary + "the rockstar"
println(maryWithNickname.name) // "Mary (the rockstar)"

val maryOlder = +mary
println(maryOlder.age) // 35

println(mary(10)) // "Mary watched Inception 10 times"

Inheritance
#

  • Inheritance: Use extends to create subclasses.
  • Constructor Parameters: Subclasses must call superclass constructor.
  • Override: Use override for fields/methods in subclass.
  • Polymorphism: Variable of parent type can refer to child instance; most specific method is called.
  • Overloading: Multiple methods with same name, different signatures (argument types/count).
  • super: Call parent method with super.methodName.
  • Built-in Methods: Can override equals, hashCode, toString, etc.
// Parent class
class Animal {
  val creatureType = "wild"
  def eat(): Unit = println("nomnomnom")
}

class Cat extends Animal { // a cat "is an" animal
  def crunch() = {
    eat()
    println("crunch, crunch")
  }
}

val cat = new Cat

// Constructor parameters
class Person(val name: String, age: Int) {
  // Auxiliary constructor
  def this(name: String) = this(name, 0)
}

class Adult(name: String, age: Int, idCard: String) extends Person(name) // must specify super-constructor

// Overriding
class Dog extends Animal {
  override val creatureType = "domestic"
  override def eat(): Unit = println("mmm, I like this bone")

  // override def eat(): Unit = super.eat()
  // calls the parent method

  // Other built-in methods that can be overridden
  // equals(obj: Any): Boolean =
  // hashCode(): Int =
  // clone(): AnyRef =
  // finalize(): Unit =
  // popular overridable method
  override def toString: String = "a dog"
}

// subtype polymorphism
val dog: Animal = new Dog
dog.eat() // the most specific method will be called
// "mmm, I like this bone"

// overloading vs overriding
class Crocodile extends Animal {
  override val creatureType = "very wild"
  override def eat(): Unit = println("I can eat anything, I'm a croc")

  // overloading: multiple methods with the same name, different signatures
  // different signature =
  //    different argument list (different number of args + different arg types)
  //    + different return type (optional)
  def eat(animal: Animal): Unit = println("I'm eating this poor fella")
  def eat(dog: Dog): Unit = println("eating a dog")
  def eat(person: Person): Unit = println(s"I'm eating a human with the name ${person.name}")
  def eat(person: Person, dog: Dog): Unit = println("I'm eating a human AND the dog")
  // def eat(): Int = 45
  // not a valid overload, because it has no parameters, and the return type is not part of the signature
  def eat(dog: Dog, person: Person): Unit = println("I'm eating a human AND the dog")
}

println(dog)
// println(dog.toString)
// "a dog"

Access Modifiers
#

  • Protected: Accessible in class and subclasses.
  • Private: Accessible only within the class.
  • Public: Default, accessible everywhere.
  • Override val: Required if parent defines member as val.
  • Protected Methods: Cannot be called on other instances, only this.
class Person(val name: String) {
  // protected = access to inside the class + children classes
  protected def sayHi(): String = s"Hi, my name is $name."
  // private = only accessible inside the class
  private def watchNetflix(): String = "I'm binge watching my favorite series..."
}

// `override val name` is required because the parent class `Person` defines `name` as a `val` member;
// this explicitly replaces the inherited member in the subclass.
class Kid(override val name: String, age: Int) extends Person(name) {
  def greetPolitely(): String = // no modifier = "public"
    sayHi() + "I love to play!"
}

val aPerson = new Person("Alice")
val aKid = new Kid("David", 5)

// Complication
class KidWithParents(override val name: String, age: Int, momName: String, dadName: String) extends Person(name) {
  val mom = new Person(momName)
  val dad = new Person(dadName)

  // can't call a protected method on ANOTHER instance of Person

  //    def everyoneSayHi(): String =
  //      this.sayHi() + s"Hi, I'm $name, and here are my parents: " + mom.sayHi() + dad.sayHi()
}

// println(aPerson.sayHi())
// method sayHi cannot be accessed as a member
println(aKid.greetPolitely()) // "Hi, my name is David. I love to play!"

Preventing Inheritance
#

  • Final: Prevents overriding of methods or inheritance of classes.
  • Sealed: Restricts subclassing to the same file.
  • Open: Explicitly marks class as intended for extension (Scala 3).
  • Heavy Inheritance: Discouraged in Scala; prefer composition or traits.
class Person(name: String) {
  final def enjoyLife(): Int = 42 // final = cannot be overridden
}

class Adult(name: String) extends Person(name) {
  // override def enjoyLife() = 999 // illegal
}

final class Animal // cannot be inherited because it is a final
// class Cat extends Animal // illegal

// Sealing a type hierarchy = inheritance only permitted inside this file
sealed class Guitar(nStrings: Int)
class ElectricGuitar(nStrings: Int) extends Guitar(nStrings)
class AcousticGuitar extends Guitar(6)
// cannot extend Guitar outside this file


// Heavy inheritance is discouraged in Scala
// no modifier = "not encouraging" inheritance
// open = specifically marked for extension
// not mandatory, good practice (should be accompanied by documentation on what extension implies)
open class ExtensibleGuitar(nStrings: Int)

Scala Objects
#

  • Object: Defines a singleton instance (only one exists).
  • Companion Object: object with the same name as a class in the same file; can access each other’s private members.
  • Static-like: Use objects for instance-independent (“static”) functionality.
  • Equality:
    • eq for reference equality (same instance).
    • ==/equals for value equality (can override).
  • Extending Classes: Objects can extend classes.
  • Scala Application: Entry point is an object with a main method.
// objects is different from instances of classes

// objects = singleton pattern
// singleton = a type with a single instance
object MySingleton { // type + the only instance of this type
  val aField = 45
  def aMethod(x: Int) = x + 1
}

val theSingleton = MySingleton
val anotherSingleton = MySingleton
val isSameSingleton = theSingleton == anotherSingleton // true

// objects can have fields and methods
val theSingletonField = MySingleton.aField
val theSingletonMethodCall = MySingleton.aMethod(99)

// classes can have companion objects
class Person(name: String) {
  def sayHi(): String = s"Hi, my name is $name"
}

// companions = class + object with the same name in the same file
object Person { // companion object
  // can access each other's private fields and methods
  val N_EYES = 2
  def canFly(): Boolean = false
}

// methods and fields in classes are used for instance-dependent functionality
val mary = new Person("Mary")
val mary_v2 = new Person("Mary")
val marysGreeting = mary.sayHi() // "Hi, my name is Mary"
mary == mary

// methods and fields in objects are used for instance-independent functionality - "static" like in Java
val humansCanFly = Person.canFly()
val nEyesHuman = Person.N_EYES

// Equality
// 1 - equality of reference - == in Java
val sameMary = mary eq mary_v2 // false, different instances
val sameSingleton = MySingleton eq MySingleton // true
// 2 - equality of "sameness" - in Java defined as .equals
// Developers can override the equals method in classes, to define what "sameness" means
// otherwise, it is the same as reference equality
val sameMary_v2 = mary equals mary_v2 // false
val sameMary_v3 = mary == mary_v2 // same as equals - false
val sameSingleton_v2 = MySingleton == MySingleton // true

// objects can extend classes
object BigFoot extends Person("Big Foot")

/*
  Scala application == object + main method
    object Objects {
      def main(args: Array[String]): Unit = { ... }
    }

  Equivalent Java application:
    public class Objects {
      public static void main(String[] args) { ... }
    }
*/

Abstract Classes and Traits
#

  • Abstract Class: Can have abstract and concrete members; cannot be instantiated.
  • Trait: Like interface, can have concrete methods; multiple traits can be mixed in.
  • Override: Required for implementing abstract members.
  • Subtype Polymorphism: Variables of abstract type can refer to concrete instances.
  • Difference: Abstract classes are “things”, traits are “behaviors”.
  • Inheritance: Single abstract class, multiple traits.
// abstract classes - can have abstract and non-abstract fields/methods
// abstract = not implemented, must be implemented in subclasses
abstract class Animal {
  val creatureType: String // abstract
  def eat(): Unit
  // non-abstract fields/methods allowed
  def preferredMeal: String = "anything" // "accessor methods"
  // Accessor methods are methods without parameters, can be overridden
  // with fields
}

// abstract classes can't be instantiated
// val anAnimal: Animal = new Animal // illegal

// non-abstract classes must implement the abstract fields/methods
class Dog extends Animal {
  override val creatureType = "domestic"
  override def eat(): Unit = println("crunching this bone")
  // overriding is legal for everything
  override val preferredMeal: String = "bones" // overriding accessor method (without args/parentheses) with a field
}

// subtype polymorphism
val aDog: Animal = new Dog // valid

// Traits - describe behaviors
// Like Java interfaces, but can have concrete methods
trait Carnivore { // Scala 3 - traits can have constructor args
  def eat(animal: Animal): Unit // can also implement methods
}

class TRex extends Carnivore {
  override def eat(animal: Animal): Unit = println("I'm a T-Rex, I eat animals")
}

// practical difference abstract classes vs traits
// one class inheritance
// multiple traits inheritance
trait ColdBlooded

class Crocodile extends Animal with Carnivore with ColdBlooded {
  override val creatureType = "croc"
  override def eat(): Unit = println("I'm a croc, I just crunch stuff")
  override def eat(animal: Animal): Unit = println("croc eating animal")
}

/*
  philosophical difference abstract classes vs traits
  - abstract classes are THINGS
  - traits are BEHAVIORS
*/

/*
  Any
    AnyRef
      All classes we write
        scala.Null (the null reference)
    AnyVal
      Int, Boolean, Char, ...
        scala.Nothing
*/

Generics
#

  • Generics: Allow code reuse for different types.
  • Type Parameter: Use [A] to make classes/methods generic.
  • Type Safety: Compiler knows the type of elements.
  • Multiple Type Parameters: E.g., trait MyMap[Key, Value].
  • Generic Methods: Methods can also have type parameters.
// goal: reuse code on different types  
  
// option 1: copy the code  
abstract class IntList {  
  def head: Int  
  def tail: IntList  
}  
class EmptyIntList extends IntList {  
  override def head = throw new NoSuchElementException  
  override def tail = throw new NoSuchElementException  
}  
class NonEmptyIntList(override val head: Int, override val tail: IntList) extends IntList  
  
abstract class StringList {  
  def head: String  
  def tail: StringList  
}  
class EmptyStringList extends StringList {  
  override def head = throw new NoSuchElementException  
  override def tail = throw new NoSuchElementException  
}  
class NonEmptyStringList(override val head: String, override val tail: StringList) extends StringList  
// ... and so on for all the types you want to support  
/*  
  Pros:  
	  - keeps type safety: you know which list holds which kind of elements  
  Cons:  
	  - boilerplate  
	  - unsustainable  
	  - copy/paste... really? 
*/  

// option 2: make the list hold a big, parent type  
abstract class GeneralList {  
  def head: Any  
  def tail: GeneralList  
}  
class EmptyGeneralList extends GeneralList {  
  override def head = throw new NoSuchElementException  
  override def tail = throw new NoSuchElementException  
}  
class NonEmptyGeneralList(override val head: Any, override val tail: GeneralList) extends GeneralList  
  
val generalListOfIntegers: GeneralList = new NonEmptyGeneralList(1, new NonEmptyGeneralList(2, new EmptyGeneralList))  
  
val GeneralFirstNumber: Any = generalListOfIntegers.head // compiler knows it's an Any, but not an Int  
/*  
  Pros:  
	  - no more code duplication  
	  - can support any type  
  Cons:  
	  - lost type safety: can make no assumptions about any element or method  
	  - can now be heterogeneous: can hold cats and dogs in the same list (not funny) 
*/  

// solution: make the list generic with a type argument  
abstract class MyList[A] { // "generic" list; Java equivalent: abstract class MyList<A>  
  def head: A  
  def tail: MyList[A]  
}  
  
class Empty[A] extends MyList[A] {  
  override def head: A = throw new NoSuchElementException  
  override def tail: MyList[A] = throw new NoSuchElementException  
}  
  
class NonEmpty[A](override val head: A, override val tail: MyList[A]) extends MyList[A]  
  
// can now use a concrete type argument  
val listOfIntegers: MyList[Int] = new NonEmpty[Int](1, new NonEmpty[Int](2, new Empty[Int]))  
val listOfIntegers_v2: MyList[Int] = new NonEmpty(1, new NonEmpty(2, new Empty)) // type argument can be inferred  
  
// the compiler now knows the real type of the elements  
val firstNumber = listOfIntegers.head // Int  
val adding = firstNumber + 3 // valid, because the compiler knows it's an Int  
  
// multiple type arguments  
trait MyMap[Key, Value]  
  
// generic methods  
object MyList {  
  def from2Elements[A](elem1: A, elem2: A): MyList[A] =  
    new NonEmpty[A](elem1, new NonEmpty[A](elem2, new Empty[A]))  
}  
  
// calling methods  
val first2Numbers = MyList.from2Elements[Int](1, 2)  
val first2Numbers_v2 = MyList.from2Elements(1, 2) // compiler can infer generic type from the type of the arguments  
val first2Numbers_v3 = new NonEmpty(1, new NonEmpty(2, new Empty))

Anonymous Classes
#

  • Anonymous Class: Create a class instance with custom implementation, without a named class.
  • Syntax: new TraitOrAbstractClass { ... }
  • Use Case: Useful for one-off implementations, especially with traits.
abstract class Animal {  
  def eat(): Unit  
}  
  
// classes used for just one instance are boilerplate-y  
class SomeAnimal extends Animal {  
  override def eat(): Unit = println("I'm a weird animal")  
}  
  
val someAnimal = new SomeAnimal  
  
val someAnimal_v2 = new Animal { // anonymous class  
  override def eat(): Unit = println("I'm a weird animal")  
}  
  
/*  
  equivalent with:
  
  class AnonymousClasses.AnonClass$1 extends Animal {    
	  override def eat(): Unit = println("I'm a weird animal")  
  } 
  
  val someAnimal_v2 = new AnonymousClasses.AnonClass$1 
  
  */  
  
// works for classes (abstract or not) + traits  
class Person(name: String) {  
  def sayHi(): Unit = println(s"Hi, my name is $name")  
}  
  
val jim = new Person("Jim") {  
  override def sayHi(): Unit = println("MY NAME IS JIM!")  
}

Case Classes
#

  • Case Class: Lightweight data structure with useful features.
  • Fields: Constructor args become fields.
  • Equality: == compares values, not references.
  • toString/hashCode: Automatically implemented.
  • Copy Method: Create new instance with some fields changed.
  • Companion Object: Automatically provided.
  • Pattern Matching: Supports extractor patterns.
  • Case Object: Singleton, for use in pattern matching.
// lightweight data structures  
case class Person(name: String, age: Int) {  
  // do some other stuff  
}  
  
// 1 - class args are now fields  
val daniel = new Person("Daniel", 99)  
val danielsAge = daniel.age  
  
// 2 - toString, equals and hashCode  
val danielToString = daniel.toString // Person("Daniel", 99)  
val danielDuped = new Person("Daniel", 99)  
val isSameDaniel = daniel == danielDuped // true, not reference equality  
  
// 3 - utility methods  
// copy method to create a new instance with some fields changed  
val danielYounger = daniel.copy(age = 78) // new Person("Daniel", 78)  
  
// 4 - CCs have companion objects  
val thePersonSingleton = Person  
val daniel_v2 = Person("Daniel", 99) // "constructor"  
  
// 5 - CCs are serializable  
// use-case: Akka  
  
// 6 - CCs have extractor patterns for PATTERN MATCHING  
  
// can't create CCs with no arg lists  
/*  
  case class CCWithNoArgs {    
	  // some code  
  }  
  val ccna = new CCWithNoArgs  
  val ccna_v2 = new CCWithNoArgs // all instances would be equal!*/  
  
// case objects are singletons, and are allowed to have no args  
// as they are singletons, therefore, does not make sense to have multiple instances  
case object UnitedKingdom {  
  // fields and methods  
  def name: String = "The UK of GB and NI"  
}  
  
case class CCWithArgListNoArgs[A]() // legal, mainly used in the context of generics

Enums
#

  • Enum: Type-safe way to define a set of values.
  • Syntax: enum Name { case A, B, ... }
  • Fields/Methods: Can add fields and methods to enum cases.
  • Companion Object: Can define additional methods.
  • Standard API: .ordinal, .values, .valueOf.
// enums are a type-safe way to define a set of values  
// enum == enumeration  
enum Permissions {  
  case READ, WRITE, EXECUTE, NONE  
  
  // add fields/methods  
  def openDocument(): Unit =  
    if (this == READ) println("opening document...")  
    else println("reading not allowed.")  
}  
  
val somePermissions: Permissions = Permissions.READ  
  
// constructor args  
enum PermissionsWithBits(bits: Int) {  
  case READ extends PermissionsWithBits(4) // 100  
  case WRITE extends PermissionsWithBits(2) // 010  
  case EXECUTE extends PermissionsWithBits(1) // 001  
  case NONE extends PermissionsWithBits(0) // 000  
}  
  
// companion object for the enum  
object PermissionsWithBits {  
  def fromBits(bits: Int): PermissionsWithBits = // whatever  
    PermissionsWithBits.NONE  
}  
  
// standard API  
val somePermissionsOrdinal = somePermissions.ordinal // 0 for READ, 1 for WRITE, etc. basically the index in the enum definition  
val allPermissions = PermissionsWithBits.values // array of all possible values of the enum  
val readPermission: Permissions = Permissions.valueOf("READ") // Permissions.READ

Handling Exceptions
#

  • Throw: Use throw to raise exceptions.
  • Try/Catch/Finally: Handle exceptions as expressions.
  • Custom Exceptions: Extend Exception or RuntimeException.

Exception Hierarchy

val aString: String = null  
// aString.length crashes with a NPE  
  
// 1 - throw exceptions  
 val aWeirdValue: Int = throw new NullPointerException // returns Nothing  
  
// Exception hierarchy:  
//  
// Throwable:  
//    Error, e.g. SOError, OOMError  
//    Exception, e.g. NPException, NSEException, ....  
  
def getInt(withExceptions: Boolean): Int =  
  if (withExceptions) throw new RuntimeException("No int for you!")  
  else 42  
  
// 2 - catch exceptions is an expression, so it returns a value  
val potentialFail = try {  
  // code that might fail  
  getInt(true) // an Int  
} catch {  
  // most specific exceptions first  
  case e: NullPointerException => 35  
  case e: RuntimeException => 54 // an Int  
  // ...
  } finally {  
  // executed no matter what  
  // closing resources  // Unit here
  }  
  
// 3 - custom exceptions  
class MyException extends RuntimeException {  
  // fields or methods  
  override def getMessage = "MY EXCEPTION"  
}  
  
val myException = new MyException  
  
/**  
 * Exercises: 
 * 
 * 1. Crash with SOError 
 * 2. Crash with OOMError 
*/  
def soCrash(): Unit = {  
  def infinite(): Int = 1 + infinite()  
  infinite()  
}  
  
def oomCrash(): Unit = {  
  def bigString(n: Int, acc: String): String =  
    if (n == 0) acc  
    else bigString(n - 1, acc + acc)  
  
  bigString(56175363, "Scala")  
}

Imports and Exports
#

  • Imports: Bring definitions into scope.
  • Alias: Use as to rename imports.
  • Wildcard: Use * to import everything from a package/object.
  • Selective Import: Import multiple or exclude with {A, B as _, *}.
  • Default Imports: scala.*, scala.Predef.*, java.lang.* are always imported.
  • Exports: Re-export methods/fields from an object for easier access.
// can define values and methods top-level  
// they will be included in a synthetic object  
// can be imported via an mypackage.* import  
val meaningOfLife = 42  
def computeMyLife: String = "Scala"  
  
object PackagesImports { // top-level definition  
  // packages = form of organization of definitions, similar to a folder structure in a normal file system  
  
  // fully qualified name  
  val aList: com.rockthejvm.practice.LList[Int] = ??? // throws NotImplementedError  
  
  // import  import com.rockthejvm.practice.LList  
  val anotherList: LList[Int] = ???  
  
  // importing under an alias  
  import java.util.{List as JList}  
  val aJavaList: JList[Int] = ???  
  
  // import everything  
  import com.rockthejvm.practice.*  
  val aPredicate: Cons[Int] = ???  
  
  // import multiple symbols  
  import PhysicsConstants.{SPEED_OF_LIGHT, EARTH_GRAVITY}  
  val c = SPEED_OF_LIGHT  
  
  // import everything EXCEPT something  
  object PlayingPhysics {  
    import PhysicsConstants.{PLANCK as _, *} // import everything except PLANCK  
    // val plank = PLANK // will not work  }  
  
  import com.rockthejvm.part2oop.* // import the mol and computeMyLife  
  val mol = meaningOfLife  
  
  // default imports: scala imports some packages and classes automatically  
  // scala.*, scala.Predef.*, java.lang.*  
  
  // exports - allows to "export" methods/fields from an object  class PhysicsCalculator {  
    import PhysicsConstants.*  
    def computePhotonEnergy(wavelength: Double): Double =  
      PLANCK / wavelength  
  }  
  
  object ScienceApp {  
    val physicsCalculator = new PhysicsCalculator  
  
    // exports create aliases for fields/methods to use locally  
    export physicsCalculator.computePhotonEnergy  
  
    def computeEquivalentMass(wavelength: Double): Double =  
      computePhotonEnergy(wavelength) / (SPEED_OF_LIGHT * SPEED_OF_LIGHT)  
      // ^^ the computePhotonEnergy method can be used directly (instead of physicsCalculator.computePhotonEnergy)  
      // useful especially when these uses are repeated  
    }  
  
  def main(args: Array[String]): Unit = {  
    // for testing  
  }  
}  
  
// usually organizing "utils" and constants in separate objects  
object PhysicsConstants {  
  // constants  
  val SPEED_OF_LIGHT = 299792458  
  val PLANCK = 6.62e-34 // scientific notation  
  val EARTH_GRAVITY = 9.8  
}