14

I am aware that in Kotlin classes will have an equals and hashcode created automatically as follows:

data class CSVColumn(private val index: Int, val value: String) {
}

My question is, is there a way to have the implementation just use one of these properties (such as index) without writing the code yourself. What was otherwise a very succinct class now looks like this:

data class CSVColumn(private val index: Int, val value: String) {

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (javaClass != other?.javaClass) {
            return false
        }
        other as CSVColumn
        if (index != other.index) {
            return false
        }
        return true
    }

    override fun hashCode(): Int {
        return index
    }

}

In Java with Lombok, I can do something like:

@Value
@EqualsAndHasCode(of="index")
public class CsvColumn {
    private final int index;
    private final String value;
}

Would be cool if there were a way to tell Kotlin something similar.

David
  • 7,652
  • 21
  • 60
  • 98
  • @Enzokie good point, but does adding that change the question at all? – David May 21 '18 at 13:09
  • It will just make first statement accurate ;) – Enzokie May 21 '18 at 13:10
  • Thanks for pointing this out. I corrected the example code. – David May 21 '18 at 13:40
  • You can write that 'equals' function much more succinctly if you just treat it as a boolean expression instead of trying to bail out early: override fun equals(other: Any?) = this === other || (other is CsvColumn && index==other.index) – Some Guy Jan 24 '19 at 18:51
  • You can shorthand `override fun hashCode() = listOf(each, specific, property).hashCode() * 31` and then `override fun equals(other: Any?) = if (other is Type) hashCode() == other.hashCode() else false`. It's not generating, but only a couple of lines to write then. – GroovyCarrot Jun 12 '19 at 11:01

6 Answers6

11

From the Data Classes documentation you get:

Note that the compiler only uses the properties defined inside the primary constructor for the automatically generated functions. To exclude a property from the generated implementations, declare it inside the class body

So you have to implement equals() and hashCode() manually or with the help of a Kotlin Compiler Plugin.

tynn
  • 38,113
  • 8
  • 108
  • 143
3

You can't do something like this for data classes, they always generate equals and hashCode the same way, there's no way to provide them such hints or options.

However, they only include properties that are in the primary constructor, so you could do this for them to only include index:

data class CSVColumn(private val index: Int, value: String) {
    val value: String = value
}

... except you can't have parameters in the primary constructor that aren't properties when you're using data classes.

So you'd have to somehow introduce a secondary constructor that takes two parameters, like this:

class CSVColumn private constructor(private val index: Int) {

    var value: String = ""

    constructor(index: Int, value: String) : this(index) {
        this.value = value
    }

}

... but now your value property has to be a var for the secondary constructor to be able to set its value.

All this to say that it's probably not worth trying to work around it. If you need an non-default implementation for equals and hashCode, data classes can't help you, and you'll need to implement and maintain them manually.


Edit: as @tynn pointed out, a private setter could be a solution so that your value isn't mutable from outside the class:

class CSVColumn private constructor(private val index: Int) {

    var value: String = ""
        private set

    constructor(index: Int, value: String) : this(index) {
        this.value = value
    }

}
zsmb13
  • 85,752
  • 11
  • 221
  • 226
1

I wrote a little utility called "stem", which allows you to select which properties to consider for equality and hashing. The resulting code is as small as it can get with manual equals()/hashCode() implementation:

class CSVColumn(private val index: Int, val value: String)  {
    private val stem = Stem(this, { index })

    override fun equals(other: Any?) = stem.eq(other)
    override fun hashCode() = stem.hc()
}

You can see its implementation here.

TheOperator
  • 5,936
  • 29
  • 42
0

I guess that we have to write equals()/hashCode() manually for now. https://discuss.kotlinlang.org/t/automatically-generate-equals-hashcode-methods/3779

It is not supported and is planning to be, IMHO.

Francis Bacon
  • 4,080
  • 1
  • 37
  • 48
0

See the following performance optimized way (with the use of value classes and inlining) of implementing a generic equals/hashcode for any Kotlin class:

@file:Suppress("EXPERIMENTAL_FEATURE_WARNING")

package org.beatkit.common

import kotlin.jvm.JvmInline

@Suppress("NOTHING_TO_INLINE")
@JvmInline
value class HashCode(val value: Int = 0) {
    inline fun combineHash(hash: Int): HashCode = HashCode(31 * value + hash)
    inline fun combine(obj: Any?): HashCode = combineHash(obj.hashCode())
}

@Suppress("NOTHING_TO_INLINE")
@JvmInline
value class Equals(val value: Boolean = true) {
    inline fun combineEquals(equalsImpl: () -> Boolean): Equals = if (!value) this else Equals(equalsImpl())
    inline fun <A : Any> combine(lhs: A?, rhs: A?): Equals = combineEquals { lhs == rhs }
}

@Suppress("NOTHING_TO_INLINE")
object Objects {
    inline fun hashCode(builder: HashCode.() -> HashCode): Int = builder(HashCode()).value

    inline fun hashCode(vararg objects: Any?): Int = hashCode {
        var hash = this
        objects.forEach {
            hash = hash.combine(it)
        }
        hash
    }

    inline fun hashCode(vararg hashes: Int): Int = hashCode {
        var hash = this
        hashes.forEach {
            hash = hash.combineHash(it)
        }
        hash
    }

    inline fun <T : Any> equals(
        lhs: T,
        rhs: Any?,
        allowSubclasses: Boolean = false,
        builder: Equals.(T, T) -> Equals
    ): Boolean {
        if (rhs == null) return false
        if (lhs === rhs) return true
        if (allowSubclasses) {
            if (!lhs::class.isInstance(rhs)) return false
        } else {
            if (lhs::class != rhs::class) return false
        }
        @Suppress("unchecked_cast")
        return builder(Equals(), lhs, rhs as T).value
    }
}

This allows you to write a equals/hashcode implementation as follows:

data class Foo(val title: String, val bytes: ByteArray, val ignore: Long) {
    override fun equals(other: Any?): Boolean {
        return Objects.equals(this, other) { lhs, rhs ->
            combine(lhs.title, rhs.title)
            .combineEquals { lhs.bytes contentEquals rhs.bytes }
        }
    }

    override fun hashCode(): Int {
        return Objects.hashCode(title, bytes.contentHashCode())
    }

}
Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60