54

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.

13 Answers 13

30

I've used this approach.

data class Person(val id: String, val name: String) {
   override fun equals(other: Any?) = other is 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) 
}
Sign up to request clarification or add additional context in comments.

6 Comments

equals should override "Any?"
This looks kind of cumbersome. Is this still the best solution up to date?
Improvent for the equals fun : override fun equals(other: Any?):Boolean{ if(other != Person) return false return EssentialData(this) == EssentialData(other) }
a more idiomatic way to write @dstibbe's improvement is: override fun equals(other: Any?) = other is Person && EssentialData(this) == EssentialData(other)
Nice improvement @KristopherNoronha.
|
15

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>
)

3 Comments

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.
Yes, you are right. I just want to share my attempt.
Thank goodness Kotlin came along and saved us from the complexity of Java.
11

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

1 Comment

I believe this is the recommended approach these days. kotlinlang.org/docs/…
7

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?

1 Comment

I think my solution is also not perfect, but has some benefits. Take a look.
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])"
}

2 Comments

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.
I agree, my solution is not very elegant. I just hacked it together for the fun of it and decided to share.
3

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.

Comments

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.

Comments

3

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

Comments

0

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

Comments

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

Comments

0

My approaches, using Reflection and java.util.Objects.hash.

General advantages:

  • It does not rely on creating auxiliary objects or calc hash by hand.
  • No need to abandon val or remove props from the constructor.
  • You can extract this logic from the class declaration and place it in specific domains if you still need the standard equals to coexist.

Allowing specific fields and ignoring everything else

import java.util.Objects

// Added more props to exemplify better
data class Person(val id: String, val name: String, val age: Int, val email: String, val phone: String) {
    // The common `equals` implementation, nothing fancy being done except using the `visibleProps` list
    override fun equals(other: Any?) =
        this === other || (other is Person && visibleProps.all { it.get(this) == it.get(other) })

    override fun hashCode() = Objects.hash(*visibleProps.map { it.get(this) }.toTypedArray())

    companion object {
        private val visibleProps = setOf(Person::id, Person::email)
    }
}

fun main() {
    val p1 = Person("1", "Alice", 30, "[email protected]", "123456789")
    val p2 = Person("1", "Bob", 40, "[email protected]", "987654321")

    println(p1 == p2) // true, considering only id and email
    println(p1 === p2) // false
    println(p1.hashCode() == p2.hashCode()) // true
}
  • If you add new properties to the data class, they will always be ignored in equals and hashCode.
  • If you need to allow more properties, just add them to visibleProps set.

Ignoring specific fields and allowing everything else

import java.util.Objects
import kotlin.reflect.full.memberProperties

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

    override fun equals(other: Any?) =
        this === other || (other is Person && visibleProps.all { it.get(this) == it.get(other) })

    override fun hashCode() = Objects.hash(*visibleProps.map { it.get(this) }.toTypedArray())

    companion object {
        private val ignoredProps = setOf(Person::id, Person::email)
        private val visibleProps = Person::class.memberProperties - ignoredProps
    }
}


// Examples

fun main() {
    val p1 = Person("1", "Alice", 30, "[email protected]", "1234567890")
    val p2 = Person("42", "Alice", 30, "[email protected]", "1234567890")

    println(p1 == p2) // true, ignoring id and email
    println(p1 === p2) // false
    println(p1.hashCode() == p2.hashCode()) // true
}

  • If you add new properties to the data class, they will be considered in equals and hashCode automatically.
  • If you need to ignore more properties, just add them to ignoredProps set.

Comments

0

summarized in three ways: first:

@JvmInline
value class HashCode(val value: Int = 0) {
    inline infix fun hash(hash: Int): HashCode =
        HashCode(31 * value + hash)

    inline infix fun hash(obj: Any?): HashCode =
        hash(obj.hashCode())
}

object ObjectHasher {
    inline fun hash(vararg objects: Any?): Int =
        objects.fold(HashCode()) { hash, it -> hash hash it }.value

    inline fun hash(vararg hashes: Int): Int =
        hashes.fold(HashCode()) { hash, it -> hash hash it }.value
}

class Equals {
    var result = Result()

    inline val value: Boolean
        get() = result.value

