I was recently assigned to fix a bug in the code. For the scope of this post I re-framed the problem as below:
Context: There is a set of objects of each type (lets say fruits). Over a series of operations different type of fruits are added to the set. In certain stages certain fruits may be replaced by others in the family. For example during stage 4, we may need to replace a green apple by a red apple. On the final page we display all the fruits selected.
Problem: While the various fruits show up, the changes are not seen. So if we had updated the apple to be a red one, the final result still displays a green apple.
If the code for the same were to be written out I would have a fruit class:
Now the main code, where we modify the set in stages
If we look at the behavior of the Set's add method then:
Context: There is a set of objects of each type (lets say fruits). Over a series of operations different type of fruits are added to the set. In certain stages certain fruits may be replaced by others in the family. For example during stage 4, we may need to replace a green apple by a red apple. On the final page we display all the fruits selected.
Problem: While the various fruits show up, the changes are not seen. So if we had updated the apple to be a red one, the final result still displays a green apple.
If the code for the same were to be written out I would have a fruit class:
publicclass Fruit {As seen here fruits are unique by their name. Thus the set can only hold one fruit of each type.
privateString name;
privateString color;
privateint cost;
@Override
publicint hashCode() {
finalint prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
publicboolean equals(Object obj) {
if (this == obj)
returntrue;
if (obj == null)
returnfalse;
if (getClass() != obj.getClass())
returnfalse;
Fruit other = (Fruit) obj;
if (name == null) {
if (other.name != null)
returnfalse;
} elseif (!name.equals(other.name))
returnfalse;
returntrue;
}
// setter getters here
}
Now the main code, where we modify the set in stages
publicstatic void main(String[] args) {If I were to run this code I would expect to see red Apple, yellow Mango and a red watermelon.
Set<Fruit> fruitBasket = new HashSet<>();
//Stage 1----------
fruitBasket.add(new Fruit("Apple", "green", 3));
fruitBasket.add(new Fruit("Watermelon", "green", 11));
//Stage 2-------------------
//replace a green apple by a red one
fruitBasket.add(new Fruit("Apple", "red", 3));
fruitBasket.add(new Fruit("Mango", "yellow", 5));
//Final Display
System.out.println("The fruits in the basket are");
for (Fruit fruit : fruitBasket) {
System.out.println(fruit.getColor() + " colored " + fruit.getName() + " costs " + fruit.getCost());
}
}
The fruits in the basket areThere's been a mistake ! I expected a red Apple not a green one. What was the bug in the above code ??
green colored Apple costs 3
green colored Watermelon costs 11
yellow colored Mango costs 5
If we look at the behavior of the Set's add method then:
public boolean add(E e) {This seems fine. We know that the HashSet internally uses a Map. The add method tries to add the passed Fruit as a key entry in the underlying map. But if we look at the documentation
return map.put(e, PRESENT)==null;
}
Adds the specified element to this set if it is not already present (optional operation).This is where things get interesting. Two Fruits are same if they have the same name. Thus for the set, the Green Apple and the Red Apple are identical. The Set will ignore the add request for the Red Apple and hence we see the Green Apple in the result.To fix the same, I changed the add code as below :
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)). If this set already contains the
element, the call leaves the set unchanged and returns false.
publicstatic void main(String[] args) {This code will now work fine. We will first remove the Apple if it exists before adding the new/modified apple. The output on running is
Set<Fruit> fruitBasket = new HashSet<>();
//Stage 1----------
//fruitBasket.add(new Fruit("Apple", "green", 3));
add(fruitBasket, new Fruit("Apple", "green", 3));
//fruitBasket.add(new Fruit("Watermelon", "green", 11));
add(fruitBasket, new Fruit("Watermelon", "green", 11));
//Stage 2-------------------
//replace a green apple by a red one
// fruitBasket.add(new Fruit("Apple", "red", 3));
add(fruitBasket, new Fruit("Apple", "red", 3));
// fruitBasket.add(new Fruit("Mango", "yellow", 5));
add(fruitBasket, new Fruit("Mango", "yellow", 5));
//Final Display
System.out.println("The fruits in the basket are");
for (Fruit fruit : fruitBasket) {
System.out.println(fruit.getColor() + " colored " + fruit.getName() + " costs " + fruit.getCost());
}
}
publicstatic void add(Set<Fruit> fruitBasket, Fruit crtFruit) {
if(fruitBasket.contains(crtFruit)) {
fruitBasket.remove(crtFruit);
}
fruitBasket.add(crtFruit);
}
The fruits in the basket areThis is interesting when you compare with Maps. In the map if a key exists, then its value field is updated with the parameters in the current request. But not for sets. You need to explicitly remove and add again.
red colored Apple costs 3
green colored Watermelon costs 11
yellow colored Mango costs 5