Find us

mobileforming,
611 North Brand, Boulevard, 11th floor
Glendale, CA 91203

Google Maps
Get in touch

Have a question for the mobileforming team?
T: 818-649-3299
E: info@mobileforming.com

Careers

Interested in joining our
amazing team? Email us on: recruiter@mobileforming.com

Current vacancies
New business

For new business enquiries please contact:
Jonathan Arnott
T: 951-229-5790
E: jonathan.arnott@mobileforming.com

© 2018 mobileforming LLC. All rights reserved.

featured post

Kotlin Equality—Not All Equal Comparisons Are Created Equal

read post

Kotlin Equality—Not All Equal Comparisons Are Created Equal

By Ryan Wieghard

Posted on: 14 February 2020

Equality operators in Kotlin

    == | !=
    === | !==
    .equals(other: Any?): Boolean

In the 1972 film, What’s Up Doc, Ryan' O’Neal plays a musicologist named Howard who is trying to get rid of the miasma which follows the mendicant trouble maker character, Judy Maxwell, played by Barbara Streisand. When he tries to say goodbye to her—hopefully for good—he explains as politely as possible that she is “just different.” Judy retorts:

I know I’m different, but from now on I am going to try to be the same.

The same as what?

The same as people who aren’t different (What’s Up Doc).

whats-up-docWhat’s Up Doc (1972)

As a Kotlin developer, there are many times when one is in Howard’s position. Not because one is a musicologist that studies the intrinsic musical attributes of igneous rocks, but because one is in the position of trying to categorize an object as being “different” or “the same.” In what follows, we will discuss the different equality operators in Kotlin and what nuances can both assist or deceive if one is not careful of their formal definition.

To illustrate this, imagine you are using a super secret third party library to manage a list of its super secret third party library objects of class, TripleCrownWinner. You have a local instance of a set of these objects horseList, and the library will periodically update its list to give the end-user a list of Triple Crown winners to date. The interface which provides the list is called updateHorseList(newHorseList: List<TripleCrownWinner>). In the documentation the super secret third party library emphasizes that TripleCrownWinners that have failed a test for performance enhancing drugs are not guaranteed to be returned in the newHorseList.

You are working for an agency trying to evaluate a particular gene sequence responsible for hypertrophic adaptations in horses which, if present, could prove advantageous to clean steeds competing against a juiced field. In order to study this phenomenon, your project has its own function which returns a recent Triple Crown winner which was convicted of using PEDs: getDopingTripleCrownWinner(): TripleCrownWinner.

You agency requires you to update your local instance by only adding horses that meet an equestrian standard of “Eliteness,” which you are checking with the predicate function isAnEliteHorse.

override fun updateHorseList(newHorseList: List<TripleCrownWinner>) {
   horseList = mutableListOf<TripleCrownWinner>()
   val dopingHorse = getDopingTripleCrownWinner()
   horseList.add(dopingHorse)
   horseList.addAll(newHorseList.filter { isAnEliteHorse(it) })
}

However, you discover an edge case in which you are duplicating the contents of dopingHorse in your horseList. You discover that newHorseList sometimes contains a dopingHorse, and the business rules require that horseList contain only unique instances of convicted dopers.

Wishing to impress your colleagues with your deftness at the Kotlin language, you exclaim, “I know exactly which operator to use!” You change the predicate to the following:

  override fun updateHorseList(newHorseList: List<TripleCrownWinner>) {
   horseList = mutableListOf<TripleCrownWinner>()
   val dopingHorse = getDopingTripleCrownWinner()
   horseList.add(dopingHorse)
   horseList.addAll(newHorseList.filter { isAnEliteHorse(it) && it !== dopingHorse })
  }

Your logic is met with much criticism from the code reviewing gallery. “It’s all wrong!” “That’s not the right operator at all!” And those inveterate Java devotees who refuse to even sniff at Kotlin cry: “That code won’t compile! They meant to write != !”

But you know something that the critics don’t. You know that the super secret third party library is guaranteed to not duplicate any horses, doping or not. So you say, ”I don’t need to compare the object’s contents, I just need to see if it’s refers to the exact same object!”

In order to see if variables point to the same object reference, one uses === | !==. The fancy name for this is referential equality.

Referential equality is useful since it is incredibly performant. The language simply needs to see if the references are equal which can be performed in constant time O(1) versus traversing the entire object graph which would require O(n) comparisons where n is the number of properties in the object.

To your horror, after testing the changed code, the above does not achieve the desired ends! You have duplicated dopingHorse in the horseList.

“How!” “Why!” “Wherefore!?”

