Lesson 4 - Reference data types in Kotlin

Kotlin OOP Reference data types in Kotlin

In the previous lesson, RollingDie in Kotlin - Constructors and random numbers, we created our first regular object in Kotlin, a rolling die. All data types in Kotlin are reference types. We didn't care much because we only worked with numbers or texts so far, which were objects as well, however, they always carried only a single value. What has changed now is that using just one variable we can access multiple properties or even access the same object through different variables. It's important to know what exactly is going on inside the program, otherwise, we'd end up with undesired results.

An application, more so, its thread, allocates memory from the operating system in the form of a stack. It accesses this memory at very high speeds, but the application can't control its size and the resources are assigned by the operating system. Kotlin stores only references to our objects into this memory. The second type of memory is called heap, that's where these objects are actually stored. We'll try to work with references today.

Let's create a new console application and add a simple class that will represent a "user". For clarity, we'll omit comments and won't bother with access modifiers:

class User(var name: String, var age: Int) {
    override fun toString(): String {
        return name
    }
}

The class has two simple public properties, a constructor, and an overriden toString(), so users can be printed simply. Let's create an instance of this class in our main() function in the main file:

val u: User? = User("James Brown", 28)

We declared the u variable as nullable so we can try to empty the reference as well. Let's see how this situation looks like in memory:

Stack and heap in computer memory

Kotlin stores all variables in memory twice - in the stack and then in the heap. In the stack, only the reference is stored, i.e. a link to the heap, where the real object is stored.

Both stack and heap are located in the RAM memory. The difference is in the access and in the size. The heap is almost unlimited memory, which is, however, complicated to access so it ends up being slower. On the other hand, stack memory is fast, but limited in size.

There are several reasons why things are done this way:

  1. The stack size is limited.
  2. When we want to use the same object multiple times, e.g. to pass it as a parameter into several methods, we don't need to copy it. We only have to pass a small type containing the reference to the object instead of copying a whole heavy-weight object.
  3. Thanks to references we are able to create structures with dynamic size easily, for example array-like structures in which we can add new elements at run-time. These elements reference each other, like a string of objects.

In some languages, e.g. in C++, there's a big difference between the terms pointer and reference. Fortunately, Kotlin doesn't have pointers and uses the concept of references, which is ironically more similar to pointers in C++. The terms pointer and reference mentioned here all mean references in Kotlin and C++ terms don't have anything to do with it.

Now let's declare two variables of the User type:

var u: User? = User("James Brown", 28)
var v: User? = User("Jack White", 32)

Here's what this would look like in memory:

Reference values in computer memory in Kotlin

Now let's assign the v variable to the u variable. The link will be copied but we will still have only one object. Now, our code should look something like this:

var u = User("James Brown", 28)
var u = User("Jack White", 32)
u = v

Memory-wise, it would look like so:

Reference values in computer memory in Kotlin

Now, let's verify the reference mechanism, so we can confirm that it truly works this way :) First, we'll print both variables before and after re-assigment. Additionally, we'll also print the hash code using the hashCode() method to see that the objects are different. Since, we'll be printing several times, I will make the snippet short. Let's edit the code as follows:

// variable declaration
var u = User("James Brown", 28)
var v = User("Jack White", 32)
println("u: $u ${u.hashCode()} ")
println("v: $v ${v.hashCode()}\n")

// assignment
u = v
println("u: $u ${u.hashCode()} ")
println("v: $v ${v.hashCode()}\n")

The output:

u: James Brown 666988784
v: Jack White 1414644648

u: Jack White 1414644648
v: Jack White 1414644648

Let's change the name of the user v and based off what we know, the change should be reflected in the variable u. We'll add the following to our code:

v.name = "John Doe"
println("u: $u ${u.hashCode()} ")
println("v: $v ${v.hashCode()}\n")

We've changed the object in the variable v. Now let's print v and u once more:

u: James Brown 666988784
v: Jack White 1414644648

u: Jack White 1414644648
v: Jack White 1414644648

u: John Doe 1414644648
v: John Doe 1414644648

The user u changes along with v because both variables point to the same object. If you're asking how to create a true copy of an object, the easiest way is to re-create the object by using the constructor and initializing the new object with the same data. We can also clone an object, but we'll go over that some other time. Let's get back to James Brown:

