Adding a System.debug changes code behavior
Which is a bit unexpected for me as Accounts with different fields should result in a different hash. I know that using sObjects for map keys is bad practice but still interested in how adding a debug statement can change the code behavior?
Map and Set have internal state you cannot directly observe. Internally, they look a bit like this:
class Bucket<U> {
Integer hashCode;
U[] values;
}
public class Map<T, U> {
List<Bucket<U>> buckets = new List<Bucket<U>>();
public U put(T key, U value) {
Integer hashCode = value.hashCode();
Bucket<U> temp;
for(Bucket<U> bucket: buckets) {
if(bucket.hashCode == hashCode) {
temp = bucket;
}
}
// hash not found, make new bucket
if(temp == null) {
buckets.add(temp = new Bucket<U>());
temp.hashCode = hashCode;
}
for(Integer i = 0; i < temp.values.size(); i++) {
if(temp.values[i].equals(value)) {
U result = temp.values[i];
temp.values[i] = value;
return result; // Returns old value
}
}
temp.values.add(value); // Adds new value to bucket
}
}
Of course, this isn't the actual code that happens, just sort of pseudocode. As you can see, a lot of stuff is going on under the covers.
The main point here is that if you change the value, and thus its hashCode, it will no longer be found in its original bucket, which is cached inside the map.
When you force a System.debug, the internal state of the Map is refreshed and buckets are recalculated. This can change the number of keys internally and end up with fewer total elements as well.
This occurs with both Map and Set objects. If you choose to modify the hashCode of a value for a key, you will corrupt the collection's internal state until you debug it, which fixes it.
As you've observed, this causes problems when you insert sObject records, or later modify any of their fields, or otherwise use objects that have unstable hashCode values.
Side note: Using sObject keys is not a Bad Practice™. In fact, I use this technique fairly often. sObject keys allows you to perform certain types of checks efficiently without wrapper classes. However, using this technique means you need to consciously make decisions to avoid corrupting the internal state of the collection.