You go back and investigate how your brilliantly constructed code could ever fail you. You notice that the object reference of dopingHorse from getDopingTripleCrownWinner() is different from the dopingHorse which is contained in newHorseList. You double check the reference manual for the super secret third party library, and you find in almost illegible fine print that the super secret third party library performs super secret third party library algorithms for memory management which may require objects to be created and destroyed again. This means you can’t guarantee that once you have a reference to a given dopingHorse that a new dopingHorse is not created at a later time with the same contents but a different object reference.

Crestfallen, you console yourself with the fact you can still solve the problem by making sure that no two dopingHorses are added by changing the predicate to the following:

 isAnEliteHorse(it) && it != dopingHorse

This comparison is known as structural equality and it makes sure the two TripleCrownWinner objects being compared have the exact same contents. All Kotlin classes extend from the class Any and therefore inherit its standard of equality, which is structural.

For example, given the following class definition

class TripleCrownWinner(
  id: Int, 
  isConvictedOfADopingViolation: Boolean, 
  isElite: Boolean, 
  superSecretByteArray: List<Byte>
)

then, in the absence of any custom .equals(other: Any?): Boolean definition, the following test fails since the contents of each of the following instances are not equal given each objects' divergence on the isElite property:

val theCarlLewisOfHorses = TripleCrownWinner(1, false,true, secretByteArrayList)
val theSnailPacedHorse = TripleCrownWinner(1, false, false, secretByteArrayList)

Assertions.assertThat(theCarlLewisOfHorses == theSnailPacedHorse).isTrue() // fails

The following test, however, is potentially successful or unsuccessful, since the first superSecretByteArray defined in waterfordCrystal need only conform to the interface List which itself does not itself specify a specific .equality() relation.

val waterfordCrystal = TripleCrownWinner(1, true, true, secretByteArrayList)
val secretByteArrayListCopy = arrayListOf<Byte>()
waterfordCrystal.secretByteArrayList.forEach { byte ->
  secretByteArrayListCopy.add(byte)
}

val justify = TripleCrownWinner(1, true, true, secretByteArrayListCopy)
Assertions.assertThat(waterfordCrystal == justify).isTrue()

Here are two scenarios in which the above would pass and fail, respectively:

//ArrayList utilizes structural equality by 
//maintaining its parent Any's definition

val secretByteArrayList = arrayListOf(0x3f.toByte(), 0xe3.toByte())
val waterfordCrystal = TripleCrownWinner(1, true, true, secretByteArrayList)
val secretByteArrayListCopy = arrayListOf<Byte>()
waterfordCrystal.secretByteArrayList.forEach { byte ->
  secretByteArrayListCopy.add(byte)
}

val justify = TripleCrownWinner(1, true, true, secretByteArrayListCopy)
Assertions.assertThat(waterfordCrystal == justify).isTrue() // passes
class StrictEquivalenceMutableList: ArrayList<Byte>() {
  override fun equals(other: Any?): Boolean {
    return other?.let {
        this === it
    } ?: false
  }
}

val secretByteArrayList = StrictEquivalenceMutableList()
secretByteArrayList.addAll(listOf(0xe5.toByte(), 0xe7.toByte()))
val waterfordCrystal = TripleCrownWinner(1, true, true, secretByteArrayList)
val secretByteArrayListCopy = arrayListOf<Byte>()
waterfordCrystal.superSecretByteArray.forEach { byte ->
    secretByteArrayListCopy.add(byte)
}

val justify = TripleCrownWinner(1, true, true, secretByteArrayListCopy)
waterfordCrystal.superSecretByteArray.forEachIndexed { index, num ->
  Assertions.assertThat(num == justify.superSecretByteArray.get(index)) // succeeds
}
Assertions.assertThat(waterfordCrystal == justify).isTrue() // fails

Since structural equality is recursive, then things can get surprising when comparing two objects that have approximately the same content, yet are not considered equal since a child has a stricter version of equality than the parent. E.g. In the above example, the child Byte lists are not referentially equal, even though the two objects are otherwise structurally equivalent.

bs-sp-preakness-justify-partnership-0516Justify, A Triple Crown winner

Let us see what that the default .equals(other: Any?): Boolean behavior would look like for a hypothetical TripleCrownWinner definition which chose not to have any custom .equals() methods, and didn’t use any crazy Collections with thorny .equals(other: Any?): Boolean overrides.

val affirmed = TripleCrownWinner(1, false, false, listOf(Byte.MIN_VALUE))
val omaha = TripleCrownWinner(1, false, false, listOf(Byte.MIN_VALUE))
val citation = TripleCrownWinner(1, false, false, listOf(Byte.MIN_VALUE))

