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:

<reference> var1 “Tomato”

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:

<reference> var1 <reference> var2 “Tomato”

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 the Random() 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!

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo