3 ms·
Member-by-member sub-equals might make more sense in some situations, but not all (as the article discusses, equality is context-dependent). By making equals()
by lazulicurio 7y ago
Member-by-member sub-equals might make more sense in some situations, but not all (as the article discusses, equality is context-dependent). By making equals() an instance method, encapsulation is broken by tying an external concern (equality) to the internal implementation of a class. It would have been better if Object had no instance equals() or hashCode() method at all, with static methods providing non-reference-equality semantics. Something like:
@FunctionalInterface
interface Equivalence<T>
{
boolean equals(T one, T other);
}
@FunctionalInterface
interface Comparison<T>
extends Equivalence<T>
{
int compare(T one, T other);
@Override
default boolean equals(final T one, final T other)
{
return compare(one, other) == 0;
}
}
@FunctionalInterface
interface HashFunction<T>
{
int hashCode(T obj);
}
interface Equalitor<T>
extends Equivalence<T>, HashFunction<T>
{
}
interface Comparator<T>
extends Comparison<T>, Equalitor<T>
{
}
interface Equatable
{
Equalitor<? super Object> equivalence();
}
interface Comparable
extends Equatable
{
@Override
Comparator<? super Object> equivalence();
}
final class Objects
{
static int compare(final Object one, final Object other)
{
if (one == other)
{
return 0;
}
if (one == null)
{
return -1;
}
if (other == null)
{
return 1;
}
if (one instanceof Comparable oneCmp && other instanceof Comparable otherCmp)
{
final Comparison<? super Object> cmp = oneCmp.equivalence();
if (equals(cmp, otherCmp.equivalence()))
{
return cmp.compare(one, other);
}
}
throw new SomeRuntimeException();
}
static boolean equals(final Object one, final Object other)
{
if (one == other)
{
return true;
}
if (one instanceof Equatable oneEq && other instanceof Equatable otherEq)
{
final Equivalence<? super Object> eq = oneEq.equivalence();
if (equals(eq, otherEq.equivalence()))
{
return eq.equals(one, other);
}
}
return false;
}
static int hashCode(final Object obj)
{
if (obj instanceof Equatable objEq)
{
final HashFunction<? super Object> hf = objEq.equivalence();
return hf.hashCode(obj);
}
return someIntrinsicHashCode(obj);
}
}
(The generic parameters aren't correct, but to give a rough idea)
- kstenerud 7y agoYes, that's why I was recommending that the default implementation be a compiler-generated member-by-member non-reference comparison, with the option to override that behavior by providing your own custom implementation of equals(). Then you get the intuitive result for the 80% case, and have the means to write custom code for the other 20%. Not having an equals in the root object makes equality checks on type-erased values impossible because there would be no standard, reliable way to do it (C++ solved this with operator overloads). hashCode, on the other hand, would have been better as an interface so that the developer is forced to think about how it gets generated (because a default implementation is more likely to get it wrong than right). The Map classes, for example, would only accept key values that implement Hashable.
- lazulicurio 7y agoWhat I was trying to get across with my example was that you can define a standard, reliable way to perform equality checks on type-erased values without having equals on the root object. Granted, it looks a bit wonky, but it provides safer behavior than an instance equality method.
- kstenerud 7y agoI understand, but the issue here is with making a fundamental operation (equality) an optional interface, which would make things unnecessarily complicated for the user. We always need the ability to check for equality, regardless of type (so much so that we have an "=" operator). The trouble comes with deciding what equality even means (as this article pointed out), because it really depends on the types you're comparing. C++ decided that the best way to solve it would be to allow operator overloading. Unfortunately, overloading "=" worked so well that they thought "Hey! We could do the same for ALL operators!", and the mess that is C++ operator overloading was born. The Java folks figured that they'd learned from C++ mistakes, and would avoid them by forbidding operator overloading entirely, thus throwing the baby out with the bathwater (and also they cheated because the "+" operator is overloaded for strings). What they should have done is simply allowed overloading of the "=" operator ONLY, then maybe also add a special notation to say "I mean the actual reference (i.e. pointer value) as opposed to the object contents" (to cover the 20% use case where you actually DO want to know if it's the same reference). This would have obviated the need for equals(), made "=" work the way you'd intuitively expect it to, and also allowed implementors to override the default implementation to "do the right thing", even when you don't know the type of the values you are comparing (for example after fetching from an untyped collection).