Pinocio
Unregistered
|
|
How editors can permit publication of this article which contains so many misleading and simply wrong statements and interpretations? I think this is a shame.
|
Alex Blewitt
Unregistered
|
|
I'd happily address concerns that you have with the article if you would be kind enough to elaborate what you feel is 'simply wrong' with the article, and should it need corrections, retract incorrect statements. However, unsubstantiated examples won't help anyone understand why you feel this is wrong.
|
Pinocio
Unregistered
|
|
With pleasure. http://jdagon.com/archives/pinocio/2004_06.html#000041
|
Alex Blewitt
Unregistered
|
|
I'm glad that Pinocio has taken the time to respond in detail to the article that I wrote; specifically, because he confirms why I worte the article in the first place -- there are just some things that programmers don't get with OOP.
He quotes the 'Liskov Substitution Principle' as follows:
"if you decide to create a class B extending the class A , then you must be able to SUBSTITUTE an instance of the class A with the instance of class B anywhere in the program"
And indeed, it is correct that you should be able to substitute between the two. Specifically, the subclass should obey the contract of the superclass. [This is why, when overriding a method, you cannot add to the set of exceptions, because you cannot change the superclass' method contract.]
How he then goes on to argue that in fact changing the superclass' method contract (by ignoring symmetry) is desirable; thus, falsifies his own point.
He claims that substituting instance A for instance B should not affect behaviour of a program. True, but it doesn't imply that it will work the same way. Using a Collection, for example, you can substitute both an instance of LinkedList and HashSet; that's an example of the substitution principle. You can substitute providing that the *class provides the same contract*, and not that *it is implemented the same way*. Indeed, you can substitute HashSet for SortedSet; they work in different ways, and will result in a different order when printing out to System.out, for example. I offer the following example:
import java.util.*; public class Test { public static void main(String args[]) { Collection s1 = new LinkedList(); s1.add("Z"); s1.add("A"); Collection s2 = new TreeSet(); s2.add("Z"); s2.add("A"); System.out.println(s1); System.out.println(s2); System.out.println(s1.equals(s2)); } }
The subtitution principle says that you should be able to replace any Collection with any other Collection. Clearly, the purpose of these different classes is to affect how the data is stored and presented. Thus from the collection example, whilst it is safe for a program to replace (say) a LinkedList with a SortedSet, you *will* get different behaviour. Choosing which implementation is a matter that good programmers will decide on which implementation suits their purpose.
In any case, extending a class is not just about implementation -- it's about type, as well. Point3D and Point2D are distinct types, and that's the key; they *shouldn't* be the same. And the reason why I showed Point3D as an example in this case, is because a co-ordinate of [1,1] in 3D space is [1,1,0] -- so [1,1] shouldn't be equal to (say) [1,1,6], which it will otherwise answer. Or indeed, any of the other points [1,1,8] or [1,1,-10].
Using the LiskovSubstitutionPrinciple as the sole arguement is actually very amusing; firstly, it argues that the substitutions should behave in the same way (actually, it is a weaker form of contract behaviour, as used in contract programming, Eiffel and such like) but argues that depending on the order of comparison, a.equals(b) should return true, but b.equals(a) returns false. This is clearly an incorrect point to argue that substitution should be allowed to occur.
Secondly, as shown above, the Liskov Subtitution Principle is solely concerned with replacing a like-for-like implementation, and *not* for subtypes representing a different type. Point3D and Point2D are *different* types; Point3D isn't being designed as a plug-and-play replacement for a Point2D. Indeed, for many of the objects and sub-class relationships, the Liskov Substitution Principle just doesn't hold. You can't replace (say) a Container with a JButton (and get exactly the same behaviour); you can't replace a HashSet (of type Set) with a SortedSet (also of type Set) (because the sorted set will output them in a different order); you can't replace a FilterInputStream with a FileInputStream (the reads come from different places).
In fact, the only time that the Liskov Substitution Principle needs to be obeyed is when you are designing the subclass to *be* a plug-in replacement for the superclass. Unfortunately, Pinocio is under the misguided impression that *every* subclass needs to play this role; and that isn't the case. What specificially needs to happen is that the subclass needs to obey the superclass' contract, whatever that contract might be. Substituting (say) a DB2 database driver for an Oracle database driver will certainly happen; but that doesn't mean to say that for every program I have I can plug-and-play that without making any other changes to the code. If I use a JDBC URL jdbc:db2:SAMPLE, and try loading that with an Oracle driver, it won't find it.
The Liskov Substitution Principle is therefore a weak argument, since it only comes into play when you have a like-for-like. When you're creating a subtype (as the example of Point2D and Point3D used), the substitution principle just doesn't need to hold.
Pinocio also said "So, extending Point2D in order to specify Point3D object is not legitimate". This example was used to highlight the danger of using the instanceof, because not every 2D point is equal to a 3D point (in fact, it's a essentially line in 3D space and a plane in 4D space).
My advice to Pinocio is to read up about design by contract, and specifically to obey subclass contracts without trying to justify violation of common sense ideas based on a principle that he misunderstood to apply to any subclass.
|
Anonymous
Unregistered
|
|
First of all, this is a plain lie:
"How he then goes on to argue that in fact changing the superclass' method contract (by ignoring symmetry) is desirable; thus, falsifies his own point."
I said that extending Point2D to produce Point3D and use equals() you describe is incorrect. And I referred to the work of Liskov to explain it. Secondly, you dont seem to understand this "principle" as you do not see that what you call "design by contract" is the essentially the same. You wrote:
"He claims that substituting instance A for instance B should not affect behaviour of a program. True, but it doesn't imply that it will work the same way. Using a Collection, for example, you can substitute both an instance of LinkedList and HashSet; that's an example of the substitution principle. You can substitute providing that the *class provides the same contract*, and not that *it is implemented the same way*. Indeed, you can substitute HashSet for SortedSet; they work in different ways, and will result in a different order when printing out to System.out, ...."
Substituting LinkedList and HashSet is not the example LSP. The correct example would be if you have a program using Collection and then you put LinkedLink instead of Collection. Liskov Principol requires LinkedList to provide the same service as Collection does as the characterization of the programs correctness. In more perceivable for you words, LikenList must respect contracts declared in its superclass Collection.
Rephrasing my initial note in the design by contract terms:
Point2D class specifies/declares what equals() for each Point2D means, and each subclass must respect this contract. You provided a rationalization of your approach by ignoring this contract in Point3D, which extends Point2D. So, your reasoning is flawed.
I think you owe JavaWorld readers taking back this piece of the article.
|
Alex Blewitt
Unregistered
|
|
"I said that extending Point2D to produce Point3D and use equals() you describe is incorrect. And I referred to the work of Liskov to explain it."
And I used executable examples, in the form of the source code described in the article and in the downloadable resources to show that even when using instanceof, you don't get plug-and-play substitutionallity as you are expecting.
You seem to misunderstand the Liskov principle, referenced by citeseer at: http://citeseer.ist.psu.edu/liskov94family.html
What it says is that the behaviour is described in terms of its pre- and post- contracts of the methods. Specifically, (on page 14):
2. Subtype methods preserve the supertype methods' behaviour. The following rules must hold: 2a. Signature rule: Must have the same arguments; the result of the sub-method must be a subtype of the super-method; the exceptions in sub-method must be a subset of the super-method 2b. Methods rule: for all methods, the pre-condition of the super-method must imply the pre-condition of the sub-method, and the post-condition of the sub-method must imply the post-condition of the super-method.
Along with other condititions described here, he then goes on to say that provided the sub-type obeys all of these requirements, then it can be subsitituted for instances of the supertype.
What this means is that the so-called substitution principle has always been about preserving the behaviour in terms of its contractual definitition, which is explicitly described in the Object specification.
This example was shown in the ones I quoted; for example, both LinkedList and HashSet are Collections, with the result that they both obey the (implicit) contract that is described in the Collection class, and thus are interchangeable. The fact that they both obey the same contract doesn't imply that they do the same thing, and this seems to be a major misunderstanding of the Liskov Substitution Principle, especially when it has been re-written in different forms.
You also said: "Point2D class specifies/declares what equals() for each Point2D means, and each subclass must respect this contract. You provided a rationalization of your approach by ignoring this contract in Point3D, which extends Point2D. So, your reasoning is flawed."
The equals() contract is defined by Object, which ALL objects must obey. Point2D then adds further contractual behaviour to it. Point3D then adds further contractual behaviour to it. This is allowable by the specification of subtyping; it does not say 'The sub-method's contract must be exactly the same'; rather it says 'The sub-method's contract must imply the super-method's contract'. So what Point3D is doing is it is further refining the contract to provide a tighter specification. In general, any overridden method may tighten, but not weaken, the super-method's specification. This is precisely what the code does.
Lastly, take the time to look at (and run) the executable examples shown in the resources and see for yourself what erroneous behaviour occurs when using 'instanceof' (there's Point and BadPoint examples in there that shows both). No matter how much you try to argue that using 'instanceof' is more correct, the code stands for itself.
|
Pinocio
Unregistered
|
|
You wrote: "The equals() contract is defined by Object, which ALL objects must obey. Point2D then adds further contractual behaviour to it. Point3D then adds further contractual behaviour to it. This is allowable by the specification of subtyping; it does not say 'The sub-method's contract must be exactly the same'; rather it says 'The sub-method's contract must imply the super-method's contract'. So what Point3D is doing is it is further refining the contract to provide a tighter specification. In general, any overridden method may tighten, but not weaken, the super-method's specification. This is precisely what the code does."
You have described it right, but you do not apply it correctly in your article. Your Point3D violates the contract in Point2D not just adds new behavior. Point2D equals() contract says when any two Point2D instance are equals, but Point3D (which IS Point2D) changed this contract. Here is the code demonstrating the danger:
void example(Point2D aFirstPoint, Point2D aSecondPoint) { if (aFirstPoint.equals(aSecondPoint)) { paint(); } }
This method relies on this contract. If somewhere in the program you decide to pass an instance of Point3D (which extends Point2D) as the second parameter, paint() will not be called, because Point3D implementation violates the contact of Point2D.
Next. Sure, subclasses add the new behavior and may completely change some methods, but they should not change public contacts defined in the superclass.
You are trying to prove your point with incorrect design, and if readers, who only starting learning java, embrace this approach with getClass(), it will cause a lot of harm. It can be done safely only if the class is final. Despite this mistake, I would always welcome your attempt to justify usage of getClass() if you can.
I would also recommend you to look into the code of JDK. They tend to use either instanceof or the runtime analog of instanceof.
|
Alex Blewitt
Unregistered
|
|
I will hereby conclude that using 'instanceof' is bad, and 'getClass()' is the only correct way of implementing equals. Less interested readers may want to skip to the conclusion at the end :-).
Pinocio's argument is based on the following assumptions:
A1. Equality needs to compare like-for-subtype. A2. LSP holds when you use an instanceof implementation of .equals(). A3. LSP holds in general as a good design principle. A4. That the Java programming language solely uses good design in its implementation, and thus reading examples of coding in the Java language will be examples of good design.
The following are unarguable statements of fact:
S1. Using '.getClass()' provides an implementation of a type-for-type comparison; specifically, it denies type-for-subtype comparisons. S2. Using 'instanceof' provides behaviour where type-for-subtype comparison in one direction, as well as type-for-type comparisons. S3. The equals contract indicates whether some other object is "equal to" this one. The contract is unambiguous in its specification: It provides an equivalence relation on non-null references [i.e., it is reflexive, symmetric, and transitive, and that equals(null) is false] S3a. It is reflexive; i.e. x.equals(x) is always true S3b. It is symmetric; i.e. x.equals(y) should return true IF AND ONLY IF y.equals(x) returns true S3c. It is transitive; i.e. x.equals(y) and y.equals(z) implies x.equals(z) S3d. It is consistent; that is, they return the same value over multiple calls with the same arguments S3e. For any non-null x, x.equals(null) returns false.
S1 and S2 are observations about the behaviour of the code examples as shown. S3 is from the equals() method spec, as linked in the references.
I will conclude this argument by showing:
P1. Equality should only compare type-for-type; using type-for-subtype leads to a violation of the equality contract (either symmetry or transitivity). P2. LSP isn't held for an implementation of 'instanceof', despite this being the main argument proposed for using it. P3. LSP isn't held for a vast number of Java objects in any case. P4. That LSP is not a proven theory; it was a postulated principle. P5. The interpretation of LSP applying to Java classes and the extends relation is in fact false.
--- Proof follows ---
Pinocio wrote: "You have described [the contract specifications] right, but you do not apply it correctly in your article. Your Point3D violates the contract in Point2D not just adds new behavior. Point2D equals() contract says when any two Point2D instance are equals, but Point3D (which IS Point2D) changed this contract."
No, it doesn't. You misunderstood the contract. Statements S1 and S2 describe two behaviours that are incompatible with each other. You cannot conclude that S1 is false because you assume S2 is true; conversely, it is not possible for me to conclude that S2 is true because I assume S1 to be false. All the description of the equality method is that it "indicates whether some other object is "equal to" this one." (S3), from which it is possible to prove neither S1 nor S2.
The contract, as shown by the good code example in the article, explicitly states that the two types must be the SAME (S1). Thus, Point2D can be compared with Point2D, and Point3D can be compared with Point3D, but it is never the case that Point2D can be compared with Point3D. In fact, this is clear from the definition of a point in 2-d space and one in 3-d space; a single (x,y) co-ordinate defines a line in 3-d space; and since a line and a point are different, they are incomparable. Your assumption A1 does negate S1, but you fail to show that your assumption A1 is valid.
You understood it as:
"If somewhere in the program you decide to pass an instance of Point3D (which extends Point2D) as the second parameter, paint() will not be called, because Point3D implementation violates the contact of Point2D." (A1)
An instance of Point2D can NEVER be equal to an instance of Point3D. For some strange reason, you are assuming (A1) that it can; then claiming that the code does not do what you expect (S2). You are correct in observing that .getClass() does not permit this; however, you never discharge your assumption A1.
Consider other implementations of Point/3D:
public class Point { int points[]; public Point(int x, int y) { points = new int[2]; points[0] = x; points[1] = y; } public Point3D(int x, int y, int z) { points = new int[3]; points[0] = x; points[1] = y; points[2] = z; } }
-- OR --
public abstract class Point { int x, y; }
public class Point2D extends Point { } public class Point3D extends Point { int z; }
(with obvious methods for constructors and so forth missed out).
In both these cases, regardless of whether 'instanceof' or '.getClass()' is used to compare them, an instance of a 2-D point will never be equal to a 3-D point. Why should it be any different when implemented as shown in the article? The answer; it shouldn't. They are DIFFERENT types, even though one inherits from another.
These examples, including the Point in the article, are all showing the same thing. Using an implementation of S1 will give consistent behaviour for any of these implementations; using S2 will result in inconsistencies depending on which implementation is chosen. Thus, S2 is negates the equality contract (S3d) if the implementation is refactored.
Let's take the devil's advocate approach. Assume A2, that your interpretation of LSP is correct, and that using instanceof will guarantee the LSP behaviour.
You cite as your example:
"void example(Point2D aFirstPoint, Point2D aSecondPoint) if (aFirstPoint.equals(aSecondPoint)) paint();"
and then claim that using 'instanceof' is the only implementation that gives the same result substituting aSecondPoint with an instance of Point3D. Whilst this is true, the reverse is not; I can substitute aFirstPoint with an instance of Point3D (aSecondPoint being Point2D) and get different behaviour than if both were Point2D.
This is an example of the asymmetry implicit in the 'instanceof' operator, and this asymmetry violates S3b. This is enough to show that A1 and A2 do not hold.
Importantly, neither implementation (getClass/instanceof) gives the so-desired LSP property.
A counter example has been found that invalidates your assumption A2; using neither 'instanceof' nor '.getClass()' provides the behaviour of the LSP.
So now we know A2 to be false; this proves P2.
I go further to show that A3 is also false; that LSP holds as a good design for OO systems. I do so by again showing a counter example (of which there are many hundreds in the Java language). Recall again that Pinocio's interpretation of the LSP is:
"if you decide to create a class B extending the class A , then you must be able to SUBSTITUTE an instance of the class A with the instance of class B anywhere in the program"
I have shown above that this does not hold using implementation of 'instanceof' for equals (and acknowledge that using 'getClass()' it also does not hold). Now I will show that the LSP does not hold of many other objects:
o Object defines .toString() o Object's default implementation of .toString() returns results similar to "123456@java.lang.Object" o Non-object subclasses override .toString() to change behaviour to print out a more meaningful result
Thus, I can write a program:
public boolean LSPHoldsFor(Object object) { return object.toString().contains("@"); }
When I call the code with:
LSPHoldsFor(new Object());
I get the desired result 'true'.
Now I replace it with an subtype of Object. I choose, at random, a String/Vector/LinkedList/ArrayList/HashSet/SortedSet/Component ...
LSPHoldsFor(new String() / new Vector() / new LinkedList() / new ArrayList() / ... )
ALL return false.
I can do the same thing using your 'compare' method, too:
compare(new Object(), new String()); -> returns false compare(new String(), new String()); -> returns true
So, either A3 is correct, and that String/Vector/LinkedList/ArrayList... are all incorrectly implemented, or assumption A3 is incorrect and the code works correctly.
A3 is incorrect because (in general) every overridden method exists to change the behaviour of the superclass. Such behaviour would be visible to a caller; and since overridden methods are in classes that are a sub-type of an overridden class, the use of overriding will (in general) violate the LSP EVERY TIME. Note that the methods are not actually implemented incorrectly; but whereas the LSP assumes exactly the same behaviour, in actual fact they obey its contract. You can have two methods, implemented differently (and that give the same result), but that obey the same contract. (Consider the implementation of 'add' in 'HashSet' and 'TreeSet'. They both obey the same contract -- they are inserted into the collection without duplicates -- but they work in a different manner that is visible to the outside.)
You cannot even argue that Object should be considered a special case; there is nothing in your interpretation of the LSP that deals with special cases. Even if it did, you can come up with further counter-claims:
LSPHoldsFor(Component component) { return component instanceof JComponent; }
This returns true for some instances of Component, but not others.
LSPHoldsFor(new Component()); -> true LSPHoldsFor(new JButton()); -> false
Two entirely unrelated examples disproving your interpretation of the LSP; and only one is needed to disprove your theory. Therefore A3 is proven false; point P3 is proven.
[As a note; the reason why this is the case is because (a) the LSP was never proven as a theory, only a postulated goal of something to work towards, and (b) that the LSP talked in terms of mathematical types, and defined a relation (<) between them. At no point did Liskov (or others) claim that these types were Java (or any OO language) classes, and they certainly did not state that the relation (<) was the 'extends' relationship with Java. Common mis-interpretations of the LSP statement have come to result in Pinocio's interpretation of the LSP, which has now been demonstrably falsified using Java classes with the (<) relation. The statement actually began: "What is wanted here is something like the following substitution property:" -- it was never proven, only postulated. Hence P4 and P5.]
We now have A1, A2 and A3 proven false.
You cite: "I would also recommend you to look into the code of JDK. They tend to use either instanceof or the runtime analog of instanceof." This is based on assumption A4, that the Java code libraries consist solely of good examples.
They don't. Some examples of bad code in the Java language:
o Stack is a subclass of Vector. It shouldn't be a subclass; it should be composition, with an instance of Vector being held in an instance of Stack. o Date. Despite many pages of the virtues of UTC over GMT (despite the fact that most computers sync every few hours), they then go and make the hideous mistake of considering this the year '104', and indexing months from 0..11 o The AWT Thread being a non-daemon thread, despite the fact that (a) daemon thread capability is what lets a VM shut down after all non-user activity has finished, and (b) the fact that prior to Swing, the AWT was a daemon thread. That's why everyone has to call System.exit() when using a GUI, because of some fool's implementation issue, and not fixed (Java bug 4030718) despite implementations showing that it can occur. [I wrote a mechanism that loaded and re-wrote the classes.zip file at the time to show it was possible.] o Inconsistent naming of 'size()' and 'length()' and '.length' instead of a consistent '.getSize()'
Here are four counter-examples of your assumption that Java = good design. I suggest that to become familiar with good design, you practice in other OO languages such as Eiffel or Objective C. Thus A4 is now falsified.
So, we have assumptions A1-A4 falsified.
On what grounds does the choice of 'instanceof' or 'getClass()' now reside?
instanceof provides LSP: proven false, not applicable instanceof is asymmetric; proven true, a violation of the equals contract
These are the only two arguments that Pinocio has ever used to substantiate using instanceof. In both cases, these have been proven false.
Further, I have shown that using 'instanceof' results in an asymmetric behaviour; which is a violation of S3b. In fact, only '.getClass()' results in symmetric behaviour.
We now have the last point, P1. That is, the implementation of equals itself.
The SPEC just says "whether some other object is "equal to" this one". The CONTRACT is listed above in S3.
The equals method therefore says nothing of the type of the argument; just whether it obeys the CONTRACT.
Point2D p2d = new Point2D(1,1); Point3D p3d = new Point3D(1,1,1); p2d.equals(p3d);
Any implementation (using getClass/instanceof/a new and funky method found in the future) that gives the result 'true' WILL VIOLATE symmetry. Thus;
assert(p2d.equals(p3d) == p3d.equals(p2d));
The only way this will work is if in BOTH cases 'false' is returned. If one returns true, the other will return false.
In fact, if it were possible to arrange this (and yes, I can see a way -- left for the reader as an exercise) then you could have:
Point2D p2d = new Point2D(1,1); Point3D p3d1 = new Point3D(1,1,1); Point3D p3d2 = new Point3D(1,1,2);
assert(p2d.equals(p3d1) == p3d1.equals(p2d)); // even if hypothetically true assert(p2d.equals(p3d2) == p3d2.equals(p2d)); // even if hypothetically true assert(p3d1.equals(p3d2)); // false assert(p3d2.equals(p3d1)); // false
Thus breaking transitivity (S3c).
Note that this has appealed taking the two objects, without even relying on any particular hierarchy. It does not even matter which implementation of .equals() you use, because once you allow like-for-subtype comparisons, you break symmetry (S3b) OR transitivity (S3c).
Thus, the only way to provide both S3b and S3c is using an implementation which forbids like-for-subtype comparisons. This proves my final point P1; and therefore confirms that '.getClass()' is the only way that you can write the .equals() contract because of S1.
Conclusion ==========
I have now shown that arguments based on the LSP as a reason for bending the equality contract are false; because the LSP does not hold in general. I have also shown in any case that 'instanceof' does not obey the LSP in an equality relation; so even if the LSP were true in general, 'instanceof' is still not the solution. Further, I have shown by direct appeal of the equality contract that (regardless of implementation) it is not safe to compare like-for-subtype; as soon as you admit that, you break either symmetry or transitivity. And there is no way that breaking an object's contract can ever be seen as a correct implementation.
I'd like to thank Pinocio for delving deeper down into this thread, because it has made me work on expressing these ideas in a more detailed manner; hopefully this will stand as a record of the future for the correct implementation of the equals method. Of course, I invite you to expose any flaws in the reasoning; however, you will not find any logical flaws, just presentational ones.
|
FatBloke
Unregistered
|
|
"a co-ordinate of [1,1] in 3D space is [1,1,0]" is not true. A co-ordinate of [1,1] in 3D space is [1, 1, undefined]. Sorry. The rest of the discussion was very useful and thankyou to you both.
|
Quartz
Unregistered
|
|
I agree with the fact that .equals should be symmetrical. The problem is that .getClass().equals() is not symmetrical if you have proxies. For instance, if you're using Hibernate, Spring, AspectJ, etc, 2 objects loaded from the database might be the same but the class name is not. What to do in those cases?
[]s Gustavo
|
Nicky Bodentien
Unregistered
|
|
It is my opinion (at current time) that if you write a superclass that requires the use of getClass to define equals, then your class hierarchy should be regarded with some suspicion.
The example where Point3D is a specialization of Point2D is illustrative. As you correctly state, a point in space IS NOT a point in the plane. So in this case, it is correct to use getClass in equals to demand type equality. But if Point3D is never a Point2D (they can't even be equal), then perhaps neither should be a subtype of the other. This is based on the, admittedly purist, opinion that S should only be a subtype of T if S is-a T.
In fact, I think that demanding type equality in the equals method is the same as saying that "no subtype instance can ever be (equal to) a supertype instance", or, in other words, that the is-a relationship doesn't hold.
There ARE examples where objects of different types are equal in a very meaningful way. Consider a Point2D, which represents a point in the plane by means of an x- and y- coordinate. Consider the subsequent addition of RadialPoint2D, which ALSO represents a point in the plane, but by means of an angle and distance from origo. Now, if a given Point2D and a given RadialPoint2D both represent the same point in the plane they are mathematically equivalent, and it makes sense in every possible way to deem them equal. Instanceof is ideal for this purpose.
So, in general: If a portion of your class hierarchy represents an is-a relationship, you should seriously consider defining equals using only instanceof. If, on the other hand, it does not hold that the subclass instance is-a superclass instance (which can certainly be convenient as well), you should consider defining equals using getClass, not defining equals at all, or rethinking your class hierarchy. All three have its merits.
Copyright 2004 Nicky Bodentien. Permission granted to publish this on the JavaWorld site.
|
Anonymous
Unregistered
|
|
The main issue I had with this article was in regards to the relationship between Point2D and Point3D. I agree with the argument that this should not be an is-a relationship. Just becuase they have similar behaviors, or contain some equal internal data-structure does not in anyway prove them to be sub-types of each other. The only logical step to make would be a has-a relationship, and a poor one at best. A Point3D then has-a Point2D.
One of the major issues I have with most so called "Object Oriented Programmer's" are their lack of understanding of what it means to be one. By creating short cuts and forcing unnatural relationships among objects, and yet expecting the code to function properly, while being easily managed: this simply is not OOP. Refering back to the Point2D/3D relationship, asking them to be drawn onto the screen would point out their inability to be related, or doing simple addition or subtraction among them would also destroy the is-a relationship.
In the case pointed out in the article, you are correct in assuming they should not use instanceof, but as stated by another commentor, if you need to find the extact implementing class, then is-a relationship becomes invalid. You should have never extended the Point2D class if you never intended to use the super class directly with that relationship in mind.
The author tries to make a sound point from a weak example, and finds himself struggling to hold onto his argument. I think you did bring to light the other contracts that were not apparent to me regarding equals(Object obj), and I do thank you for that. But, to be honest, the article left me doubtful of any future articles from you to be read. So, to Pinocio, congrats on pointing out the flaw, and congrats to Nicky for clarifying the point (no pun intended).
Greg (I should really consider signing up, if I intend to comment).
|
cyranoVR
Unregistered
|
|
Quote:
I agree with the fact that .equals should be symmetrical. The problem is that .getClass().equals() is not symmetrical if you have proxies. For instance, if you're using Hibernate, Spring, AspectJ, etc, 2 objects loaded from the database might be the same but the class name is not. What to do in those cases? []s Gustavo
I encountered that very problem - Hibernate subclassed my objects at runtime and broke my application, due to reliance on a getClass()-based equals() implementation.
I agree with Nicky - requiring getClass() is a symptom of abuse of OOP. It indicates that you are using inheritance to reduce time at the keyboard, rather than defining a correct class hierarchy. So, your equals() implementation fulfills its contract, but (oops) now your Class Hierarchy is wrong. If a Point3D can't ever be a Point2D, perhaps it shouldn't inherit from it, hmm? In fact, the author aludes to that the correct implementation would be to have Point3D delegate its -2D operations to a Point2D field - but inexplicitly forgot this point to pursue his largely pedantic proof of the correctness of the getClass()-based strategy.
Don't get me wrong, I appreciate that his proof is thorough and solid. However, I have an equally solid proof of my own: namely, Hibernate dynamically subclassed my classes at runtime and broke my getClass()-reliant application. Imagine trying to explain to your Manager that the error page they are seeing is expected because the underlying code is semantically correct - even though it violates your Business Requirements! Now imagine getting a pink slip.
For more information on Hibernate specific issues, you can check out this thread:
Hiberate Forums: equals() / hashCode(): Is there *any* non-broken approach?
For everybody else, I would offer the following bit of advice: if using instanceof in your equals() implementation violates symmetry, then you probably are using inheritance for the wrong reasons.
|
Alex Hristov
Unregistered
|
|
Besides being full of logical incosistencies, I find your attitude to be quite opinionated and you greately overestimate the logical consistency of your "proofs".
First, a counter-example of your kernel assertion that "P1. Equality should only compare type-for-type; using type-for-subtype leads to a violation of the equality contract (either symmetry or transitivity)."
(Code has been reduced in lieu of simplicity)
Code:
public class Rectangle { public int sideA; public int sideB; public Rectangle(int a, int b) { sideA=a;sideB=b; } public boolean equals(Object o) { if (o == null) return false; if (!(o instanceof Rectangle) ) return false; return ((Rectangle)o).sideA == sideA && ((Rectangle)o).sideB == sideB; } }
public class Square extends Rectangle { public Square(int side) { super(side,side); } public boolean equals(Object o) { if (o == null) return false; if (o instanceof Rectangle) return ((Rectangle)o).sideA == sideA && ((Rectangle)o).sideB == sideB; return false; } }
public class Icon extends Square { public static final int ICON_SIZE = 32; public Icon() { super(ICON_SIZE); } public boolean equals(Object o) { return super.equals(o) && ((Rectangle)o).sideA == ICON_SIZE; }
}
Well, this example certainly fulfils all contrats, and does NOT compare type-for-type. As you are *so* fond of saying, one counter-example topples a theory. Well, so long to P1.
You want an example with attribute-extension? Sure:
Let A,B,C be types defined so that A = {c1,c2,...,cN,a1,a2,...,aM}, B = {c1,c2,...,cN,b1,b2,...,bR} and C = {c1,c2,...,cN}, where ci,ai and bi are typed fields for which the = operator is defined with the customary properties. We define the "=" operator on these types to act as follows :
T1 = T2 <-> T1.ci = T2.ci for every i=1..N.
where T1 and T2 are instances of any of the above types, and the notation T.x represents the value of field x in the instance T
1) Reflexivity For every i T1.ci = T1.ci -> T1 = T1
2) Symmetry T1 = T2 -> T1.ci = T2.ci (for every i) -> T2.ci = T1.ci (for every i) -> T2 = T1
3) Transitivity T1 = T2 -> T1.ci = T2.ci for every i T2 = T3 -> T2.ci = T3.ci for every i From the transitivity of = on elements of the types follows that T1.ci = T3.ci for every i, hence T1 = T3.
The = operator defined as above fulfils all contractual obligations. Whether the results of a=b according to this definition make sense *to you* or not is your *OPINION*, nothing more, nothing less, and your opinion certainly doesn't constitute any kind of proof. For *me* it is perfectly fine to say that
Person:{SSN='1234'} = Teacher:{SSN='1234',Name='Peter'} = Parent:{SSN='1234',Son='John'}
and to say that these three instances are equal as per the above definition of that operator. By the way, I have just proved false the other part of your assertion, that "using type-for-subtype leads to a violation of the equality contract".
The rest of the message is just a sequence of generalization fallacies,
1. "Pinocio interprets X to be Z" ("1. Peter interprets the mass-energy equivalence to be that apples are animals") 2. "Z is wrong" ("2. Apples are not animals") 3. "Therefore X is wrong". ("3. Therefore the mass-energy equivalence is wrong")
or, altenratively,
"X for the (botched) design Point3D extends Point2D -> X in general" ("There isn't any meaningful and compliant definition of equals using instanceof in the Point2D/3D example THEREFORE there isn't any meaningful and compliant definition of equals using instanceof in ANY case")
Non sequitors :
1. ' Object's default implementation of .toString() returns results similar to "123456@java.lang.Object" ' 2. Here is some function that relies on a non-contractual behaviour of Object 3. Therefore LSP is not good design.
and mis-representations like:
"(in general) every overridden method exists to change the behaviour of the superclass.Such behaviour would be visible to a caller, and since overridden methods are in classes that are a sub-type of an overridden class, the use of overriding will (in general) violate the LSP EVERY TIME".
Wow!. This one deserves sepcial mention. LSP deals with CONTRACTUAL GUARANTEES in terms of preconditions and postconditions. An overriden method CAN change the behaviour of the ancestor method, but it CANNOT strengthen the preconditions and cannot relax the postconditions. If a program is written so that it relies on non-contractual behaviour, it's not the LSP the one who's wrong, it's the program that is crap. Even if LSP did not exist, the program would still be crap.
|
Alex Hristov
Unregistered
|
|
There's a typo in the above Rectangle-Square-Icon example. The equals of Square should be:
Code:
return ((Rectangle)o).sideA == sideA && ((Rectangle)o).sideB == ((Rectangle)o).sideA;
|
|