Search code examples
javaguavaapache-commons

How to make a distinction between two equals objects in a Sorted collection?


I may be wrong but for me, we can override equals for an object so that you consider them has being meaningfully equals. All the entry in a map have distinct keys, and all the entries in set have distinct values (not meaningfully equals)

But when using a TreeMap or a TreeSet, you can provide a comparator. I noticed that when a comparator is provided, the object's equals method is bypassed, and two objets are considered equals when the comparator returns 0. Thus, we have 2 objects but inside of a map keyset, or a set, only one is kept.

I'd like to know if it is possible, using a sorted collection, to make a distinction for two different instances.

Here's an easy sample:

public static void main(String[] args) {
    TreeSet<String> set = new TreeSet<String>();
    String s1 = new String("toto");
    String s2 = new String("toto");
    System.out.println(s1 == s2);
    set.add(s1);
    set.add(s2);
    System.out.println(set.size());
}

Notice that using new String("xxx") bypass the use of the String pool, thus s1 != s2. I'd like to know how to implement a comparator so that the set size is 2 and not 1.

The main question is: for two distinct instances of the same String value, how can i return something != 0 in my comparator?

Note that i'd like to have that comparator respect the rules:

Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and y. (This implies that compare(x, y) must throw an exception if and only if compare(y, x) throws an exception.)

The implementor must also ensure that the relation is transitive: ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0.

Finally, the implementer must ensure that compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z.

It is generally the case, but not strictly required that (compare(x, y)==0) == (x.equals(y)). Generally speaking, any comparator that violates this condition should clearly indicate this fact. The recommended language is "Note: this comparator imposes orderings that are inconsistent with equals."

I can use a trick like:

public int compare(String s1,String s2) {
  if s1.equals(s2) { return -1 }
  ...
}

It seems to work fine but the rules are not respected since compare(s1,s2) != -compare(s2,s1)

So is there any elegant solution to this problem?


Edit: for those wondering why i ask such a thing. It's more by curiosity than any real life problem.

But i've already been in a situation like that and though about a solution to this problem:

Imagine you have:

class Label {
  String label;
}

For each label you have an associated String value. Now what if you want to have a map, label->value. But now what if you want to be able to have twice the same label as a map key? Ex "label" (ref1) -> value1 "label" (ref2) -> value2 You can implement equals so that two distinct Label instances are not equals -> i think it works for HashMap.

But what if you want to be able to sort these Label objects by alphabetical order? You need to provide a comparator or implement comparable. But how can we make an order distinction between 2 Labels having the same label? We must! compare(ref1,ref2) must not return 0. But should it return -1 or 1 ? We could compare the memory address or something like that to take such a decision but i think it's not possible in Java...


Solution

  • If you're using Guava, you can make use of Ordering.arbitrary(), which will impose an additional order on elements which remains consistent for the life of the VM. You can use this to break ties in your Comparator in a consistent way.

    However you could be using the wrong data structure. Have you considered using a Multiset (e.g. TreeMultiset), which allows multiple instances to be added?