Non Data Class == Member Comparison

What exactly does Kotlin do when using the == operator when comparing two classes? Does it do memberwise comparison (as I hope it does)?

I know a data class it will generate an equals operator, but what if it is a regular class? Can I count on memberwise comparison or do I need to write the code to do this? I tried overloading equals, but I get complaints about hashCode even though I’m not putting the objects in a collection.

When I look under the hood at the Java code for a data class version, Kotlin is generating code I don’t use. Also, the class members can vary and the state changes have complex behavior differences, another reason using a data class seems inappropriate, but maybe I’m wrong here?

If I can’t count on a memberwise comparison for operator == of non-data classes, what is the recommended best practice?

Thanks.

The equals operator == in Kotlin is just an alias for the equals() method so no, it doesn’t do memberwise comparisons, as neither does Java.

To provide a custom equals check implementation, override the equals(other: Any?): Boolean function.

See: https://kotlinlang.org/docs/reference/equality.html


Also, the reason why you are getting complainants about the hashCode is probably due to a "violation of the general contract of hashCode" where:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

See: https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode--

It sounds like you are suggesting I write my own hashCode() and equals() methods and not use a data class?

If so, I’m not seeing any examples of good equals() and hashCode() implementations. Is there a canonical example of these out there? My class has a nullable enum, a Boolean, and a handful of Strings, so a simple example should go a long way. My Google fu isn’t finding any examples, just some theoretical discussion.

Thanks.

Yes and no. Data classes are a great shortcut if you can put all members that are part of the comparison in the primary constructor (and they aren’t arrays). In that case kotlin can generate the hashCode and equals methods (as well as a few other ones).
I tend to use data classes when all fields of the class are part of equals and they are all declared in the primary constructor.
When you don’t use data classes you have to write your own equals method. As @SackCastellon said, you should also always write a hashCode function that follows the rule he stated, simplified: All properties that are part of equals should be a part in the hashCode method.
The last option is to just use reference equality. This is the default implementation of equals and is fine in some cases.
So you have 3 options: reference equality, custom equals + hashCode or data classes. Each have their own advantages and disadvantages.

If you use Intellij you can use it’s generator to create the equals and hashCode implemenation. It’s under Code>Generate (Cmd N or Ctrl N, not sure about the windows shortcut). It uses a common implementation. There is not much you can do wrong for equals as long as your null check is correct and hashCode just sums all the hashes of each member after multipling them with a prime number. It’s not the best hash algorithm (collision wise you could probably create a far more sophisticated algorithm), but it’s fast and it is good enough in most cases.

Not exactly, I was talking about the regular classes. More precisely I was trying to answer your question:

For data classes, most of the time, you won’t need to override the generated equals and hashCode methods.

Understood, but I’m still not getting my questions answered.

So what if I have a regular class? I need to write the equals and hashCode right? If so, is there a canonical example?

@Wasabi375 answered that question here:

I really hope and assume per default that you are using an IDE. Every IDE that I know can generate equals and hashcode for you.

Intellij can generate them for you: https://www.jetbrains.com/help/idea/generate-equals-and-hashcode-wizard.html

Stepping back a bit:

kotlin.Any (which all Kotlin objects ultimately extend) has methods equals() and hashCode(). So you can call those methods on every object.

That’s why == can use equals(); it will always work.

Any has a very basic, default implementation of equals(): it uses reference equality, which treats an object as equal to itself but different from every other object (even one with the same class and fields).

That’s the safest implementation, and in many cases it’s just what you want. (For example, if you make two Socket connections to the same port, you’ll still need to treat them differently.)

When you do want some different objects to be treated as equal (e.g. in value objects), then you’ll need to override equals() (and hashCode()) to specify how to compare the objects.

Writing an equals() implementation isn’t easy; the docs for theat method spell out several conditions that an implementation must follow, and it’s easy to break them by mistake. (Which leads to some very subtle and hard-to-find bugs.)

I’d suggest reading this article, which spells out some of the common pitfalls, and how to avoid them. (It even gives a pattern for handling equality between superclasses and subclasses, but that’s a pretty advanced and complex approach, and it’s usually much simpler and safer not to allow that.)

And if you do override equals(), then you must override hashCode() too, so that they match.

