Java: clean way to automatically throw UnsupportedOperationException when calling hashCode() and equals()?
Asked Answered
S

4

14

We've got an OO codebase where in quite a lot of cases hashcode() and equals() simply don't work, mostly for the following reason:

There is no way to extend an instantiable class and add a value component while preserving the equals contract, unless you are willing to forgo the benefits of object-oriented abstraction.

That's a quote from "Effective Java" by Joshua Bloch and there's more on that subject in a great Artima article here:

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

And we're perfectly fine with that, this is not what this question is about.

The question is: seen that it is a fact that in some case you cannot satisfy the equals() contract, what would be a clean way to automatically make hashcode() and equals() throw an UnsupportedOperationException?

Would an annotation work? I'm thinking about something like @NotNull: every @NotNull contract violation does throw an exception automatically and you have nothing else to do besides annotating your parameters/return value with @NotNull.

It's convenient, because it's 8 characters ("@NotNull") instead of constantly repeating the same verification/throw exception code.

In the case that I'm concerned about, in every implementation where hashCode()/equals() makes no sense, we're always repeating the same thing:

@Override
public int hashCode() {
    throw new UnsupportedOperationException( "contract violation: calling hashCode() on such an object makes no sense" );
}

@Override
public boolean equals( Object o ) {
    throw new UnsupportedOperationException( "contract violation: calling equals() on such an object makes no sense" );
}

However this is error prone: we may by mistake forget to cut/paste this and it may results in users misusing such objects (say by trying to put them in the default Java collections).

Or if annotation can't be made to create this behavior, would AOP work?

Interestingly the real issue it the very presence of hashCode() and equals() at the top of the Java hierarchy which simply makes no sense in quite some cases. But then how do we deal with this problem cleanly?

Suggestible answered 5/2, 2010 at 7:11 Comment(6)
+1 for refusing to implement hashCode and equals when not needed, and even making sure they cannot be called by throwing an exception. This is a welcome change to the mantra you often hear that the first thing you must do is implement those two methods (and spend a lot of thought on them to make them work properly), even when most objects never need either method.Footed
A related peeve I have with the auto-generated methods that Eclipse gives you when you write a new class that implements an interface is that they are all generated to return null, return false, do nothing. I'd like the default to be throw UnsupportedOperationException("TODO").Footed
@Footed I do exactly that with my Eclipse templates, all generated method bodies throw UnsupportedOperationExceptionGlobate
@Thilo: I disagree. It violates the principle of least surprise. To be more specific, it could have unexpected side-effects when dealing with both the Java Framework and any third-party code. By default, .equals() is identical to == for classes, meaning that it checks if two variables point to the same reference. Which there is no harm to doing.Hashum
@Bemrose: I disagree. If you want to check if they're the same reference, you use == (which there's indeed no harm doing altough I don't tend to need to do that very often), not equals(), which would be very harmful. Because you cannot know if the equals() method has been overriden or not and so relying on Object's default equals() method using == instead of directly using == yourself is very harmful.Suggestible
@Thilo: i've changed my Eclipse templates to do exactly that. It also adds the current date, so i can tell how long methods have been unimplmented.Ephemeron
S
4

Why don't you let your IDE (Eclipse/NetBeans/IntelliJ) generate the hashCode() and equals() methods for you. They are doing quite a good job at it.

AOP will work, of course, but it's quite a complication. And this will mean you won't be able to use these objects with almost no collection or utility.

The other logical solution is to just remove the implementations of those methods where they do not work, thsus effectively leaving only the implementations in Object.

