• Post Reply Bookmark Topic Watch Topic
  • New Topic
programming forums Java Mobile Certification Databases Caching Books Engineering Micro Controllers OS Languages Paradigms IDEs Build Tools Frameworks Application Servers Open Source This Site Careers Other all forums
this forum made possible by our volunteer staff, including ...
Marshals:
  • Campbell Ritchie
  • Devaka Cooray
  • Knute Snortum
  • Paul Clapham
  • Tim Cooke
Sheriffs:
  • Liutauras Vilda
  • Jeanne Boyarsky
  • Bear Bibeault
Saloon Keepers:
  • Tim Moores
  • Stephan van Hulst
  • Ron McLeod
  • Piet Souris
  • Frits Walraven
Bartenders:
  • Ganesh Patekar
  • Tim Holloway
  • salvin francis

equals() and transitivity  RSS feed

 
Bartender
Posts: 10759
68
Eclipse IDE Hibernate Ubuntu
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
This question is fairly lengthy, so bear with me...

I've been away for a while working on a solution to the "equals" problem; and I've come to the conclusion that equality and Liskov just don't mix. Probably old hat for some, but quite a revelation to me: "is-a" and "equal" simply aren't compatible.

My theory: You cannot create a generalised equals() method that follows all the dictates of Object.equals() - in particular, transitivity - that works for subtypes AND follows LSP. Even with fancy reflective logic.

The reason: Actually, there's more than one:
1. The whole notion of "equality" is at odds with LSP, since objects are likely to need to refine its meaning as you go down a hierarchy.
Therefore, a ColouredSquare cannot behave the same way as a Square for the purposes of equality if it introduces a 'colour' component into the evaluation.
2. The equals() contract itself is unenforceable because it relies on both objects "following the rules", and you may not have written one of them. The best you can do is to follow it from your side, and hope that the person who wrote the object passed to your equals() method has done the same.
3. Transitivity is impossible to comply with for different types in a hierarchy because it involves three objects, any combination of which may be different OR the same. If they're all the same, or all different, there's no problem; the problems arise when precisely TWO of them are the same type, since they will perforce be compared on a different basis to the ones that are different.
The only solution seems to be to:
(a) Disallow comparisons between types that redefine equals().
(b) Introduce some other, artificial, precondition that forces compliance - the most common one being "default values".
The first violates LSP by definition; the second is ... well ... artificial.

So, my question is: Is this a peculiarity of Java, or statically-typed languages in general; or are there ones out there that DON'T have this problem? And, if so, can anyone give me an example?

My reason for asking is that I detest "class-based" equals() methods with a passion. To me, they're a simplistic solution to a problem that is plainly NOT simple.
You know the ones I mean. The first test after an identity check is:
if (obj == null) || this.getClass() != obj.getClass())
  return false;


The problem is that many texts recommend it without explaining the side-effects: the most obvious one being that if I define an anonymous subtype that doesn't even override equals(), that object will STILL be "unequal" to its parent; which seems to me like a huge violation of the Principle of Least Astonishment.

What say you? Does anyone have an answer to my question? I'd be really interested to know if anyone knows of a "good" solution to this, even in other languages (i should perhaps add that I am familiar with EqualsBuilder).

I don't even mind if you tell me that I'm making a mountain out of a molehill.

Winston
 
Bartender
Posts: 4568
9
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Have you ever seen this article?

http://www.artima.com/lejava/articles/equality.html

The section "Pitfall #4" is the relevant one. It uses the same example, and points out the same problems. You can try and fix the lack of symmetry by making equality more general (which makes it non-transitive), or more strict (using class-based comparisons, which has the problem with anonymous classes you mention).

It then proposes a solution that involves an additional, overrideable method canEqual(). It gives you an equals() method that satisfies the contract, but can deal with anonymous classes. Whether it breaks LSP is arguable, but the articles gives an argument why it doesn't:

