Equality Workbook
Ten short exercises on Kotlin equality — structural == versus referential ===, null-safe comparison, data-class equals and hashCode, and Comparable with the comparison operators.
Practice problems for In Kotlin, == Is the One You Actually Want. Each takes a minute or two. Most exercises auto-check: implement the function (or type) in the editor and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, all on JetBrains’ Kotlin server. The == versus === observation exercises stay attempt-then-reveal: press Run to try them, then click Show answer. Nothing here is a trick question, just direct practice of the syntax from the lesson.
== versus ===
1. Value or identity
Write code that builds two Strings with equal contents but as different objects, then both comparisons — annotate what each returns.
fun main() {
val a = "hello"
val b = StringBuilder("hel").append("lo").toString()
// TODO: print a == b (structural) and a === b (referential)
println(a)
println(b)
} Show answer Hide answer
val a = "hello"
val b = StringBuilder("hel").append("lo").toString()
a == b // true — equal contents (calls .equals())
a === b // false — different objects 2. Null-safe comparison
Implement bothEqual(a, b) — true when the two nullable strings hold equal text or are both null, with no explicit null check.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun nullSafe() {
Assert.assertTrue("equal text is equal", bothEqual("hi", "hi"))
Assert.assertTrue("both null counts as equal", bothEqual(null, null))
Assert.assertFalse("different text is not equal", bothEqual("hi", "bye"))
}
}
//sampleStart
fun bothEqual(a: String?, b: String?): Boolean =
false // TODO: true when a and b hold equal text OR are both null
//sampleEnd Show answer Hide answer
fun bothEqual(a: String?, b: String?) = a == b== calls .equals() and handles null on both sides, so null == null is true and neither side throws.
3. Null-safe by default
Implement sameText(a, b) — value equality for two nullable strings, where both-null counts as equal.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun valueEquality() {
Assert.assertTrue("equal text", sameText("hi", "hi"))
Assert.assertTrue("both null", sameText(null, null))
Assert.assertFalse("only one null", sameText("hi", null))
}
}
//sampleStart
fun sameText(a: String?, b: String?): Boolean =
false // TODO: compare a and b for value equality
//sampleEnd Show answer Hide answer
fun sameText(a: String?, b: String?) = a == b== handles null on both sides without throwing — null == null is true. No Objects.equals wrapper needed.
defining equality
4. Detect a fresh copy
Write code that confirms toList() produced a new object whose contents still equal the original.
fun main() {
val a = listOf(1, 2, 3)
val b = a.toList()
// TODO: print a == b (contents) and a === b (identity)
println(a)
println(b)
} Show answer Hide answer
val a = listOf(1, 2, 3)
val b = a.toList()
a == b // true — same contents
a === b // false — toList() allocated a new list 5. Free value equality
Make Point a type so that Point(1, 2) == Point(1, 2) is true without writing equals yourself.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun valueEquals() {
Assert.assertTrue("equal points are equal", Point(1, 2) == Point(1, 2))
Assert.assertFalse("different points are not", Point(1, 2) == Point(3, 4))
}
}
//sampleStart
// TODO: make Point a data class so equal points compare equal
class Point(val x: Int, val y: Int)
//sampleEnd Show answer Hide answer
data class Point(val x: Int, val y: Int) 6. Works correctly in a HashSet
Make Point a type whose equals and hashCode agree, so a HashSet deduplicates two equal points down to a size of one.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun dedupes() {
Assert.assertEquals("equal points dedupe to one",
1, hashSetOf(Point(1, 1), Point(1, 1)).size)
}
}
//sampleStart
// TODO: make Point a data class so the set dedupes equal points
class Point(val x: Int, val y: Int)
//sampleEnd Show answer Hide answer
data class Point(val x: Int, val y: Int)
hashSetOf(Point(1, 1), Point(1, 1)).size // 1data class generates equals and a matching hashCode — the contract hash-based collections rely on.
ordering
7. Make a type comparable
Make Version implement Comparable so < and > work, comparing major first then minor.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun ordering() {
Assert.assertTrue("higher minor is greater", Version(2, 1) > Version(2, 0))
Assert.assertTrue("higher major wins", Version(3, 0) > Version(2, 9))
Assert.assertFalse("equal versions are not greater", Version(2, 0) > Version(2, 0))
}
}
//sampleStart
class Version(val major: Int, val minor: Int) : Comparable<Version> {
// TODO: compare major first, then minor
override fun compareTo(other: Version): Int = 0
}
//sampleEnd Show answer Hide answer
class Version(val major: Int, val minor: Int) : Comparable<Version> {
override fun compareTo(other: Version) =
compareValuesBy(this, other, { it.major }, { it.minor })
} 8. Use the operator
Given the Version above, write the expression comparing whether Version(2, 1) is greater than Version(2, 0).
fun main() {
class Version(val major: Int, val minor: Int) : Comparable<Version> {
override fun compareTo(other: Version) =
compareValuesBy(this, other, { it.major }, { it.minor })
}
val greater = false // TODO: is Version(2, 1) greater than Version(2, 0)?
println(greater)
} Show answer Hide answer
Version(2, 1) > Version(2, 0) // true> delegates to compareTo.
9. Sort by a key
Implement youngestFirst(people) — the people sorted youngest first by age.
import org.junit.Test
import org.junit.Assert
data class Person(val age: Int)
class Test {
@Test fun byAge() {
Assert.assertEquals("sorted youngest first",
listOf(Person(20), Person(30), Person(40)),
youngestFirst(listOf(Person(30), Person(20), Person(40))))
}
}
//sampleStart
fun youngestFirst(people: List<Person>): List<Person> =
people // TODO: sort youngest first by age
//sampleEnd Show answer Hide answer
fun youngestFirst(people: List<Person>) = people.sortedBy { it.age } 10. Sort by two keys
Implement sortedPeople(people) — sorted by lastName, then firstName as a tiebreaker.
import org.junit.Test
import org.junit.Assert
data class Person(val firstName: String, val lastName: String)
class Test {
@Test fun byTwoKeys() {
val people = listOf(
Person("Alan", "Turing"),
Person("Ada", "Lovelace"),
Person("Grace", "Hopper"),
)
Assert.assertEquals("sorted by lastName, then firstName",
listOf(
Person("Grace", "Hopper"),
Person("Ada", "Lovelace"),
Person("Alan", "Turing"),
),
sortedPeople(people))
}
}
//sampleStart
fun sortedPeople(people: List<Person>): List<Person> =
people // TODO: sort by lastName, then firstName
//sampleEnd Show answer Hide answer
people.sortedWith(compareBy({ it.lastName }, { it.firstName })) Going deeper: ordering and the floating-point trap
12. Wire up the comparison operators
Make Money comparable so Money(500) > Money(250) works, ordered by cents.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun ordering() {
Assert.assertTrue(Money(500) > Money(250))
Assert.assertFalse(Money(100) > Money(100))
}
}
//sampleStart
data class Money(val cents: Int) {
// TODO: implement Comparable<Money> so > works, ordered by cents
}
//sampleEnd Show answer Hide answer
data class Money(val cents: Int) : Comparable<Money> {
override fun compareTo(other: Money) = cents.compareTo(other.cents)
}<, >, <=, >= all route through compareTo. data class separately gives you a value-based ==.
13. The NaN surprise
Double.NaN == Double.NaN is false. But wrap a NaN in a data class and compare two of them with == — is it true or false? Reveal to check.
Show answer Hide answer
true. The == operator on primitive Double follows IEEE 754 (NaN equals nothing), but a data class builds its equals from .equals(), which uses a total ordering where NaN.equals(NaN) is true. So Box(Double.NaN) == Box(Double.NaN) is true — the opposite of the bare-Double answer. (Likewise 0.0 == -0.0 is true but (0.0).equals(-0.0) is false.) Keep floating-point values out of data class identity fields, map keys, and set elements when NaN or signed zero can occur.
Back to the lesson, In Kotlin, == Is the One You Actually Want, or on to the next one: destructuring.
Comments