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.

Try it — edit, then press Run 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.

Implement it, then press Run to 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.

Implement it, then press Run to check 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.

Try it — edit, then press Run 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.

Implement it, then press Run to check 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.

Implement it, then press Run to check 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   // 1

data 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.

Implement it, then press Run to check 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).

Try it — edit, then press Run 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.

Implement it, then press Run to check 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.

Implement it, then press Run to check 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.

Implement it, then press Run to check 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