Constructors
Constructors are responsible for creating instances of a class. Kotlin classes are generally defined with a primary constructor, providing the main way of creating an object:
class Point(var x: Double, var y: Double)
In this example, the primary constructor specifies that two values of type
Double, representing a point’s \(x\) and \(y\) coordinates, need
to be provided whenever we try to create a Point. These values will be
used to initialize properties named x and y.
But what if we want to create objects of this class in other ways? For
example, what if we want there to be a ‘default point’ of \( (0,0) \),
created when no values are provided for x and y?
This could achieved by specifying default arguments of 0.0 in the
constructor:
class Point(var x: Double = 0.0, var y: Double = 0.0)
However, this isn’t entirely satisfactory. It allows us to create a Point
by supplying a value for only one of the two coordinates, and this feels
unintuitive.
Secondary Constructors
A better solution to this problem is to define an additional constructor
with an empty parameter list. This secondary constructor is specified
within the body of the class, using the constructor keyword:
class Point(var x: Double, var y: Double) {
constructor(): this(0.0, 0.0)
}
The parameter list of this secondary constructor is followed immediately
by a colon, then this(0.0, 0.0). This syntax invokes the primary
constructor, providing values of 0.0 for parameters x and y.
A secondary constructor must always delegate either to the primary
constructor or to another secondary constructor, using the this() syntax
shown above.
If required, the secondary constructor can have a body, enclosed in {},
which can do other work, but it always has to delegate.
This ensures that the primary constructor is ultimately always used as part of the process of initializing an object.
Task 12.3.1
-
Edit the file
Construct.kt, in thetasks/task12_3_1subdirectory of your repository. You should see the following code:class Point(var x: Double, var y: Double) fun main() { val p = Point(4.5, 7.0) println("(${p.x}, ${p.y})") }Make sure that this code compiles.
-
Change
main()so that it attempts to create thePointobject without specifying values forxandy. Try compiling the code. What errors do you see? -
Modify the class definition so that it includes the secondary constructor described above. Compile and run the program to verify that this fixes the issue.
-
Now change the first line of
main()to beval p = Point(4, 7)Try recompiling the code. What happens?
-
Add another secondary constructor to
Pointthat fixes the issue.Hints
-
Your secondary constructor will need parameters. What type do they need to be?
-
Note that these parameters should NOT be prefixed with
valorvar. Those keywords are used to specify properties, and you are only allowed to do that in the primary constructor parameter list or separately in the body of the class. -
Remember that Kotlin has extension functions to support type conversion. For example, you can invoke
toDouble()on a number to create aDoublerepresentation of that number…
-
Another Example
Consider this class definition:
import java.time.LocalDate
class Person(var name: String, val birth: LocalDate) {
var isMarried = false
}
This represents a person using three properties: their name, their date of birth, and their marital status. Notice that the latter is defined and initialized in the body of the class, rather than being specified as part of the primary constructor.
We are free to mix these different styles of property definition and initialization as we see fit. The design decision made here was that when we represent a person, it should be assumed that they are not married by default.
Notice also that properties name and isMarried are defined using var,
but birth is defined using val. This is because a person’s date of
birth is a fixed value that can never change, whereas their name and marital
status can both change during their lifetime.
Task 12.3.2
-
Edit the file named
Person.kt, in thetasks/task12_3_2subdirectory of your repository. Add to this file thePersonclass definition shown above. Be sure to include theimportstatement as well. -
Now add a
main()function that creates aPersonobject and then prints the person’s name, date of birth and marital status.Note: you can create a
LocalDateobject to represent date of birth like this:val date = LocalDate.of(1997, 8, 23)Check that your program compiles and runs successfully.
-
Dates are often manipulated as strings, in ISO 8601 format (e.g.,
"1997-08-23"). Add a secondary constructor toPersonthat allows date of birth to be supplied as such a string. -
Modify
main()so that when thePersonobject is created, date of birth is now specified as a string. Recompile and run the program to verify that it behaves as expected.
Initializer Blocks
There’s a problem with the Person class. In the implementation shown above,
it is possible to create a Person object using any string as the person’s
name. It would be better if we could impose some restrictions—e.g.,
preventing use of an empty string, or a string consisting solely of spaces.
However, there is no way do to this currently, because the primary
constructor just assigns values to properties.
There are a couple of different ways of solving this problem. One of them
is to give the Person class an initializer block.
Initializer blocks are blocks of code enclosed in braces, marked with the
init prefix. Any blocks marked in this way will be injected into the object
construction process. You can do almost anything you like inside an
initializer block, but a common thing to do is property validation:
import java.time.LocalDate
class Person(var name: String, val birth: LocalDate) {
var isMarried = false
init {
require(name.isNotBlank()) { "Name cannot be blank" }
}
}
In the example above, the name parameter is checked to make sure that
it is not blank, i.e., that its length is greater than zero and its contents
do not consist solely of whitespace characters. If this is not true, an
IllegalArgumentException will be thrown and object construction will fail.
Note that initializer blocks are effectively incorporated into the primary constructor, even in cases where that primary constructor is implicit rather than explicit. Thus they always execute when an instance of the class is created.
Task 12.3.3
-
Copy the file
Person.ktfrom thetask12_3_2subdirectory of your repository to thetask12_3_3subdirectory. -
Edit the copied file and modify
main()so that it reads the user’s name and date of birth usingreadln(), then creates aPersonobject using these strings.Compile the program and run it to verify that you can create a
Personwith a zero-length name, or a name consisting only of spaces. -
Modify the class so that it has the initializer block shown above. Recompile the program and try running it again to verify that invalid names trigger an exception.