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 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 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 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
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 checks whether two references point to the same object instance in memory. This type of Kotlin equality is verified using 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 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
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
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 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 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 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 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)
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
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.