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/varto 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, 0Exercise 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 overflowMethod Notations #
- Infix:
obj method argfor single-arg methods, e.g.mary likes "Movie". - Prefix: Unary operators, e.g.
-mary. Allowed for+,-,!,~. - Postfix:
obj method(discouraged, needs import). - Apply:
obj()orobj(args)callsapplymethod. - 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
extendsto create subclasses. - Constructor Parameters: Subclasses must call superclass constructor.
- Override: Use
overridefor 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:
objectwith 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:
eqfor reference equality (same instance).==/equalsfor value equality (can override).
- Extending Classes: Objects can extend classes.
- Scala Application: Entry point is an
objectwith amainmethod.
// 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 genericsEnums #
- 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.READHandling Exceptions #
- Throw: Use
throwto raise exceptions. - Try/Catch/Finally: Handle exceptions as expressions.
- Custom Exceptions: Extend
ExceptionorRuntimeException.
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
asto 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
}