4.3. Defining Auxiliary Constructors

Problem

You want to define one or more auxiliary constructors for a class to give consumers of the class different ways to create object instances.

Solution

Define the auxiliary constructors as methods in the class with the name this. You can define multiple auxiliary constructors, but they must have different signatures (parameter lists). Also, each constructor must call one of the previously defined constructors.

The following example demonstrates a primary constructor and three auxiliary constructors:

// primary constructor
class Pizza (var crustSize: Int, var crustType: String) {

  // one-arg auxiliary constructor
  def this(crustSize: Int) {
    this(crustSize, Pizza.DEFAULT_CRUST_TYPE)
  }

  // one-arg auxiliary constructor
  def this(crustType: String) {
    this(Pizza.DEFAULT_CRUST_SIZE, crustType)
  }

  // zero-arg auxiliary constructor
  def this() {
    this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
  }

  override def toString = s"A $crustSize inch pizza with a $crustType crust"

}

object Pizza {
  val DEFAULT_CRUST_SIZE = 12
  val DEFAULT_CRUST_TYPE = "THIN"
}

Given these constructors, the same pizza can be created in the following ways:

val p1 = new Pizza(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE)
val p2 = new Pizza(Pizza.DEFAULT_CRUST_SIZE)
val p3 = new Pizza(Pizza.DEFAULT_CRUST_TYPE)
val p4 = new Pizza

Discussion

There are several important points to this recipe:

  • Auxiliary constructors are defined by creating methods named this.

  • Each auxiliary constructor must begin with a call to a previously defined constructor.

  • Each constructor must have a different signature.

  • One constructor calls another constructor with the name this.

In the example shown, all of the auxiliary constructors call the primary constructor, but this isn’t necessary; an auxiliary constructor just needs to call one of the previously defined constructors. For instance, the auxiliary constructor that takes the crustType parameter could have been written like this:

def this(crustType: String) {
  this(Pizza.DEFAULT_CRUST_SIZE)
  this.crustType = Pizza.DEFAULT_CRUST_TYPE
}

Another important part of this example is that the crustSize and crustType parameters are declared in the primary constructor. This isn’t necessary, but doing this lets Scala generate the accessor and mutator methods for those parameters for you. You could start to write a similar class as follows, but this approach requires more code:

class Pizza () {

  var crustSize = 0
  var crustType = ""

  def this(crustSize: Int) {
    this()
    this.crustSize = crustSize
  }

  def this(crustType: String) {
    this()
    this.crustType = crustType
  }
  // more constructors here ...

  override def toString = s"A $crustSize inch pizza with a $crustType crust"

}

To summarize, if you want the accessors and mutators to be generated for you, put them in the primary constructor.

Note

Although the approach shown in the Solution is perfectly valid, before creating multiple class constructors like this, take a few moments to read Recipe 4.5. Using that recipe can often eliminate the need for multiple constructors.

Generating auxiliary constructors for case classes

A case class is a special type of class that generates a lot of boilerplate code for you. Because of the way they work, adding what appears to be an auxiliary constructor to a case class is different than adding an auxiliary constructor to a “regular” class. This is because they’re not really constructors: they’re apply methods in the companion object of the class.

To demonstrate this, assume that you start with this case class in a file named Person.scala:

// initial case class
case class Person (var name: String, var age: Int)

This lets you create a new Person instance without using the new keyword, like this:

val p = Person("John Smith", 30)

This appears to be a different form of a constructor, but in fact, it’s a little syntactic sugar—a factory method, to be precise. When you write this line of code:

val p = Person("John Smith", 30)

behind the scenes, the Scala compiler converts it into this:

val p = Person.apply("John Smith", 30)

This is a call to an apply method in the companion object of the Person class. You don’t see this, you just see the line that you wrote, but this is how the compiler translates your code. As a result, if you want to add new “constructors” to your case class, you write new apply methods. (To be clear, the word “constructor” is used loosely here.)

For instance, if you decide that you want to add auxiliary constructors to let you create new Person instances (a) without specifying any parameters, and (b) by only specifying their name, the solution is to add apply methods to the companion object of the Person case class in the Person.scala file:

// the case class
case class Person (var name: String, var age: Int)

// the companion object
object Person {

  def apply() = new Person("<no name>", 0)
  def apply(name: String) = new Person(name, 0)

}

The following test code demonstrates that this works as desired:

object CaseClassTest extends App {

  val a = Person()         // corresponds to apply()
  val b = Person("Pam")    // corresponds to apply(name: String)
  val c = Person("William Shatner", 82)

  println(a)
  println(b)
  println(c)

  // verify the setter methods work
  a.name = "Leonard Nimoy"
  a.age = 82
  println(a)
}

This code results in the following output:

Person(<no name>,0)
Person(Pam,0)
Person(William Shatner,82)
Person(Leonard Nimoy,82)

See Also

  • Recipe 6.8, demonstrates how to implement the apply method in a companion object so you can create instances of a class without having to use the new keyword (or declare your class as a case class).

  • Recipe 4.5, demonstrates an approach that can often eliminate the need for auxiliary constructors.

  • Recipe 4.14, details the nuts and bolts of how case classes work.

Get Scala Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.