Kotlin Equality

Kotlin equality refers to the mechanism by which the Kotlin compiler determines whether two objects are considered equal. Unlike some programming languages that have only one type of equality, Kotlin provides two distinct types of equality comparisons: structural equality and referential equality.

Kotlin equality is implemented through specific operators and functions that allow developers to compare objects in different ways depending on their needs. The Kotlin equality system is designed to be intuitive while providing powerful flexibility for complex object comparisons.

Structural Equality in Kotlin

Structural equality in Kotlin determines whether two objects have the same content or value. This type of Kotlin equality is checked using the == operator, which internally calls the equals() function.

The == Operator

The == operator is the primary tool for checking structural equality in Kotlin. When you use == to compare two objects, Kotlin automatically handles null safety and calls the appropriate equals() method.

val name1 = "John"
val name2 = "John"
val name3 = "Jane"

println(name1 == name2) // true
println(name1 == name3) // false

The != Operator

The != operator is the negation of structural equality in Kotlin. It returns true when objects are not structurally equal.

val age1 = 25
val age2 = 30

println(age1 != age2) // true
println(age1 != 25)   // false

Custom equals() Implementation

For custom classes, you can override the equals() method to define your own structural equality logic. This is crucial for proper Kotlin equality behavior in your custom objects.

data class Person(val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Person) return false
        return name == other.name && age == other.age
    }
}

val person1 = Person("Alice", 28)
val person2 = Person("Alice", 28)
println(person1 == person2) // true

Referential Equality in Kotlin

Referential equality in Kotlin checks whether two references point to the same object instance in memory. This type of Kotlin equality is verified using the === operator.

The === Operator

The === operator compares object references rather than their content. This Kotlin equality check is useful when you need to verify that two variables reference the exact same object instance.

val list1 = mutableListOf(1, 2, 3)
val list2 = mutableListOf(1, 2, 3)
val list3 = list1

println(list1 === list2) // false (different objects)
println(list1 === list3) // true (same object reference)
println(list1 == list2)  // true (same content)

The !== Operator

The !== operator is the negation of referential equality in Kotlin. It returns true when two references point to different object instances.

val obj1 = Any()
val obj2 = Any()
val obj3 = obj1

println(obj1 !== obj2) // true
println(obj1 !== obj3) // false

Kotlin Equality with Data Classes

Data classes in Kotlin automatically generate equals() and hashCode() methods, making structural equality comparisons straightforward. This automatic generation ensures consistent Kotlin equality behavior across your data objects.

data class Student(val id: Int, val name: String, val grade: Double)

val student1 = Student(1, "Bob", 85.5)
val student2 = Student(1, "Bob", 85.5)
val student3 = Student(2, "Carol", 90.0)

println(student1 == student2) // true (structural equality)
println(student1 === student2) // false (referential equality)
println(student1 == student3) // false

Null Safety in Kotlin Equality

Kotlin equality operations are null-safe by design. The == operator can safely compare null values without throwing exceptions, making Kotlin equality more robust than many other languages.

val nullValue1: String? = null
val nullValue2: String? = null
val nonNullValue = "Hello"

println(nullValue1 == nullValue2) // true
println(nullValue1 == nonNullValue) // false
println(nullValue1 === nullValue2) // true

Arrays and Kotlin Equality

Arrays in Kotlin have special equality behavior. Structural equality for arrays requires using the contentEquals() method, while referential equality works as expected with ===.

val array1 = arrayOf(1, 2, 3)
val array2 = arrayOf(1, 2, 3)

println(array1 == array2) // false (arrays don't override equals)
println(array1.contentEquals(array2)) // true (content comparison)
println(array1 === array2) // false (different references)

Collections and Kotlin Equality

Collections in Kotlin implement proper structural equality through their equals() methods. This makes Kotlin equality comparisons with collections intuitive and reliable.

val list1 = listOf(1, 2, 3)
val list2 = listOf(1, 2, 3)
val set1 = setOf(1, 2, 3)
val set2 = setOf(3, 2, 1) // Different order

println(list1 == list2) // true
println(set1 == set2) // true (sets ignore order)

Primitive Types and Kotlin Equality

Primitive types in Kotlin follow predictable equality rules. Numbers, booleans, and characters compare by value for both structural and referential equality due to Kotlin’s optimization.

val int1 = 42
val int2 = 42
val bool1 = true
val bool2 = true

println(int1 == int2) // true
println(int1 === int2) // true (cached integers)
println(bool1 === bool2) // true

String Equality in Kotlin

String equality in Kotlin demonstrates both types of equality clearly. String literals with the same content may share references due to string interning, but this shouldn’t be relied upon.

val str1 = "Hello"
val str2 = "Hello"
val str3 = String("Hello".toCharArray())

println(str1 == str2) // true (structural equality)
println(str1 == str3) // true (structural equality)
println(str1 === str2) // true (string interning)
println(str1 === str3) // false (different objects)

Enum Equality in Kotlin

