There's also the question of which hash code. Do you use a fast but low quality hash function, or a slower but higher quality one? Does your hash function need to be secure against hash collision attacks? Does it have to be deterministic? The correct choice of hash function can depend on the data structure, and the same object might have to be hashed with different hash functions (or different hash function seeds) in the same program.
My experience with platform design has consistently been that handling version evolution in the presence of distant teams increases complexity by 10x, and it's not just about some mechanical notion of backwards compatibility. It's a particular constraint for Java because it supports separate compilation. This enables extremely fast edit/run cycles because you only have to recompile a minimal set of files, and means that downloading+installing a new library into your project can be done in a few seconds, but means you have to handle the case of a program in which different files were compiled at different times against different versions of each other.
[1] https://github.com/titzer/virgil/blob/master/lib/util/Map.v3
This is partly my style too; I try to avoid using maps for things unless they are really far flung, and the things that end up serving as keys in one place usually end up serving as keys in lots of other places too.
Java doesnt make this very composable
Rust’s approach to the Hash and Eq problem is to make them opt-in but provide a derive attribute that autoimplements them with minimal boilerplate for most types.
Also, Rust’s Hash::hash implementations don’t actually hash anything themselves, they just pass the relevant parts of the object to a Hasher passed as a parameter. This way types aren’t stuck with just a single hash implementation, and normal programmers don’t need to worry about primes and modular arithmetic.
Fully separating implementation of an interface from the data can create quite a lot of additional complexities. See my comment elsewhere about encapsulation and version stability.
Sometimes you have no choice. I had multiple times where I needed to store additional data for instances of class X but had not control over it, so I had to store it in a separate structure and keep track of things by object identity.
> You either should write proper hash code
And object identity fulfills all the requirements of a "proper" hash code.
Further: default implementations should be synthesized by the runtime. Prevents subtle bugs when the equals(...) and hashCode() implementations are incorrect or missing. eg contract for HashMap.
To backfill, perhaps Object could extend new base class NakedObject, and implement your new interfaces Equalable, Hashable. Then classes which don't need identity can extend NakedObject.
Maybe the "canonical Object" was motivated by prior experience with Self or some such. It definitely calmed down people new to Java & OOP. I think it was the right call at the time.