Sinnard answered 5/2, 2010 at 7:20 Comment(1)
@Bozho: +1, that's what Hemal Panda said he was doing in his comment and I didn't think about it (even tough we're using custom templates). I was too focused on having something like "@NotNull" :)Suggestible
A
11

I agree with your assessment of this being a problem with hashCode and equals being defined in Object in the first place. I've long held the view that equality should be handled in the same way as ordering - with an interface saying "I can be compared with an instance of X" and another saying "I can compare two instances of X".

On the other hand, has this actually caused any bugs for you? Have people been trying to use equals and hashCode where they shouldn't? Because even if you can make every single class in your codebase throw an exception when those methods are called inappropriately, that won't be true of other classes you're using, whether from the JDK or third party libraries.

I'm sure you could do this with AOP of some form or other, whether that's normal annotation processing or something else - but do you have evidence that the reward would be worth the effort?

Another way of looking at it: this is only in the case where you're extending another class which already overrides hashCode and equals, right? Otherwise you can use the "equality = identity" nature of Object's hashCode/equals methods, which can still be useful. Do you have very many classes which fall into this category? Could you not just write a unit test to find all such types via reflection, and check that those types throw an exception when you call hashCode/equals? (This could either be automated if they have a parameterless constructor, or have a manual list of types which have been checked - the unit test could fail if there's a new type which isn't on the "known good" list.)

Autonomous answered 5/2, 2010 at 7:19 Comment(3)
+1 for wanting a separate interface for equals/hashCode. That would prevent people from using objects as hash keys that just don't work there (an Immutable interface would also be nice in that regard).Footed
Hi Jon, regarding the reward/effort this is of course a concern: but the same probably could have been said if, when annotation came out, I suggested an @NotNull annotation. All it would take would be someone to write this once, then we could all reuse it. Interestingly I'm using custom templates and didn't think about simply making, by default, hashCode() and equals() throw the UnsupportedOperationException and then simply change the implementation for classes where equals() and hashCode() makes sense.Suggestible
I think nullability tends to be a much bigger concern. I've seen lots of bugs due to nullity concerns, but I can't remember ever trying to hash something which shouldn't be hashed.Autonomous
L
6

I don’t see why you think that "in some case you cannot satisfy the equals() contract"? The meaning of equality is defined by the class. Thus, using Object’s equal is perfectly valid. If you’re not overriding equals then you’re defining each instance as being unique.

There seems to be a misconception that equals is one of those methods that always needs overriding, and that it must check all of its fields. I would argue for the opposite – don’t override equals unless your definition of equality differs.

I also disagree with the artima article, in particular “Pitfall #3: Defining equals in terms of mutable fields”. It’s perfectly valid for a class to defined its equality based on mutable fields. It’s up the user to be aware of this when using it with collections. If a mutable object defines its equality on its mutable state, then don't expect two instances to be equals after one has changed.

I think that throwing UnsupportedOperation violates the sprint of equals. Object’s equals states:

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

So, I should be able to call equals and get a true or false value depending on either Object’s equals definition or the overridden equals definition.

Lemniscus answered 5/2, 2010 at 8:13 Comment(6)
+1: even if nothing else is useful, you could still apply object identity as the equality measure by simply using the equals/hashCode implementation in Object.Ambassador
@Joachim: Yes, but if object equality is not meaningful for his class, and he does not need equals/hashCode at all, throwing UnsupportedOperation just makes more sense. At the very least this helps finding errors when some part of the code mistakenly does call these methods.Footed
@Kuo: -1, I don't think it, I know it, and it has been proven :) You disagree with Martin Odersky (the Scala author) and Joshua Bloch. Fine. You're entitled to your opinions. It has been proven that you simply cannot guarantee the transitivity contract of equals(), for a start. Even the almighty Jon Skeet from SO fame and c.l.j.p. fame agrees with my statement that it's the very presence of equals() and hashCode() at the top of the object hierarchy that is a problem here. My concern is exactly what Thilo commented: where it makes no sense, it should throw an UnsupportedOperationException.Suggestible
@SyntaxT3rr0r: What exactly can be "proven"? It's obviously possible for a class to define hashCode and equals in broken fashion (e.g. have those functions return random values), but that in no way prevents properly-designed classes from implementing equals so as to behave transitively with any other properly-designed class. As for whether they belong in Object, I would posit that if X and Y can coexist in the same collection, it should be possible to ask X if it's equivalent to Y and that X should be able to answer without difficulty.Billy
@SyntaxT3rr0r: A bigger problem with equals and hashCode stems from the fact that Java makes no distinction between object references which are used to encapsulate identity, those which encapsulate only immutable attributes other than identity, those which encapsulate unshared mutable state, those which encapsulate references to instances of mutable type but won't expose those instances to anything which might mutate them, and those which encapsulate both mutable state and identity. For two collections to sensibly compare themselves to each other, they must know...Billy
...what the references therein represent.Billy
S
4

Why don't you let your IDE (Eclipse/NetBeans/IntelliJ) generate the hashCode() and equals() methods for you. They are doing quite a good job at it.

AOP will work, of course, but it's quite a complication. And this will mean you won't be able to use these objects with almost no collection or utility.

The other logical solution is to just remove the implementations of those methods where they do not work, thsus effectively leaving only the implementations in Object.

Sinnard answered 5/2, 2010 at 7:20 Comment(1)
@Bozho: +1, that's what Hemal Panda said he was doing in his comment and I didn't think about it (even tough we're using custom templates). I was too focused on having something like "@NotNull" :)Suggestible
B
1

There are at least two equivalence relations which can be defined between all objects in Java or .NET:

  • Two object references X and Y are fully equivalent if overwriting X with a reference to Y would not alter the present or future behavior of any members of X or Y.

  • Two object references X and Y have equivalent state if, in a program which has not persisted the values returned from identity-related hash function, swapping all references to X with all references to Y would leave program state unchanged.

I have one reference (X) which points to a FordBlazer. I have another (Y) which points to a SiameseCat. Are they equivalent? No they aren't, so X.equals(Y) should be false. The fact that the objects' types have no relationship to each other isn't a problem--if anything, it makes things easier (if the only thing that can be equivalent to a SiameseCat is another SiameseCat, the fact that Y isn't a SiameseCat means that X.equals() doesn't have to examine anything else.

While there may be some debate about whether a particular object should implement the first or second definition of equivalence, it's worth noting that any object which defines equals report distinct objects as unequal regardless of any other aspects of their state will be consistent with itself (if X.Equals(X) doesn't match X.Equals(Y), that means that Y doesn't behave the same as X). Thus, if one doesn't have anything better to do with equals, the default definition inherited from object is a perfectly good and meaningful one.

The only situation where hashCode might have trouble will be if code might (ill-advisedly) mutate some aspect of an object while it is stored in a HashTable. the proper remedy for that is to have hashCode not depend upon any mutable aspects of an object's state. If an object's state has no meaningful immutable aspects other than its class, simply make up an arbitrary number for that class and have hashCode always return that. Large hash tables will perform poorly with such objects, but small hash codes will work just fine. The fact that one can't define a good hash code for a type shouldn't prevent it from being used in a HashTable with a dozen or so items in it.

Billy answered 4/8, 2013 at 17:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.