Concerns about the plan for `==`

Brian Goetz brian.goetz at oracle.com
Fri Jun 24 15:04:17 UTC 2022


I don't have an answer for you, but I can add some information to the mix.

Currently there are _nine_ "implementations" of `==`; one for 
references, and one for each of the eight primitives. Regardless of 
whether or not they are perfect tests of substitutibility (curse you, 
floating point), the eight primitive `==` functions are highly 
domain-specific.  They can be so because the primitives are 
monomorphic.  In a sense, we've allowed primitives to "overload" `==` 
because monomorphism means we can define `==` with full knowledge of the 
domain, and without worry about non-well-definedness or the various 
other problems of `equals` in extensible class hierarchies (as EJ 
exhaustively catalogued.)

What's being proposed here is that we evolve `Object==` from "compare 
identities" to a case analysis, to account for the fact that Object will 
describe more things:

     case (IdentityObject a, IdentityObject b) -> identity==(a, b)
     case (ValueObject a, ValueObject b) -> (isNull(a) == isNull(b)) && 
(type(a) == type(b))
&& (state(a) == state(b))
     default -> false

Just as `identity==` was the best we could do as a default on 
polymorphic identity objects, this is the best we can do on polymorphic 
mixed identity/value objects.  (There's a whole digression into 
overloading `==` on value types, but I'm not going to go there right now.)

While we're not making the problem of "`==` is unreliable" better, and 
arguably making it incrementally worse by making it work in more cases 
that look a little like the cases in which it is unreliable, we *are* 
making something better here: you can now use `.equals()` everywhere.  
One of the complains about `==` is that sometimes you use `==` and 
sometimes you use `.equals()` and sometimes you can accidentally use one 
where you should use the other.  But this is because you couldn't 
previous use .equals() on primitives, so an `equals()` method would 
necessarily do things like:

     boolean equals(Object o) {
         return o instanceof Foo f
             && f.size == this.size
             && f.name.equals(this.name);
     }

What stinks here is that at each point, you have to ask yourself 
"equals, or =="?  Now you can have a fixed rule: always say `.equals()`:

     boolean equals(Object o) {
         return o instanceof Foo f
             && f.size.equals(this.size)   // works on int!
             && f.name.equals(this.name);
     }

(The equals method on primitives is monomorphic so will JIT away, for 
anyone worried about the performance.)

It is a little sad because we had to resolve the problem by using the 
unfortunate spelling all the time, because `==` got the good name, but 
that's not a new problem.  But it means the cognitive load can disappear 
if we train ourselves to uniformly use `.equals()`.

We will surely have about a million calls to make `===` or `eq` or 
something else sugar for `.equals()`.  We can consider that, but I don't 
think its essential to do that now.


On 6/15/2022 1:51 PM, Kevin Bourrillion wrote:
> What I think I understand so far:
>
> The current plan for `==` for all bucket 2+ types (except the 8 
> _primitive_ types, as I still use the word) is to have it perform a 
> fieldwise `==` comparison: identity equality for bucket 1 fields, what 
> it's always done for primitive fields, and of course recurse for the rest.
>
> If we consider that the broadest meaning of `a == b` has always been 
> "a and b are definitely absolutely indistinguishable no matter what", 
> then this plan seems to compatibly preserve that, which makes sense 
> for purposes of transition.
>
> What concerns me:
>
> It's good for transition, at least on the surface, but it's a bad 
> long-term outcome.
>
> Users hunger for a shorter way to write `.equals()`, and they will 
> think this is it. I would not underestimate the pushback they will 
> experience to writing it out the long way in cases where `==` at least 
> *seems* to do the right thing. Because in some number of cases, it 
> *will* do the same thing; specifically, if you can recurse through 
> your fields and never hit a type that overrides equals().
>
> This is extremely fragile. A legitimate change to one type can break 
> these expectations for all the types directly or indirectly depending 
> on it, no matter how far away.
>
> In supporting our Java users here, there's no good stance we can take 
> on it: if we forbid this practice and require them to call `.equals`, 
> we're being overzealous. If we try to help them use it carefully, at 
> best users will stop seeing `Object==Object` as a code smell (as we 
> have spent years training them to do) and then will start misusing it 
> even for reference types again.
>
> btw, why did I say it's good for transition "on the surface"? Because 
> for any class a user might migrate to bucket 2+, any existing calls to 
> `==` in the wild are extremely suspect and *should* be revisited 
> anyway; this is no less true here than it is for existing 
> synchronization etc. code.
>
> What's an alternative?:
>
> I'm sure what I propose is flawed, but I hope the core arguments are 
> compelling enough to at least help me fix it.
>
> The problem is that while we /can/ retcon `==` as described above, 
> it's not behavior anyone  really /wants/. So instead we double down on 
> the idea that non-primitive `==` has always been about identity and 
> must continue to be. That means it has to be invalid for bucket 2+ (at 
> compile-time for the .val type; failing later otherwise?).
>
> This would break some usages, but again, only at sites that deserve to 
> be reconsidered anyway. Some bugs will get fixed in the process. And 
> at least it's not the language upgrade itself that breaks them, only 
> the specific decision to move some type to new bucket. Lastly, we 
> don't need to break anyone abruptly; we can roll out warnings as I 
> proposed in the email "We need help to migrate from bucket 1 to 2".
>
> A non-record class that forgets to override equals() from Object even 
> upon migrating to bucket 2+ is also suspect. If nothing special is 
> done, it would fail at runtime just like any other usage of 
> `Foo.ref==Foo.ref`, and maybe that's fine.
>
> Again, I'm probably missing things, maybe even big things, but I'm 
> just trying to start a discussion. And if this can't happen I am just 
> searching for a solid understanding of why.
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-spec-observers/attachments/20220624/ecbb4f71/attachment.htm>


More information about the valhalla-spec-observers mailing list