Reference types in computer memory in Kotlin

Now what will happen to him, you ask? He'll be "eaten" by what we call the Garbage collector.

Garbage collector

Garbage collector and dynamic memory management

We can allocate memory statically in our programs, meaning that we declare how much memory we'll need in the source code. However, we also don't need to specify it. In that case, we're dealing with dynamic memory management.

In the past, particularly in the era of the languages C, Pascal, and C++, direct memory pointers were used for what we call references in Kotlin. Altogether, it worked like this: we'd ask the operating system for a piece of memory of certain size. Then, it would reserve it for us and give us its address. We'd then create a pointer to this place, through which we worked with the memory. The problem was that no one was looking after what we put into this memory, the pointer just pointed to the beginning of the reserved memory. When we put something larger there, it would be simply stored anyway and overwrite the data beyond our memory's limits, which belonged to some another program or even to the operating system (in this case, OS would probably kill or stop our application). We would often overwrite our program's data in memory and the program would start to behave chaotically. Imagine that you add a user to an array and it ends up changing the user's environment color which is something that has nothing to do with it. You would spend hours checking the code for mistakes, and you would end up finding out that there's a memory leak in the user's creation that overflew into the color values in memory.

The other problem was when we stopped using an object, we had to free its memory manually, and if we didn't, the memory would remain occupied. If we did this in a method and forgot to free the memory, our application would start to freeze. Eventually, it would crash the entire operating system. An error like this is very hard to pin-point. Why does the program stop working after a few hours? Where in thousands of lines of code should we look for the mistake? We have no clue. We can't follow anything, so we'd end up having to look through the entire program line by line or examining the computer memory which is in binary. cringes. A similar problem occurs when we free memory somewhere and then use the same pointer again, forgetting it has been already freed, it would point to a place where something new might be already stored, and we would corrupt this data. It would lead to uncontrollable behavior in our application and it could even lead to this:

Blue Screen Of Death – BSOD in Windows

A colleague of mine once said: "The human brain can't even deal with its own memory, so how could we rely on it for program memory management?" Of course, he was right, except for a small group of geniuses, people became tired of solving permanent and unreasonable errors. For the price of a slight performance decrease, managed languages were developed with what we call a Garbage collector, these include Kotlin and Swift. C++ is still used of course, but only for specific programs, e.g. for operating system parts or commercial 3D game engines where you need to maximize the system's performance. Kotlin is suitable for 99% of all other applications, mainly due to its automatic memory management.

Garbage collector

Garbage collector is a program that runs in parallel with our applications, in a separate thread. It weaks up time after time and looks in memory for objects to which there is no longer a reference. It removes them and frees the memory. The performance loss is minimal and it'll significantly reduce the suicide rate of programmers who're trying to debug broken pointers in the evenings. We can even affect how GC runs in the code, although it's not needed in 99% of cases. Because the language is managed and doesn't work with direct pointers, it isn't possible to disrupt the memory anyhow, letting it overflow etc., the interpreter will take care of the memory automatically.

The null value

You should at least passively know the null keyword from the Type system: Null safety in Kotlin lesson. In Kotlin, nullable variables can carry the special value null. null indicates that the reference doesn't point to any data. When we set a variable v to null, we only delete this one reference. If there are still any references to our object, it will still exist. If not, GC will remove the object. Let's change the last lines of our program:

v = null
println("u: $u ${u?.hashCode()} ")
println("v: $v ${v?.hashCode()} ")

The output:

u: James Brown 666988784
v: Jack White 1414644648

u: Jack White 1414644648
v: Jack White 1414644648

u: John Doe 1414644648
v: John Doe 1414644648

u: John Doe 1414644648
v: null null

We can see that the object still exists and the variable u points to it; however, there is no reference in the variable v anymore. null is sometimes used both in Kotlin (it may cause errors though) and in databases. We'll get back to reference types in future. In the next lesson, Warrior for the arena in Kotlin, we'll program something practical again to gain experience. Spoiler: we're making a warrior object for the arena :)


 

 

Article has been written for you by Samuel Kodytek
Avatar
Do you like this article?
No one has rated this quite yet, be the first one!
Activities (4)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!