ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 02.01.2026

Просмотров: 3501

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Section 30.2

Chapter 30 · Object Equality

693

The comparison “p equals cp” invokes p’s equals method, which is defined in class Point. This method only takes into account the coordinates of the two points. Consequently, the comparison yields true. On the other hand, the comparison “cp equals p” invokes cp’s equals method, which is defined in class ColoredPoint. This method returns false, because p is not a ColoredPoint. So the relation defined by equals is not symmetric.

The loss in symmetry can have unexpected consequences for collections. Here’s an example:

scala> HashSet[Point](p) contains cp res11: Boolean = true

scala> HashSet[Point](cp) contains p res12: Boolean = false

So even though p and cp are equal, one contains test succeeds whereas the other one fails.

How can you change the definition of equals so that it becomes symmetric? Essentially there are two ways. You can either make the relation more general or more strict. Making it more general means that a pair of two objects, x and y, is taken to be equal if either comparing x with y or comparing y with x yields true. Here’s code that does this:

class ColoredPoint(x: Int, y: Int, val color: Color.Value) extends Point(x, y) { // Problem: equals not transitive

override def equals(other: Any) = other match { case that: ColoredPoint =>

(this.color == that.color) && super.equals(that) case that: Point =>

that equals this case _ =>

false

}

}

The new definition of equals in ColoredPoint has one more case than the old one: If the other object is a Point but not a ColoredPoint, the method forwards to the equals method of Point. This has the desired effect of making equals symmetric. Now, both “cp equals p” and “p equals cp” result in true. However, the contract for equals is still broken. Now the

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 30.2

Chapter 30 · Object Equality

694

problem is that the new relation is no longer transitive! Here’s a sequence of statements that demonstrates this. Define a point and two colored points of different colors, all at the same position:

scala> val redp = new ColoredPoint(1, 2, Color.Red) redp: ColoredPoint = ColoredPoint@6bc

scala> val bluep = new ColoredPoint(1, 2, Color.Blue) bluep: ColoredPoint = ColoredPoint@6bc

Taken individually, redp is equal to p and p is equal to bluep:

scala> redp == p res13: Boolean = true

scala> p == bluep res14: Boolean = true

However, comparing redp and bluep yields false:

scala> redp == bluep res15: Boolean = false

Hence, the transitivity clause of equals’s contract is violated.

Making the equals relation more general seems to lead to a dead end. We’ll try to make it stricter instead. One way to make equals stricter is to always treat objects of different classes as different. That could be achieved by modifying the equals methods in classes Point and ColoredPoint. In class Point, you could add an extra comparison that checks whether the run-time class of the other Point is exactly the same as this Point’s class, as follows:

// A technically valid, but unsatisfying, equals method class Point(val x: Int, val y: Int) {

override def hashCode = 41 * (41 + x) + y override def equals(other: Any) = other match {

case that: Point =>

this.x == that.x && this.y == that.y && this.getClass == that.getClass

case _ => false

}

}

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 30.2

Chapter 30 · Object Equality

695

You can then revert class ColoredPoint’s implementation back to the version that previously had violated the symmetry requirement:6

class ColoredPoint(x: Int, y: Int, val color: Color.Value) extends Point(x, y) {

override def equals(other: Any) = other match { case that: ColoredPoint =>

(this.color == that.color) && super.equals(that) case _ => false

}

}

Here, an instance of class Point is considered to be equal to some other instance of the same class only if the objects have the same coordinates and they have the same run-time class, meaning .getClass on either object returns the same value. The new definitions satisfy symmetry and transitivity because now every comparison between objects of different classes yields false. So a colored point can never be equal to a point. This convention looks reasonable, but one could argue that the new definition is too strict.

Consider the following slightly roundabout way to define a point at coordinates (1, 2):

scala> val pAnon = new Point(1, 1) { override val y = 2 } pAnon: Point = $anon$1@6bc

Is pAnon equal to p? The answer is no because the java.lang.Class objects associated with p and pAnon are different. For p it is Point, whereas for pAnon it is an anonymous subclass of Point. But clearly, pAnon is just another point at coordinates (1, 2). It does not seem reasonable to treat it as being different from p.

So it seems we are stuck. Is there a sane way to redefine equality on several levels of the class hierarchy while keeping its contract? In fact, there is such a way, but it requires one more method to redefine together with equals and hashCode. The idea is that as soon as a class redefines equals (and hashCode), it should also explicitly state that objects of this class are never equal to objects of some superclass that implement a different equality

6Given the new implementation of equals in Point, this version of ColoredPoint no longer violates the symmetry requirement.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 30.2

Chapter 30 · Object Equality

696

method. This is achieved by adding a method canEqual to every class that redefines equals. Here’s the method’s signature:

def canEqual(other: Any): Boolean

The method should return true if the other object is an instance of the class in which canEqual is (re)defined, false otherwise. It is called from equals to make sure that the objects are comparable both ways. Listing 30.1 shows a new (and final) implementation of class Point along these lines:

class Point(val x: Int, val y: Int) { override def hashCode = 41 * (41 + x) + y

override def equals(other: Any) = other match { case that: Point =>

(that canEqual this) &&

(this.x == that.x) && (this.y == that.y) case _ =>

false

}

def canEqual(other: Any) = other.isInstanceOf[Point]

}

Listing 30.1 · A superclass equals method that calls canEqual.

The equals method in this version of class Point contains the additional requirement that the other object can equal this one, as determined by the canEqual method. The implementation of canEqual in Point states that all instances of Point can be equal.

Listing 30.2 shows the corresponding implementation of ColoredPoint. It can be shown that the new definition of Point and ColoredPoint keeps the contract of equals. Equality is symmetric and transitive. Comparing a Point to a ColoredPoint always yields false. Indeed, for any point p and colored point cp, “p equals cp” will return false because “cp canEqual p” will return false. The reverse comparison, “cp equals p”, will also return false, because p is not a ColoredPoint, so the first pattern match in the body of equals in ColoredPoint will fail.

On the other hand, instances of different subclasses of Point can be equal, as long as none of the classes redefines the equality method. For in-

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 30.2

Chapter 30 · Object Equality

697

class ColoredPoint(x: Int, y: Int, val color: Color.Value) extends Point(x, y) {

override def hashCode = 41 * super.hashCode + color.hashCode override def equals(other: Any) = other match {

case that: ColoredPoint => (that canEqual this) &&

super.equals(that) && this.color == that.color case _ =>

false

}

override def canEqual(other: Any) = other.isInstanceOf[ColoredPoint]

}

Listing 30.2 · A subclass equals method that calls canEqual.

stance, with the new class definitions, the comparison of p and pAnon would yield true. Here are some examples:

scala> val p = new Point(1, 2) p: Point = Point@6bc

scala> val cp = new ColoredPoint(1, 2, Color.Indigo) cp: ColoredPoint = ColoredPoint@11421

scala> val pAnon = new Point(1, 1) { override val y = 2 } pAnon: Point = $anon$1@6bc

scala> val coll = List(p)

coll: List[Point] = List(Point@6bc)

scala> coll contains p res16: Boolean = true

scala> coll contains cp res17: Boolean = false

scala> coll contains pAnon res18: Boolean = true

These examples demonstrate that if a superclass equals implementation defines and calls canEqual, then programmers who implement subclasses can

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index