One potential criticism of the canEqual approach is that it violates the Liskov Substitution Principle (LSP)... The reasoning is that the LSP states you should be able to use (substitute) a subclass instance where a superclass instance is required. In the previous example, however, “coll.contains(cp)” returned false even though cp's x and y values matched those of the point in the collection. Thus it may seem like a violation of the LSP, because you can't use a ColoredPoint here where a Point is expected. We believe this is the wrong interpretation, though, because the LSP doesn't require that a subclass behaves identically to its superclass, just that it behaves in a way that fulfills the contract of its superclass.

The problem with writing an equals method that compares run-time classes is not that it violates the LSP, but that it doesn't give you a way to create a subclass whose instances can equal superclass instances....



What do you think of that approach?
 
Marshal
Posts: 64494
225
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Winston Gutkowski wrote: . . . equality and Liskov just don't mix. . . .

No, it doesn't. It is in Effective™ Java 2nd edition by Joshua Bloch, page 38

There is no way to extend an instantiable class and add a value component while preserving the equals contract

Bloch uses a smaller exampled than ColouredSquare: ColoredPoint, but it behaves exactly as your example. Bloch shows a solution which uses

Favour composition over inheritance

But you are right: equals methods will only work if they are correctly overridden “at both ends”. You will find links to three articles about equals in this old post of mine. The first link (Odersky Sppon and Venners) discusses the relationship between the LSP and equals.

There is a contradiction between the different methods for t‍esting class equality. If you use instanceof, except in a final class, you breach symmetry if you add fields in the subclass. On the other hand using Class#equals means that subclasses are implicitly unequal. An insuperable obstacle if you use inheritance.
 
Winston Gutkowski
Bartender
Posts: 10759
68
Eclipse IDE Hibernate Ubuntu
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Matthew Brown wrote:What do you think of that approach?


Oddly enough, that's precisely the article that I based my "solution" on.

I've always though it was a brilliant approach, but it manifestly does break LSP in practical terms. Specifically, it means that subclasses that override equals() aren't subclasses for the purposes of equality, so you'll be able to mix them in supertyped collections in ways that may be surprising - even though they probably shouldn't be.

The approach in this article is a bit more sophisticated, but it's also quite a bit more complex, and relies on "default values".

There just doesn't seem to be a comprehensive solution to this issue - at least not one that I've heard of. But thanks for the try.

Winston
 
author
Posts: 23832
140
C++ Chrome Eclipse IDE Firefox Browser Java jQuery Linux VI Editor Windows
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Winston Gutkowski wrote:
2. The equals() contract itself is unenforceable because it relies on both objects "following the rules", and you may not have written one of them. The best you can do is to follow it from your side, and hope that the person who wrote the object passed to your equals() method has done the same.



In my opinion, this is probably the biggest issue. In most, with the exception of a few classes, the equals() method is a very simple implementation. In fact, you can argue that most are cut-n-pasted from a common example provided by Sun Microsystems in the 90's. And from that example, it is kinda obvious that it is concerned with the two instances being the same type.

In other words, even the classes that are "following the rules" seems to be concerned with reflexivity and symmetry only.

Henry
 
Winston Gutkowski
Bartender
Posts: 10759
68
Eclipse IDE Hibernate Ubuntu
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Campbell Ritchie wrote:You will find links to three articles about equals in this old post of mine. The first link (Odersky Sppon and Venners) discusses the relationship between the LSP and equals.


Thanks Campbell; I've actually read all three (and, of course, JB's chapter - which is what got me started down this path to begin with).

