Type-parameterized complement to Object.equals(Object)

dmytro sheyko dmytro.sheyko.jdk at gmail.com
Sun Apr 19 17:17:22 UTC 2020


Yes, I agree with you. And thank you for pointing this out. Sorry for being
impudent, I thought that the more detailed the proposal would be, the more
attention it can attract. Please consider my email as an invitation to
discussion in order to build a consensus.

As Rémi Forax pointed out, IDEs provide an inspection/analysis to detect
incorrect usage of equals. However their analysis is based on assumption
that instances of different classes can't be equal to each other and
therefore they typically check whether one class is inheritor of the other.
Thus NetBeans, Eclipse and IDEA complain if I try to compare
`java.sql.Time` and `java.sql.Date`. NetBeans and Eclipse also complain if
I try to compare collections e.g. `java.util.TreeMap` with
`java.util.LinkedHashMap` or `java.util.ArrayList` with
`java.util.LinkedList`. IDEA complain only if I try to compare collections
parameterized with different types, e.g. `java.util.ArrayList<String>` with
`java.util.LinkedList<Integer>`. This looks suspect indeed, but honestly
they can be equal - they can be both empty. SpotBugs complains on an
attempt to compare `java.util.AbstractMap.SimpleEntry` with
`java.util.AbstractMap.SimpleImmutableEntry`. Eclipse and NetBeans do the
same, but IDEA doesn't. Perhaps IDEA has list of exceptions in order to
avoid false positives, while Eclipse and NetBeans do not. And SpotBugs does
more sophisticated analysis, but sometimes it also makes mistakes.
Nevertheless the assumption that instances of different classes can't be
equal to each other is wrong. But this is the best thing they can do. They
do not have information which different classes can be comparable to each
other.

The fact that various tools try to solve this problem and the fact that the
result of their work is not always satisfactory means that there is a
problem.
The suggested `Equable` interface do not detect incorrect usage of equals.
It attempts to solve the problem from another direction.

Well, in some cases (but definitely not in all cases) method `equals` is
overridden this way:
    class ExampleA {
        @Override
        public boolean equals(Object o) {
            if (o instanceof ExampleA) {
                Example that = (ExampleA) o;
                return <<compare `this` with `that`>>
            }
            return false;
        }
    }
The part of code <<compare `this` with `that`>> can be extracted to
separate method. Let's call it `equ` (it's often named as `strictEquals`,
`contentEquals`, `isEqualTo` etc).
    class ExampleA {
        @Override
        public boolean equals(Object o) {
            if (o instanceof ExampleA) {
                Example that = (ExampleA) o;
                return equ(that);
            }
            return false;
        }
        public boolean equ(ExampleA that) {
            return <<compare `this` with `that`>>
        }
    }
The inheritors of ExampleA may override the behavior, but not the equality
property.

Thus if we have two values of classes ExampleB and ExampleC and these
classes have common base class ExampleA, it makes sense to compare these
values.
And the fact that we extracted `equ` method plays important role here. If
we use it, this becomes clear that the values are comparable.
Please note that if we use plain old `equals`, this is not so clear because
`equals` is overly permissive. Moreover IDE inspection may report a false
positive here.

Also if we have two values of classes ExampleB and ExampleC and the one of
the classes has parent ExampleA and the other does not, it does not make
sense to compare these values.
It would be a compilation error if we try to use `equ` method.

And finally if we have two values of classes ExampleB and ExampleC and none
of the the classes have parent like ExampleA, we can't make a decision, the
values may be comparable or may be not.
And here the only thing we can use is `equals` and rely on IDE inspections.
In this case the IDE inspection whether one class is inheritor of the other
looks reasonable.

Returning to ExampleA class, we can extract `Equable` interface in order to
make the pattern more universal
    class ExampleA extends Equable<ExampleA> {
        @Override
        public boolean equals(Object o) {
            if (o instanceof ExampleA) {
                Example that = (ExampleA) o;
                return equ(that);
            }
            return false;
        }
        @Override
        public boolean equ(ExampleA that) {
            return <<compare `this` with `that`>>
        }
    }
This will let us write an analog if `Object.equals` method
    public static <B, A extends Equable<B>> boolean equ(A a, B b) {
        return (a == b) || (a != null ? (b != null && a.equ(b)) : b ==
null);
    }

So with `Equable` interface we can mark the class (or interface) that is
the root of hierarchy of mutually comparable classes.
Alternatively we can use annotation for this purpose, however in this case
we can only rely on IDE inspections.
Also `Equable.equ` has some small bonus. It lets implementors of
`Comparable` classes to provide efficient alternative to `compareTo() ==
0`, especially when `equals` and `compareTo` are inconsistent.

Of course, it does not make sense to make all classes that override
`equals` `Equable`. If `equals` is overridden as
    class ExampleZ {
        @Override
        public boolean equals(Object o) {
            if (o == null) return false;
            if (getClass() == o.getClass()) {
                Example that = (ExampleZ) o;
                return <<compare `this` with `that`>>
            }
            return false;
        }
    }
where all subclasses are comparable only with itself, maybe there are no
benefits.

Your comments are welcome.

Thank you,
Dmytro


On Sat, Apr 18, 2020 at 6:20 PM Brian Goetz <brian.goetz at oracle.com> wrote:

> Without commenting on the merits of the proposal, let me point out that
> asking for a sponsor at this point is skipping many important steps.
>
> I would recommend you focus first on *building consensus* that (a) there
> is a problem, (b) you have identified the correct problem, (c) it is a
> problem that needs to be solved, (d) it is problem that needs to be solved
> in the JDK, (e) that you have evaluated the possible solutions and analyzed
> their strengths and weaknesses, and (f) that the correct solution has been
> identified.  (There are more steps, but you get the idea.)
>
> On Apr 17, 2020, at 7:44 AM, dmytro sheyko <dmytro.sheyko.jdk at gmail.com>
> wrote:
>
> I need a sponsor for following JDK change.
>
>
>


More information about the discuss mailing list