-
Notifications
You must be signed in to change notification settings - Fork 554
8289389: Fix warnings: type should also implement hashCode() since it overrides Object.equals() #821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
8289389: Fix warnings: type should also implement hashCode() since it overrides Object.equals() #821
Conversation
|
👋 Welcome back andy-goryachev-oracle! A progress list of the required criteria for merging this PR into |
Webrevs
|
kevinrushforth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a complete review, just one thing I spotted quickly.
|
Just a general note, shouldn't most of these use I see a lot of code in the form of: And even a few where hashes are calculated manually for two values: These could be written as: |
| h = 31 * h + value.hashCode(); | ||
| } | ||
| return h; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an example, but wouldn't: Objects.hash(relative, origin, value) here work just as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I would just turn this class into a record and everything will be taken care of. I'm not sure it is within the scope of the fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such a change is definitely out of scope. More importantly, we can't use record anywhere in JavaFX until we bump the minimum version of the JDK needed to run JavaFX. I plan to start a discussion on the openjfx-dev mailing list, since it has been something I've wanted to do for a while now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hjohn: yes, but at a price: Object.hash(Object ...) incurs overhead by creating a temporary Object[] + boxing of a Boolean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't realize you checked how this is optimized by the JIT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to reduce collisions, the hash of each component is typically added to h * 31 even when that hash is 0, whereas you skip the h = 31 * h in the case of null. It might not be a problem in practice, since value and origin are unlikely to collide, being of different types, but you might want to look at it.
In any case, I need to time to look at it, which I won't have until after JavaFX 19 RDP1, so let's leave this until then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you bring a good point, Kevin, thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use Boolean.hashCode(relative); for a boolean.
Kevin, I checked Effective Java 3rd Edition and it also says to use 0 (or some other constant) for null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using 0 for a null component of the hash is fine, as long as the accum variable is initialized to something > 0, and as long as you accumulate all values, even a 0. By which I mean that the following is not OK:
int h = 1;
if (comp1 != null) h = 31 * h + comp1.hashCode();
if (comp2 != null) h = 31 * h + comp2.hashCode();
nor is this:
int h = (comp1 == null) ? 0 : comp1.hashCode();
h = 31 * h + (comp2 == null) ? 0 : comp2.hashCode();
but this is:
int h = 1;
h = 31 * h + (comp1 == null) ? 0 : comp1.hashCode();
h = 31 * h + (comp2 == null) ? 0 : comp2.hashCode();
Anyway, the latest change in this PR is good.
nlisker
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except for one case, looks good. I think that nearly all of the equals implementations are not the best (or just wrong), but it's outside the scope of the PR.
| if (obj == null) return false; | ||
| return id == ((RT22599_DataType)obj).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I note that the equals method here is wrong. The hashCode implementation is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does look dubious. Since this was written for a specific unit test, it might have been intentional to have a "wrong" equals method, but if so, a comment should have been added to that effect. In any case, fixing equals is out of scope for this PR. And given what equals does, the proposed hashCode method is correct.
| if (obj == null) return false; | ||
| return id == ((RT22599_DataType)obj).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as ListViewTest.
| if (obj == null) return false; | ||
| return id == ((RT22599_DataType)obj).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as ListViewTest.
| if (obj == null) return false; | ||
| return id == ((RT22599_DataType)obj).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as ListViewTest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this and ListViewTest - the logic in hashCode() follows the logic in equals().
it is fairly safe change, as these objects are never put into a hashtable and never accessed outside of the test context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
| return false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as ListViewTest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the case of FXMLoader, hashCode() also follows the logic of equals().
However, we could mix something specific to FXMLoader to the hash, to avoid collision with origin URL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine as you now have it. If anyone feels it is worth it, we could file a follow-on issue to look into whether equals should be changed, but I don't know whether it is worth it.
| h = 31 * h + value.hashCode(); | ||
| } | ||
| return h; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use Boolean.hashCode(relative); for a boolean.
Kevin, I checked Effective Java 3rd Edition and it also says to use 0 (or some other constant) for null.
| public int hashCode() { | ||
| return sourceClip.hashCode(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you ignoring the significant fields? equals compares priority, loopCount, volume, balance, rate and pan in addition.
| if (player == null) { | ||
| return 0; | ||
| } | ||
| return player.hashCode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, but can also be a ternary return player == null ? 0 : player.hashCode();
|
I really must wonder at some of these changes. They seem... unnecessary. Just because equals is overridden, doesn't mean hashCode must be as well, especially not if the objects involved are mutable -- it might in fact be better to leave it alone or return a constant value from it (or only use immutable fields). For example, let's take TLDR; don't use mutable fields in |
|
John, thank you for your comments! I fully agree with the idea of not putting objects with mutable fields that participate in either equals() or hashCode() into Hashtable. This scenario might still be legitimate, for example by not modifying the fields in question after instantiation. I also agree that some of the changes might look unnecessary, or perhaps we should either return super.hashCode() or maybe getClass().hashCode() -- not a constant, as hashCode() guarantees some variability, while a constant does not. I do think, however, that suppressing this warning or turning it off is not a good idea. it forces the developer to think about exactly this issue. Do you have any specific objections? |
Risky, but that's on the user... they'll have to check the source code to see how hashCode is implemented before doing this kind of thing.
Yes,
Yeah, I suppose you can "surpress" the warning by overriding it with I think many of these have |
This not true - there's a contract between
|
Thanks, I'm well aware. The issue here is that once a mutable object is stored in a map, you can have either:
This isn't mentioned anywhere aside from the small note ("provided no information used in {@code equals} comparisons on the object is modified"). |
|
John: You do bring a good point, I am sure someone have tripped this trap by adding a mutable object to a hashtable and then mutating it afterwards. We do have some words of caution in the java.util.Map interface javadoc: "Note: great care must be exercised if mutable objects are used as map keys..." At the same time, I feel like this discussion goes beyond the scope of this PR, as it falls under the rubric of design decisions in the client code. Is there anything specific you think should be changed in this PR, or are you against these code changes in principle? |
|
Yes, it's beyond the scope of PR. I suppose people will have to be careful as with all classes that are mutable and have decided to override equals/hashCode -- no reason to stop now I suppose. |
kevinrushforth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good now. I noted a couple places that might need a change.
| if (obj == null) return false; | ||
| return id == ((RT22599_DataType)obj).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does look dubious. Since this was written for a specific unit test, it might have been intentional to have a "wrong" equals method, but if so, a comment should have been added to that effect. In any case, fixing equals is out of scope for this PR. And given what equals does, the proposed hashCode method is correct.
| if (obj == null) return false; | ||
| return id == ((RT22599_DataType)obj).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
| return false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine as you now have it. If anyone feels it is worth it, we could file a follow-on issue to look into whether equals should be changed, but I don't know whether it is worth it.
| h = 31 * h + value.hashCode(); | ||
| } | ||
| return h; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using 0 for a null component of the hash is fine, as long as the accum variable is initialized to something > 0, and as long as you accumulate all values, even a 0. By which I mean that the following is not OK:
int h = 1;
if (comp1 != null) h = 31 * h + comp1.hashCode();
if (comp2 != null) h = 31 * h + comp2.hashCode();
nor is this:
int h = (comp1 == null) ? 0 : comp1.hashCode();
h = 31 * h + (comp2 == null) ? 0 : comp2.hashCode();
but this is:
int h = 1;
h = 31 * h + (comp1 == null) ? 0 : comp1.hashCode();
h = 31 * h + (comp2 == null) ? 0 : comp2.hashCode();
Anyway, the latest change in this PR is good.
|
|
||
| @Override | ||
| public int hashCode() { | ||
| int h = sourceClip.getLocator().getURI().hashCode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is URI::hashCode() guaranteed to return a non-zero value? If not, then initializing h to 1 and accumulating this would be better.
|
|
||
| @Override | ||
| public int hashCode() { | ||
| int h = Float.floatToIntBits(x); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best to initialize h to 1 and accumulate x.
|
excellent suggestions, Kevin - many thanks! |
kevinrushforth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good.
I note that by initializing the hash to CLASSNAME.class.hashCode(), instead of a constant, the hash code produced will not be predictable or stable across different invocations of the JVM. This is entirely permissible by the spec of Object.hashCode(), so it is just a (somewhat) interesting note.
|
@andy-goryachev-oracle This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be: You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 12 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. As you do not have Committer status in this project an existing Committer must agree to sponsor your change. Possible candidates are the reviewers of this PR (@kevinrushforth, @nlisker) but any other Committer may sponsor as well. ➡️ To flag this PR as ready for integration with the above commit message, type |
I'd say this is a desired outcome. There is a group of vulnerabilities caused by predictive nature of hashCode, for example, a DOS attack which places large number of objects into the same hashtable bin, see I know this has no impact on this PR, just wanted to add add to your interesting note, Kevin. |
|
If there are no objections, I'd like to integrate this PR on Monday (25 July) |
|
/integrate |
|
@andy-goryachev-oracle |
|
/sponsor |
|
Going to push as commit 075cc80.
Your commit was automatically rebased without conflicts. |
|
@nlisker @andy-goryachev-oracle Pushed as commit 075cc80. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
Progress
Issue
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jfx pull/821/head:pull/821$ git checkout pull/821Update a local copy of the PR:
$ git checkout pull/821$ git pull https://git.openjdk.org/jfx pull/821/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 821View PR using the GUI difftool:
$ git pr show -t 821Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/821.diff