I stumbled across the source of AtomicInteger
and realized that
new AtomicInteger(0).equals(new AtomicInteger(0))
equals false
.
Why is this? Is it some "defensive" design choice related to concurrency issues? If so, what could go wrong if it was implemented differently?
(I do realize I could use get
and ==
instead.)
equals
is not only used for equality but also to meet its contract withhashCode
, i.e. in hash collections. The only safe approach for hash collections is for mutable object not to be dependant on their contents. i.e. for mutable keys a HashMap is the same as using an IdentityMap. This way the hashCode and whether two objects are equal does not change when the keys content changes.So
new StringBuilder().equals(new StringBuilder())
is also false.To compare the contents of two AtomicInteger, you need
ai.get() == ai2.get()
orai.intValue() == ai2.intValue()
Lets say that you had a mutable key where the hashCode and equals changed based on the contents.
prints
As you can see we now have 10 keys, all equal and with the same hashCode!
AtomicInteger inherits from Object and not Integer, and it uses standard reference equality check.
If you google you will find this discussion of this exact case.
One limitation with Java is that there is no means of distinguishing a mutable-class instance which can and will be mutated, from a mutable-class instance which will never be exposed to anything that might mutate it(*). References to things of the former type should only be considered equal if they refer to the same object, while references to things of the latter type should often be considered equal if the refer to objects with equivalent state. Because Java only allows one override of the virtual
equals(object)
method, designers of mutable classes have to guess whether enough instances will meet the latter pattern (i.e. be held in such a way that they'll never be mutated) to justify havingequals()
andhashCode()
behave in a fashion suitable for such usage.In the case of something like
Date
, there are a lot of classes which encapsulate a reference to aDate
that is never going to be modified, and which want to have their own equivalence relation incorporate the value-equivalence of the encapsulatedDate
. As such, it makes sense forDate
to overrideequals
andhashCode
to test value equivalence. On the other hand, holding a reference to anAtomicInteger
that is never going to be modified would be silly, since the whole purpose of that type centers around mutability. AnAtomicInteger
instance which is never going to be mutated may, for all practical purposes, simply be anInteger
.(*) Any requirement that a particular instance never mutate is only binding as long as either (1) information about its identity hash value exists somewhere, or (2) more than one reference to the object exists somewhere in the universe. If neither condition applies to the instance referred to by
Foo
, replacingFoo
with a reference to a clone ofFoo
would have no observable effect. Consequently, one would be able to mutate the instance without violating a requirement that it "never mutate" by pretending to replaceFoo
with a clone and mutating the "clone".On the face of it, it seems like a simple omission but it maybe it does make some sense to actually just use the idenity equals provided by
Object.equals
For instance:
seems reasonable, but
b
isn't reallya
, it is designed to be a mutable holder for a value and therefore can't really replacea
in a program.also:
should work but what if b's value changes in between.
If this is the reason it's a shame it wasn't documented in the source for
AtomicInteger
.As an aside: A nice feature might also have been to allow
AtomicInteger
to be equal to an Integer.trouble it would mean that in order to be reflexive in this case Integer would have to accept
AtomicInteger
in it's equals too.I suspect that comparing the values is a no-go since there's no way to do it atomically in a portable fashion (without locks, that is).
And if there's no atomicity then the variables could compare equal even they never contained the same value at the same time (e.g. if
a
changed from0
to1
at exactly the same time asb
changed from1
to0
).Imagine if
equals
was overriden and you put it in aHashMap
and then you change the value. Bad things will happen:)