47

Suppose I only want one or two fields to be included in the generated equals and hashCode implementations (or perhaps exclude one or more fields). For a simple class, e.g.:

data class Person(val id: String, val name: String)

Groovy has this:

@EqualsAndHashCode(includes = 'id')

Lombok has this:

@EqualsAndHashCode(of = "id")

What is the idiomatic way of doing this in Kotlin?

My approach so far

data class Person(val id: String) {
   // at least we can guarantee it is present at access time
   var name: String by Delegates.notNull()

   constructor(id: String, name: String): this(id) {
      this.name = name
   }
}

Just feels wrong though... I don't really want name to be mutable, and the extra constructor definition is ugly.

Braian Coronel
  • 22,105
  • 4
  • 57
  • 62
Jonathan Schneider
  • 26,852
  • 13
  • 75
  • 99

10 Answers10

24

I've used this approach.

data class Person(val id: String, val name: String) {
   override fun equals(other: Person) = EssentialData(this) == EssentialData(other)
   override fun hashCode() = EssentialData(this).hashCode()
   override fun toString() = EssentialData(this).toString().replaceFirst("EssentialData", "Person")
}

private data class EssentialData(val id: String) {
   constructor(person: Person) : this(id = person.id) 
}
Duncan McGregor
  • 17,665
  • 12
  • 64
  • 118
  • equals should override "Any?" – Dmytro Karataiev Sep 24 '18 at 18:01
  • Do you mean other should be Any? ? If so, maybe, but this way the compiler should use the Any.equals(any) when it knows that the rhs is not a Person. I suppose that there is an edge case where you have lost the type of the rhs... – Duncan McGregor Sep 26 '18 at 06:30
  • 2
    This looks kind of cumbersome. Is this still the best solution up to date? – Jay N Aug 08 '19 at 21:41
  • 3
    Improvent for the ```equals``` fun : ```override fun equals(other: Any?):Boolean{ if(other != Person) return false return EssentialData(this) == EssentialData(other) } ``` – dstibbe Nov 13 '19 at 22:22
  • 1
    a more idiomatic way to write @dstibbe's improvement is: `override fun equals(other: Any?) = other is Person && EssentialData(this) == EssentialData(other)` – Kristopher Noronha Mar 22 '20 at 14:59
  • 1
    Nice improvement @KristopherNoronha. – dstibbe Mar 23 '20 at 19:48
11

This approach may be suitable for property exclusion:

class SkipProperty<T>(val property: T) {
  override fun equals(other: Any?) = true
  override fun hashCode() = 0
}

SkipProperty.equals simply returns true, which causes the embeded property to be skipped in equals of parent object.

data class Person(
    val id: String, 
    val name: SkipProperty<String>
)
Fartab
  • 4,725
  • 2
  • 26
  • 39
  • 1
    It's creative, so +1 for that, but this is not a solution I would employ. You have the extra `.property` on any access to a field whose lack of participation in the equals/hashCode of its containing class is frankly solely an implementation detail of that class. You could of course override `get()/set()` on that property to do this automatically, but ooof. Heavy for such a requirement. – Jonathan Schneider Feb 24 '20 at 14:06
  • 1
    Yes, you are right. I just want to share my attempt. – Fartab Feb 25 '20 at 17:57
8

This builds on @bashor's approach and uses a private primary and a public secondary constructor. Sadly the property to be ignored for equals cannot be a val, but one can hide the setter, so the result is equivalent from an external perspective.

data class ExampleDataClass private constructor(val important: String) {
  var notSoImportant: String = ""
    private set

  constructor(important: String, notSoImportant: String) : this(important) {
    this.notSoImportant = notSoImportant
  }
}
Nils-o-mat
  • 1,132
  • 17
  • 31
  • 1
    I believe this is the recommended approach these days. https://kotlinlang.org/docs/data-classes.html#properties-declared-in-the-class-body – Eagle Dec 07 '22 at 07:19
6

I also don't know "the idomatic way" in Kotlin (1.1) to do this...

I ended up overriding equals and hashCode:

data class Person(val id: String,
                  val name: String) {

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

        other as Person

        if (id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }
}

Isn't there a "better" way?

Peti
  • 1,670
  • 1
  • 20
  • 25
3

Here's a somewhat creative approach:

data class IncludedArgs(val args: Array<out Any>)

fun includedArgs(vararg args: Any) = IncludedArgs(args)


abstract class Base {
    abstract val included : IncludedArgs

    override fun equals(other: Any?) = when {
        this identityEquals other -> true
        other is Base -> included == other.included
        else -> false
    }

    override fun hashCode() = included.hashCode()

    override fun toString() = included.toString()
}

class Foo(val a: String, val b : String) : Base() {
    override val included = includedArgs(a)
}

fun main(args : Array<String>) {
    val foo1 = Foo("a", "b")
    val foo2 = Foo("a", "B")

    println(foo1 == foo2) //prints "true"
    println(foo1)         //prints "IncludedArgs(args=[a])"
}
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
  • Interesting solution! I personally wouldn't trade a few lines of boilerplate assignment, e.g. `val name: String = name` in @bashor's example for inheritance from a `Base` class that serves to make up for a missing language feature. – Jonathan Schneider Apr 12 '15 at 23:30
  • 1
    I agree, my solution is not very elegant. I just hacked it together for the fun of it and decided to share. – Kirill Rakhman Apr 12 '15 at 23:32