I guess what I'm asking is 'why':

  • Is LSP wrong?
  • Are we misinterpreting it when it comes to 'equality'?
  • Is "transitivity" essential? Because it would seem to be the only stumbling block to the nirvana of freely comparable subtypes.
    I suspect the answer to that one is "yes", because otherwise the order in which subtypes are added to a supertyped collection might make a difference as to whether they're allowed or not; which is probably even worse than breaking LSP. However, I've yet to find an article that explains precisely why it's a requirement, beyond "it's specified in Object.equals()".
  • Is it a by-product of static typing? That's why I'm so interested to know if there are languages around that don't have this problem.

  • Angelika Langer's "part 2" article has an even more sophisticated solution than Odersky's (and actually goes into the business of the "symmetry" aspect), but it's a LOT more complex, for not a lot of practical improvement that I can see.

    Henry Wong wrote:In my opinion, this is probably the biggest issue. In most, with the exception of a few classes, the equals() method is a very simple implementation...


    For final classes, you're absolutely right - and for them, the problem we're discussing doesn't really exist - but hierarchies are still a reasonable paradigm (aren't they?).

    I just thought everyone had now been so indoctrinated with the notion that "class-based equals() methods are best, because they're really simple" - a "Murphy solution" if I ever heard one - that there doesn't seem to have been much further movement on the subject (I certainly haven't seen many articles more recent than Odersky's (2009)).

    Winston
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    And just one final question:

    Is it an argument for a Discriminator<T> interface? ie: one that works like java.util.Comparator, but for equality comparisons. It seems to me that if we had one, this whole business of "LSP violation" would go away, because then we could tell collections how to determine if mixed types are "equal" or not.

    Winston
     
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    In my opinion the problem with the canEqual approach is that it ignores the basic inheritance principle completely.

    When a class defines its equality method, it lists what makes an object of its type equal another object. In order for us to be able to compare two objects, they should be mutually comparable. Subclasses have attributes inherited from their super class, so yes there are some common attributes there -- the only mutually comparable attributes between the two mixed type objects are the super class attributes. So how can the canEqual approach be so right if for mixed type comparisons it also considers the attributes that are not even present in the super class. Wouldn't that violate the basic IS-A.

    Granted a ColorPoint is a Point ( the basic IS-A thing ). But shouldn't that also imply that the only attributes that are mutually comparable between a Point and ColorPoint are the point co-ordinates without the color. As such shouldn't the mixed type comparison be based on only the point coordinates.

     
    Matthew Brown
    Bartender
    Posts: 4568
    9
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:But shouldn't that also imply that the only attributes that are mutually comparable between a Point and ColorPoint are the point co-ordinates without the color. As such shouldn't the mixed type comparison be based on only the point coordinates.



    But then you break the equals() contract. And as far as I'm concerned, of all the disadvantages that a particular approach can have, breaking the contract is the worst. Because any code out there (in collection classes, or whatever) is entitled to assume the contract will be met.

    Specifically:

    The only way to safely get that to work is to not allow ColourPoint to use the colour attribute even when comparing to another ColourPoint. In other words, to make equals() final. Which does indeed solve the problem - but surely that is ignoring the principles of inheritance? When possible, yes, make it final...but sometimes that isn't what is needed.

    The nice thing about canEqual() is that by default you get the typical behaviour you want, but a subclass is allowed to decide that, actually, this new attribute is an important part of its identity and equals() shouldn't ignore it - which means that an object that doesn't even have the attribute can't be considered equal.

     
    Matthew Brown
    Bartender
    Posts: 4568
    9
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Winston Gutkowski wrote:And just one final question:

    Is it an argument for a Discriminator<T> interface? ie: one that works like java.util.Comparator, but for equality comparisons. It seems to me that if we had one, this whole business of "LSP violation" would go away, because then we could tell collections how to determine if mixed types are "equal" or not.



    That would make sense. It seems to me that part of the problem is that there's no perfect solution. In some contexts you need one type of behaviour, and in other contexts you need another. Comparators solve that problem for the similar problem of ordering.
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    That is one way of looking at it. But I like Joshua Bloch's recommendation of using composition for such cases. Obviously then you can't have class hierarchies that define their own equality but the way I look at it, the canEqual approach caters to a limited use cases but violates far more use cases on the substitution principle. That is of course just my opinion.

    I think if we had a way in which the subclasses could introduce an important attribute in such a way that for

    superclass - superclass comparisons, the superclass attributes mattered
    for subclass1 - subclass1 comparisons, the subclass1 attributes mattered
    for subclass1 - subclass2 comparisons, the superclass attributes only mattered regardless of which equals method was invoked.
    for subclass1 - superclass comparisons, the superclass attributes only mattered regardless of which equals method was invoked,

    the transitivity requirement would also not be broken.

    But I am not aware of any such solution.

    Edit -- Just realized that the following

    superclass - superclass comparisons, the superclass attributes mattered
    for subclass1 - subclass1 comparisons, the subclass1 attributes mattered
    for subclass1 - subclass2 comparisons, the superclass attributes only mattered regardless of which equals method was invoked.
    for subclass1 - superclass comparisons, the superclass attributes only mattered regardless of which equals method was invoked,

    would break the trasitivity requirement. But still I think the canEqual approach caters to a limited number of use cases but violates far more cases on substitution principal.
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    Also I don't get such an inheritance scheme in which a subclass object that has the same superclass state as another superclass object should unequal it.

    I mean let us suppose you have a Person and an Employee class. An Employee is-a Person. When you compare an Employee with a Person, they can only be same Person or not based on Person attributes.

    If I think too hard, I can come up with only non real world example. But perhaps I'm not able to see the specialized contexts where mixed type comparisons that depend on attributes that are not even a property of a type should make sense.

    I see no reason for a ColorPoint to extend Point. Even if we weren't having this discussion, I wouldn't consider making it a subclass of Point.
     
    Java Cowboy
    Posts: 16084
    88
    Android IntelliJ IDE Java Scala Spring
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    In Scala you have case classes, which are a kind of Java Beans-like classes, i.e. classes that mostly just hold a number of related data fields together. The Scala compiler automatically generates a number of methods for a case class, including hashCode() and equals() methods.

    Inheritance with case classes is discouraged in Scala (and might even be disallowed in a future version of Scala), mainly because the kind of problems discussed here.

    Case classes can extend traits (which are somewhat like Java interfaces, but which can also contain method implementations, but no member variables) and case classes should not extend other classes.
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    On another note, if we can have such a scenario that we have three objects such as the following

    superclass ( state A) , subclass1 ( state A + state 1), subclass1 ( state A + state 2)

    this is always going to break either the transitivity rule or the substitution principle. And this is a characteristics of this use case ( I mean it's independent of the implementation ).
    I see the problem not so much in mixed type comparisons but in cases such as the following --

    superclass ( state A) , subclass1 ( state A + state 1), subclass1 ( state A + state 2)

    The question is should such cases not signal incorrect inheritance scheme? If that is too harsh/a biased way of looking at it, wouldn't it be quite right to say that implementing such an inheritance scheme and getting things to work as desired in all the cases would be fraught with difficulties, if we assume that it is not impossible to find a solution that caters to all the cases.

    In the ColorPoint case, the concept of two different colors for the same point coordinates is either hypothetical or we are considering more than a 2 dimensional or a different dimensional space. If it's the latter case, then they should be different Points.
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Jesper de Jong wrote:In Scala you have case classes...
    Case classes can extend traits (which are somewhat like Java interfaces, but which can also contain method implementations, but no member variables) and case classes should not extend other classes.


    Aha! Now that's the sort of thing I was looking for. Thanks a lot Jesper; that makes a LOT of sense.

    I find it interesting that inheritance is discouraged, because that tends to suggest that equality really is a case where inheritance and LSP simply don't jive, or can't both be satisfied. I notice also that Odersky's article mentions Scala right at the top, and I believe the technique comes from it.

    Winston
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:On another note, if we can have such a scenario that we have three objects such as the following
    superclass ( state A) , subclass1 ( state A + state 1), subclass1 ( state A + state 2)


    Yup. You're absolutely right. I was confusing type difference with content difference; thanks for pointing it out.

    In the ColorPoint case, the concept of two different colors for the same point coordinates is either hypothetical or we are considering more than a 2 dimensional or a different dimensional space. If it's the latter case, then they should be different Points.


    Hmmm. I think that's more along the lines of the "Circle/Ellipse" (or "Square/Rectangle") discussion - should Circle be a subtype of Ellipse? Or indeed, vice-versa?

    From what I've heard from you all, I suspect that equality, specialization and LSP just don't mix, so you just have to accept it. Odersky's solution (the one that Matthew linked) seems to me the most "objective" without getting overly complex.

    Winston
     
    Campbell Ritchie
    Marshal
    Posts: 64494
    225
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    When I started Java® I was taught aboiut inheritance and the IS‑A relationship. What we didn't realise was that adding methods or fields to classes makes inheritance very awkward to use. You end up in a situation like what you are describing, Winston, where you either breach the LSP or symmetry of the equals() method.
    Did you notice what Langer writes about cases of impossibility of writing an equals() method? I think she uses java.sql.TimeStamp#equals() as an example where they failed to maintain the general contract. Yes: look here
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:Did you notice what Langer writes about cases of impossibility of writing an equals() method? I think she uses java.sql.TimeStamp#equals() as an example where they failed to maintain the general contract. Yes: look here


    I do remember browsing them way back when I first read the article, but I clearly missed the significance of that particular one. Thanks for pointing it out.

    Winston
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:Also I don't get such an inheritance scheme in which a subclass object that has the same superclass state as another superclass object should unequal it.
    I mean let us suppose you have a Person and an Employee class. An Employee is-a Person. When you compare an Employee with a Person, they can only be same Person or not based on Person attributes.


    Ah, but are they "equal"?

    Again, we have an asymmetric relationship: An Employee is plainly a Person, but not every Person is an Employee, so it stands to reason that an Employee should NOT be equal to another Person unless that Person is ALSO an Employee - ie, the differentiating factor IS type.

    I'm also not sure that the answer is to simply make every "is-a" relationship a "has-a" one. Composition certainly has its merits, but it tends to lead to proliferation of interfaces, and I'm pretty certain that hierarchies still have a role to play in good OO design - not the least reason being that they're easy to understand. Would you naturally assume that a Dog is a composite of an Animal?

    Winston
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Matthew Brown wrote:The nice thing about canEqual() is that by default you get the typical behaviour you want, but a subclass is allowed to decide that, actually, this new attribute is an important part of its identity and equals() shouldn't ignore it - which means that an object that doesn't even have the attribute can't be considered equal.


    Bingo. There's an interview around somewhere between JB and Bill Venners (I think), where JB goes into precisely this point. Apparently, he's got more mail about that Chapter 8 in Effective Java than any other part of the book. But he makes a very good case for the use of instanceof (rather than class equality) based on the Principle of Least Surprise.

    To me, the canEqual() technique simply refines the instanceof approach by decoupling it. Yes, it violates LSP; but only when equals() is overridden. Class-based equals() methods violate it by definition.

    If I can find that interview, I'll link it.

    And BTW, thank you all for your contributions. I'm glad I'm not the only one who finds this interesting - I was starting to wonder if I wasn't turning into an "equality dweeb".

    Winston

     
    Campbell Ritchie
    Marshal
    Posts: 64494
    225
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    In the case of Person/Employee we have an example where the use of instanceof will fall down badlyYes, you are right about the IS‑A relationship, Winston. This is a little bit like the square extends rectangle problem and the LSP, but not quite as bad. Maybe the answer in this case is a method like thisThat would use the same logic as the original equals method, and would allow you to use the stricter getClass().equals() test in the equals method.
     
    Campbell Ritchie
    Marshal
    Posts: 64494
    225
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Winston Gutkowski wrote: . . . I'm not the only one who finds this interesting . . .

    No, I was taught that equals methods are nice and simple. I still see people writing things like this, however
     
    Campbell Ritchie
    Marshal
    Posts: 64494
    225
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    A few minutes ago, I wrote: . . . I was taught that equals methods are nice and simple. . . .

    I was also taught that somebody is on their way to help already, and the cheque's in the post, and the Council are consulting the people.
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:In the case of Person/Employee we have an example where the use of instanceof will fall down badly...


    Agreed, but I think Chan was talking about the situation where there isn't any distinguishing feature between a Person and an Employee - ie, they both use the same crtieria for equality.

    In that case, I still say that there's a case to be made for not allowing mutual comparison for the reasons I gave above - and the beauty of the canEqual() approach is that it allows you to do this if you want to.

    Winston
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:

    A few minutes ago, I wrote: . . . I was taught that equals methods are nice and simple. . . .

    I was also taught that somebody is on their way to help already, and the cheque's in the post, and the Council are consulting the people.


    Yeah. I've bought a few bridges too....

    Winston
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Winston Gutkowski wrote:There's an interview around somewhere between JB and Bill Venners (I think)...


    Found it. I should add that it's page 17, and the whole interview is worth reading, because it's very good.

    Winston
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Winston Gutkowski wrote:Ah, but are they "equal"?

    Again it we have an asymmetric relationship: An Employee is plainly a Person, but not every Person is an Employee, so it stands to reason that an Employee should NOT be equal to another Person unless that Person is ALSO an Employee - ie, the differentiating factor IS type.

    I'm also not sure that the answer is to simply make every "is-a" relationship a "has-a" one. Composition certainly has its merits, but it tends to lead to proliferation of interfaces, and I'm pretty certain that hierarchies still have a role to play in good OO design - not the least reason being that they're easy to understand. Would you naturally assume that a Dog is a composite of an Animal?



    I just realized a few things--

    I think that if we have cases such as the following --

    superclass ( state A) , subclass1 ( state A + state 1), subclass1 ( state A + state 2)

    and we assume that the superclass is a concrete class, it might be better to make the subclass1 a top level class if it needs to set different rules for its objects' equality. For the Person-Employee case, I would go with making the Employee class a top level class, even if we assume that a Person can have at most one set of Employee state. I know I only cited that example, but my apologies. I think the Employee class is best suited as a top level class. But my analysis might be flawed.

    I think if a class defines its own equality scheme, and a subclass needs to override that sense of equality, the subclass had better be a top level class, for inheritance in such cases does not make sense. Inheritance is about using the top class attributes. A subclass can override other things -- but the equals method! It's like the DNA of its parent.

    Now coming to the Animal-Dog example, the Animal class can at best be abstract because unless you know it's a Cat or a Dog or whatever other Animals are there, it can't really have have a way to distinguish one Animal from another. So I believe if a superclass has that capability to determine equality even for its subclass objects, by all means it should override the equals method ( the way the AbstractList does it -- Note that we don't have the equals method in the AbstractCollection class ). Otherwise I think that job is best suited for the subclasses.

    I think the way Comparators work might provide a way out. But I confess that's just a passing thought. I am yet to read Winston's TheRoadToEquality article completely and to analyze Campbell's way for the Person-Employee case ( yeah I know that applies only if for some reason Employee needs to subclass Person ).

     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:
    I think if a class defines its own equality scheme, and a subclass needs to override that sense of equality, the subclass had better be a top level class, for inheritance in such cases does not make sense. Inheritance is about using top class attributes. A subclass can override other things -- but the equals method! It's like the DNA of its parent.



    I know that might be a biased way of looking at things. But the way I see it, this approach might help in keeping the objects simple and hence more manageable.
     
    Campbell Ritchie
    Marshal
    Posts: 64494
    225
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    By top‑level class, do you mean a class which is not a subclass of Person?
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:I think that if we have cases such as the following --
    superclass ( state A) , subclass1 ( state A + state 1), subclass1 ( state A + state 2)
    and we assume that the superclass is a concrete class, it might be better to make the subclass1 a top level class if it needs to set different rules for its objects' equality. For the Person-Employee case, I would go with making the Employee class a top level class, even if we assume that a Person can have at most one set of Employee state.


    Well, in essence that's what the canEqual() approach does; except that it does allow interim classes to be instantiated. Every class in a hierarchy that overrides canEqual() - and therefore equals(), because the two methods work in tandem - creates its own isolated subgroup for the purposes of equality (and ONLY for that), with the overriding class at its head; otherwise subclasses are "equal" to their parent by default.

    My solution simply adds a superclass (called, strangely enough, Equal) that extends Object that essentially makes all objects "equal" rather than unequal, because that allows a subclasser to write ALL overriding methods the same way and use super.equals() and super.hashCode() consistently - ie, it makes equals() work rather the way a constructor does.

    Now coming to the Animal-Dog example, the Animal class can at best be abstract because unless you know it's a Cat or a Dog or whatever other Animals are there, it can't really have have a way to distinguish one Animal from another...


    Which actually dovetails with my approach. Equal assumes that there's no way of distinguishing between objects until it's told how.

    There are two other other problems with an Animal hierarchy though:
    1. Depending on its sophistication, it could be quite deep, since it could involve Phylae, Classes, Orders, and all the other stuff we learnt in O-Level Biology. Do you really want to be using composition for a structure as complex as that?
    2. It's highly probable that you will want something akin to a "class-based" equals() method, since it's unlikely that you will ever want ANY subspecies to be "equal" to its parent, no matter how similar they are.

    My Equal class therefore defines a subclass called ByClass that enforces this by making canEqual() a class-equality test (and also, final). After that, overriding is precisely the same as for Equal - actually slightly simpler, since you no longer need to worry about canEqual(). And, while I generally dislike class-based equals() methods, at least this approach documents the fact that it's what you've chosen to do.

    However, I fear I've strayed into an explanation of my approach rather than sticking to the business of equality and LSP.

    Hope it helps anyway.

    Winston
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:By top‑level class, do you mean a class which is not a subclass of Person?



    I meant a class that extends the java.lang.Object class only. Because if the Person class had a superclass, and the Employee class extended that superclass, composing an Employee with any class in the Person hierarchy would become awkward.

    I mean I think that would be one way.
     
    Saloon Keeper
    Posts: 10232
    216
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    While I think the issue is interesting, I haven't really experienced many problems with it. Problems usually arise when people abuse inheritance.

    Others have already made a case for it, but a ColoredPoint is really a bad example. Points aren't colored. A point is a point is a point. Instead, you may have a Dot that has a Color and has a Point (and maybe a thickness).

    When you find yourself thinking about how inheritance screws with your equals implementation, you should probably take it as a hint that your design is wrong.

    Whenever you redefine equals() and hashCode(), they should probably either be final, or in a final class. The only point I see for overriding a redefined equals() method is for performance reasons, not semantics. This also favors the instanceof operator over getClass().
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Stephan van Hulst wrote:While I think the issue is interesting, I haven't really experienced many problems with it. Problems usually arise when people abuse inheritance.

    Others have already made a case for it, but a ColoredPoint is really a bad example. Points aren't colored. A point is a point is a point. Instead, you may have a Dot that has a Color and has a Point (and maybe a thickness).


    Yup, I'd agree with you there. However, there are cases where entities do have a natural structure: employee hierarchies, inventory products, accounts and security structures spring to mind; and to my mind using composition in cases like that just doesn't feel right. A Manager "is-a" Employee, but an Employee may also "have-a" Manager, and the two relationships are different.

    When you find yourself thinking about how inheritance screws with your equals implementation, you should probably take it as a hint that your design is wrong.


    So presumably you're saying that a hierarchy can only ever have one definition of equality, then. That would suggest to me that it can only ever be the most general one, but it's an interesting thought. Specialization, in terms of collections, is then strictly a programmatic responsibility - ie, it depends entirely on what you put in the collection.

    It also suggests that all types in a hierarchy must be identified the same way. This may well be a reasonable stipulation, but I need to think about it a bit to see if I can come up with an example where it doesn't hold.

    Thanks for giving me the "opposite view", Stephan. Just what I needed.

    Winston
     
    Rancher
    Posts: 989
    9
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    I'd agree with Stephan, and would add that if a class is too general to have an equals method defined then don't define it, hint there would be to make it abstract as well. It's easier to add code than remove it so don't add the equals method when it's not conclusive/required.
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    Winston, would it not be right that for cases that need to have Manager is-A Employee relationship, the Manager class does not need to have its own equals method. As long as we can distinguish one Employee from another, that should suffice.
     
    Chan Ag
    Rancher
    Posts: 1090
    14
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:

    Campbell Ritchie wrote:By top‑level class, do you mean a class which is not a subclass of Person?



    I meant a class that extends the java.lang.Object class only. Because if the Person class had a superclass, and the Employee class extended that superclass, composing an Employee with any class in the Person hierarchy would become awkward.

    I mean I think that would be one way.



    I just realized, thanks to a certain someone, that a top level class in Java is not a class that extends just the java.lang.Object class and no other class. A top level class in Java means a class that is not an inner class or a nested class.
    Thanks for pointing it out, Campbell.
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Chan Ag wrote:Winston, would it not be right that for cases that need to have Manager is-A Employee relationship, the Manager class does not need to have its own equals method. As long as we can distinguish one Employee from another, that should suffice.


    Yes. And in the case of Employees, the chances are that there will be some form of ID that distinguishes one from another; and the same is also true of accounts and inventory products.

    On the other hand, if your system also has to take account of "people" in the abstract (eg, a customer, or a contractor), you may not have such an identifier. But you could then make Person an abstract class and have separate hierarchies of Employees and Non-employees. Which is why Stephan's post has really got me thinking (never a bad thing). I still think that there's merit to the business of overriding equals() "objectively"; I just need to come up with a good practical example. And in the Employee/Other case above, I certainly think there's merit in a superclass that makes all objects "equal" by default, rather than identity-based.

    As an aside, people are actually notoriously difficult to identify, because all the ways we normally use to identify someone (name, address, dob, etc) aren't unique. Even combinations can't be guaranteed, because there are always the "John Smith"s; and Sod's Law says that, no matter what weird and wonderful combination you choose, you will find the exception.

    Winston
     
    Stephan van Hulst
    Saloon Keeper
    Posts: 10232
    216
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    As for the employee/manager example, it shows us how easy it is to relate our data structures too much to natural speech. We say that a manager is an employee, but isn't it more correct to say that some employees have a manager role?
     
    Winston Gutkowski
    Bartender
    Posts: 10759
    68
    Eclipse IDE Hibernate Ubuntu
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator

    Stephan van Hulst wrote:As for the employee/manager example, it shows us how easy it is to relate our data structures too much to natural speech. We say that a manager is an employee, but isn't it more correct to say that some employees have a manager role?


    Oddly enough, that very thought occurred to me when I was responding ; and unfortunately, the best answer I can give you is: Maybe.

    Personally, I like the idea of it being determined by type - ie:but it's certainly not a hard-and-fast rule.

    If "being a Manager" is simply an attribute, then you're forced into logical (and therefore runtime) enforcement of rules. If it's type-based, then you can use the compiler as your friend; and I was taught that, all other things being equal, that's usually the better way to go.

    Winston
     
    Campbell Ritchie
    Marshal
    Posts: 64494
    225
    • Likes 1
    • Mark post as helpful
    • send pies
    • Quote
    • Report post to moderator
    Back to the LSP: Have you seen the square‑rectangle example? There seem to be many copies of it available all across the net.
    The problem with the square‑rectangle example is that a square “IS‑A” rectangle, but a square object “ISN'T‑A” rectangle object. Are we saying that a Manager is an Employee but a Manager object isn't an Employee object? I found Bloch's quote about the impossibility of adding fields and maintaining the equals contract when you add fields to a class yesterday, and I think the lesson is that inheritance is difficult and you need to be careful about it.
     
    The government thinks you are too stupid to make your own lightbulb choices. But this tiny ad thinks you are smart:
    how do I do my own kindle-like thing - without amazon
    https://coderanch.com/t/711421/engineering/kindle-amazon
    • Post Reply Bookmark Topic Watch Topic
    • New Topic
    Boost this thread!