Enums in Kotlin have well-defined equality behavior. Enum constants with the same name are both structurally and referentially equal, making Kotlin equality with enums straightforward.

enum class Color { RED, GREEN, BLUE }

val color1 = Color.RED
val color2 = Color.RED
val color3 = Color.GREEN

println(color1 == color2) // true
println(color1 === color2) // true (same enum constant)
println(color1 == color3) // false

Complete Example: Kotlin Equality in Action

Here’s a comprehensive example demonstrating all aspects of Kotlin equality in a practical scenario:

fun main() {
    // Data class with automatic equals implementation
    data class Book(val title: String, val author: String, val pages: Int)
    
    // Custom class with manual equals implementation
    class Magazine(val title: String, val issue: Int) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is Magazine) return false
            return title == other.title && issue == other.issue
        }
        
        override fun hashCode(): Int {
            return title.hashCode() * 31 + issue
        }
    }
    
    // Testing structural equality with data classes
    val book1 = Book("1984", "George Orwell", 328)
    val book2 = Book("1984", "George Orwell", 328)
    val book3 = book1
    
    println("=== Data Class Equality ===")
    println("book1 == book2: ${book1 == book2}") // true
    println("book1 === book2: ${book1 === book2}") // false
    println("book1 === book3: ${book1 === book3}") // true
    
    // Testing custom equals implementation
    val magazine1 = Magazine("Tech Weekly", 42)
    val magazine2 = Magazine("Tech Weekly", 42)
    val magazine3 = Magazine("Tech Weekly", 43)
    
    println("\n=== Custom Class Equality ===")
    println("magazine1 == magazine2: ${magazine1 == magazine2}") // true
    println("magazine1 === magazine2: ${magazine1 === magazine2}") // false
    println("magazine1 == magazine3: ${magazine1 == magazine3}") // false
    
    // Testing null safety
    val nullBook: Book? = null
    val anotherNullBook: Book? = null
    
    println("\n=== Null Safety ===")
    println("nullBook == anotherNullBook: ${nullBook == anotherNullBook}") // true
    println("nullBook === anotherNullBook: ${nullBook === anotherNullBook}") // true
    println("nullBook == book1: ${nullBook == book1}") // false
    
    // Testing collections
    val bookList1 = listOf(book1, book2)
    val bookList2 = listOf(Book("1984", "George Orwell", 328), Book("1984", "George Orwell", 328))
    
    println("\n=== Collection Equality ===")
    println("bookList1 == bookList2: ${bookList1 == bookList2}") // true
    println("bookList1 === bookList2: ${bookList1 === bookList2}") // false
    
    // Testing arrays
    val intArray1 = arrayOf(1, 2, 3)
    val intArray2 = arrayOf(1, 2, 3)
    
    println("\n=== Array Equality ===")
    println("intArray1 == intArray2: ${intArray1 == intArray2}") // false
    println("intArray1.contentEquals(intArray2): ${intArray1.contentEquals(intArray2)}") // true
    println("intArray1 === intArray2: ${intArray1 === intArray2}") // false
    
    // Testing primitive types
    val num1 = 100
    val num2 = 100
    val largeNum1 = 1000
    val largeNum2 = 1000
    
    println("\n=== Primitive Type Equality ===")
    println("num1 == num2: ${num1 == num2}") // true
    println("num1 === num2: ${num1 === num2}") // true (cached)
    println("largeNum1 === largeNum2: ${largeNum1 === largeNum2}") // true
    
    // Testing string equality
    val str1 = "Kotlin"
    val str2 = "Kotlin"
    val str3 = "Kot" + "lin"
    
    println("\n=== String Equality ===")
    println("str1 == str2: ${str1 == str2}") // true
    println("str1 === str2: ${str1 === str2}") // true (interned)
    println("str1 == str3: ${str1 == str3}") // true
    println("str1 === str3: ${str1 === str3}") // true (compile-time optimization)
}

Output:

=== Data Class Equality ===
book1 == book2: true
book1 === book2: false
book1 === book3: true

=== Custom Class Equality ===
magazine1 == magazine2: true
magazine1 === magazine2: false
magazine1 == magazine3: false

=== Null Safety ===
nullBook == anotherNullBook: true
nullBook === anotherNullBook: true
nullBook == book1: false

=== Collection Equality ===
bookList1 == bookList2: true
bookList1 === bookList2: false

=== Array Equality ===
intArray1 == intArray2: false
intArray1.contentEquals(intArray2): true
intArray1 === intArray2: false

=== Primitive Type Equality ===
num1 == num2: true
num1 === num2: true
largeNum1 === largeNum2: true

=== String Equality ===
str1 == str2: true
str1 === str2: true
str1 == str3: true
str1 === str3: true

This comprehensive example demonstrates how Kotlin equality works across different data types and scenarios. Understanding these equality concepts is essential for writing robust Kotlin applications, whether you’re developing mobile apps, web services, or any other type of software using the Kotlin programming language.