Objects, Companion classes & objects

object in Scala

object HelloWorld {
def main(args: Array[String]): Unit = {
println("Hello,World! Welcome to Scala!!")
}
}

In Scala, object is a keyword used to define a singleton object. A singleton object is an instance of a class that is created only once, meaning there can be only one instance of it in the program. This concept is used in Scala to encapsulate utility methods, constants, or as a replacement for static members (since Scala doesn’t have static methods like Java).

  • Object is automatically instantiated. No need to use new keyword to instantiate.
  • We do not have a static keyword in Scala.
  • In object , all the methods are by default static .

Key Features of object in Scala:

  1. Singleton: An object is created once and used globally in the program. It cannot be instantiated multiple times.
    • No new Keyword: You don’t need to (and cannot) use the new keyword to create an instance of an object.
  2. No Constructor: Since an object is a singleton, it does not have a constructor like a class.
  3. Static-Like Behavior: In Java, static methods and variables are associated with a class rather than an instance. Scala doesn’t have the static keyword, so you use an object to define methods or fields that should be globally accessible without requiring an instance of a class.
  4. Companion Object: When an object has the same name as a class and is defined in the same file, it is called a companion object. It can access the private members of the class and vice versa. The object provides methods and functionality that relate to the class but do not require an instance of the class.
  5. Application Entry Point: In Scala, the entry point of a program is defined within an object using the main method. This main method is executed when the program starts.

Differences Between object and class:

Aspectobjectclass
InstantiationOnly one instance (singleton).Can have multiple instances.
ConstructorNo constructor.Can define constructors.
UsageUsed for static-like functionality, utilities, or entry points.Defines a blueprint for creating objects.
CreationAutomatically instantiated.Needs to be instantiated using new.

Utility Methods: Like Java static utility classes.

object MathUtils {
def add(a: Int, b: Int): Int = a + b
}
println(MathUtils.add(2, 3)) // Output: 5

In short, an object in Scala is a singleton that is useful for defining methods, constants, and as an entry point for Scala applications. It helps achieve a static-like structure without requiring the static keyword like in Java.

object Internals – How object is mapped to Java class?

In Scala, an object is a singleton instance that provides a way to define methods and values without needing to create multiple instances. When Scala code is compiled to Java bytecode (for the JVM), Scala’s object gets mapped to Java in a specific way to accommodate the concept of singletons.

Here’s how a Scala object is represented in Java:

Mapping of Scala object to a Java Class

Scala compiles an object into a final class with a private constructor and a static instance of itself. This pattern ensures that only one instance of the class exists (similar to the Singleton Pattern in Java). Additionally, Scala generates a static method to access the singleton instance of the class.

Example:

Scala Code:

object HelloWorld {
def greet(): Unit = {
println("Hello, World!")
}
}

How it is translated to Java :

public final class HelloWorld {
// Private constructor to prevent instantiation
private HelloWorld() {}

// Static instance of the singleton object
public static final HelloWorld MODULE$ = new HelloWorld();

// Method in the singleton object
public void greet() {
System.out.println("Hello, World!");
}
}

In this Java equivalent:

  • The Scala object HelloWorld is mapped to a final class named HelloWorld.
  • A static instance MODULE$ is created to represent the single instance of the object.
  • The greet method is accessible via the singleton instance HelloWorld.MODULE$.greet().

Accessing the Singleton in Java

If you were to access this Scala object from Java, you would do it using the static MODULE$ field, which holds the single instance of the object:

HelloWorld.MODULE$.greet();  // Access the greet method of the singleton instance

In this case, MODULE$ provides access to the only instance of the HelloWorld object.

Static Methods in Objects

In Scala, an object can hold methods that look like static methods (from a Java perspective). In Java, methods in a Scala object are accessed via the MODULE$ instance, but conceptually they behave similarly to static methods.

Static Initialization in Scala object

In Java, static initialization blocks are used to execute code when the class is first loaded. In Scala, any code inside an object that is not part of a method is treated as a static initialization block, and it is executed when the object is first accessed.

Scala code :

object Singleton {
println("Singleton object created!")
}

Java translation :

public final class Singleton {
public static final Singleton MODULE$;

static {
MODULE$ = new Singleton();
System.out.println("Singleton object created!");
}

private Singleton() {}
}
  • The static block ensures that the println statement is executed when the MODULE$ singleton instance is initialized.

