6

I'm implementing an A* algorithm in Java, and I'm using a TreeSet as an easy way of keeping the open list sorted. If you're not familiar with A*, it's basically a function to get the shortest path from A to B, and the open list is a list of nodes (in my case Tiles) sorted based on their closeness to B.

My objects implements a compareTo() function for sorting like so:

@Override
public int compareTo( Tile b ) 
{
    return ( this.f< b.f) ? -1 : ( this.f> b.f) ? 1 : 0;
}

My problem comes when I try to add some tiles to the open list - TreeSet seems to use compareTo() to check if the object is already there, rather than equals(). As it's possible that two different Tiles have the same f value, the TreeSet thinks that the object already exists in the list and doesn't add it.

According to the docs (or at least, how I'm reading it), it should use equals:

"Adds the specified element to this set if it is not already present. More formally, adds the specified element e to this set if the set contains no element e2 such that (e==null ? e2==null : e.equals(e2))." (emphasis mine).

How can I get the TreeSet to use equals() when calling add() or contains() and compareTo() for sorting? For info, my Tile class doesn't override the equals() function, so it should get the default return a == b.

If what I'm trying to do isn't possible with TreeSet, what's the proper collection that I should be using?

divillysausages
  • 7,883
  • 3
  • 25
  • 39

3 Answers3

6

This is the expected behavior, as per the TreeSet docs:

Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all element comparisons using its compareTo (or compare) method, so two elements that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface.

You cannot get TreeSet to use equals when calling add or contains. Your best bet is to make your compareTo method consistent with equals by comparing by both the f property as well as whatever other properties equals cares about.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • please check TreeSet#add method java-doc, it says #equals is used, when in reality it is not – hoaz May 16 '13 at 17:13
  • 1
    The TreeSet docs state that all those bets are off if the comparator is not consistent with equals. You've demonstrated for yourself that that's the case, whatever your interpretation of the docs. – Louis Wasserman May 16 '13 at 17:17
  • you can check implementation, it never calls equals – hoaz May 16 '13 at 17:23
  • the problem is that in legitimate cases, the `f` can be equal on 2 different objects. I'm not sure what you mean by "make your `compareTo` method consistent with `equals`", as to me, they're 2 different methods – divillysausages May 16 '13 at 17:36
  • Your best bet, in that case, is almost certainly to use a `TreeMap` mapping the comparison values -- perhaps the `f` values themselves -- to lists or sets of the `Tile`s with that `f` value. If `f` is an integer value, then that might be a `TreeMap>`. – Louis Wasserman May 16 '13 at 17:42
  • @divillysausages: Well, if you have "legitimate cases" not to use a `TreeSet` as it should be used by design, just don't do it. @Louis Wasserman has answered your question correctly, though. You should honour him by just accepting it. – kriegaex May 18 '13 at 10:49
  • 3
    One more constructive hint: What I used to do in similar situations when I had distinct objects with equal values according to `compareTo` in `TreeSet`s, was to add another piece of unique information as an additional comparison criterion at the end of the logic. Maybe something like an object id, unique coordinates or so. This might also work for you. – kriegaex May 18 '13 at 12:00
0

I hope the following example helps the SO users. I had similar situation and the way I dealt was to use id in addition to value. In my case, value refers to a frequency.

Please note the use of 'paddedText' - this is important else 3>11 if you do string comparison.

public int compareTo(Object o) {
  String oText= o.getValue()+"";
  oText = String.format("%20s",oText).replace(' ', '0')+o.getName();
  paddedText = this.value +""; 
  paddedText=String.format("%20s",paddedText).replace(' ', '0') + this.name;
         return oText.compareTo(paddedText);
 }
Amit
  • 660
  • 6
  • 8
0

The answers here already led me to create this. I'll leave it here because there still seems to be only the gist of a solution.

    @Override
    public int compare(TileState lhs, TileState rhs) {
        int compare = (lhs.getTaxicab + lhs.mMoves.size())
                - (rhs.getTaxicab + rhs.mMoves.size());
        if (compare == 0 && !lhs.equals(rhs)) return 1;
        return compare;
    }

You only need to worry when your compare method returns 0.

If that's the case and equals would return false, then return anything other than 0.

Fletcher Johns
  • 1,236
  • 15
  • 20