Menu
Coddy logo textTech

equals, hashCode, toString

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 35 of 57.

Every class inherits toString(), equals() and hashCode() from Any. The inherited versions work on identity: toString() gives the class name and a hash code, and equals() is true only for the same object. Overriding toString() gives objects a readable text, used by println and string templates:

class Money(val cents: Int) {
    override fun toString() = "$%d.%02d".format(cents / 100, cents % 100)
}

Inside main:

val price = Money(1250)
println(price)
println("total: ${Money(99)}")

Output:

$12.50
total: $0.99

== calls equals(), while === checks whether two references point to the same object. To compare by value, override equals. Its parameter is Any?, so it checks the type first and then compares the relevant properties:

class Isbn(code: String) {
    val digits = code.filter { it.isDigit() }

    override fun equals(other: Any?) = other is Isbn && other.digits == digits
    override fun hashCode() = digits.hashCode()
}

Inside main:

val a = Isbn("978-0-441-17271-9")
val b = Isbn("9780441172719")
println(a == b)
println(a === b)

Output:

true
false

Objects that are equal must have equal hash codes. HashSet and HashMap find entries by hashCode() first and only then call equals(), so overriding equals without hashCode makes equal objects look different in them. Always override both, computed from the same properties:

class Isbn(code: String) {
    val digits = code.filter { it.isDigit() }

    override fun equals(other: Any?) = other is Isbn && other.digits == digits
    override fun hashCode() = digits.hashCode()
    override fun toString() = "ISBN $digits"
}

Inside main:

val shelf = hashSetOf(Isbn("978-0-441-17271-9"), Isbn("9780441172719"), Isbn("0-14-143951-3"))
println(shelf.size)
println(Isbn("9780441172719") in shelf)

Output:

2
true

A data class generates all three from its constructor properties. Write them yourself when equality should use fewer properties or a normalized form, such as an email address that ignores case:

class User(val email: String, val name: String) {
    private val key = email.trim().lowercase()

    override fun equals(other: Any?) = other is User && other.key == key
    override fun hashCode() = key.hashCode()
    override fun toString() = "$name <$key>"
}

Inside main:

val users = listOf(User("Ada@Example.com", "Ada"), User("ada@example.com ", "A. Lovelace"))
println(users.distinct())

Output:

[Ada <ada@example.com>]
challenge icon

Challenge

Easy

Two products are the same when their sku matches, whatever the other fields say. Override equals() and hashCode() in Product on sku only, and toString() so a product prints as A1 tea 450. Then complete mergeCatalog(products), which keeps the first occurrence of every product in the original order.

The supplied code reads products as sku,name,price lines and a last line with a sku to look up. It prints every kept product, removed: 2 and A1 in catalog: true, looking the sku up in a HashSet.

Your code goes in Product.kt and Functions.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    val products = input.dropLast(1).map { line ->
        val (sku, name, price) = line.split(",")
        Product(sku, name, price.toInt())
    }
    val kept = mergeCatalog(products)
    for (p in kept) println(p)
    println("removed: ${products.size - kept.size}")
    val probe = Product(input.last(), "?", 0)
    println("${input.last()} in catalog: ${probe in kept.toHashSet()}")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground