Почему два AtomicIntegers никогда не равны?
Я наткнулся на источник AtomicInteger
и понял, что
new AtomicInteger(0).equals(new AtomicInteger(0))
равно false
.
Почему это? Это какой-то "защитный" выбор дизайна, связанный с проблемами concurrency? Если да, то что может пойти не так, если бы оно было реализовано по-другому?
(Я понимаю, что вместо get
и ==
я мог бы использовать.)
Ответы
Ответ 1
Отчасти это связано с тем, что AtomicInteger
не является заменой общего назначения для Integer
.
В пакете java.util.concurrent.atomic
указано:
Атомные классы не являются заменами общего назначения для java.lang.Integer
и связанные классы. Они не определяют методы таких как hashCode
и compareTo
. (Поскольку атомные переменные ожидается, что они будут мутированы, они плохо подходят для клавиш хеш-таблицы.)
hashCode
не реализован, и так происходит с equals
. Частично это объясняется гораздо большим обоснованием, которое обсуждается в архивах списков рассылки, о том, следует ли AtomicInteger
расширять Number
или нет.
Одна из причин, почему класс AtomicXXX не является заменой примитива и не реализует интерфейс Comparable
, заключается в том, что в большинстве сценариев нет смысла сравнивать два экземпляра класса AtomicXXX, Если два потока могут получить доступ и изменить значение AtomicInteger
, то результат сравнения недействителен, прежде чем использовать результат, если поток изменяет значение AtomicInteger
. Такое же обоснование хорошо подходит для метода equals
- результат для теста равенства (который зависит от значения AtomicInteger
) действителен только до того, как поток мутирует один из рассматриваемых AtomicInteger
.
Ответ 2
На первый взгляд, это похоже на простое упущение, но, может быть, действительно имеет смысл фактически просто использовать idenity, равный Object.equals
Например:
AtomicInteger a = new AtomicInteger(0)
AtomicInteger b = new AtomicInteger(0)
assert a.equals(b)
кажется разумным, но b
на самом деле не является a
, он предназначен как изменяемый держатель для значения и поэтому не может реально заменить a
в программе.
и
assert a.equals(b)
assert a.hashCode() == b.hashCode()
должен работать, но что, если значение b изменяется между ними.
Если это повод, то это было не стыдно, что он не был зарегистрирован в источнике для AtomicInteger
.
В стороне: хорошая функция также могла бы позволить AtomicInteger
быть равно Integer.
AtomicInteger a = new AtomicInteger(25);
if( a.equals(25) ){
// woot
}
это означало бы, что для того, чтобы быть рефлексивным, в этом случае Integer должен был бы принимать AtomicInteger
в нем тоже.
Ответ 3
Я бы сказал, что поскольку точка AtomicInteger
заключается в том, что операции могут выполняться атомарно, было бы трудно обеспечить, чтобы эти два значения сравнивались атомарно, и поскольку AtomicIntegers обычно являются счетчиками, вы бы получили некоторые нечетное поведение.
Поэтому, не гарантируя, что метод equals
синхронизирован, вы не будете уверены, что значение атомного целого не изменилось к моменту возврата equals
. Однако, поскольку вся точка целого атома не использует синхронизацию, вы получите небольшую выгоду.
Ответ 4
Я подозреваю, что сравнение значений - это не-go, потому что нет способа сделать это атомарно переносимым способом (без блокировок, то есть).
И если нет атомарности, тогда переменные могли бы сравниться одинаково, даже если они никогда не содержали одно и то же значение одновременно (например, если a
изменено с 0
до 1
точно в то же время, что и b
изменено от 1
до 0
).
Ответ 5
AtomicInteger наследует от Object, а не Integer, и использует стандартную проверку равенства ссылок.
Если вы google, вы найдете это обсуждение этого точного случая.
Ответ 6
Представьте, что если equals
был переопределен, и вы поместили его в HashMap
, а затем измените значение. Плохие вещи произойдут:)
Ответ 7
equals
правильно реализована: экземпляр AtomicInteger
может только соответствовать самому себе, поскольку только тот же самый экземпляр будет, по-видимому, хранить одну и ту же последовательность значений с течением времени.
Напомним, что классы Atomic*
действуют как ссылочные типы (например, java.lang.ref.*
), предназначенные для обертывания фактического "полезного" значения. В отличие от этого в функциональных языках (см., Например, Clojure Atom
или Haskell IORef
), различие между ссылками и значениями довольно размыто в Java (видоизменяемость), но оно все еще существует.
Учитывая, что текущее завернутое значение класса Atomic в качестве критерия равенства является вполне понятным заблуждением, поскольку это подразумевает, что new AtomicInteger(1).equals(1)
.
Ответ 8
Одно ограничение с Java заключается в том, что нет возможности различать экземпляр изменяемого класса, который может и будет мутирован, из экземпляра mutable-class, который никогда не будет подвергаться действиям, которые могут его изменить (*). Ссылки на вещи первого типа следует считать равными, если они относятся к одному и тому же объекту, тогда как ссылки на вещи последнего типа часто считаются равными, если они относятся к объектам с эквивалентным состоянием. Поскольку Java допускает только одно переопределение виртуального метода equals(object)
, разработчики изменяемых классов должны угадать, будут ли достаточные экземпляры соответствовать последнему шаблону (т.е. Быть сохранены таким образом, что они никогда не будут мутированы), чтобы оправдать наличие equals()
и hashCode()
ведут себя как способ, подходящий для такого использования.
В случае с чем-то вроде Date
существует много классов, которые инкапсулируют ссылку на Date
, которая никогда не будет изменена и которая хочет иметь собственное отношение эквивалентности, включает в себя эквивалентность значений инкапсулированного Date
. По существу, для Date
имеет смысл переопределить equals
и hashCode
для проверки эквивалентности значений. С другой стороны, сохранение ссылки на AtomicInteger
, которое никогда не будет изменено, было бы глупо, поскольку вся цель этого типа сосредотачивается вокруг изменчивости. Экземпляр AtomicInteger
, который никогда не будет мутированным, может, во всех практических целях, просто быть Integer
.
(*) Любое требование, которое конкретный экземпляр никогда не мутирует, является только обязательным, если либо (1) информация о его хэш-значении идентичности существует где-то, либо (2) более чем одна ссылка на объект существует где-то во Вселенной. Если ни одно условие не относится к экземпляру, указанному в Foo
, замена Foo
ссылкой на клон Foo
не будет иметь наблюдаемого эффекта. Следовательно, можно было бы мутировать экземпляр без нарушения требования о том, что он "никогда не мутирует", притворяясь заменой Foo
клоном и мутацией "клона".
Ответ 9
equals
используется не только для равенства, но и для удовлетворения его контракта с hashCode
, т.е. в хэш-наборах. Единственный безопасный подход для хеш-коллекций - это то, что изменяемый объект не зависит от их содержимого. то есть для измененных ключей HashMap совпадает с использованием IdentityMap. Таким образом, хэш-код и равны ли два объекта, не изменяется при изменении содержимого клавиш.
Итак, new StringBuilder().equals(new StringBuilder())
также неверно.
Чтобы сравнить содержимое двух AtomicInteger, вам нужно ai.get() == ai2.get()
или ai.intValue() == ai2.intValue()
Предположим, что у вас есть изменяемый ключ, где hashCode и равно меняются в зависимости от содержимого.
static class BadKey {
int num;
@Override
public int hashCode() {
return num;
}
@Override
public boolean equals(Object obj) {
return obj instanceof BadKey && num == ((BadKey) obj).num;
}
@Override
public String toString() {
return "Bad Key "+num;
}
}
public static void main(String... args) {
Map<BadKey, Integer> map = new LinkedHashMap<BadKey, Integer>();
for(int i=0;i<10;i++) {
BadKey bk1 = new BadKey();
bk1.num = i;
map.put(bk1, i);
bk1.num = 0;
}
System.out.println(map);
}
печатает
{Bad Key 0=0, Bad Key 0=1, Bad Key 0=2, Bad Key 0=3, Bad Key 0=4, Bad Key 0=5, Bad Key 0=6, Bad Key 0=7, Bad Key 0=8, Bad Key 0=9}
Как вы можете видеть, теперь у нас есть 10 ключей, все одинаковые и с одинаковым хэш-кодом!