This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 6.8, “How to create object instances without using the ‘new’ keyword.”
Update!
This text was written for Scala 2. Things have changed a bit in Scala 3, and this approach is generally NOT NEEDED in Scala 3.
Problem
You’ve seen that Scala code looks cleaner when you don’t always have to use the new
keyword to create a new instance of a class, like this:
val a = Array( Person("John"), Person("Paul") )
So you want to know how to write your code to make your classes work like this.
Solution
There are two ways to do this:
- Create a companion object for your class, and define an
apply
method in the companion object with the desired constructor signature. - Define your class as a “case class.”
You’ll look at both approaches next.
Creating a companion object with an apply
method
To demonstrate the first approach, define a Person
class and Person
object in the same file. Define an apply
method in the object that takes the desired parameters. This method is essentially the constructor of your class:
class Person { var name: String = _ } object Person { def apply(name: String): Person = { var p = new Person p.name = name p } }
Given this definition, you can create new Person
instances without using the new
keyword, as shown in these examples:
val dawn = Person("Dawn") val a = Array(Person("Dawn"), Person("Elijah"))
The apply
method in a companion object is treated specially by the Scala compiler and lets you create new instances of your class without requiring the new
keyword. (More on this in the Discussion.)
Declare your class as a “case class”
The second solution to the problem is to declare your class as a case class, defining it with the desired constructor:
case class Person (var name: String)
This approach also lets you create new class instances without requiring the new
keyword:
val p = Person("Fred Flinstone")
With case classes, this works because the case class generates an apply
method in a companion object for you. However, it’s important to know that a case class creates much more code for you than just the apply
method. This is discussed in depth in the Discussion.
Discussion
An apply
method defined in the companion object of a class is treated specially by the Scala compiler. There is essentially a little syntactic sugar baked into Scala that converts this code:
val p = Person("Fred Flinstone")
into this code:
val p = Person.apply("Fred Flinstone")
The apply
method is basically a factory method, and Scala’s little bit of syntactic sugar lets you use the syntax shown, creating new class instances without using the new
keyword.
Providing multiple constructors with additional apply
methods
To create multiple constructors when manually defining your own apply
method, just define multiple apply
methods in the companion object that provide the constructor signatures you want:
class Person { var name = "" var age = 0 } object Person { // a one-arg constructor def apply(name: String): Person = { var p = new Person p.name = name p } // a two-arg constructor def apply(name: String, age: Int): Person = { var p = new Person p.name = name p.age = age p } }
You can now create a new Person instance in these ways:
val fred = Person("Fred") val john = Person("John", 42)
I’m using the term “constructor” loosely here, but each apply
method does define a different way to construct an instance.
Providing multiple constructors for case classes
To provide multiple constructors for a case
class, it’s important to know what the case class
declaration actually does.
If you look at the code the Scala compiler generates for the case
class example, you’ll see that see it creates two output files, Person$.class and Person.class. If you disassemble Person$.class with the javap
command, you’ll see that it contains an apply
method, along with many others:
$ javap Person$ Compiled from "Person.scala" public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{ public static final Person$ MODULE$; public static {}; public final java.lang.String toString(); public scala.Option unapply(Person); public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve(); public java.lang.Object apply(java.lang.Object); }
You can also disassemble Person.class to see what it contains. For a simple class like this, it contains an additional 20 methods; because all of this extra code isn’t needed for some purposes, some developers don’t like case classes.
See Recipe 4.14, “Generating Boilerplate Code with Case Classes”, for a thorough discussion of what code is generated for case
classes, and why.
Note that the apply
method in the disassembled code accepts one String
argument:
public Person apply(java.lang.String);
That String
corresponds to the name
field in your case
class constructor:
case class Person (var name: String)
So, it’s important to know that when a case
class is created, it writes the accessor and (optional) mutator methods only for the default constructor. As a result, (a) it’s best to define all class parameters in the default constructor, and (b) write apply
methods for the auxiliary constructors you want.
This is demonstrated in the following code, which I place in a file named Person.scala:
// want accessor and mutator methods for the name and age fields case class Person (var name: String, var age: Int) // define two auxiliary constructors object Person { def apply() = new Person("<no name>", 0) def apply(name: String) = new Person(name, 0) }
Because name
and age
are declared as var
fields, accessor and mutator methods will both be generated. Also, two apply
methods are declared in the object: a no-args constructor, and a one-arg constructor.
As a result, you can create instances of your class in three different ways, as demonstrated in the following code:
object Test extends App { val a = Person() val b = Person("Al") val c = Person("William Shatner", 82) println(a) println(b) println(c) // test the mutator methods a.name = "Leonard Nimoy" a.age = 82 println(a) }
Running this test object results in the following output:
Person(<no name>,0) Person(Al,0) Person(William Shatner,82) Person(Leonard Nimoy,82)
For more information on case classes, see Recipe 4.14, “Generating Boilerplate Code with Scala Case Classes”.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |