Kotlin Primer
Constructors
Note
There is a lot of code in this section that can be significantly reduced; I'm starting the section conceptually similar to how you would write the equivalent Java code, followed by a section on simplifying it after we've finished the basic concepts.
Primary Constructors
Now that we've defined the basics of properties (there will be much more cool stuff to come later...), we can talk about how Constructors work.
Kotlin wants to be terse, which is good, because I don't like to type. (He says after typing in everything you've just read...) One of its best tricks is to combine the definition of a constructor directly in the definition of the class.
Let's take a look at how we can pass in a name to a Person instance when creating it.
class Person(name: String) {
// Define a new class named Person with a _primary constructor_.
// This primary constructor takes a name parameter, which is
// non-open (final in Java terms) and is of type String
var name: String = "No Name"
// Define a read/write property called name of type String
// initialized to "No name"
init {
// Define an initializer. This is a block of code that will
// run as the body of the primary constructor.
this.name = name
// Assign the property name to the constructor parameter
// name. The "this." qualifies it to distinguish the property
// from the parameter. Alternatively, we could have just
// used a unique name for the parameter and not needed
// the "this." qualification.
}
}
fun main() {
val person = Person("Scott")
println(person.name)
}
Kotlin separates the concept of primary and secondary constructors. The Primary Constructor is specified directly after the name of the class. Secondary Constructors are specified in the body of the class.
Secondary Constructors
You can define alternative constructors. For example, if we wanted to be able to skip passing in a value for the name we could define the following
class Person(name : String) {
var name: String = "No Name"
constructor(): this("No Name") // SECONDARY CONSTRUCTOR
init {
this.name = name
}
}
fun main() {
val person = Person("Scott")
println(person.name)
val person2 = Person()
println(person2.name)
}
On line 3 we're defining a secondary constructor that calls the primary constructor passing in "No Name". You can have any number of secondary constructors.
If you have defined a primary constructor, all secondary constructors must directly or
indirectly call it by using the constructor(...) : this(...) syntax. For example:
class Person(name: String) {
var name: String = "No Name"
constructor(n: Int): this("No Name " + n)
constructor(): this(42)
init {
this.name = name
}
}
fun main() {
val person = Person("Scott")
println(person.name)
val person2 = Person()
println(person2.name)
val person3 = Person(10)
println(person3.name)
}
This time, line 3 defines a secondary that takes an Int (similar to Java's primitive int
behind the scenes, but treated like an object in code) and appends it after "No Name" before
passing it to the primary constructor.
Line 4 defines another secondary constructor that takes no parameters, and passes 42 to the
other secondary constructor, which will then append it to "No Name" and pass it to the primary
constructor.
The init block then runs as the body of the primary constructor to set the name property.
Calling Superclass Constructors
If you only need to call the primary constructor from a subclass, things are pretty simple:
open class Person(name: String) {
var name: String = "No Name"
init {
this.name = name
}
}
class Student(name: String): Person(name)
fun main() {
val student = Student("Scott")
println(student.name)
}
Note that Person is defined as open so we can create subclasses, and has a primary
constructor. We call that primary constructor on line 8, right after the superclass name, passing
in the value passed to the primary constructor of Student. Student can do more than that,
of course. For example:
open class Person(name: String) {
var name: String = "No Name"
init {
this.name = name
}
}
class Student(
name: String,
gpa: Float
): Person(name) {
var gpa: Float = 0F
init {
this.gpa = gpa
}
}
fun main() {
val student = Student("Scott", 3.99F) // Sooooo close...
println(student.name)
println(student.gpa)
}
Things get a little trickier if you want to call secondary constructors in a superclass. In that case, you cannot define a primary constructor in the subclass. If you define a primary constructor, all secondary constructors must call it, which doesn't give us the choice of which super constructor to call... Here's an example:
open class Person(name: String) {
constructor(): this("No Name")
var name: String = "No Name"
init {
this.name = name
}
}
class Student: Person {
constructor(): super()
constructor(name: String): super(name)
var gpa: Float = 0F
}
fun main() {
val student = Student("Scott")
student.gpa = 3.99F
val student2 = Student()
student2.gpa = 3.50F
println(student.name)
println(student.gpa)
println(student2.name)
println(student2.gpa)
}
So we can pick and choose which superclass constructors are called, but this makes it impossible to pass the gpa to a student constructor and assign it! Time to start looking at better ways to write the code we've been seeing...
Let's introduce some new concepts that can make constructors (and functions) much simpler...
Default Parameter Values
First, default values for parameters...
Most of the time in Java, we define alternate constructors or overloaded functions just to provide default values or different subsets of parameters. Kotlin allows us to specify default values for missing parameters to constructors and functions to avoid having to write those overloads.
Let's start by defining a primary constructor for Person in Kotlin that takes the name and
gives it a default value.
open class Person(
name: String = "No Name"
) {
var name: String = "No Name"
init {
this.name = name
}
}
fun main() {
val person1 = Person()
val person2 = Person("Scott")
println(person1.name)
println(person2.name)
}
Now we can create a Person with or without a name using the same constructor. Let's add the
Student subclass:
open class Person(
name: String = "No Name"
) {
var name: String = "No Name"
init {
this.name = name
}
}
class Student(
gpa: Float,
name: String = "No Name",
): Person(name) {
var gpa: Float = 0F
init {
this.gpa = gpa
}
}
fun main() {
val person1 = Person()
val person2 = Person("Scott")
println(person1.name)
println(person2.name)
val student = Student(3.99F, "Scott")
val student2 = Student(3.5F)
println(student.name)
println(student.gpa)
println(student2.name)
println(student2.gpa)
}
Now we're able to require the GPA in the Student constructor! Note that parameters with default
values must appear after any parameters that do not have default values... Unless...
Naming Parameters in a Call
Kotlin allows you to explicitly name your parameters when calling a function or constructor. For example:
open class Person(
name: String = "No Name"
) {
var name: String = "No Name"
init {
this.name = name
}
}
class Student(
gpa: Float,
name: String = "No Name",
): Person(name) {
var gpa: Float = 0F
init {
this.gpa = gpa
}
}
fun main() {
val person1 = Person()
val person2 = Person(name="Scott")
println(person1.name)
println(person2.name)
val student = Student(3.99F, name="Scott")
val student2 = Student(3.5F)
val student3 = Student(gpa = 3.0F)
val student4 = Student(
gpa = 2.5F,
name = "Mike"
)
println(student.name)
println(student.gpa)
println(student2.name)
println(student2.gpa)
println(student3.name)
println(student3.gpa)
println(student4.name)
println(student4.gpa)
}
Note
It's a good practice to specify names for all parameters if you use a name for any of them. Also - if you have more than one or two parameters, it's a good practice to name them (though you can name them when using one or two parameters)
This also allows us to specify parameters out of order, or even define default-valued parameters before non-default-valued parameters:
open class Person(
name: String = "No Name"
) {
var name: String = "No Name"
init {
this.name = name
}
}
// NOTE PARAMETER ORDER CHANGE!!!
class Student(
name: String = "No Name",
gpa: Float,
): Person(name) {
// We swapped the order of the parameters
// the "name" with a default value, comes first.
// (NOTE: We can no longer call the constructor without a
// name specified _unless_ we name the gpa parameter in the call!)
var gpa: Float = 0F
init {
this.gpa = gpa
}
}
fun main() {
val person1 = Person()
val person2 = Person("Scott")
println(person1.name)
println(person2.name)
val student = Student("Scott", 3.99F)
// We create a Student instance with positional
// parameters as before, but the parameter order
// is reversed in the constructor definition
val student2 = Student(gpa=3.5F)
// We create a Student instance _without_ a name
// passed in. Note that we _must_ name the gpa
// parameter, as it follows a default-valued
// parameter in the constructor definition. |
val student3 = Student(gpa=3.99F, name="Scott")
// We create a Student instance with named parameters
// as before, gpa first. When you name parameters,
// you can pass them in any order! |
println(student.name)
println(student.gpa)
println(student2.name)
println(student2.gpa)
println(student3.name)
println(student3.gpa)
}
Note
I do not recommend putting parameters without default values after parameters with default values; this can cause confusion, but I wanted to point out the requirement for naming in this case.
Initializers Can Access Primary Constructor Parameters
A really nice optimization is that property initializers have access to primary constructor
parameters. This eliminates the need for many init blocks, as they often just initialize
properties.
open class Person(
name: String = "No Name"
) {
var name: String = name // direct access to primary constructor parameter
}
// NOTE PARAMETER ORDER CHANGE!!!
class Student(
name: String = "No Name",
gpa: Float,
): Person(name) {
var gpa: Float = gpa // direct access to primary constructor parameter
}
fun main() {
val student = Student("Scott", 3.99F)
println(student.name)
println(student.gpa)
}
Wow! Code reduction! Love it!
Looking at lines 4 and 12, we reference the primary constructor parameters in the property
initializers. That removes the need for a dummy initializer value, and gets rid of the init
blocks. Nifty! But the next step is the biggie...
Note
My grandmother on my mom's side always hated the word "biggie" for some reason. Trips to Wendy's were always accompanied by her cringing.
Define Properties in the Primary Constructor
Here's one of my favorite things about Kotlin. If your constructor is passing in values that are directly used to initialize properties, you can define the properties directly in the constructor. It's easiest to understand this in an example...
open class Person(
var name: String = "No Name"
)
class Student(
name: String = "No Name",
var gpa: Float,
): Person(name)
fun main() {
val student = Student("Scott", 3.99F)
println(student.name)
println(student.gpa)
}
Check out the terseness, but still very readable. (I'd say even more readable now, as you no longer have the duplication and required explicit association of the constructor parameters and properties.)
By adding var or val in front of a primary constructor parameter, you
- Define a property for the class
- Initialize the property to the value passed in (or default value)
Line 2 defines the name property of Person and line 8 defines the gpa property of Student.
Because Student inherits name from Person, we don't add var or val to name in the
Student constructor; it's just a normal constructor parameter that we pass on to the superclass.
Because these are var properties, we can modify them the same way we did before. We could also
make them val properties, in which case, once the value has been set at creation time, we cannot
change it.
You may have noticed that we no longer have a body ({ ... }) for these classes. If everything
you need to define is in the constructors, you don't need a body and can remove the curly braces!
Property Subsets in Constructors
One last thought before I pass out or my fingers refuse to keep typing...
Suppose we have several properties, some of which must be specified together or not at all.
For example, let's define a Section class that defines text in a document that may have a header
and footer:
open class Section(
val text: String,
val header: String = "",
val footer: String = "",
)
I'd like to impose a restriction such that if we specify a header or footer, the other must also be specified.
We could add a check in the intializer...
open class Section(
val text: String,
val header: String = "",
val footer: String = "",
) {
init {
if (header.isEmpty() != footer.isEmpty()) {
throw IllegalArgumentException(
"Header and Footer must both be specified, or neither specified"
)
}
}
}
fun main() {
val section = Section("Some text", "Section 1")
}
If you run this, you'll get the IllegalArgumentException.
But... I really prefer to catch things at compile-time when possible. So I'd really like to have constructors that either require neither header nor footer, or both header and footer. So we add a secondary constructor and tweak the primary:
open class Section(
val text: String,
val header: String,
val footer: String,
) {
constructor(text: String): this(text, "", "")
}
fun main() {
val section1 = Section("Some text")
val section2 = Section("Some text", "Section 1", "End of Section 1")
val section3 = Section("Some text", "Section 1") // will not compile
}
Our primary constructor now requires both header and footer, and the secondary only requires the text, passing the default values to the primary. Perfect!
But what if we have parameters that create more complex groupings?
Let's create a very-poorly-designed class to represent a location (very-poorly-designed because we should use inheritance [and later, Kotlin's sealed classes!]). Start with
open class Location(
val lat: Double,
val lon: Double,
val street: String,
val city: String,
val state: String,
val zip: String,
)
Here we want to have either lat/lon, or street/city/state/zip. (Yes... this is crazy gross, but I'm tired and this demonstrates the concept and my brain won't think of another example right now so there).
So we try
open class Location(
val lat: Double,
val lon: Double,
val street: String,
val city: String,
val state: String,
val zip: String,
) {
constructor(
lat : Double,
lon : Double,
): this(lat, lon, "", "", "", "")
constructor(
street: String,
city: String,
state: String,
zip: String,
): this(0.0, 0.0, street, city, state, zip)
}
fun main() {
// good
val location1 = Location(39.149810, -76.911257)
// (sing it with me... "My Harris Teeter")
// good
val location2 = Location("123 Sesame St", "New York", "NY", "10001")
// uh oh...
val location3 = Location(39.149810, -76.911257, "123 Sesame St", "New York", "NY", "10001")
}
The primary constructor is public by default. In this case, we really don't want that. So let's
make it private.
open class Location private constructor(
val lat: Double,
val lon: Double,
val street: String,
val city: String,
val state: String,
val zip: String,
) {
constructor(
lat: Double,
lon: Double,
): this(lat, lon, "", "", "", "")
constructor(
street: String,
city: String,
state: String,
zip: String,
): this(0.0, 0.0, street, city, state, zip)
}
fun main() {
// good
val location1 = Location(39.149810, -76.911257)
// (sing it with me... "My Harris Teeter")
// good
val location2 = Location("123 Sesame St", "New York", "NY", "10001")
// uh oh...
val location3 = Location(39.149810, -76.911257, "123 Sesame St", "New York", "NY", "10001")
}
If you run this you'll get an error compiling location3, as the constructor that takes all of
those arguments is private.
A little ugly, but poof! Private primary constructor. I haven't had to do this too often, but I have had to do it...