Chapter 4. classes and objects: A Bit of Class
It’s time we looked beyond Kotlin’s basic types.
Sooner or later, you’re going to want to use something more than Kotlin’s basic types. And that’s where classes come in. Classes are templates that allow you to create your own types of objects, and define their properties and functions. Here, you’ll learn how to design and define classes, and how to use them to create new types of objects. You’ll meet constructors, initializer blocks, getters and setters, and you’ll discover how they can be used to protect your properties. Finally, you’ll learn how data hiding is built into all Kotlin code, saving you time, effort and a multitude of keystrokes.
Object types are defined using classes
So far, you’ve learned how to create and use variables from Kotlin’s basic types, such as numbers, String
s and arrays. You know, for example, that when you write the code:
var x = 6
this creates an Int
object with a value of 6, and a reference to the object is assigned to a new variable named x
:
Behind the scenes, these types are defined using classes. A class is a template that defines what properties and functions are associated with objects of that type. When you create an Int
object, for example, the compiler checks the Int
class and sees that it requires an integer value, and has functions such as toLong
and toString
.
You can define your own classes
If you want your application to deal with types of objects that Kotlin doesn’t have, you can define your own types by writing new classes. If you’re building an application that records information about dogs, for example, you might want to define a Dog
class so that you can create your own Dog
objects, and record the name, weight and breed of each dog:
So how do you go about defining a class?
How to design your own classes
When you want to define your own class, you need to think about the objects that will be created from that class. You need to consider:
* The things each object knows about itself.
* The things each object can do.
The things an object knows about itself are its properties. They represent an object’s state (the data), and each object of that type can have unique values. A Dog
class, for example, might have name
, weight
and breed
properties. A Song
class might have title
and artist
properties.
The things an object knows about itself are its properties.
The things an object can do are its functions. They determine an object’s behavior, and may use the object’s properties. The Dog
class, for example, might have a bark
function, and the Song
class might have a play
function.
The things an object can do are its functions.
Here are some examples of classes with their properties and functions:
When you know what properties and functions your class should have, you’re ready to write the code to create it. We’ll look at this next.
Let’s define a Dog class
We’re going to create a Dog
class that we can use to create Dog
objects. Each Dog
will have a name, weight and breed, so we’ll use these for the class properties. We’ll also define a bark
function so that the size of the Dog
’s bark depends on its weight.
Here’s what our Dog
class code looks like:
defines the name of the class (Dog
), and the properties that the Dog
class has. We’ll take a closer look at what’s going on behind the scenes a few pages ahead, but for now, all you need to know is that the above code defines the name
, weight
and breed
properties—and when the Dog
object is created, values are assigned to these properties.
A function that’s defined inside a class is called a member function. It’s sometimes called a method.
You define any class functions in the class body (inside the curly braces {}
). We’re defining a bark
function, so the code looks like this:
Now that you’ve seen the code for the Dog
class, let’s look at how you use it to create a Dog
object.
How to create a Dog object
You can think of a class as a template for an object, as it tells the compiler how to make objects of that particular type. It tells the compiler what properties each object should have, and each object made from that class can have its own values. Each Dog
object, for example, would have name
, weight
and breed
properties, with each Dog
having its own values.
We’re going to use the Dog
class to create a Dog
object, and assign it to a new variable named myDog
. Here’s the code:
When the code runs, it creates a new Dog
object, and the arguments are used to assign values to the Dog
’s properties. In our case, for example, we’re creating a new Dog
object where the name
property is “Fido”, the weight
property is 70 pounds, and the breed
property is “Mixed”:
Now that you’ve seen how to create a new Dog
object, let’s look at how you access its properties and functions.
How to access properties and functions
Once you’ve created an object, you can access its properties using the dot operator (.
). If you wanted to print a Dog
’s name
, for example, you would use code like this:
You can also update any properties that you have defined using the var
keyword. As an example, here’s how you would update the Dog
’s weight
property to 75 pounds:
Note that the compiler won’t let you update any properties that you’ve defined using the val
keyword. If you try to do so, you’ll get a compiler error.
You can also use the dot operator to call an object’s functions. If you wanted to call the Dog
’s bark
function, for example, you would use the following code:
What if the Dog is in a Dog array?
You can also add any objects you create to an array. If you wanted to create an array of Dog
s, for example, you would use code like this:
This defines a variable named dogs
, and as it’s an array that you’re populating with Dog
objects, the compiler makes its type array<Dog>
. Two Dog
objects are then added to the array.
You can still access the properties and functions of each Dog
object in the array. As an example, suppose you wanted to update the second Dog
’s weight
and make it bark
. To do this, you would get a reference to the second item in the dogs
array using dogs[1]
, and then use the dot operator to access the Dog
’s weight
property and bark
function:
This is like saying “get the second object from the dogs array, change its weight to 15 pounds, and make it bark.”
Create a Songs application
Before we go any further into how classes work, we’re going to give you some more class practice by creating a new Songs project. We’ll add a Song
class to the project, and create and use some Song
objects.
Create a new Kotlin project that targets the JVM, and name the project “Songs”. Then create a new Kotlin file named Songs.kt by highlighting the src folder, clicking on the File menu and choosing New → Kotlin File/Class. When prompted, name the file “Songs”, and choose File from the Kind option.
Next, add the following code to Songs.kt:
Test drive
When we run the code, the following text gets printed in the IDE’s output window:
Playing the song Going Underground by The Jam
Stopped playing Going Underground
Playing the song Make Me Smile by Steve Harley
Now that you’ve seen how to define a class and use it to create objects, let’s dive into the mysterious world of object creation.
The miracle of object creation
When you declare and assign an object, there are three main steps:
Declare a variable.
var myDog = Dog("Fido", 70, "Mixed")
Create an object.
var myDog = Dog("Fido", 70, "Mixed")
Link the object to the variable by assigning a reference.
var myDog = Dog("Fido", 70, "Mixed")
The big miracle happens at step two—when the object is created. There’s a lot going on behind the scenes, so let’s take a closer look.
How objects are created
When we define an object using code like:
it looks like we’re calling a function named Dog
. But even though it looks and feels a lot like a function, it’s not. Instead, we’re calling the Dog
constructor.
A constructor contains the code that’s needed to initialize an object. It runs before the object can be assigned to a reference, which means that you get a chance to step in, and do things to make the object ready for use. Most people use constructors to define an object’s properties and assign values to them.
Each time you create a new object, the constructor for that object’s class is invoked. So when you run the code:
var myDog = Dog("Fido", 70, "Mixed")
the Dog
class constructor gets called.
A constructor runs when you instantiate an object. It’s used to define properties and initialize them.
What the Dog constructor looks like
When we created our Dog
class, we included a constructor; it’s the parentheses and the code in between in the class header:
The Dog
constructor defines three properties—name
, weight
and breed
. Each Dog
has these properties, and when the Dog
gets created, the constructor assigns a value to each property. This initializes the state of each Dog
, and ensures that it’s set up correctly.
Let’s take a look at what happens behind the scenes when the Dog
constructor gets called.
Behind the scenes: calling the Dog constructor
Let’s go through what happens when we run the code:
var myDog = Dog("Fido", 70, "Mixed")
The system creates an object for each argument that’s passed to the Dog constructor.
It creates a
String
with a value of “Fido”, anInt
with a value of 70, and aString
with a value of “Mixed”.The system allocates the space for a new Dog object, and the Dog constructor gets called.
The Dog constructor defines three properties: name, weight and breed.
Behine the scenes, each property is a variable. A variable of the appropriate type is created for each property, as defined in the constructor.
class Dog(val name: String,
var weight: Int,
val breed: String) {
}Each of the Dog’s property variables is assigned a reference to the appropriate value object.
The
name
property, for example, is assigned a reference to the “Fido”String
object, and so on.Finally, a reference to the Dog object is assigned to a new Dog variable named myDog.
That’s right—a property is a variable that’s local to the object.
This means that everything you’ve already learned about variables applies to properties. If you define a property using the val
keyword, for example, this means that you can’t assign a new value to it. You can, however, update any properties that have been defined using var
.
In our example, we’re using val
to define the name
and breed
properties, and var
to define the weight
:
class Dog(val name: String, var weight: Int, val breed: String) {
...
}
This means that we can only update the Dog
’s weight
property, and not the Dog
’s name
or breed
.
An object is sometimes known as an instance of a particular class, so its properties are sometimes called instance variables.
Code Magnets
Somebody used fridge magnets to write a noisy new DrumKit
class, and a main
function that prints the following output:
ding ding ba-da-bing!
bang bang bang!
ding ding ba-da-bing!
Unfortunately, the magnets have got scrambled. Can you piece the code back together again?
Code Magnets Solution
Somebody used fridge magnets to write a noisy new DrumKit
class, and a main
function that prints the following output:
ding ding ba-da-bing!
bang bang bang!
ding ding ba-da-bing!
Unfortunately, the magnets have got scrambled. Can you piece the code back together again?
Going deeper into properties
So far you’ve seen how to define a property by including it in the class constructor, and how doing so assigns a value to that property when the constructor is called. But what if you need to do something a little different? What if you want to validate a value before assigning it to a property? Or what if you want to initialize a property with a generic default value so that you don’t need to add it to the class constructor?
To find out how you can do this kind of thing, we need to take a closer look at constructor code.
Behind the scenes of the Dog constructor
As you already know, our current Dog
constructor code defines three properties for the name
, weight
and breed
of each Dog
object, and assigns a value to each one when the Dog
constructor is called:
class Dog(val name: String, var weight: Int, val breed: String) {
...
}
You can do this so concisely because the constructor code uses a shortcut for performing this kind of task. When the Kotlin language was developed, the brains behind it felt that defining and initializing properties was such a common action that it was worth making the syntax to do it very concise and simple.
If you were to perform the same action without using the shortcut, here’s what the code would look like:
Here, the three constructor parameters—name_param
, weight_param
and breed_param
—have no val
and var
prefixes, which means that they no longer define properties. They are plain old parameters, just like the ones you see in function definitions. The name
, weight
and breed
properties are instead defined in the main body of the class. Each one is assigned the value of the associated constructor parameter.
So how does this allow us to do more with our properties?
Flexible property initialization
Defining properties in the main body of the class gives you a lot more flexibility than adding them to the constructor, as it means that you no longer have to initialize each one with a parameter value.
Suppose that you wanted to assign a default value to a property without including it in the constructor. You might, for example, want to add an activities
property to the Dog
class, and initialize it with a default array containing a value of “Walks”. Here’s the code to do this:
Alternatively, you might want to tweak the value of a constructor parameter before assigning it to a property. You might, for example, want to record an uppercase String
for the breed
property instead of the value that’s passed to the constructor. To do this, you would use the toUpperCase
function to create an uppercase version of the String
, which you would then assign to the breed
property like this:
Initializing a property in this way works well if you want to assign a simple value or expression to it. But what if you need to do something more complex?
How to use initializer blocks
If you need to initialize a property to something more complex than a simple expression, or if there’s extra code you want to run when each object is created, you can use one or more initializer blocks. Initializer blocks are executed when the object is initialized, immediately after the constructor is called, and they’re prefixed with the init
keyword. Here’s an example of an initializer block that prints a message whenever a Dog
object is initialized:
Your class can have multiple initializer blocks. Each one runs in the order in which it appears in the class body, interleaved with any property initializers. Here’s an example of some code with multiple initializer blocks:
As you’ve seen, there are various ways in which you can initialize your variables. But is it necessary?
You MUST initialize your properties
Back in Chapter 2, you learned that every variable you declare in a function must be initialized before it can be used. This also applies to any properties you define in a class: you must initialize properties before you try to use them. This is so important that if you declare a property without initializing it in either the property declaration or the initializer block, the compiler will get very upset and refuse to compile your code. The following code, for example, won’t compile because we’ve added a new property named temperament
which hasn’t been initialized:
Nearly all of the time, you’ll be able to assign default values to your properties. In the above example, for instance, your code will compile if you initialize the temperament
property to ""
:
How do you validate property values?
Earlier in the chapter, you learned how to directly get or set a property’s value using the dot operator. You already know, for example, that you can print the Dog
’s name
using:
println(myDog.name)
and that you can set its weight
to 75 pounds using:
myDog.weight = 75
But in the hands of the wrong person, allowing direct access to all our properties in this way can be quite a dangerous weapon. Because what’s to prevent someone writing the following code:
A Dog
with negative weight would be a Bad Thing.
To stop this kind of thing from happening, we need some way of validating a value before it’s assigned to a property.
The solution: custom getters and setters
If you want to tweak a property’s return value, or validate a value before it gets assigned to a property, you can write your own getters and setters.
Getters and setters let you, well, get and set property values. A getter’s sole purpose in life is to send back a return value, the value of whatever it is that particular getter is supposed to be getting. And a setter lives and breathes for the chance to take an argument value, and use it to set the value of a property.
Note
If you’re into being all formal about it, you might prefer to call them accessors and mutators instead.
Writing custom getters and setters lets you protect your property values, and they give you more control over what values are returned or assigned. We’ll show you how they work by adding two new things to our Dog
class:
* A custom getter to return the Dog’s weight in kilograms.
* A custom setter to validate a proposed value for the Dog’s weight before we assign it.
Let’s start by creating a custom getter to return the Dog
’s weight in kilograms.
How to write a custom getter
In order to add a custom getter that will allow us to return the Dog
’s weight in kilograms, we’re going to do two things: add a new property to the Dog
class named weightInKgs
, and write a custom getter for it which will return the appropriate value. Here’s the code to do both these things:
The line:
get() = weight / 2.2
defines the getter. It’s a no parameter function named get
that you add to the property. You add it to the property by writing it immediately below the property declaration. Its return type must match that of the property whose value you want to return or the code won’t compile. In the above example, the weightInKgs
property is a Double
, so the property’s getter must also return a Double
.
Note
Technically, getters and setters are optional parts of the property declaration.
Each time you ask for the value of a property using code like:
myDog.weightInKgs
the property’s getter gets called. The above code, for example, calls the getter for the weightInKgs
property. The getter uses the Dog
’s weight
property to calculate the Dog
’s weight
in kilograms, and returns the result.
Note that in this example, we didn’t need to initialize the weightInKgs
property because its value is derived in the getter. Each time the property’s value is required, the getter is called, which figures out the value that should be returned.
Now that you know how to add a custom getter, let’s look at how you add a custom setter by adding one to the weight
property.
How to write a custom setter
We’re going to add a custom setter to the weight
property so that the weight can only be updated to a value greater than 0. To do this, we need to move the weight
property definition from the constructor to the class body, and then add the setter to the property. Here’s the code to do that:
The following code defines the setter:
set(value) {
if (value > 0) field = value
}
A setter is a function named set
that’s added to the property by writing it beneath the property declaration. A setter has one parameter—usually named value
—which is the proposed new value of the property.
In the above example, the value of the weight
property is only updated if the value
parameter is greater than 0. If you try and update the weight
property to a value that’s less than or equal to 0, the setter stops the property from being updated.
A property’s setter runs each time you try to set a property’s value. The following code, for example, calls the weight property’s setter, passing it a value of 75:
myDog.weight = 75
The setter updates the value of the weight
property by means of the field
identifier. field
refers to the property’s backing field, which you can think of as being a reference to the underlying value of the property. Using field
in your getters and setters in place of the property name is important, as it stops you getting stuck in an endless loop. When the following setter code runs, for example, the system tries to update the weight
property, which results in the setter being called again... and again... and again:
The full code for the Dogs project
We’re nearly at the end of the chapter, but before we go, we thought we’d show you the entire code for the Dogs project.
Create a new Kotlin project that targets the JVM, and name the project “Dogs”. Then create a new Kotlin file named Dogs.kt by highlighting the src folder, clicking on the File menu and choosing New → Kotlin File/Class. When prompted, name the file “Dogs”, and choose File from the Kind option.
Next, add the following code to Dogs.kt:
class Dog(val name: String,
weight_param: Int,
breed_param: String) {
init {
print("Dog $name has been created. ")
}
var activities = arrayOf("Walks")
val breed = breed_param.toUpperCase()
init {
println("The breed is $breed.")
}
var weight = weight_param
set(value) {
if (value > 0) field = value
}
val weightInKgs: Double
get() = weight / 2.2
fun bark() {
println(if (weight < 20) "Yip!" else "Woof!")
}
}
fun main(args: Array<String>) {
val myDog = Dog("Fido", 70, "Mixed")
myDog.bark()
myDog.weight = 75
println("Weight in Kgs is ${myDog.weightInKgs}")
myDog.weight = -2
println("Weight is ${myDog.weight}")
myDog.activities = arrayOf("Walks", "Fetching balls", "Frisbee")
for (item in myDog.activities) {
println("My dog enjoys $item")
}
val dogs = arrayOf(Dog("Kelpie", 20, "Westie"), Dog("Ripper", 10, "Poodle"))
dogs[1].bark()
dogs[1].weight = 15
println("Weight for ${dogs[1].name} is ${dogs[1].weight}")
}
Test drive
When we run the code, the following text gets printed in the IDE’s output window:
Dog Fido has been created. The breed is MIXED.
Woof!
Weight in Kgs is 34.090909090909086
Weight is 75
My dog enjoys Walks
My dog enjoys Fetching balls
My dog enjoys Frisbee
Dog Kelpie has been created. The breed is WESTIE.
Dog Ripper has been created. The breed is POODLE.
Yip!
Weight for Ripper is 15
Your Kotlin Toolbox
You’ve got Chapter 4 under your belt and now you’ve added classes and objects to your toolbox.
Note
You can download the full code for the chapter from https://tinyurl.com/HFKotlin.
Get Head First Kotlin 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.