That’s why Kotlin provides data classes, which take care of all those things for you! (And much more, besides.) If you’re writing a value class, I’d strongly consider trying to make it a data class, to take advantage of all that. (Or if not, consider whether you could simply fall back to Any's reference equality.)

But as a basic idea, here’s the approach I generally use:

override fun equals(other: Any?) = other === this ||
	(other is ThisClass
	&& field1 == other.field1
	&& field2 == other.field2
	&& field3 == other.field3)

override fun hashCode() = (1009
	+ (field1?.hashCode() ?: 0) * 19
	+ (field2?.hashCode() ?: 0) * 109
	+ (field3?.hashCode() ?: 0)

override fun toString() = "MyClass(field1=$field1, field2=$field2, field3=$field3)"

Thanks. The equals function looks about what I expected. The hashCode was unexpected, but okay. I assume it’s some prime number magic? Not sure what to do with that.

Maybe this additional information will help with the discussion? I have very little background in Java, so comparisons to Java are not very helpful for me. I’m a C++ programmer, so I can’t help but see things from that perspective. That said, here’s some articles I’ve read on the topic and my commentary.

Introduction to Data Classes in Kotlin by Rajeev Singh Oct 18, 2017

"Although the properties of a data class can be mutable (declared using var), It’s strongly recommended to use immutable properties (declared using val) so as to keep the instances of the data class immutable.

Immutable objects are easier to work with and reason about while working with multi-threaded applications. Since they can not be modified after creation, you don’t need to worry about concurrency issues that arise when multiple threads try to modify an object at the same time."

In my case, I have data where each field can and will be changed independently from the other fields, so making the class immutable seems inadvisable and therefore a data class is inadvisable as well. If I don’t use a data class, I do not get a == operator, or at least I can’t count on the behavior I want. I will need to make my own equals and therefore my own hashCode, even though I will never use the hashCode method.

If that didn’t confuse me enough, this seems to contradict what above description of the == operator does:

Equality in Kotlin (‘==’, ‘===’ and ‘equals’) by Suneet Agrawal Sep 4, 2018

== operator is used to compare the data of two variables.
Please don’t misunderstand this equality operator with the Java == operator as both are different. == operator in Kotlin only compares the data or variables, whereas in Java or other languages == is generally used to compare the references. The negated counterpart of == in Kotlin is != which is used to compare if both the values are not equal to each other.

All I want to do is compare for equivalence. I don’t care about equality. The descriptions of equals just don’t give me a warm fuzzy that the behavior is understood.

At this point, I think I’m better off to delete the == and === not to mention the != and other comparison operators.

Is there a way to do that so if == or any other comparison operator is used, I get an error?

If there is, I can avoid this entire problem.

Not really. While immutability is great when you structure your program to support it, it’s not necessary and in some cases not even a good idea.

You don’t strictly have to, but it’s considered good practise and since you can auto generate it, it’s not even much work.

I don’t like this article. It seems to make this big distinction between a == b and a.equals(b) but this is not really true.
As the article corectly stated there are 3 different ways to do equality checks in kotlin (on a syntax level)

  1. a == b
  2. a.equals(b)
  3. a === b

When we look at functionality this is no longer true. Kotlin will replace a == b with a?.equals(b) ?: a === b which is just a.equals(b) in a null safe way. So there is no difference between a == b and a.equals(b). The 1 exception to that are primitive types(Int, Float, etc). They will be replaced with the special jvm comparison instruction of their number type (which as pointed out in the article is different than the implementation of the function Float.equals(float), but in general you want to use the special instruction anyways).
Therefor in kotlin it is consider bad practice to use a.equals(b). Unless you break the contract of the equals method(transitive, reflexive and symetrical, a value never equals null) it always behaves the same(float is an exception, but not a relevant one).

Now to data class vs custom equals implementation. It doesn’t matter. You can go either way and get the same outcome. Kotlin just generates the equals method for you when you use data classes.
My advice use them whenever possible. They just make live easier for you.
If you can’t and you still need equals there are a few rules that you should follow. They are defined by the function contract described here:

  • It is reflexive : for any non-null reference value x , x.equals(x) should return true .
  • It is symmetric : for any non-null reference values x and y , x.equals(y) should return true if and only if y.equals(x) returns true .
  • It is transitive : for any non-null reference values x , y , and z , if x.equals(y) returns true and y.equals(z) returns true , then x.equals(z) should return true .
  • It is consistent : for any non-null reference values x and y , multiple invocations of x.equals(y) consistently return true or consistently return false , provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x , x.equals(null) should return false .

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y , this method returns true if and only if x and y refer to the same object ( x == y has the value true ).

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

While it’s considered good practice to implement hashCode, it’s not enforced althoug classes like HashMap or LinkedHashSet (default implementations behind mapOf and setOf) might not work properly if you don’t.

Than implementing equals/data classes together with == is exactly what you are looking for. If you are looking for reference equality === is what you need. Also non data classes that don’t implement equals inherit the default version from Any(java Object) which implements reference equality.

TL;DR

  • Use data classes when you care about equality
  • If not possible implement your own equals/hashCode methods (Idea has a good code generator for that)
  • If you don’t care about equality, do whatever you want
  • Always use a == b instead of a.equals(b) for equality
  • use a === b for reference equality (data saved at same memory location)

That does help make it clearer. That article did throw me off so I’m glad it threw you off too. At least it makes me feel better I understand this better. Thanks. :slight_smile:

I just took a look at a few of the authors other articles about kotlin. This is not the only article that is either just plain wrong in some cases or at least misleading. The Article about inline functions is particullary bad, claiming you are supposed to inline short simple pure functions (which in most cases is better done by the jvm and even produces a warning in kotlin, because of that). Than his example misses the inline keyword on a function that takes a lambda. This function would have been the poster boy example of a function that should be inlined. That’s the problem with the internet. Everyone can post everything, there is a great lot of information out there, but some of it is false.

1 Like

I will need to make my own equals and therefore my own hashCode, even though I will never use the hashCode method.

Failing to do so can lead to situations like:

val s = setOf(MyClass(1))
println(MyClass(1) in s) // Probably prints 'false'

The hashcode is used by most maps, sets, caches, etc. So even though you’re unlikely to call hashCode() yourself, making it inconsistent with equals() is a really bad idea that’s likely to come back to bite you some day.

1 Like

Right. If you for some reason really don’t want to implement hashCode, replace its implementation with TODO(). Then you’ll at least notice when you use it accidentally instead of getting wrong results.

If you are running on the JVM the Apache Commnons Lang library has some classes to help you and to get all the details correct: