Instruction
Classes & Instances
Classes are named types. They’re one of the pillars of object-oriented programming, a style of programming where the types have both data and behaviors. In classes, data takes the form of properties and behavior is implemented using functions called methods.
Class instances are useful for storing data in your program, and these classes let you declare a set of characteristics for an instance. Creating objects from a class saves time and effort because you don’t have to declare these characteristics every time. So this is a fundamental lesson worth your time, which you now gain instead of wasting it defining characteristics again.
Note: For all the coding examples, you’ll use Kotlin Playground as linked below: Kotlin Playground
Classes are simple to create. Try the following class definition in Kotlin Playground and add it to your Kotlin file outside the main()
function:
class Food(val name: String, val price: String) {
val item1
get() = "$name $price"
}
Not that bad, right? In this example, you have an item of food in the list and need to add a class to identify it for future reference. The mandatory keyword class
is followed by the name of the class, Food
.
The primary constructor for the class is inside the parentheses after the class name. For Food
, you indicate two mutable String
properties, name
and price
. Everything in the curly brackets is a member of the class. Phew, this is making me hungry!
You create an instance of a class by using the class name and adding arguments to the constructor.
Note: A constructor is a special member function that’s called upon when an instance of the class is created to set variables or properties. You’ll learn how to create your own in later lessons. For now, use the one provided.
Add this code inside the brackets of your main()
function:
val fruit = Food(name = "Tomato", price = "0.20")
You added code that states that Food
has another property named item1
with a custom getter that uses the other properties in its definition. It uses the name
and price
properties to compute what item1
is: “Tomato, 0.20”. Now, paste the final line underneath the one you just added:
println(fruit.item1) // > Tomato 0.20
Try running it yourself. As you can see, creating classes is relatively simple. If you’d like more practice, copy your class format and create a few classes.
References & Memory
Classes fall under the category of reference types, which is distinct from them being identified as named types. This classification of classes as reference types is a separate aspect of how classes can be defined and utilized. Reference types mean a variable of a class type that doesn’t store an actual instance but is a reference to a location in the memory that stores said instance.
Time for an example! Delete your previous code and type the following code where you put the previous class code. Remember, classes always go before the main
function. You’re continuing with the same theme:
class Food(val name: String)
var var1 = Food(name = "Tomato")
It would look something like this in memory:
See how the var1
links itself to the food name Tomato
using a reference? That’s basically what’s happening inside the memory.
Now, create a completely new variable, var2
, and assign the value of var1
to it. This code goes into the main
function:
var var2 = var1
Note: If you run it, nothing will appear!
Don’t worry; it’s supposed to be like that because you’re saving these variables to the unseen memory of the program itself. Now, you just made two references inside both var1
and var2
, so they reference the same place in memory, visually presented like this:
In Kotlin, objects built from reference types like classes aren’t directly housed in your everyday memory space. Instead, they reside in a special zone called the heap. Think of it as the object’s cozy home. But how do you locate these objects when you need them? That’s where references come in. They act as house addresses, pointing us to the exact location of objects within the heap.
Here’s the twist: references themselves usually reside in a different memory area known as the stack. But if a reference happens to be part of an object’s blueprint, it moves right in with the object on the heap. Imagine a house address being engraved on the house itself!
Note: Both the heap and the stack have essential roles in the execution of any program:
The system uses a stack that acts like a notepad, temporarily holding information relevant to the current function’s work. The CPU meticulously keeps track of the stack, ensuring efficient use of this memory space.
Whenever a function creates a variable, the variable is placed onto this stack, like adding a note to the notepad. Once the function finishes, the stack cleans up by discarding these temporary variables, like throwing away used notes.
The heap serves as a vast memory reservoir in Kotlin. Unlike the stack, it’s not limited to the current function’s needs. The system can freely request and grant chunks of memory from this pool to store objects created from reference types like classes. This memory allocation is dynamic, meaning the size and lifetime of objects on the heap can vary.
One key difference between the heap and the stack is memory management. The stack automatically discards data when a function is done. The heap, however, doesn’t have this automatic cleanup. To free up space on the heap, you need to explicitly tell the system to reclaim unused objects.
Working With References
Since a class is a reference type, when you assign to a variable of a class type, the system doesn’t copy the instance. Only a reference is copied because the class is a reference type.
Now, the tomato reigns supreme over your fridge, and you must log it into your database! Try this in a blank kotlin playground space and add the following class, which should look familiar, above your main()
function per usual:
class Food(val name: String) {
val title
get() = "$name"
}
Then add this code inside your main()
function:
val tomato = Food(name = "Supreme Tomato")
var fridgeLeader = tomato
println(tomato.title) // Supreme Tomato
println(fridgeLeader.title) // Supreme Tomato
Run the code to see the results. You’ll see the printed results match the ones in the comments. As you can see, you assign a new variable, fridgeLeader
, to the tomato
class instance.
Even though you only stated the name of the tomato, the data was also stated for fridgeLeader
since they both reference the same class instance. This proves that Supreme Tomato is the fridgeLeader
and a tomato
!
What you just did is an example of implied sharing among class instances. It presents a new way of thinking when passing data around. For instance (haha, sorry), if you change the tomato
class instance, anything holding a reference to tomato
, in this case, the fridgeLeader
variable, would automatically see it and update. Get it?
What did the tomato say to his friends when he was leaving?
I’ll ketch-up with you later!
Class Instance Identity
In the code you just completed, it’s easy to see that tomato
and fridgeLeader
point to the same class instance. The code is short, and both references are named variables.
But what if you want to see if the value behind a variable is the correct Supreme Tomato? You might think about checking the value of title
, but how would you know it’s the Supreme Tomato you’re looking for and not some imposter or doppelgänger? Or worse, what if Supreme Tomato changed its title again?!
Worry not, because you have drumroll Kotlin! And in Kotlin, the ===
operator lets you check if the identity of one class instance is equal to the identity of another.
Add this line of code at the end of the main
function to the one you just did above:
println(fridgeLeader === tomato) // true
Run and observe the results. Similar to how the ==
operator checks if two values are equal, the ===
identity operator compares the memory address of two references. It tells you whether the values of the references are the same; that is, they point to the same block of data on the heap.
Add the next bit of code. You can add print statements if you want to see the results for yourself, but the // shows what they should be:
val imposterTomato = Food(name = "Evil Tomato")
println(tomato === fridgeLeader) // true
println(tomato === imposterTomato) // false
println(imposterTomato === fridgeLeader) // false
Assigning existing variables changes the instances that the variables reference. So, as you see below, if you re-assign the value of fridgeLeader
to imposterTomato
, then print the statement and run it, the identity changes.
If you want to try this, copy this into your code within the main function:
fridgeLeader = imposterTomato
println(tomato === fridgeLeader) // false
fridgeLeader = tomato
println(tomato === fridgeLeader) // true
This code shows how the ===
operator can tell the difference between the Supreme Tomato you’re looking for and an imposter-Tomato. This can be particularly useful when you can’t rely on the regular equality, ==
, to compare and identify class instances you need to remain identical.
Type the following code and add it to the one you just did:
// 1
var imposters = (0..100).map {
Food(name = "Evil Tomato")
}
//2
imposters.map {
it.name == "Supreme Tomato"
}.contains(true) // true
In the code above, the equality operator isn’t enough to identify the original Supreme Tomato.
-
//1
creates the fake, imposter Tomatoes by introducing a new variable. -
//2
shows that equality (==
) is ineffective when Supreme Tomato can’t be identified by his name alone.
However, if you try the code below, you can verify that the references themselves are equal and separate the real Supreme Tomato from the crowd of evil ones. Add this code and witness the results:
//1
println(imposters.contains(tomato)) // false
//2
val mutableImposters = mutableListOf<Food>()
mutableImposters.addAll(imposters)
mutableImposters.contains(tomato) // false
mutableImposters.add(Random().nextInt(5), tomato)
//3
println(mutableImposters.contains(tomato)) // true
The above code helps you spot your Supreme Tomato amongst the imposters quickly and accurately, with less liability for error.
-
//1
checks to ensure the real Supreme Tomato is not found among the imposters. -
//2
hides the “real” Tomato among the imposters. -
//3
shows you can now find your Supreme Tomato among the imposters.
Note: You must import the
java.util.*
package to work with theRandom()
class, so your code might not fully run if you’re using Kotlin Playground and not your own Kotlin file.
You may find that you won’t use the identity operator ===
very much in your day-to-day Kotlin. But it’s important to understand what it does and what it demonstrates about the properties of reference types. And congratulations! Now you can identify imposters, and your Supreme Tomato will guard your fridge safely!
What did the programmer say to his code? I’m proud of you from my head to-ma-toes!