14

I have added some simple objects to a TreeSet, but when I call the TreeSet's remove() and contains() methods, they don't work. However, when I iterate over the set the object is printed. Employee objects shall be added to the set while the objects uniqueness is based on the objects name property. The Id property is the value that should be sorted, but which is not unique.

public class Employee {
    private String name;
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

 // Two objects are considered equal if their names are equal
    @Override
    public boolean equals(Object o) {
    if (o == null)
        return false;
    if (this == o)
        return true; 
    if (o.getClass() == this.getClass()) {
        Employee p = ( Employee) o;
        if (p.getName() != null && this.getName() != null)
        return this.getName().equals(p.getName());
        else
        return false;
    } else {
        return false;
    }
    }
} 

//*******************************************************

public class EmployeeComp implements Comparator<Employee> {

    // Sort Ids, but allow duplicates, hence this function is never returning 0
    @Override
    public int compare(Employee p1, Employee p2) {
    int re = 0;

    boolean scoreLt = (p1.getId() > p2.getId());
    boolean scoreGt = (p1.getId() < p2.getId());

    if(scoreLt)
        re = -1;
    if(scoreGt)
        re = 1;
    else 
        re = -1;                       
         return re;                 
    }    
}
//*******************************************************
// Collection shall store unique names with ordered values like:
// Alex, 923
// Toni, 728
// Eddi, 232
// Peter, 232
// Eddi, 156  *** not allowed
import java.util.TreeSet;


public class Main {
    private static EmployeeComp comp = new EmployeeComp(); 
    private static TreeSet<Employee> employees = new TreeSet<Employee>(comp); 

    public static void main(String[] args) {

    Employee p1 = new Employee();
    p1.setName("Eddi");
    p1.setId(232);

    Employee p2 = new Employee();
    p2.setName("Toni");
    p2.setId(728);

    Employee p3 = new Employee();
    p3.setName("Peter");
    p3.setId(232);

    Employee p4 = new Employee();
    p4.setName("Alex");
    p4.setId(923);

    employees.add(p1);
    employees.add(p2);
    employees.add(p3);
    employees.add(p4);

    // Here, contains() and remove() should check the object address
    // and not perform their actions based on compareTo

       } 
}
user1812379
  • 827
  • 3
  • 11
  • 22
  • No, I have done that first, but commented it out later, so this cannot be the reaosn. – user1812379 Jun 06 '13 at 19:14
  • @fge sure, you right, I overlooked this. – Victor Sorokin Jun 06 '13 at 19:22
  • OK, see solution, and one thing you have to keep in mind: you cannot do what you want with either `.equals()`/`.hashCode()` or a `Comparator` alone -- you cannot both have your cake and eat it. My `BigDecimal` example should already have given you a hint! – fge Jun 06 '13 at 23:44
  • Note about my solution: there is another one which uses Guava's `Equivalence`, which I would personally prefer; and since Guava would be used, it means you get `Ordering` as a bonus – fge Jun 07 '13 at 09:49

2 Answers2

32

A TreeSet inserts/removes according to the results of Comparable, not .equals()/.hashCode()!