    inline fun <reified T : Any> equal(lhs: T?, rhs: T?): Equals {
        result = result.equal(lhs, rhs)
        return this
    }

    @JvmInline
    value class Result(val value: Boolean = true) {
        inline fun equal(next: () -> Boolean): Result =
            if (value) Result(next()) else this

        inline fun <reified T : Any> equal(lhs: T?, rhs: T?): Result =
            equal { lhs == rhs }
    }

    companion object {
        @JvmStatic
        inline fun <reified T : Any> T.equal(rhs: Any?, builder: Equals.(T) -> Unit): Boolean {
            if (rhs == null) return false
            if (this === rhs) return true
            if (this::class != rhs::class) return false

            return Equals().apply {
                builder(rhs as T)
            }.value
        }
    }
}

Example:

data class Person(val id: String, val name: String, val age: Int, val email: String) {
    override fun equals(other: Any?): Boolean =
        this.equal(other) { other ->
            equal(id, other.id)
            equal(name, other.name)
            equal(age, other.age)
        }

    override fun hashCode(): Int =
        ObjectHasher.hash(id, name, age)
}

second:

abstract class DataObj(vararg fields: Any?) {
    private val fields: DataFields by lazy { DataFields(fields) }

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

    override fun hashCode() =
        fields.hashCode()

    override fun toString() =
        fields.toString()

    private data class DataFields(val args: Array<out Any?>) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as DataFields

            return args.contentEquals(other.args)
        }

        override fun hashCode(): Int =
            args.contentHashCode()
    }
}

Example:

data class Person(val id: String, val name: String, val age: Int, val email: String) : DataObj(id, name, age)</code></pre>

third:

data class Valueless<T>(private val value: T) : ReadOnlyProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T =
        value

    override fun equals(other: Any?): Boolean =
        true

    override fun hashCode(): Int =
        0

    companion object {
        @JvmStatic
        inline val <reified T> T.valueless: Valueless<T>
            get() = Valueless(this)
    }
}

Example:

data class Person(val id: String, val name: String, val age: Int, val email: Valueless)

Comments

0

How about this solution?

Usage example one:

class ProductOne(
    val id: Int,
    val name: String,
    val manufacturer: String
) {
    override fun equals(other: Any?): Boolean = equalsHelper(other, ProductOne::name, ProductOne::manufacturer)
    override fun hashCode(): Int = hashCodeHelper(ProductOne::name, ProductOne::manufacturer)
    override fun toString(): String = toStringHelper(ProductOne::name, ProductOne::manufacturer)
}

Usage example two:

class ProductTwo(
    val id: Int,
    val name: String,
    val manufacturer: String
) {
    companion object {
        private val PROPS = arrayOf(ProductTwo::name, ProductTwo::manufacturer)
    }

    override fun equals(other: Any?): Boolean = equalsHelper(other, *PROPS)
    override fun hashCode(): Int = hashCodeHelper(*PROPS)
    override fun toString(): String = toStringHelper(*PROPS)
}

Usage example three:

class ProductThree(
    val id: Int,
    val name: String,
    val manufacturer: String
) {
    companion object {
        private val EXCLUDED_PROPS = setOf(ProductThree::id)
        private val PROPS = (ProductThree::class.declaredMemberProperties - EXCLUDED_PROPS).toTypedArray()
    }

    override fun equals(other: Any?): Boolean = equalsHelper(other, *PROPS)
    override fun hashCode(): Int = hashCodeHelper(*PROPS)
    override fun toString(): String = toStringHelper(*PROPS)
}

Helpers:

fun <T : Any> T.equalsHelper(other: Any?, vararg props: KProperty1<T, *>): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false

    for (prop in props) {
        @Suppress("UNCHECKED_CAST")
        if (prop.get(this) != prop.get(other as T)) return false
    }

    return true
}

fun <T : Any> T.hashCodeHelper(vararg props: KProperty1<T, *>): Int {
    return props.fold(31) { acc, prop -> acc * prop.get(this).hashCode() }
}

fun <T : Any> T.toStringHelper(vararg props: KProperty1<T, *>): String {
    return buildString {
        append(this::class.simpleName)
        append("(")
        props.joinToString() { prop ->
            "${prop.name} = ${prop.get(this@toStringHelper).toString()}"
        }
        append(")")
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.