Как использование оператора == повышает производительность по сравнению с аналогами?
В книге "Эффективная JAVA" Джошуа Блоха, когда я читал о статических фабричных методах, было следующее утверждение
Способность статических фабричных методов возвращать один и тот же объект из повторяющихся вызовов позволяет классам в любой момент поддерживать строгий контроль над тем, какие экземпляры существуют. Классы, которые делают это, называются контролируемыми экземплярами. Есть несколько причин для написания управляемых экземпляром классов. Контроль экземпляра позволяет классу гарантировать, что он является одноэлементным (элемент 3) или нереализуемым (элемент 4). Кроме того, он позволяет неизменному классу (элемент 15) гарантировать, что не существует двух одинаковых экземпляров: a.equals(b) тогда и только тогда, когда a==b. Если класс дает такую гарантию, его клиенты могут использовать оператор == вместо метода equals(Object), что может привести к повышению производительности. Типы enum (пункт 30) предоставляют эту гарантию.
Чтобы выяснить, как оператор == повышает производительность, я взглянул на String.java
Я видел этот фрагмент
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Под улучшением производительности что он здесь подразумевает? как это приносит улучшение производительности.
Он хочет сказать следующее
Если каждый класс может гарантировать, что a.equals (b) тогда и только тогда, когда a==b, это означает, что к нему предъявляется косвенное требование, что не может быть объектов, ссылающихся на 2 разных пространства памяти, и при этом все еще хранятся те же данные, которые являются памятью. потеря Если они содержат одни и те же данные, это один и тот же объект. То есть они указывают на одну и ту же область памяти.
Прав ли я в этом заключении?
Если я ошибаюсь, можете ли вы помочь мне понять это?
6 ответов
Часть, заключенная в кавычки, означает, что неизменный класс может выбирать интернирование своих экземпляров. Это легко реализовать с помощью Guava's Interner
, например:
public class MyImmutableClass {
private static final Interner<MyImmutableClass> INTERN_POOL = Interners.newWeakInterner();
private final String foo;
private final int bar;
private MyImmutableClass(String foo, int bar) {
this.foo = foo;
this.bar = bar;
}
public static MyImmutableClass of(String foo, int bar) {
return INTERN_POOL.intern(new MyImmutableClass(foo, bar));
}
@Override
public int hashCode() {
return Objects.hashCode(foo, bar);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true; // fast path for interned instances
if (o instanceof MyImmutableClass) {
MyImmutableClass rhs = (MyImmutableClass) o;
return Objects.equal(foo, rhs.foo)
&& bar == rhs.bar;
}
return false;
}
}
Здесь конструктор делается закрытым: все экземпляры должны быть через MyImmutableClass.of()
фабричный метод, который использует Interner
чтобы убедиться, что если новый экземпляр equals()
к существующему экземпляру, вместо этого возвращается существующий экземпляр.
Стажировка может использоваться только для неизменных объектов, под которыми я подразумеваю объекты, наблюдаемое состояние которых (т. Е. Поведение всех доступных извне методов, в частности equals()
а также hashCode()
) не изменяется для времени жизни объектов. Если вы интернируете изменяемые объекты, поведение будет неправильным при изменении экземпляра.
Как уже говорили многие другие люди, вы должны тщательно выбирать, какие объекты интернировать, даже если они неизменны. Делайте это только в том случае, если набор интернированных значений мал по сравнению с количеством дубликатов, которые у вас могут быть. Например, интернировать не стоит Integer
как правило, потому что существует более 4 миллиардов возможных значений. Но стоит интернировать наиболее часто используемых Integer
ценности, и на самом деле, Integer.valueOf()
значения интернов от -128 до 127. С другой стороны, перечисления отлично подходят для интернирования (и они интернированы по определению), поскольку набор возможных значений невелик.
Для большинства классов в целом вам нужно выполнить анализ кучи, например, используя jhat
(или, чтобы подключить мой собственный проект, fastthat), чтобы решить, достаточно ли дубликатов, чтобы гарантировать интернирование. В других случаях просто будьте проще и не проходите стажировку.
Если каждый класс может гарантировать, что a.equals(b) тогда и только тогда, когда a==b, это означает, что к нему предъявляется косвенное требование, что не может быть объектов, ссылающихся на 2 разных пространства памяти, и при этом все еще хранятся те же данные, которые являются памятью. потеря Если они содержат одни и те же данные, это один и тот же объект. То есть они указывают на одну и ту же область памяти.
Да, это то, на что автор ссылается.
Если вы можете (для данного класса, это не будет возможно для всех, в частности, это не может работать для изменяемых классов) вызвать ==
(это одиночный код операции JVM) вместо equals
(это динамический вызов метода), он экономит (некоторые) накладные расходы.
Так работает для enum
например.
И даже если кто-то назвал equals
метод (который был бы хорошей практикой защитного программирования, вы не хотите привыкать использовать ==
для объектов IMHO), этот метод может быть реализован как простой ==
(вместо того, чтобы смотреть на потенциально сложное состояние объекта).
Между прочим, даже для "нормальных" методов равенства (таких как String), в их реализации, вероятно, будет хорошей идеей сначала проверить идентичность объекта, а затем быстро посмотреть на состояние объекта (что и делает String#equals, как вы выяснили).
Если вы можете гарантировать, что не существует двух экземпляров объекта, так что их семантические значения эквивалентны (т.е. если x
а также y
ссылаются на разные случаи [x != y
] затем x.equals(y) == false
для всех x
а также y
), то это означает, что вы можете сравнить объекты двух ссылок на равенство, просто проверив, ссылаются ли они на один и тот же экземпляр. ==
делает.
Реализация ==
по сути, просто сравнивает два целых числа (адреса памяти) и, как правило, будет быстрее, чем практически все нетривиальные реализации .equals()
,
Стоит отметить, что это не прыжок, который можно сделать для String
с, так как вы не можете гарантировать, что любые два экземпляра String
не эквивалентны, например:
String x = new String("hello");
String y = new String("hello");
поскольку x != y && x.equals(y)
недостаточно просто сделать x == y
проверить на равенство.
В тех случаях, когда сложные значения инкапсулируются с использованием ссылок на неизменяемые объекты, обычно существует три сценария, которые могут возникнуть при сравнении двух ссылок:
Это ссылки на один и тот же объект (очень быстро)
Это ссылки на разные объекты, которые инкапсулируют разные значения (часто быстрые, но иногда медленные)
Это ссылки на разные объекты, которые инкапсулируют одно и то же значение (обычно всегда медленное).
Если объекты будут обнаруживаться равными чаще, чем нет, минимизация частоты случая 3 может иметь существенное значение. Если объекты часто будут очень близки, также может быть существенное значение для обеспечения того, чтобы медленные подслучаи случая 2 не бывает очень часто
Если кто-то удостоверится, что для любого данного значения никогда не будет более одного объекта, который содержит это значение, код, который наблюдает, что две ссылки идентифицируют разные объекты, может сделать вывод, что они инкапсулируют разные значения, без необходимости фактически исследовать рассматриваемые значения. Однако ценность этого часто ограничена. Если рассматриваемые объекты являются большими, сложными, вложенными коллекциями, которые иногда будут очень похожи, каждый может рассчитывать каждую коллекцию и кэшировать 128-битный хэш ее содержимого; две коллекции с разным содержимым вряд ли будут иметь совпадающие значения хеш-функции, а коллекции с разными значениями хеш-функции могут быть быстро распознаны как неравные. С другой стороны, наличие ссылок, которые инкапсулируют один и тот же контент, обычно идентифицируют один и тот же объект, даже если существует несколько ссылок на идентичные коллекции, что может улучшить производительность в случае, когда все остальное всегда плохое.
Подход, который можно использовать, если не хотите использовать отдельную интернированную коллекцию, состоит в том, чтобы каждый объект сохранял long
порядковый номер, так что всегда можно определить, какой из двух идентичных в противном случае объектов был создан первым, вместе со ссылкой на самый старый объект, который, как известно, содержит одинаковое содержимое. Чтобы сравнить две ссылки, начните с определения самого старого объекта, известного как эквивалентный каждому. Если самый старый объект, который соответствует первому, не совпадает с самым старым объектом, который соответствует второму, сравните содержимое объектов. Если они совпадают, один будет новее, чем другой, и этот объект может расценивать другой как "самый старый объект из известных".
Чтобы ответить на ваши вопросы...
Под улучшением производительности что он подразумевает здесь [Строка]? Как это приносит улучшение производительности.
Это НЕ пример того, о чем говорит Блох. Блох говорит об управляемых экземплярами классах, и String
это не такой класс!
Прав ли я в этом заключении?
Да, это правильно. Управляемый экземплярами класс, для которого экземпляры являются неизменяемыми, может гарантировать, что объекты, которые "одинаковы", всегда будут равны согласно ==
оператор.
Некоторые наблюдения, хотя:
Это относится только к неизменным объектам. Или, точнее, к объектам, где мутация не влияет на семантику равенства.
Это относится только к полностью управляемым экземпляром классам.
Контроль экземпляра может быть дорогим. Рассмотрим форму (частичного) экземпляра управления, предоставляемого классом String
intern
метод и пул строк.Пул строк фактически является хеш-таблицей слабых ссылок на объекты String. Это занимает дополнительную память.
Каждый раз, когда вы интернируете String, он вычисляет хеш-код строки и проверяет хеш-таблицу, чтобы увидеть, была ли похожая строка уже интернирована.
Каждый раз, когда выполняется полный сборщик мусора, слабые ссылки в пуле строк приводят к дополнительной "трассировке" работы для сборщика мусора и, возможно, к дополнительной работе, если сборщик мусора решает прервать ссылки.
Обычно вы получаете схожие издержки при реализации ваших собственных классов, управляемых экземплярами. Когда вы проводите анализ затрат и выгод, эти накладные расходы учитывают преимущества более быстрого сравнения экземпляров.
Я думаю, что это означает это:
Если вам нужно проверить две сложные структуры на равенство, вам, как правило, нужно сделать много тестов, чтобы убедиться, что они одинаковы.
Но если из-за какой-то хитрости языка вы знали, что две сложные, но равные структуры не могут существовать одновременно, тогда вместо проверки равенства путем сравнения их по крупицам вы можете просто проверить, что они находятся в одном и том же месте в памяти, и вернуть false, если они не.
Если кто-то может создавать объекты, то вы не можете гарантировать, что два объекта не могут быть созданы одинаково, но являются разными экземплярами... но если вы контролируете создание объектов и создаете только отдельные объекты, вам не нужно сложное равенство тесты.