This means, BTW, that the objects of your Set do implement Comparable (if they didn't, each time you'd have tried and inserted a member, you'd have been greeted with a ClassCastException).

To be more accurate, TreeSet is an implementation of SortedSet.

If you want a .equals()/.hashCode()-compatible set, use, for instance, a HashSet.

For the illustration, here is what happens with BigDecimal (posted a few hours ago here):

final BigDecimal one = new BigDecimal("1");
final BigDecimal oneDotZero = new BigDecimal("1.0");

final Set<BigDecimal> hashSet = new HashSet<>();
// BigDecimal implements Comparable of itself, so we can use that
final Set<BigDecimal> treeSet = new TreeSet<>();

hashSet.add(one);
hashSet.add(oneDotZero);
// hashSet's size is 2: one.equals(oneDotZero) == false

treeSet.add(one);
treeSet.add(oneDotZero);
// treeSet's size is... 1! one.compareTo(oneDotZero) == 0

To quote the javadoc for Comparable, it means that BigDecimal's .compareTo() is "not consistent with .equals()".

** EDIT ** As to what the OP wants:

  • a Collection which will not accept duplicate names;
  • a sorted view of that Collection which will sort against the user's id.

As mentioned above, you cannot have one collection which does both. The solution:

  • for the first, a HashSet;
  • for the second, a copy of that set into an ArrayList, then using Collections.sort().

This means .equals() and .hashCode() must act only on the name, while a custom Comparator will act on the id. The Comparator has no other choice but to be custom since it is a comparator which is not consisten with .equals() in any event.

As to the proposed code, there are problems.

First: Employee overrides .equals() but not .hashCode(). As such, Employee breaks the .equals() contract (one part of which is that if two objects are equal, they must have the same hashcode). What is more, .hashCode() is critical for HashSet to work at all. Fix:

@Override
public int hashCode()
{
    return name == null ? 0 : name.hashCode();
}

@Override
public boolean equals(final Object obj)
{
    if (obj == null)
        return false;
    if (this == obj)
        return false;
    if (!(obj instanceof Employee))
        return false;
    final Employee other = (Employee) obj;
    return name == null ? other.name == null
        : name.equals(other.name);
}

Second: the comparator is equally as broken as Employee since it breaks the Comparator contract (for any o1 and o2, o1.compareTo(o2) == - o2.compareTo(o1)). Fix:

public final class EmployeeComp
    implements Comparator<Employee>
{
    @Override
    public int compare(final Employee o1, final Employee o2)
    {
        final int id1 = o1.getId(), id2 = o2.getId();
        if (id1 == id2)
            return 0;
        return id1 > id2 ? 1 : -1;
    }
}

Then, how to obtain a sorted copy of the set:

// "set" contains the unique employees
final List<Employee> sorted = new ArrayList<Employee>(set);
Collections.sort(list, new EmployeeComp());

DONE.

Community
  • 1
  • 1
fge
  • 119,121
  • 33
  • 254
  • 329
  • 3
    +1, good find. The default answers only work in 95% of the questions ;) – jlordo Jun 06 '13 at 19:18
  • @fge Then it didn't work because I haven't implemented equality in compareTo(), because I wanted to allow duplicates. For an order of objects like – user1812379 Jun 06 '13 at 21:23
  • @user1812379 `.compareTo()` is supposed to implement total ordering, not equality – fge Jun 06 '13 at 21:27
  • @fge I didn't implement the `compareTo() == 0` case to allow duplicates of numbers, because I wanted to sort name-value-pair-objects that contain unique names while the non-unique values shall only be sorted like (user4, 1), (user3, 2), (user7, 2), (user2, 5) etc. In this case two objects are equal if the user names are equal, but the whole set should be sorted according to the non-unique values. It seems that a Set is really the wrong choice. Maybe I should choose a TreeMap? Thanks for shedding some light on this topic. – user1812379 Jun 06 '13 at 21:42
  • Yes, which is what I have done so far, but this causes a lot of pain, because some methods like `contains()` and `remove()` are not working properly. In a Map I could use the unique user name as a key and then sort the non-unique value. – user1812379 Jun 06 '13 at 22:05
  • I have posted the code. Should have done that right at the beginning. – user1812379 Jun 06 '13 at 22:45
  • @fge I can still have duplicate names in the set and `remove(`) and contains() don't work based on their object address, so `employees.add(p1)` and afterwards calling `employees.remove(p1)` or `employees.contains(p1))` doesn't work. I don't any solution here without altering the comparator. With `p1.getName().compareTo(p2.getName())` as the comparator's return value, this would work, but then I couldn't order the Id values. – user1812379 Jun 06 '13 at 23:27
  • @fge could you clariy what you meant by "As mentioned above, you cannot have one collection which does both. " By both meaning sorting and guarantying not duplicates. Wouldn't the SortedSet be just that? – Caio Faustino Jun 06 '17 at 21:17
  • 1
    @CaioFaustino if and only if, as the answer mentions, `Comparable` is consistent with `.equals()`; that is not the case with `BigDecimal`. – fge Jun 07 '17 at 07:14
  • thanks a lot for the contrib, I took a lot from this answer, I will upvote and thanks again, I've taken a little part to one of my answers – developer_hatch Sep 28 '17 at 18:58
  • great answer. funny i had no idea that treeset only considers compare and does not consider hashcode/equals.... – tom Feb 26 '19 at 18:43
3

Your problem is conceptual.

If you want a sorted collection of unique objects: TreeSet
If you want a sorted collection were different objects can have the same comparison value for sorting purposes: PriorityQueue

Incidentally, the methods in a PriorityList are more suited to the usual needs of the second case than the ones on TreeSet. I used to think of it as TreeSet shortcomings. For example, to take the first item out of the collection.

Hope that helps :-)

Daniel Langdon
  • 5,899
  • 4
  • 28
  • 48