HashSet.contains returns false when it shouldn'

2019-07-13 22:12发布

问题:

I have this code:

public class Tray {

private Set<Block> blocks;

private int numColumns;
private int numRows;

//constructor
public Tray (int numRows, int numColumns){
    this.numColumns = numColumns;
    this.numRows = numRows;
    blocks = new HashSet<>();
}

public boolean satisfiesGoal(Tray other){

    Block thisBlock = this.blocks.iterator().next();
    Block otherBlock = other.blocks.iterator().next();

    boolean hashesEqual = thisBlock.hashCode() == otherBlock.hashCode(); // this is true

    boolean areEqual = thisBlock.equals(otherBlock) && otherBlock.equals(thisBlock); // this is true

    boolean contains = this.blocks.contains(otherBlock); // this is false, why?
    return contains;
}

In the main method I have added the 2 Blocks to their respective Trays. According to the debugger, the variables "hashesEqual" and "areEqual" are true, but the boolean "contains" is false. Any ideas as to why the hashes of 2 objects would be equal as well as equal according to the "equals" method, but would not contain the equal object in a HashSet?

回答1:

One possible thing that could cause this is if you have modified the objects in some way that affects their equality and hash codes after adding them to the sets. That will cause the set to malfunction, because the objects are not found in the correct slot of the hash table corresponding to their new value.

Immutable objects make for safer set elements and map keys because they do not have this risk.

If you absolutely need to modify properties that are part of an object's equality, you'll need to temporarily remove it from the set first, or else discard the set and assemble a new set afterwards.



回答2:

Are Block objects mutable?

HashSet stores its contents as keys in a HashMap, with an arbitrary Object as the value.

As per this article,

If an object's hashCode() value can change based on its state, then we must be careful when using such objects as keys in hash-based collections [emphasis mine] to ensure that we don't allow their state to change when they are being used as hash keys. All hash-based collections assume that an object's hash value does not change while it is in use as a key in the collection. If a key's hash code were to change while it was in a collection, some unpredictable and confusing consequences could follow. This is usually not a problem in practice -- it is not common practice to use a mutable object like a List as a key in a HashMap.



回答3:

The code for contains in openjdk is pretty simple - it eventually just calls HashMap.getEntry which uses the hash code and equals to check if the key exists. My guess is that your error is in thinking that the item is in the set already. But you could easily confirm that that is wrong by directly declaring a Set in the code you have posted and adding the items to that collection.

Try adding the following unit test:

Set<Block> blocks = new HashSet<>();
blocks.add(thisBlock);
assertTrue(thisBlock.hashCode() == otherBlock.hashCode() && thisBlock.equals(otherBlock));
assertTrue(blocks.contains(otherBlock));

If the first assertion passes and the second fails then you've found a bug in Java. I find that pretty unlikely.

Also make sure you have the openjdk source code available so you can step into Java methods while debugging. That way you can step into contains and check exactly where it is failing.

Also note that your code this.blocks.iterator().next() creates a new iterator each time the function is called and then returns the first item in the iteration. In other words it picks the first item in the set (note that this is not the least by natural order). If you are trying to iterate through the two sets in sequence and compare values then that's not what your code does at the moment.