16

Strangely the default JDK 6 implementation of AbstractList::equals() does not seems to check first if the two lists have the same size:

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof List))
        return false;
    ListIterator<E> e1 = listIterator();
    ListIterator e2 = ((List) o).listIterator();
    while(e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    return !(e1.hasNext() || e2.hasNext());
}

If both lists contains lots of items, or items taking time to compare, it will compare them all before realizing that one list is shorter than the other; which seems to me really inefficient as the equality could have been made without even calling one compare.

Especially that for lots of situations lists sizes would most of the time differ. Furthermore, most Java List implementations have O(1) size() performance (even LinkedList, which keep its size in cache).

Is there a good reason for this default implementation?

Laurent Grégoire
  • 4,006
  • 29
  • 52

1 Answers1

12

The operation of the equals method is specified in some detail, and it requires the O(n) behavior. While this may be suboptimal for subclasses whose size method is O(1), for some subclasses the size method may itself be O(n) and the requested behavior would actually be a degradation. In any event the spec is clear and this change cannot be made.

Note that a subclass may override equals if desired, inserting a size comparison when appropriate.

Reference.

Sajal Dutta
  • 18,272
  • 11
  • 52
  • 74
  • 3
    So if understand well, it's inefficient because it has been documented as such? :) – Laurent Grégoire Sep 23 '13 at 11:38
  • 3
    @Laurent Not because of "it has been documented as such", because of the design decision with the reason being "for some subclasses the size method may itself be O(n) and the requested behavior would actually be a degradation" :) – Sajal Dutta Sep 23 '13 at 11:49
  • 3
    I understand, but degrading the performance at the first place for all implementation because "some of them" could be slower does not seems a good decision to me. I would have made an optimized equals for O(1) sized lists, and un-optimized for the rest. Especially for ArrayList which is a workhorse in Java... – Laurent Grégoire Sep 23 '13 at 13:16
  • @Laurent You can always override and being consistent is actually good in long run. :) – Sajal Dutta Sep 23 '13 at 13:20
  • `List::size()` performance is currently *not* consistent at the first place: O(n) or O(1), so I do not see really the point of trying to make equals() consistent between different List implementations... Another option would have been to assume or even require size() to have O(1) performance (which is the case for all major default implementation), and optimizing always the equals(). – Laurent Grégoire Sep 23 '13 at 13:28
  • @Laurent It will make `equals()` a lot slower for lists where `size()` is O(n). It is simply a trade-off, and the developers of Java decided that this was better than the alternative for most cases. – Mark Rotteveel Sep 23 '13 at 14:10
  • Simply a (linked) list implementation could have a `size()` requiring to traverse to the end of the list = full scan. Leaving it to the child class that has knowledge about `size()` is fine. – Joop Eggen Sep 23 '13 at 14:11
  • Most of the linked list implementation I know, including the one in the JDK, cache the size value so size() is O(1). Requiring/strongly recommend O(1) for List::size() in the first place may have been wiser. – Laurent Grégoire Sep 23 '13 at 14:18
  • If you don't like the default, change it. Premature optimization that can have the potential to double the processing time is a bad idea. You can always add the size check if you want, but you can't remove it once it is there (without reimplementing the whole method from scratch). – Ajax Jul 01 '15 at 03:06