Type-parameterized complement to Object.equals(Object)

dmytro sheyko dmytro.sheyko.jdk at gmail.com
Sat Apr 18 14:40:39 UTC 2020


Thank you for your response, Rémi.

> The solution you propose for Java does not work because you can not
implement the same interface with different type arguments.
> Let say i've a class Vehicle and a subclass Car, using Equable<T>, i will
want to write
>  class Vehicle implements Equable<Vehicle> { ... }
>  class Car extends Vehicle implements Equable<Car> { ... }

In this particular case `Car` can perfectly remain `Equable<Vehicle>`,
there is not need to make it `Equable<Car>`.
Even though instance of `Car` can never be equal to instance of `Vehicle`,
it's still possible to assign instance of `Car` to variable of type
`Vehicle`
and when compiler see comparison between `Car` and `Vehicle`, there is a
chance that they are equal. In this case `equ` works at least not worse
than `equals`.
The benefit of `equ` becomes more apparent when `Car` or `Vehicle` is
compared with something totally unrelated.

You are right that it's impossible to implement the same interface with
different type arguments. This can be an obstacle for classes that
implement, let's say,
both `java.util.Set` and `java.util.List` at the same time (assuming that
List extends Equable<List<?>> and Set extends Equable<Set<?>>).
But I would consider this case rather as abnormal because it's impossible
to implement the contract for equality (`equals`+`hashCode`)
for `List` and `Set` simultaneously not even taking `Equable` into account.
The `Equable` will just reveal such problems.

If there will such an example, when it's completely sane and legal to
implement both `List` and `Set`, there is a plan B. The interfaces such as
`List` and `Set`
will not extend `Equable<...>` directly, but provide `Equable<...>` object
instead. E.g.
    interface Set<E> extends Collection<E> {
        :
        Equable<Set<?>> asEquableSet();
        :
    }
    interface List<E> extends Collection<E> {
        :
        Equable<List<?>> asEquableList();
        :
    }
Although I doubt there will such an example.

After all the argument that  it's impossible to implement the same
interface with different type arguments is also applicable to `Comparable`,
but it does not prevent it to exist.
The `Equable` is just more general variant of `Comparable`.


> You can also note that the status quo is not as bad as you may think
because all IDEs provide an inspection/analysis to detect such case.

Right, IDEs sometimes smart with analysis, however I am not sure that they
are able to detect all such cases simply because they do not have
enough information to make right decision. Marking classes/interfaces with
`Equable` is the way to provide this information to the compiler or IDE.

By the way, I tried following example with IntelliJ IDEA
    static class A {
        @Override
        public boolean equals(Object o) {
            return o instanceof A;
        }
    }
    static class B extends A {}
    static class C extends A {}

    if (new B().equals(new C())) {
        System.out.println("!");
    }
And it reports "'equals()' between objects of inconvertible types 'B' and
'C'", which is definitely a false alarm (and luckily an ignorable warning).

Regards,
Dmytro

On Sat, Apr 18, 2020 at 2:25 PM Remi Forax <forax at univ-mlv.fr> wrote:

