You already know that objects are really complex structures and variables only point to objects. This time, you will learn about equality and how to understand that variables point to the same object. In addition, you will finally fully understand the meaning of the val keyword and avoid one of the most common beginner mistakes: assuming that the val keyword guarantees immutability.

Comparison

Imagine a situation: you receive two identical messages from your friend. The messages are “Hi” and “Hi”. You see them and understand: the messages are the same. If you want to compare these messages in Kotlin, you can store them as string values:

val msg1 = "Hi"
val msg2 = "Hi"

Then you can use the comparison operator ==. For example, print(msg1 == msg2) gives true, and print(msg1 == "Hello") gives false. Variables msg1 and msg2 have the same state, which is called structural equality. Also, you may check for inequality using the operator != . For example, print(msg1 != "Hello") gives true.

Note that some complex data types may not have the operator==. We will discuss this later. Box in the following examples has this operation

Let’s look at an example of copying a mutable object. Suppose you have a box that stores balls, and you can add one ball to it. Try to copy this box and change the original:

val blueBox = Box(3)          // box with 3 balls
val azureBox = blueBox 
println(blueBox == azureBox ) // true, it's a copy
blueBox.addBall()             // add a ball to the first box
println(blueBox == azureBox ) // true, the second box also contains 4 balls

When you change the first box, its copy changes, too. This is because blueBoxand azureBoxpoint to the same object. How do you check this? Let’s see how to check the referential equality.

Referential equality

You know that variables can have the same state and can be the same (point to the same object). In both cases, == returns true. However, Kotlin provides a special operator === to check if the variables point to the same object. For example:

val blueBox = Box(3)
val azureBox = blueBox 
val cyanBox = Box(3)
println(blueBox == azureBox)  // true
println(blueBox === azureBox) // true, azureBox points to the same object
println(blueBox == cyanBox)   // true
println(blueBox === cyanBox)  // false, cyanBox points to another object

So, blueBox and cyanBox have the same state, but they point to different objects. In this case, if you change the state of blueBoxcyanBox remains the same:

blueBox.addBall()
println(blueBox == cyanBox) // false

Also, you may check for referential inequality with the operator !== . For example, print(blueBox !== cyanBox) gives true.

Another interesting thing about the === operator is the equality of immutable objects. Let’s look at the following example:

var two = 2
var anotherTwo = 2
println(two === anotherTwo) // true

These variables point to the same object! Don’t worry about this: as you remember, you cannot change an immutable object, so if you try to do something with the variable, it will point to a new object and other variables will still point to the old object. Try to change two:

two++
println(two)        // 3
println(anotherTwo) // 2

So, immutable objects are really useful and help you avoid a lot of possible problems with copying.

Base types and equality

You are already quite familiar with objects in Kotlin: you have worked with text and number data a lot. In many programming languages, primitive data types – or primitives – store the most often used simple data types. Their internal structure is organized in its own way. This is not the case in Kotlin. As you might have guessed, the familiar IntStringFloat, and Double in Kotlin are also objects! But there is a nuance. For example, the Int or String variables behave just like the primitive types of data in other programming languages, but at the same time, they are objects – in other words, immutable objects. Let’s look how it works:

var a: Int = 100
val anotherA: Int = a
println(a == anotherA)  // true
println(a === anotherA) // true
a = 200
println(a == anotherA)  // false
println(a === anotherA) // false

As you can see, when we change the value of the variable a = 200, we do not change its object – the variable a is assigned a new reference to the object with the value 200.

Let’s look at one example, but now with the Double type of data:

var d1: Double = 1.5
val d2 = d1
println(d1 === d2) // true
d1 += 1            // d1 is 2.5 now
println(d1 === d2) // false

As you can see, the result is the same. Now, let’s look at an example with modifiable objects:

val list1: MutableList<Int> = mutableListOf()
val list2: MutableList<Int> = list1
list1.add(1)
println("list1 $list1") // list1 [1]
println("list2 $list2") // list2 [1]
list2.add(5)
println("list1 $list1") // list1 [1, 5]
println("list2 $list2") // list2 [1, 5]

As you can see in this example, the variables list1 and list2 refer to the same object. When you change the object through the first variable, we see the updated data through the second variable.

Conclusion

Let’s go over the main points of the topic once again:

  • Structural equality of variables implies equality of inner states.
  • You can use the operators == and != to check structural equality.
  • Referential equality of variables means that these variables point to the same object.
  • You can use the operators === and !== to check referential equality.
  • The val keyword means that you cannot reassign the variable, not immutability.

Leave a Reply