line

Code Quality Improvement Techniques Part 22 (opens in new tab)

The post argues that developers should avoid overriding the equals method to compare only a subset of an object’s properties, as this violates the fundamental principles of identity and structural equivalence. Implementing "partial equality" often leads to subtle, hard-to-trace bugs in reactive programming environments where UI updates depend on detecting changes through equality checks. To ensure system reliability, equals must strictly represent either referential identity or total structural equivalence.

Risks of Partial Equality in Reactive UI

  • Reactive frameworks such as Kotlin’s StateFlow, Flow, and Android’s LiveData utilize distinctUntilChanged logic to optimize performance.
  • These "observable" patterns compare the new object instance with the previous one using equals; if the result is true, the update is ignored to prevent unnecessary re-rendering.
  • If a UserProfileViewData object only compares a userId field, the UI will fail to reflect changes to a user's nickname or profile image because the framework incorrectly assumes the data has not changed.
  • To avoid this, any comparison logic that only checks specific fields should be moved to a uniquely named function, such as hasSameIdWith(), instead of hijacking the standard equals method.

Defining Identity vs. Equivalence

  • Identity (Referential Equality): This indicates that two references point to the exact same object instance, which is the default behavior of Object.equals() in Java or Any.equals() in Kotlin.
  • Equivalence (Structural Equality): This indicates that two objects are logically the same because all their properties match. In Kotlin, data class implementations provide this by default for all parameters defined in the primary constructor.
  • Proper implementation of equivalence requires that all fields within the object also have clearly defined equality logic.

Nuances and Implementation Exceptions

  • Kotlin Data Class Limitations: Only properties declared in the primary constructor are included in the compiler-generated equals and hashCode methods; properties declared in the class body are ignored by default.
  • Calculated Caches: It is acceptable to exclude certain fields from an equality check if they do not change the logical state of the object, such as a cachedValue used to store the results of a heavy mathematical operation.
  • Context-Dependent Equality: The definition of equality can change based on the model's purpose. For example, a mathematical model might treat 1/2 and 2/4 as equal, whereas a UI display model might treat them as different because they represent different strings of text.

When implementing equals, prioritize full structural equivalence to prevent data-stale bugs in reactive systems. If you only need to compare a unique identifier, create a dedicated method instead of repurposing the standard equality check.