Key Points

  • Scala object is translated into a final Java class with a static instance (MODULE$).
  • The private constructor prevents instantiation from outside the class, ensuring singleton behavior.
  • Methods defined in Scala object are like static methods in Java and are accessed via the singleton instance MODULE$.
  • Companion objects are mapped to singleton classes in Java, with their instance stored in MODULE$.
  • Any initialization code in the Scala object is executed when the singleton instance is first accessed, just like static initialization in Java.

Conclusion

In Scala, object provides a concise way to define singletons, and when compiled to Java bytecode, it is transformed into a class with a static instance (MODULE$) and a private constructor to enforce the singleton property. This allows Scala’s singleton model to integrate smoothly with the JVM.


What are companion classes & objects ?

In Scala, a companion class and a companion object are a pair of entities (a class and an object) that share the same name and are defined in the same source file. Despite having the same name, they serve different purposes:

  • Companion Class: Represents a blueprint for creating instances (objects) with their own state and behavior.
  • Companion Object: Holds static-like members (methods, values) that are shared among all instances of the class, similar to static methods and fields in Java. It is a singleton object, meaning only one instance of the object exists.

Characteristics

  1. Same Name: Both the class and the object must have the same name.
  2. Same File: They must be defined in the same source file.
  3. Access to Private Members: Companion objects and companion classes can access each other’s private members.

Use Cases

  1. Factory Methods: Companion objects are often used to create factory methods, which provide a convenient way to create instances of the class.
  2. Static Methods and Values: They can hold static methods or constants that are common across all instances of the companion class.
  3. Apply Method for Simplified Object Creation: The apply method in a companion object allows the class to be instantiated without explicitly using the new keyword.
  4. Utility Methods: Companion objects can store utility methods that logically belong to the class but don’t require access to instance-specific data.

Companion Class and Object for Factory Method

class Person(val name: String, val age: Int) {
def display(): Unit = {
println(s"Name: $name, Age: $age")
}
}

object Person {
// Factory method to create a Person instance
// Apply method allows creating an instance without 'new'
def apply(name: String, age: Int): Person = new Person(name, age)

// Static-like method
def fromFullName(fullName: String): Person = {
val parts = fullName.split(" ")
new Person(parts(0), 0) // Default age of 0 for example
}
}

Usage :

val person1 = Person("Alice", 25)  // Uses the apply method, no need for 'new'
val person2 = Person.fromFullName("Bob Smith") // Uses custom factory method

person1.display() // Output: Name: Alice, Age: 25
person2.display() // Output: Name: Bob, Age: 0

Explanation:

  • Companion Class: Person is the class that defines the structure (name and age) and behavior (display method) of a Person object.
  • Companion Object: Person (object) defines a factory method apply to create Person instances, so you don’t need to explicitly use the new keyword. It also defines a utility method fromFullName to create a Person based on a full name.
  • The apply method in the companion object makes object instantiation simpler. Instead of writing new Person("Alice", 25), you can just write Person("Alice", 25).

Companion Objects Holding Static Methods

Companion objects are ideal for holding static methods that don’t need access to the class instance.

class Circle(val radius: Double) {
def area: Double = Circle.pi * radius * radius
}

object Circle {
val pi: Double = 3.14159 // Static-like field

def circumference(radius: Double): Double = 2 * pi * radius // Static-like method
}

Usage :

val circle = new Circle(5)
println(s"Area: ${circle.area}") // Output: Area: 78.53975
println(s"Circumference: ${Circle.circumference(5)}") // Output: Circumference: 31.4159

Companion Object: Stores the constant value pi and the method circumference, which are shared across all Circle instances. The companion class Circle can reference the pi value directly in its instance method area.

Accessing Private Members Between Companion Class and Object

Companion objects and classes can access each other’s private members, which is a special feature of this relationship.

class BankAccount(private var balance: Double) {
// Method to show balance (for internal use only)
private def showBalance(): Unit = {
println(s"Current balance: $$balance")
}
}

object BankAccount {
def apply(initialBalance: Double): BankAccount = new BankAccount(initialBalance)

// Companion object can access private members
def printBalance(account: BankAccount): Unit = {
account.showBalance() // Accessing private method
}
}

Usage :

