Classes and Objects
Syntax#
class MyClass{} // curly braces are optional here as class body is empty
class MyClassWithMethod {def method: MyClass = ???}
new MyClass() //Instantiate
object MyObject // Singleton object
class MyClassWithGenericParameters[V1, V2](vl: V1, i: Int, v2: V2)
class MyClassWithImplicitFieldCreation[V1](val v1: V1, val i: Int)
new MyClassWithGenericParameters(2.3, 4, 5)
or with a different type:new MyClassWithGenericParameters[Double, Any](2.3, 4, 5)
class MyClassWithProtectedConstructor protected[my.pack.age](s: String)
Instantiate Class Instances
A class in Scala is a ‘blueprint’ of a class instance. An instance contains the state and behavior as defined by that class. To declare a class:
class MyClass{} // curly braces are optional here as class body is empty
An instance can be instantiated using new
keyword:
var instance = new MyClass()
or:
var instance = new MyClass
Parentheses are optional in Scala for creating objects from a class that has a no-argument constructor. If a class constructor takes arguments:
class MyClass(arg : Int) // Class definition
var instance = new MyClass(2) // Instance instantiation
instance.arg // not allowed
Here MyClass
requires one Int
argument, which can only be used internally to the class. arg
cannot be accessed outside MyClass
unless it is declared as a field:
class MyClass(arg : Int){
val prop = arg // Class field declaration
}
var obj = new MyClass(2)
obj.prop // legal statement
Alternatively it can be declared public in the constructor:
class MyClass(val arg : Int) // Class definition with arg declared public
var instance = new MyClass(2) // Instance instantiation
instance.arg //arg is now visible to clients
Instantiating class with no parameter: {} vs ()
Let’s say we have a class MyClass with no constructor argument:
class MyClass
In Scala we can instantiate it using below syntax:
val obj = new MyClass()
Or we can simply write:
val obj = new MyClass
But, if not paid attention, in some cases optional parenthesis may produce some unexpected behavior. Suppose we want to create a task that should run in a separate thread. Below is the sample code:
val newThread = new Thread { new Runnable {
override def run(): Unit = {
// perform task
println("Performing task.")
}
}
}
newThread.start // prints no output
We may think that this sample code if executed will print Performing task.
, but to our surprise, it won’t print anything. Let’s see what’s happening here. If you pay a closer look, we have used curly braces {}
, right after new Thread
. It created an annonymous class which extends Thread
:
val newThread = new Thread {
//creating anonymous class extending Thread
}
And then in the body of this annonymous class, we defined our task (again creating an annonymous class implementing Runnable
interface). So we might have thought that we used public Thread(Runnable target)
constructor but in fact (by ignoring optional ()
) we used public Thread()
constructor with nothing defined in the body of run()
method. To rectify the problem, we need to use parenthesis instead of curly braces.
val newThread = new Thread ( new Runnable {
override def run(): Unit = {
// perform task
println("Performing task.")
}
}
)
In other words, here {}
and ()
are not interchangeable.
Singleton & Companion Objects
Singleton Objects
Scala supports static members, but not in the same manner as Java. Scala provides an alternative to this called Singleton Objects. Singleton objects are similar to a normal class, except they can not be instantiated using the new
keyword. Below is a sample singleton class:
object Factorial {
private val cache = Map[Int, Int]()
def getCache = cache
}
Note that we have used object
keyword to define singleton object (instead of ‘class’ or ‘trait’). Since singleton objects can not be instantiated they can not have parameters. Accessing a singleton object looks like this:
Factorial.getCache() //returns the cache
Note that this looks exactly like accessing a static method in a Java class.
Companion Objects
In Scala singleton objects may share the name of a corresponding class. In such a scenario the singleton object is referred to as a Companion Object. For instance, below the class Factorial
is defined, and a companion object (also named Factorial
) is defined below it. By convention companion objects are defined in the same file as their companion class.
class Factorial(num : Int) {
def fact(num : Int) : Int = if (num <= 1) 1 else (num * fact(num - 1))
def calculate() : Int = {
if (!Factorial.cache.contains(num)) { // num does not exists in cache
val output = fact(num) // calculate factorial
Factorial.cache += (num -> output) // add new value in cache
}
Factorial.cache(num)
}
}
object Factorial {
private val cache = scala.collection.mutable.Map[Int, Int]()
}
val factfive = new Factorial(5)
factfive.calculate // Calculates the factorial of 5 and stores it
factfive.calculate // uses cache this time
val factfiveagain = new Factorial(5)
factfiveagain.calculate // Also uses cache
In this example we are using a private cache
to store factorial of a number to save calculation time for repeated numbers.
Here object Factorial
is a companion object and class Factorial
is its corresponding companion class. Companion objects and classes can access each other’s private
members. In the example above Factorial
class is accessing the private cache
member of it’s companion object.
Note that a new instantiation of the class will still utilize the same companion object, so any modification to member variables of that object will carry over.
Objects
Whereas Classes are more like blueprints, Objects are static (i.e. already instantiated):
object Dog {
def bark: String = "Raf"
}
Dog.bark() // yields "Raf"
They are often used as a companion to a class, they allow you to write:
class Dog(val name: String) {
}
object Dog {
def apply(name: String): Dog = new Dog(name)
}
val dog = Dog("Barky") // Object
val dog = new Dog("Barky") // Class
Instance type checking
Type check: variable.isInstanceOf[Type]
With pattern matching (not so useful in this form):
variable match {
case _: Type => true
case _ => false
}
Both isInstanceOf
and pattern matching are checking only the object’s type, not its generic parameter (no type reification), except for arrays:
val list: List[Any] = List(1, 2, 3) //> list : List[Any] = List(1, 2, 3)
val upcasting = list.isInstanceOf[Seq[Int]] //> upcasting : Boolean = true
val shouldBeFalse = list.isInstanceOf[List[String]]
//> shouldBeFalse : Boolean = true
But
val chSeqArray: Array[CharSequence] = Array("a") //> chSeqArray : Array[CharSequence] = Array(a)
val correctlyReified = chSeqArray.isInstanceOf[Array[String]]
//> correctlyReified : Boolean = false
val stringIsACharSequence: CharSequence = "" //> stringIsACharSequence : CharSequence = ""
val sArray = Array("a") //> sArray : Array[String] = Array(a)
val correctlyReified = sArray.isInstanceOf[Array[String]]
//> correctlyReified : Boolean = true
//val arraysAreInvariantInScala: Array[CharSequence] = sArray
//Error: type mismatch; found : Array[String] required: Array[CharSequence]
//Note: String <: CharSequence, but class Array is invariant in type T.
//You may wish to investigate a wildcard type such as `_ <: CharSequence`. (SLS 3.2.10)
//Workaround:
val arraysAreInvariantInScala: Array[_ <: CharSequence] = sArray
//> arraysAreInvariantInScala : Array[_ <: CharSequence] = Array(a)
val arraysAreCovariantOnJVM = sArray.isInstanceOf[Array[CharSequence]]
//> arraysAreCovariantOnJVM : Boolean = true
Type casting: variable.asInstanceOf[Type]
With pattern matching:
variable match {
case _: Type => true
}
Examples:
val x = 3 //> x : Int = 3
x match {
case _: Int => true//better: do something
case _ => false
} //> res0: Boolean = true
x match {
case _: java.lang.Integer => true//better: do something
case _ => false
} //> res1: Boolean = true
x.isInstanceOf[Int] //> res2: Boolean = true
//x.isInstanceOf[java.lang.Integer]//fruitless type test: a value of type Int cannot also be a Integer
trait Valuable { def value: Int}
case class V(val value: Int) extends Valuable
val y: Valuable = V(3) //> y : Valuable = V(3)
y.isInstanceOf[V] //> res3: Boolean = true
y.asInstanceOf[V] //> res4: V = V(3)
Remark: This is only about the behaviour on the JVM, on other platforms (JS, native) type casting/checking might behave differently.
Constructors
Primary Constructor
In Scala the primary constructor is the body of the class. The class name is followed by a parameter list, which are the constructor arguments. (As with any function, an empty parameter list may be omitted.)
class Foo(x: Int, y: String) {
val xy: String = y * x
/* now xy is a public member of the class */
}
class Bar {
...
}
The construction parameters of an instance are not accessible outside its constructor body unless marked as an instance member by the val
keyword:
class Baz(val z: String)
// Baz has no other members or methods, so the body may be omitted
val foo = new Foo(4, "ab")
val baz = new Baz("I am a baz")
foo.x // will not compile: x is not a member of Foo
foo.xy // returns "abababab": xy is a member of Foo
baz.z // returns "I am a baz": z is a member of Baz
val bar0 = new Bar
val bar1 = new Bar() // Constructor parentheses are optional here
Any operations that should be performed when an instance of an object is instantiated are written directly in the body of the class:
class DatabaseConnection
(host: String, port: Int, username: String, password: String) {
/* first connect to the DB, or throw an exception */
private val driver = new AwesomeDB.Driver()
driver.connect(host, port, username, password)
def isConnected: Boolean = driver.isConnected
...
}
Note that it is considered good practice to put as few side effects into the constructor as possible; instead of the above code, one should consider having connect
and disconnect
methods so that consumer code is responsible for scheduling IO.
Auxiliary Constructors
A class may have additional constructors called ‘auxiliary constructers’. These are defined by constructor definitions in the form def this(...) = e
, where e
must invoke another constructor:
class Person(val fullName: String) {
def this(firstName: String, lastName: String) = this(s"$firstName $lastName")
}
// usage:
new Person("Grace Hopper").fullName // returns Grace Hopper
new Person("Grace", "Hopper").fullName // returns Grace Hopper
This implies each constructor can have a different modifier: only some may be available publicly:
class Person private(val fullName: String) {
def this(firstName: String, lastName: String) = this(s"$firstName $lastName")
}
new Person("Ada Lovelace") // won't compile
new Person("Ada", "Lovelace") // compiles
In this way you can control how consumer code may instantiate the class.