> Hi Dmytro,
> this was discussed at length at the time Scala was the new kid,
> the solution you propose for Java does not work because you can not
> implement the same interface with different type arguments.
>
> Let say i've a class Vehicle and a subclass Car, using Equable<T>, i will
> want to write
>   class Vehicle implements Equable<Vehicle> { ... }
>   class Car extends Vehicle implements Equable<Car> { ... }
>
> The other good reason to not do any changes on equality now is that we may
> revisit how equality works if we introduce operator overloading on inline
> type as part of Valhalla.
>
> You can also note that the status quo is not as bad as you may think
> because all IDEs provide an inspection/analysis to detect such case.
>
> regards,
> Rémi
>
> ----- Mail original -----
> > De: "dmytro sheyko" <dmytro.sheyko.jdk at gmail.com>
> > À: "discuss" <discuss at openjdk.java.net>
> > Envoyé: Samedi 18 Avril 2020 09:01:37
> > Objet: Fwd: Type-parameterized complement to Object.equals(Object)
>
> > Hello,
> >
> > I need a sponsor for following JDK change.
> >
> > Summary
> > -------
> >
> > Provide means for type safe comparison for equality by introducing
> > type-parameterized interface `java.lang.Equable<T>` with method `boolean
> > equ(T)`.
> > (The actual names of the interface and the method is subject to
> discussion.)
> >
> >
> > Goals
> > -----
> >
> > The primary goal is to avoid programmer mistakes when values of different
> > and
> > incomparable types are compared using `equals` method, which always
> returns
> > `false`
> > in such cases according to its contract.
> >
> >
> > Non-Goals
> > ---------
> >
> > Custom hashers and equators for Collection Framework are out of the scope
> > of this proposal.
> >
> >
> > Motivation
> > ----------
> >
> > This is quite common approach to use `equals` method to compare value for
> > equality.
> > However, it may happen that the values have different incomparable types
> > and, therefore,
> > the result of such a comparison is doomed to `false`. For example,
> > instances of
> > `java.lang.Integer` and `java.lang.Long` will never be equal even though
> > they represent
> > the same numerical value. I.e. this code
> >
> >    Long l = 42L;
> >    Integer i = 42;
> >    if (l.equals(i)) { // always false
> >        ...
> >    } else {
> >        ...
> >    }
> >
> > is very likely a mistake because the else-branch will never be taken. The
> > same applies to attempts
> > to compare among themselves instances of `java.lang.Short`,
> > `java.lang.String`, `java.util.Date`,
> > `java.util.List`, `java.util.Set`, `java.util.Map` etc. The reason why
> such
> > erroneous comparisons
> > remain unnoticed is the fact that the `equals` method accepts any value.
> > One way to make mistakes
> > with such comparisons more visible it to use `compareTo` method.
> >
> >    Long l = 42L;
> >    Integer i = 42;
> >    if (l.compareTo(i) == 0) { // compilation error
> >        ...
> >    } else {
> >        ...
> >    }
> >
> > However this approach has some drawbacks:
> > * It's counter-intuitive and unnecessarily verbose. It's not clear at
> > glance why `compareTo`
> >  is used instead of `equals`.
> > * It can be inefficient because there are cases when an attempt to figure
> > out whether given value
> >  is less or greater that the other value requires more effort than an
> > attempt to figure out
> >  whether the values are just not equal. For example, when we compare
> > strings and have noticed
> >  that the strings have different length we can stop here and do not scan
> > them in order to
> >  figure out, which one is greater than the other. Another example is the
> > class that represents
> >  rational number as a reduced fraction. To make sure that the rational
> > numbers are equal, we can
> >  just compare their numerators and denominators. But in order to find out
> > which one is greater
> >  than the other we can't avoid arithmetic operations (multiplications).
> > * It's not applicable to the classes that do not implement
> > `java.lang.Comparable<T>`. For example,
> >  collections, complex numbers, points in 2D or 3D space etc.
> >
> > Therefore it's proposed to introduce interface `Equable` in order to
> > explicitly mark comparable classes.
> >
> >    public interface Equable<T> {
> >        boolean equ(T o);
> >    }
> >
> > Let `Comparable` extend it and provide default method for `equ`.
> >
> >    public interface Comparable<> extends Equable<T> {
> >        int compareTo(T o);
> >
> >        default boolean equ(T o) {
> >            return compareTo(o) == 0;
> >        }
> >    }
> >
> > Provide efficient implementation of `equ` method for some JDK classes, at
> > least those that are in
> > `java.lang` package. Also it would be desiable to let `java.util.List`,
> > `java.util.Set`,
> > `java.util.Map` and `java.util.Map.Entry` to extend `Equable` interface
> as
> > well (but without
> > extending `Comparable`).
> >
> > ===
> >
> > I am sending a set of patches. I split the whole change into several
> > logical parts.
> >
> > * equable-0.patch contain `java.lang.Equable`, `java.lang.Comparable`,
> > `java.util.Objects.equ`
> >
> > * equable-1.patch contain `j.l.Boolean`, `j.l.Byte`, `j.l.Character`,
> > `j.l.Double`, `j.l.Enum`,
> >  `j.l.Float`, `j.l.Integer`, `j.l.Long`, `j.l.Short` and `j.l.String`
> >
> > * equable-2.patch contain `j.u.Optional`, `j.u.OptionalDouble`,
> > `j.u.OptionalInt`,
> >  `j.u.OptionalLong`
> >
> > * equable-3.patch contain `j.u.List`, `j.u.AbstractList`, `j.u.Set`,
> > `j.u.AbstractSet`, `j.u.Map`,
> >  `j.u.AbstractMap`
> >
> > * equable-4.patch is a tricky one. It contains
> > `com.sun.tools.jdi.EventSetImpl`. Previously this class
> >  extended `java.lang.ArrayList` and implemented `java.lang.Set` through
> > `EventSet`. Therefore it appeared
> >  that it implemented both `java.util.List` and `java.util.Set` at the
> same
> > time. This is not right,
> >  because these interfaces have different contracts regarding equality. So
> > I had to change it by extending
> >  `java.lang.AbstractSet`.
> >
> > Thank you,
> > Dmytro
>


More information about the discuss mailing list