val account = BankAccount(1000)
BankAccount.printBalance(account) // Output: Current balance: $1000
  • The companion object BankAccount can access the private method showBalance in the companion class BankAccount, even though this method is private. This demonstrates the special relationship between companion classes and objects.

Summary of Use Cases

  • Factory Methods: Create instances without needing new, providing a cleaner syntax.
  • Static Methods and Constants: Define methods or values that are common to all instances, such as mathematical constants or utility methods.
  • Simplified Object Creation with Apply: The apply method can reduce the need for the new keyword, simplifying instantiation.
  • Access to Private Members: Companion objects can access private fields and methods of the companion class, and vice versa, allowing for encapsulation while still maintaining flexibility in initialization or utility functions.

By using companion objects effectively, you can define both instance-specific logic (in the class) and shared or utility logic (in the companion object) in a clean and organized manner.


Example code :

class CompanionDemo {
var x = 5

def getValue(): Unit ={
println(s"Value of x is: ${x} and value of y is: ${CompanionDemo.y}" )
}
}


object CompanionDemo {
var y = 2.2

def main(args: Array[String]): Unit = {
val objectForCompanionDemoClass = new CompanionDemo()
println(s"Value of x when retrieved from companion object is ${objectForCompanionDemoClass.x}")
println(s"Value of y when retrieved from companion object is ${y}")

objectForCompanionDemoClass.getValue
}

}


Case classes and case objects

In Scala, case classes and case objects are special types of classes and objects designed to make working with immutable data, pattern matching, and functional programming more convenient. Case classes and case objects come with several useful features by default, such as immutability, equality, and easy instance creation without new.

Case Class

A case class is a regular class that is immutable by default and comes with additional features automatically generated by the compiler.

Regular class (immutable) + lot of additional features (extra autogenerated code)

Features of Case Classes:

  • Immutability: All fields are immutable (val), though you can override this to make fields mutable with var.
  • Automatic equals and hashCode: Case classes have structural equality, meaning two instances with the same values are considered equal.
  • **Pattern Matching: Case classes are particularly useful in pattern matching, making it easy to deconstruct instances.
  • Automatically Generated Methods: The compiler automatically generates methods like toString, equals, hashCode, and copy.
  • No Need for new: You can create instances of case classes without using the new keyword, thanks to the automatically generated apply method.

Example – case class

case class Person(name: String, age: Int)

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

// Automatic toString, equals, and hashCode
println(person1) // Output: Person(Alice, 25)

// Equality comparison based on structure
println(person1 == Person("Alice", 25)) // Output: true

// Copy method to create a new instance with some modifications
val person3 = person1.copy(age = 26)
println(person3) // Output: Person(Alice, 26)

// Pattern matching
person2 match {
case Person(name, age) => println(s"Name: $name, Age: $age")
}
  • The Person case class automatically gets methods like equals, toString, and copy.
  • You can create instances without new.
  • Structural equality (==) works based on the fields of the case class.
  • The copy method allows you to create new instances with some fields changed.
  • Case classes can be easily used in pattern matching.

Example :

case class Car(name: String, model: String) // define a case class
{
val carName = name
val carModel = model

def printDetails(): Unit ={
println(s"Car Name is: ${carName} and Car Model is: ${carModel}")
}
}

object CaseClassDemoA {
def main(args: Array[String]): Unit = {
val bmw = Car("BMW", "550") // 1. No need to write "new", since "apply" method is auto generated in case class
bmw.printDetails()

//bmw.name = "B.M.W" //2. constructor parameter is val by default. therefore mutator method is not autogenerated and hence you can not change the name. However, if you change the constructor parameter to var, mutator method will be auto generated and you will be able to modify the value in variable
//bmw.printDetails()

// 3. Case class autogenerate unapply method , used for pattern matching
bmw match { case Car(a,b) => println(a,b) } // (BMW, 550)

// 4. Autogenerates copy method
val mercedes = bmw.copy(name = "mercedes")
mercedes.printDetails() // Car Name is : mercedes and Car Model is : 550

// 5. equals and hashcode method
println(bmw == mercedes) // false

//6. toString method is auto-implemented
println(bmw) // Car(BMW, 550)
}
}
  • bmw match { ... }: This is a pattern matching expression. The match keyword is similar to a switch statement in other languages, but it’s more powerful as it supports deconstructing objects and working with complex patterns.Here, bmw is the object or value being matched against the pattern defined inside the { ... }.
  • case Car(a, b): This is a case clause in pattern matching. It tries to match bmw against a specific pattern, which in this case is an instance of a Car object. The Car(a, b) part is:
    • Car: A case class (or a class) called Car. The case class typically has two fields (let’s assume make and model as an example).
    • (a, b): These are pattern variables that will hold the values of the fields inside the Car object if the match is successful. In this case, a and b will represent the values of the fields of bmw.
    For example, if bmw is an instance of Car("BMW", "X5"), the pattern Car(a, b) will match, and a will be assigned "BMW" and b will be assigned "X5".

