4.15. Defining an equals Method (Object Equality)

Problem

You want to define an equals method for your class so you can compare object instances to each other.

Solution

Like Java, you define an equals method (and hashCode method) in your class to compare two instances, but unlike Java, you then use the == method to compare the equality of two instances.

There are many ways to write equals methods. The following example shows one possible way to define an equals method and its corresponding hashCode method:

class Person (name: String, age: Int) {

  def canEqual(a: Any) = a.isInstanceOf[Person]

  override def equals(that: Any): Boolean =
    that match {
      case that: Person => that.canEqual(this) && this.hashCode == that.hashCode
      case _ => false
   }

  override def hashCode:Int = {
    val prime = 31
    var result = 1
    result = prime * result + age;
    result = prime * result + (if (name == null) 0 else name.hashCode)
    return result
  }

}

This example shows a modified version of a hashCode method that Eclipse generated for a similar Java class. It also uses a canEqual method, which will be explained shortly.

With the equals method defined, you can compare instances of a Person with ==, as demonstrated in the following tests:

import org.scalatest.FunSuite

class PersonTests extends FunSuite {

  // these first two instances should be equal
  val nimoy = new Person("Leonard Nimoy", 82)
  val nimoy2 = new Person("Leonard Nimoy", 82)
  val shatner = new Person("William Shatner", 82)
  val ed = new Person("Ed Chigliak", 20)

  // all tests pass
  test("nimoy   == nimoy")   { assert(nimoy == nimoy) }
  test("nimoy   == nimoy2")  { assert(nimoy == nimoy2) }
  test("nimoy2  == nimoy")   { assert(nimoy2 == nimoy) }
  test("nimoy   != shatner") { assert(nimoy != shatner) }
  test("shatner != nimoy")   { assert(shatner != nimoy) }
  test("nimoy   != null")    { assert(nimoy != null) }
  test("nimoy   != String")  { assert(nimoy != "Leonard Nimoy") }
  test("nimoy   != ed")      { assert(nimoy != ed) }

}

As noted in the code comments, all of these tests pass.

Note

These tests were created with the ScalaTest FunSuite, which is similar to writing unit tests with JUnit.

Discussion

The first thing to know about Scala and the equals method is that, unlike Java, you compare the equality of two objects with ==. In Java, the == operator compares “reference equality,” but in Scala, == is a method you use on each class to compare the equality of two instances, calling your equals method under the covers.

As mentioned, there are many ways to implement equals methods, and the code in the Solution shows just one possible approach. The book Programming in Scala contains one chapter of more than 25 pages on “object equality,” so this is a big topic.

An important benefit of the approach shown in the Solution is that you can continue to use it when you use inheritance in classes. For instance, in the following code, the Employee class extends the Person class that’s shown in the Solution:

class Employee(name: String, age: Int, var role: String)
extends Person(name, age)
{

  override def canEqual(a: Any) = a.isInstanceOf[Employee]

  override def equals(that: Any): Boolean =
    that match {
      case that: Employee => 
        that.canEqual(this) && this.hashCode == that.hashCode
      case _ => false
  }

  override def hashCode:Int = {
    val ourHash = if (role == null) 0 else role.hashCode
    super.hashCode + ourHash
  }

}

This code uses the same approach to the canEqual, equals, and hashCode methods, and I like that consistency. Just as important as the consistency is the accuracy of the approach, especially when you get into the business of comparing instances of a child class to instances of any of its parent classes. In the case of the Person and Employee code shown, these classes pass all of the following tests:

class EmployeeTests extends FunSuite with BeforeAndAfter {

  // these first two instance should be equal
  val eNimoy1 = new Employee("Leonard Nimoy", 82, "Actor")
  val eNimoy2 = new Employee("Leonard Nimoy", 82, "Actor")
  val pNimoy = new Person("Leonard Nimoy", 82)
  val eShatner = new Employee("William Shatner", 82, "Actor")

  test("eNimoy1 == eNimoy1") { assert(eNimoy1 == eNimoy1) }
  test("eNimoy1 == eNimoy2") { assert(eNimoy1 == eNimoy2) }
  test("eNimoy2 == eNimoy1") { assert(eNimoy2 == eNimoy1) }
  test("eNimoy  != pNimoy")  { assert(eNimoy1 != pNimoy) }
  test("pNimoy  != eNimoy")  { assert(pNimoy != eNimoy1) }

}

All the tests pass, including the comparison of the eNimoy and pNimoy objects, which are instances of the Employee and Person classes, respectively.

Theory

The Scaladoc for the equals method of the Any class states, “any implementation of this method should be an equivalence relation.” The documentation states that an equivalence relation should have these three properties:

  • It is reflexive: for any instance x of type Any, x.equals(x) should return true.

  • It is symmetric: for any instances x and y of type Any, x.equals(y) should return true if and only if y.equals(x) returns true.

  • It is transitive: for any instances x, y, and z of type AnyRef, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.

Therefore, if you override the equals method, you should verify that your implementation remains an equivalence relation.

See Also

Get Scala Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.