// reflexive
Assertions.assertThat(affirmed)).isTrue();

// symmetric
Assertions.assertThat(omaha.equalss(affirmed)).isTrue();
Assertions.assertThat(affirmed.equals(omaha)).isTrue();

//transitive
Assertions.assertThat(affirmed.equals(omaha)).isTrue();
Assertions.assertThat(omaha.equals(citation)).isTrue();
Assertions.assertThat(affirmed.equals(citation)).isTrue();

//since the objects are not the same instance of TripleCrownWinner
//then the === should return false for each comparison except the reflexive case
Assertions.assertThat(affirmed === omaha).isFalse()
Assertions.assertThat(omaha === citation).isFalse()
Assertions.assertThat(citation === affirmed).isFalse()

Assertions.assertThat(affirmed === affirmed()

All of the above assertions succeed with the default equals() definition. Let us spice things up and say that the idea of two TripleCrownWinners being equivalent according to the super secret third party library would be entirely dependent on the structural equivalence of each object’s superSecretByteArray. e.g.

class TripleCrownWinner(id: Int, isConvictedOfADopingViolation: Boolean, isElite: Boolean, superSecretByteArray: List<Byte>) {
  override fun equals(other: Any?): Boolean {
        if (other !is TripleCrownWinner) return false
        return (other.superSecretByteArray == this.superSecretByteArray)
    }
}

Then given the following comparisons, these assertions will all pass, which would not happen under the default definition of .equals(other: Any?): Boolean

val secretariat = TripleCrownWinner(1, false, true, listOf(Byte.MIN_VALUE))
val justify = TripleCrownWinner(3425324, true, true, listOf(Byte.MIN_VALUE))
val whirlaway = TripleCrownWinner(534, false, true, listOf(Byte.MIN_VALUE))

Assertions.assertThat(secretariat.equals(justify)).isTrue();
Assertions.assertThat(justify.equals(whirlaway)).isTrue();

If we compare secretariat, justify, and whirlaway just as above but substituting .equals() with == then we will get the same behavior as above, and the assertions will all succeed.

”How do you know that?” jeers the inveterate Java diehard, from afar.

You kindly show him what a == b does under the hood, from the kotlin docs:

a?.equals(b) ?: (b === null)

So given the definition of .equals(other:Any?): Boolean above where only TripleCrownWinners with equivalent lists are equal, along with the knowledge that == just bootstraps any custom .equals(other: Any?): Boolean definition,

then the following tests will assuredly pass:

val secretariat = TripleCrownWinner(1, false, true, listOf(Byte.MIN_VALUE))
val justify = TripleCrownWinner(3425324, true, true, listOf(Byte.MIN_VALUE))
val whirlaway = TripleCrownWinner(534, false, true, listOf(Byte.MIN_VALUE))

Assertions.assertThat(secretariat == justify)).isTrue();
Assertions.assertThat(justify == whirlaway)).isTrue();

Emboldened by your newfound knowledge, you run the code again but this time using the != operator instead of !==. ”There is no way it’s going to fail, this time!” you declare, challengingly. Once again, you are proved wrong. You see that the dopingHorse is still being duplicated. You run the debugger and see that not only does the object reference differ between the two dopingHorses, but some of the properties are different as well. Specifically, the .id property is different. You go back to the docs and see that TripleCrownWinner has a method with the signature .equals(other: TripleCrownWinner): Boolean.

On first thought, it appears that this would be invoked implicitly based on the definition of == calling a?.equals(b). However, you realize that .equals(other: TripleCrownWinner): Boolean does not have the exact same signature of .equals(other: Any?): Boolean. Since the class does not specifically override the comparison operator for the Any? nullable, it will not be invoked by the vanilla != operator.

Since TripleCrownWinner does not override Any.equals(other: Any?): Boolean , the != comparison defaults to strict structural equality; and since the TripleCrownWinner.id property is different, the != equivalence check does not behave the way the TripleCrownWinner class specifically handles object equivalence and returns true for every TripleCrownWinner about to be added to horseList.

You update your predicate one last time to be:

 isAnEliteHorse(it) && !it.equals(dopingHorse)

You now meet both business requirements as well as the intended equivalence functionality of the TripleCrownWinner library. Hurray!

However, you are still mad, so you write the creators of the super secret third party library to please adhere more closely to Kotlin standards and provide overrides on intended changes to default functions such as .equals(other: Any?): Boolean, to avoid such headaches!

 

Works Cited

What’s Up Doc. Dir. Peter Bogdanavich. Ryan O’Neal and Barbara Streisand. Warner Bros, 1972. DVD.