Strange behavior with equality checking


#1

Using the following snippet:

// Need to also override equals & hashcode but lets ignore that for now
class Person(val name: String)
class Cat(val name: String)

fun checkEquality() {
    var person1: Person = Person("Dan")
    var pet1: Cat = Cat("Mickey")

    if (person1 == pet1) println("they're equal")
}

The if-statement doesn’t compile because the 2 variables cannot be compared for equality. This is great since they can never be equal so that catches a logic flaw. Making one of the classes open results in the same good behavior (doesn’t compile). However, if I make both of the classes open then the if-statement actually compiles.

If multiple inheritance was allowed, I could have a subclass CatPerson that extends from both so a variable of type Person might equal a variable of type Cat if they both referenced a CatPerson. Since Kotlin only allows single inheritance, it seems like this equality check would never pass (with a proper equals implementation which satisfies the equals contract).

I’m using IntelliJ 2017.3.2 with Kotlin plugin 1.2.31

Can someone please explain why the equality check is allowed when both classes are open. Is this a defect?


#2

IMO this should at least display a warning. The reason this is valid is that == gets extended to a?.equals(b) ?: (b === null), calling a equality function with this signature Person.equals(other: Any?). Now lets say we have a third class which extends from Person and implements equality to return true if other is of type Cat.


#3

@Wasabi375 Thanks for your response.

I updated the question to reflect that making one of the classes open still fails to compile (which is good) so this strange behavior only happens when both classes are open.

Regarding the reasoning that “==” calls equals and you can code equals to return true when it’s not truly equal; this explanation should have also applied to the scenario where only 1 class is made open so this is quite perplexing.

The other concern with this idea is that the equals method also comes with a contract that must be respected. Therefore the rules only need to consider correct implementations of equals which abide by the equals contract.

Is there some scenario which I haven’t considered or is this a defect?


#4

But then the important part for allowing (foo==bar) is foo may having a non-standard equals(…) member. That isn’t the case. Also i’d expect a warning for always true / false instead of an compiler error.

Fascinating question. Looking at the source, it seems like the compiler finds a type intersection. But TypeIntersector is to complicated for me to understand from just reading github code :smiley:


#5

The compiler can’t know that as someone else using the code as a library may extend Foo and create the equals method at a later time.


#6

The compiler knows that the equals method has a contract that must be followed otherwise it wouldn’t make sense for TypeIntersector to eliminate any equality checks because you could code an incorrect implementation of equals that always returns true for any class (even classes that aren’t open).

The compiler currently prevents equality checks when only one of the classes is open. So it could very well be overridden in a different project when used as a library yet the compiler still prevents checking for equality (unless both classes are declared open).

If your reasoning is correct, that implies that the current compiler is defective since it should allow comparing for equality when one of the classes is open. However, I like the current compiler implementation as bases its decisions on correct equals implementations so it catches logic flaws except that there’s a strange edge case when both classes are open (hopefully this is a defect that will be fixed).