Pattern matching is powerful in Scala, enabling concise and expressive handling of different data types and object structures.

Use Cases for Case Classes

Immutable Data Models

Case classes are ideal for representing data models where immutability is desired, such as user data, configuration data, or event models.

case class Address(city: String, country: String)
case class User(name: String, address: Address)

val user = User("Alice", Address("New York", "USA"))
val updatedUser = user.copy(address = user.address.copy(city = "Los Angeles"))
println(updatedUser) // Output: User(Alice, Address(Los Angeles, USA))
Pattern Matching

Case classes make pattern matching concise and powerful, especially when working with algebraic data types or processing different types of data in functional programming.

case class Success(message: String)
case class Failure(reason: String)

def handleResponse(response: Any): Unit = response match {
case Success(message) => println(s"Success: $message")
case Failure(reason) => println(s"Failure: $reason")
}

handleResponse(Success("All good!"))
handleResponse(Failure("Something went wrong"))
Functional Programming

Case classes fit naturally into functional programming paradigms. They are used to define algebraic data types and simplify the process of working with immutable data and pattern matching.


Case Objects

A case object is a singleton object that has many of the same features as a case class. It is mainly used when you want a single instance of a class with pattern matching capabilities, and when you don’t need to maintain any state across different instances.

Features of Case Objects :

  • Singleton: Only one instance exists.
  • Pattern Matching: Like case classes, case objects can be used in pattern matching.
  • No apply or copy methods: Since there is only one instance, there’s no need for an apply method (for instance creation) or copy (for duplication with modified fields).

Example : case objects

case object Stop
case object Start

def handleCommand(command: Any): String = command match {
case Start => "Starting the process"
case Stop => "Stopping the process"
case _ => "Unknown command"
}

println(handleCommand(Start)) // Output: Starting the process
println(handleCommand(Stop)) // Output: Stopping the process
  • Start and Stop are case objects used for pattern matching.
  • These case objects represent single values with predefined meanings (commands in this case).
  • When using pattern matching, each case object is treated as a distinct command or state.

Use Cases for Case Objects

Singleton Values for Commands or States:

Case objects are useful when you need to represent fixed, singleton values that can be used in pattern matching. For example, you could use case objects to represent different states or commands in a finite state machine or actor model.

case object Idle
case object Running
case object Stopped

def checkState(state: Any): Unit = state match {
case Idle => println("System is idle.")
case Running => println("System is running.")
case Stopped => println("System is stopped.")
case _ => println("Unknown state.")
}

checkState(Idle) // Output: System is idle.
checkState(Running) // Output: System is running.
Enum-Like Behavior

When you need a set of fixed values, such as representing different days of the week or command statuses, case objects are an elegant solution in Scala (similar to enumerations in other languages).

case object Monday
case object Tuesday
case object Wednesday

def whichDay(day: Any): String = day match {
case Monday => "It's Monday."
case Tuesday => "It's Tuesday."
case Wednesday => "It's Wednesday."
case _ => "Unknown day."
}

println(whichDay(Monday)) // Output: It's Monday.

Case Objects are singleton instances that are useful for representing fixed values or commands in pattern matching and state transitions.


Key Differences Between Case Class and Case Object

FeatureCase ClassCase Object
Instance CreationMultiple instances can be created.Only one instance (singleton).
FieldsCan have fields (parameters in constructor).No constructor parameters (no fields).
Pattern MatchingSupports pattern matching.Supports pattern matching.
Apply MethodAutomatically generated apply method for instantiation.No apply method needed.
Copy MethodAutomatically generated copy method for creating modified instances.No copy method.
EqualityEquality is structural, based on field values.Only one instance, so equality is by reference.