Путаница с ценностными классами
Я ищу пояснения к определению классов на основе значений. Я не могу представить, как последняя точка (6) должна работать вместе с первой
- (1) они являются окончательными и неизменяемыми (хотя могут содержать ссылки на изменяемые объекты)
- (6) они могут быть свободно заменяемыми при равенстве, что означает, что замена любых двух экземпляров x и y, которые равны в соответствии с equals() в любом вычислении или вызове метода, не должна вызывать видимых изменений в поведении.
Optional
такой класс.
Optional a = Optional.of(new ArrayList<String>());
Optional b = Optional.of(new ArrayList<String>());
assertEquals(a, b); // passes as `equals` delegated to the lists
b.get().add("a");
// now bite the last bullet
assertTrue(a.get().isEmpty()); // passes
assertTrue(b.get().isEmpty()); // throws
Я читаю это неправильно, или это должно было бы быть более точным?
Обновить
Ответ Эрана имеет смысл (они больше не равны), но позвольте мне переместить цель:
...
assertEquals(a, b); // now, they are still equal
assertEquals(m(a, b), m(a, a)); // this will throw
assertEquals(a, b); // now, they are equal, too
Давайте определим забавный метод m
, который делает некоторую мутацию и отменяет ее снова:
int m(Optional<ArrayList<String>> x, Optional<ArrayList<String>> y) {
x.get().add("");
int result = x.get().size() + y.get().size();
x.get().remove(x.get().size() - 1);
return result;
}
Это странный метод, я знаю. Но я думаю, это квалифицируется как "любое вычисление или вызов метода", не так ли?
5 ответов
Вы можете определить недействительность своих действий из спецификации, на которую вы ссылаетесь:
Программа может выдавать непредсказуемые результаты, если она пытается различить две ссылки на равные значения класса, основанного на значениях, будь то напрямую через равенство ссылок или косвенно через обращение к синхронизации, хешированию идентичности, сериализации или любому другому чувствительному к идентичности механизму. Использование таких чувствительных к идентичности операций в экземплярах классов, основанных на значениях, может иметь непредсказуемые последствия, и их следует избегать.
(акцент мой)
Модификация объекта является чувствительной к идентификации операцией, поскольку она влияет только на объект с определенной идентификационной информацией, представленной ссылкой, которую вы используете для модификации.
Когда ты звонишь x.get().add("");
вы выполняете операцию, которая позволяет распознать x
а также y
представлять тот же экземпляр, другими словами, вы выполняете операцию, чувствительную к личности.
Тем не менее, я ожидаю, что если будущая JVM действительно попытается заменить экземпляры на основе значений, она должна исключить экземпляры, ссылающиеся на изменяемые объекты, для обеспечения совместимости. Если вы выполняете операцию, которая производит Optional
с последующим извлечением Optional
например, … stream. findAny().get()
, было бы катастрофическим / неприемлемым, если бы промежуточная операция позволила заменить элемент другим объектом, который оказался равным в точке промежуточного Optional
использовать (если элемент не является типом значения)…
они могут быть свободно заменяемыми при равенстве. Это означает, что взаимозаменяемость любых двух экземпляров x и y, которые равны в соответствии с equals() в любом вычислении или вызове метода, не должна вызывать видимых изменений в поведении.
однажды b.get().add("a");
выполняется, a
больше не equals
в b
так что у вас нет причин ожидать assertTrue(a.get().isEmpty());
а также assertTrue(b.get().isEmpty());
даст тот же результат.
Тот факт, что класс, основанный на значениях, является неизменным, не означает, что вы не можете изменить значения, хранящиеся в экземплярах таких классов (как указано в though may contain references to mutable objects
). Это означает, что, как только вы создадите Optional
экземпляр с Optional a = Optional.of(new ArrayList<String>())
, вы не можете мутировать a
держать ссылку на другой ArrayList
,
Я думаю, что более интересный пример выглядит следующим образом:
void foo() {
List<String> list = new ArrayList<>();
Optional<List<String>> a = Optional.of(list);
Optional<List<String>> b = Optional.of(list);
bar(a, b);
}
Понятно, что a.equals(b)
правда. Кроме того, так как Optional
является окончательным (не может быть разделен на подклассы), неизменным, и оба a
а также b
обратиться к тому же списку, a.equals(b)
всегда будет правдой. (Ну, почти всегда, в зависимости от условий гонки, когда другой поток изменяет список, в то время как этот сравнивает их.) Таким образом, похоже, что это был бы случай, когда JVM могла бы заменить b
за a
или наоборот.
Как обстоят дела сегодня (Java 8 и 9 и 10) мы можем написать a == b
и результат будет ложным. Причина в том, что мы знаем, что Optional
является экземпляром обычного ссылочного типа, и то, как вещи в настоящее время реализованы, Optional.of(x)
всегда будет возвращать новый экземпляр, а два новых экземпляра никогда не будут ==
друг другу.
Тем не менее, абзац внизу определения классов на основе значений гласит:
Программа может выдавать непредсказуемые результаты, если она пытается различить две ссылки на равные значения класса, основанного на значениях, будь то напрямую через равенство ссылок или косвенно через обращение к синхронизации, хешированию идентичности, сериализации или любому другому чувствительному к идентичности механизму. Использование таких чувствительных к идентичности операций в экземплярах классов, основанных на значениях, может иметь непредсказуемые последствия, и их следует избегать.
Другими словами, "не делай этого" или, по крайней мере, не полагайся на результат. Причина в том, что завтра семантика ==
операция может измениться. В гипотетическом будущем типизированном мире, ==
может быть переопределено, чтобы типы значений были такими же, как equals
, а также Optional
может измениться от класса, основанного на значениях, к типу значения. Если это произойдет, то a == b
будет верным, а не ложным.
Одна из основных идей о типах значений заключается в том, что у них нет понятия идентичности (или, возможно, их идентичность не обнаруживается в программах Java). В таком мире, как мы можем сказать, a
а также b
"действительно" одинаковы или разные?
Предположим, мы должны были bar
метод с помощью некоторого средства (скажем, отладчика), так что мы можем проверять атрибуты значений параметров способом, который нельзя сделать с помощью языка программирования, например, путем просмотра машинных адресов. Даже если a == b
верно (помните, в мире с типизированным значением, ==
такой же как equals
) мы могли бы убедиться, что a
а также b
проживать по разным адресам в памяти.
Теперь предположим, что компилятор JIT компилируется foo
и встраивает призывы к Optional.of
, Видя, что теперь есть два фрагмента кода, которые возвращают два результата, которые всегда equals
компилятор устраняет один из блоков, а затем использует тот же результат везде, где a
или же b
используется. Теперь в нашей инструментальной версии bar
мы могли бы заметить, что два значения параметра одинаковы. JIT-компилятору разрешено делать это из-за шестого элемента маркера, который позволяет заменять значения, которые equals
,
Обратите внимание, что мы можем наблюдать это различие только потому, что мы используем экстралингвистический механизм, такой как отладчик. В языке программирования Java мы вообще не можем различить, и, следовательно, эта замена не может повлиять на результат любой программы Java. Это позволяет JVM выбирать любую стратегию реализации, которую он считает подходящей. JVM может свободно размещать a
а также b
в куче, в стеке, по одному на каждый, как отдельные экземпляры или как одни и те же экземпляры, если только программы на Java не могут различить. Когда JVM предоставляется свобода выбора реализации, это может заставить программы работать намного быстрее.
В этом суть шестого пункта пули.
Когда вы выполняете строки:
Optional a = Optional.of(new ArrayList<String>());
Optional b = Optional.of(new ArrayList<String>());
assertEquals(a, b); // passes as `equals` delegated to the lists
В assertEquals(a, b) согласно API:
- проверим, если параметры
a
а такжеb
оба необязательны - Предметы не имеют никакой ценности или,
- Существующие значения "равны" друг другу с помощью equals() (в вашем примере это равно ArrayList).
Таким образом, когда вы изменяете один из ArrayList, на который указывает необязательный экземпляр, assert потерпит неудачу в третьей точке.
В пункте 6 говорится, что если a & b равны, то они могут использоваться взаимозаменяемо, т. Е. Если метод ожидает два экземпляра класса A, а вы создали экземпляры a & b, то, если a & b проходит точку 6, вы можете отправить (a,a) or (b,b) or (a,b)
все три будут давать одинаковый результат.