3

Simpler, faster, look at there, or into the Kotlin documentation. https://discuss.kotlinlang.org/t/ignoring-certain-properties-when-generating-equals-hashcode-etc/2715/2 Only fields inside the primary constructor are taken into account to build automatic access methods like equals and so on. Do keep the meaningless ones outside.

2

Reusable solution: to have an easy way to select which fields to include in equals() and hashCode(), I wrote a little helper called "stem" (essential core data, relevant for equality).

Usage is straightforward, and the resulting code very small:

class Person(val id: String, val name: String) {
    private val stem = Stem(this, { id })

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

It's possible to trade off the backing field stored in the class with extra computation on-the-fly:

    private val stem get() = Stem(this, { id })

Since Stem takes any function, you are free to specify how the equality is computed. For more than one field to consider, just add one lambda expression per field (varargs):

    private val stem = Stem(this, { id }, { name })

Implementation:

class Stem<T : Any>(
        private val thisObj: T,
        private vararg val properties: T.() -> Any?
) {     
    fun eq(other: Any?): Boolean {
        if (thisObj === other)
            return true

        if (thisObj.javaClass != other?.javaClass)
            return false

        // cast is safe, because this is T and other's class was checked for equality with T
        @Suppress("UNCHECKED_CAST") 
        other as T

        return properties.all { thisObj.it() == other.it() }
    }

    fun hc(): Int {
        // Fast implementation without collection copies, based on java.util.Arrays.hashCode()
        var result = 1

        for (element in properties) {
            val value = thisObj.element()
            result = 31 * result + (value?.hashCode() ?: 0)
        }

        return result
    }

    @Deprecated("Not accessible; use eq()", ReplaceWith("this.eq(other)"), DeprecationLevel.ERROR)
    override fun equals(other: Any?): Boolean = 
        throw UnsupportedOperationException("Stem.equals() not supported; call eq() instead")

    @Deprecated("Not accessible; use hc()", ReplaceWith("this.hc(other)"), DeprecationLevel.ERROR)
    override fun hashCode(): Int = 
        throw UnsupportedOperationException("Stem.hashCode() not supported; call hc() instead")
}

In case you're wondering about the last two methods, their presence makes the following erroneous code fail at compile time:

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

The exception is merely a fallback if those methods are invoked implicitly or through reflection; can be argued if it's necessary.

Of course, the Stem class could be further extended to include automatic generation of toString() etc.

TheOperator
  • 5,936
  • 29
  • 42
2

Here is another hacky approach if you don't want to touch the data class.
You can reuse the entire equals() from data classes while excluding some fields.
Just copy() the classes with fixed values for excluded fields:

data class Person(val id: String,
                  val name: String)
fun main() {
    val person1 = Person("1", "John")
    val person2 = Person("2", "John")
    println("Full equals: ${person1 == person2}")
    println("equals without id: ${person1.copy(id = "") == person2.copy(id = "")}")
   
}

Output:

Full equals: false
equals without id: true
Nico
  • 2,570
  • 1
  • 9
  • 17
0

Consider the following generic approach for the implementation of equals/hashcode. The code below should have no performance impact because of the use of inlining and kotlin value classes:

@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
    }
}

With this in place, you can easily implement/override any equals/hashcode implementation in a uniform way:

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 ->
            this.combine(lhs.title, rhs.title)
                .combineEquals { lhs.bytes contentEquals rhs.bytes }
            // ignore the third field for equals
        }
    }

    override fun hashCode(): Int {
        return Objects.hashCode(title, bytes) // ignore the third field for hashcode
    } 
}
Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60
-1

You can create an annotation that represents the exclusion of the property as @ExcludeToString or with @ToString(Type.EXCLUDE) parameters by defining enum.

And then using reflection format the value of the getToString().

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExcludeToString

data class Test(
        var a: String = "Test A",
        @ExcludeToString var b: String = "Test B"
) {
    override fun toString(): String {
        return ExcludeToStringUtils.getToString(this)
    }
}

object ExcludeToStringUtils {

    fun getToString(obj: Any): String {
        val toString = LinkedList<String>()
        getFieldsNotExludeToString(obj).forEach { prop ->
            prop.isAccessible = true
            toString += "${prop.name}=" + prop.get(obj)?.toString()?.trim()
        }
        return "${obj.javaClass.simpleName}=[${toString.joinToString(", ")}]"
    }

    private fun getFieldsNotExludeToString(obj: Any): List<Field> {
        val declaredFields = obj::class.java.declaredFields
        return declaredFields.filterNot { field ->
            isFieldWithExludeToString(field)
        }
    }

    private fun isFieldWithExludeToString(field: Field): Boolean {
        field.annotations.forEach {
            if (it.annotationClass == ExcludeToString::class) {
                return true
            }
        }
        return false
    }

}

GL

Gist

Braian Coronel
  • 22,105
  • 4
  • 57
  • 62