2014-12-21 2 views
1

Предположим, что у вас есть написанный класс и использовали ленивую инициализацию для назначения одного из своих полей. Предположим, что вычисление для этого поля связано только с другими полями и гарантируется, что каждый раз он будет получать одинаковый результат. Когда два равных экземпляра класса встречаются друг с другом, им разумно делиться значением лениво инициализированного поля (если оно известно). Вы можете сделать это в методе equals(). Вот класс, показывающий, что я имею в виду.Назначение полей в методе equals()

final class MyClass { 

    private final int number; 
    private String string; 

    MyClass(int number) { 
     this.number = number; 
    } 

    String getString() { 
     if (string == null) { 
      string = OtherClass.expensiveCalculation(number); 
     } 
     return string; 
    } 

    @Override 
    public boolean equals(Object object) { 
     if (object == this) { return true; } 
     if (!(object instanceof MyClass)) { return false; } 
     MyClass that = (MyClass) object; 
     if (that.number != number) { return false; } 
     String thatString = that.string; 
     if (string == null && thatString != null) { 
      string = thatString; 
     } else if (thatString == null && string != null) { 
      that.string = string; 
     } 
     return true; 
    } 

    @Override 
    public int hashCode() { return number; } 
} 

Для меня это обмен информацией, кажется логичным, чтобы сделать, если вы собираетесь пойти на усилия лениво инициализирует поля, но я никогда не видел пример, кто использует метод equals() в этом путь.

Это обычная или стандартная техника? Если да, то как это называется? Если это не обычная техника, могу ли я спросить (рискуя поставить вопрос о том, чтобы удержать вопрос в основном на основе мнения), что люди думают об этом? Это хорошая идея использовать метод equals() для выполнения каких-либо иных действий, кроме проверки равенства?

+4

Если это действительно заставляет ваше приложение работать значительно лучше (при тестировании с реалистичными данными, подобными производственным, так же, как и для производства), подойдите к нему. Но будьте осторожны, чтобы документировать это хорошо, предпочтительно в коде, и убедиться, что есть единичные тесты, охватывающие это поведение во всех отношениях; в противном случае следующий человек после того, как вы будете поддерживать код (или себя через несколько месяцев), будет ненавидеть ваши кишки. –

ответ

1

подхода вы охарактеризовали иногда хороший, особенно в ситуациях, когда, вероятно, многие крупные неизменные объекты, несмотря на независимость, будут в конечном итоге идентичны. Поскольку сравнивать сравнительные ссылки по сравне нию гораздо быстрее, чем сравнивать большие объекты, которые оказываются равными, может быть полезно иметь код, который сравнивает два больших объекта и находит их идентичными, заменяя одну из ссылок ссылкой на другую , Чтобы это было осуществимо, нужно попытаться установить какой-то порядок между рассматриваемыми объектами, чтобы гарантировать, что повторные сравнения в конечном итоге принесут ту же каноническую ценность. Это может быть достигнуто за счет того, что объекты включают порядковый номер long и последовательно заменяют ссылки на более новые значения ссылками на значения более старого, но равного или сравнивая значение равных ссылок и отбрасывая, в зависимости от того, какая из них имеет, если таковые имеются, более низкую value (если две ссылки, которые идентифицируют отдельные, но идентичные экземпляры, сообщают о том же identityHashCode, оба должны быть сохранены).

Отвратительная, но неудачная морщина в том, что у Java очень плохая многопоточная поддержка для эффективно-неизменяемых объектов. Для эффективно-неизменяемого объекта, который должен быть потокобезопасным, любой доступ к массиву или не-final должен пройти через поле final. Самый дешевый способ выполнения этого, вероятно, состоит в том, чтобы объект содержал поле final, в которое он хранит ссылку на себя, и имеет все методы, которые получают доступ к полям final, таким образом, что это поле final, но это немного уродливо. Тем не менее, изменение ссылок на отдельные, но идентичные ссылки со ссылками на один и тот же объект может дать некоторые существенные преимущества в производительности, несмотря на глупый избыточный final доступ к полю (поскольку цель конечного поля будет гарантирована быть в кэше, разыменование будет намного дешевле обычного разыменования).

Кстати, во многих случаях можно было бы включить механизм «эквивалентности-отношения», чтобы после того, как некоторые объекты были сопоставлены и оказались равными, обнаружение того, что любой из них равен другому объекту, вызовет их все чтобы быть быстро узнаваемыми как таковые. Однако я не понял, как избежать возможности преднамеренно-противной, но законной схемы использования, вызывающей утечку памяти.

3

Это выглядит опасно для меня: использование побочного эффекта открытого метода объекта для установки состояния объекта. Это сломается, если вы подклассифицируете этот класс, а затем переопределите метод equals подкласса, что обычно нужно делать. Только не делай этого.

+2

Но действительно ли это побочный эффект? Вы не меняете ничего, что повлияло бы на то, как экземпляр видится снаружи. Класс является фактически неизменным. Кроме того, вы не можете переопределить 'equals()' - я специально сделал окончательный класс по этой причине. –

+0

@pbabcdefp 'Вы не меняете ничего, что могло бы повлиять на то, как экземпляр просматривается извне. 'Итак, изменение значения' string' не влияет на возвращаемое значение 'getString()'? – Tom

+1

@Tom Читайте код очень, очень осторожно. Вы устанавливаете строку, но исходное значение 'null' никогда не было бы возвращено методом getString()' в любом случае. –

2

«Предположим, что вычисление для этого поля связано только с другими полями и гарантирует, что каждый раз будет получать одинаковый результат».

Учитывая это предположение, вы можете утверждать, что значение лениво инициализированного поля не имеет значения, потому что, если значения остальных полей совпадают, вычисленное значение также будет одинаковым.

Редактировать Я предполагаю, что я обошел оригинальный вопрос, поэтому я тоже отвечу на него. В сценарии, который вы создали, нет ничего неправильного в том, что вы предлагаете.

Аргумент, который я хотел бы сделать, - это просто с прагматической точки зрения: что происходит, когда кто-то меняет определение getString() (или, скорее всего, меняет определение долгосрочного расчета, которое приводит к этому значению), и оно начинает полагаться на что-то, что не является частью соображений равенства объекта?

Причина, по которой общепринятая мудрость говорит о том, что equals() должен быть свободным от побочных эффектов, заключается в том, что большинство разработчиков ожидают, что он будет свободным от побочных эффектов.

+0

До тех пор, пока 'costComput' возвращает один и тот же результат каждый раз, метод' equals' по существу является побочным эффектом. Я согласен, что это не здорово, что 'дорогойComput' определено в другом классе, но это должно было сделать мой пример максимально простым. Если бы я на самом деле это сделал (и вы все можете расслабиться - у меня никогда не было), я бы поставил код, вычисляющий 'String' внутри' MyClass' вместе с комментарием, указывающим, что любые изменения кода не должны изменять факт что результат должен зависеть только от значения 'number'. –

+1

Я понимаю, откуда вы пришли, но это не вопрос о том, можно ли сделать операцию кеша (которая по сути является тем, что вы имитируете здесь) безопасна. Речь идет о том, сколько это стоит с точки зрения времени и энергии, чтобы написать это таким образом, а затем убедитесь, что никто никогда не меняет его. На данный момент, я бы сказал, что расчет должен быть феноменально дорогим, чтобы оправдать объем работы, который требуется просто, чтобы не запускать его дважды. – arootbeer

+0

Это аргумент против ленивой инициализации, которая является очень распространенной техникой. Некоторые из стандартных идиом с ленивой инициализацией столь же сложны, как и это. Идиома двойной проверки приходит в голову. –

2

Я бы не сделать это, по трем причинам:

  1. Общие принципы программно-инженерные, такие как сплоченность, слабая связь, и «не повторять себя», препятствовать против него: вашего метода equals(...) будет делать что-то не очень «равно» -y, которое перекрывается с логикой вашего метода getString(). Кто-то, обновляющий логику getString(), вполне может не заметить, нужно ли также обновлять логику equals(...). (Вы можете подумать, что логика equals(...) будет по-прежнему правильной, независимо от того, , какgetString() изменен — В конце концов, у вас только есть equals(...) скопируйте ссылку с одного объекта на эквивалентный, так что предположительно это должно всегда оставаться то же самое? —, но проблема в том, что сложные системы развиваются так, что вы не всегда можете предсказать заранее.Когда изменяется требование, вы не хотите делать случайные изменения в частях кода, которые явно не связаны с этим требованием.)

  2. Безопасность резьбы. Ваше поле string в настоящее время не volatile, а ваш метод getString() в настоящее время не synchronized, поэтому здесь нет никакой попытки обеспечить безопасность потоков; но если бы вы сделали остальную часть класса потокобезопасной, было бы совершенно невозможно изменить equals(...), чтобы быть потокобезопасным, не рискуя взаимоблокировками. (Это немного перекрывается с точкой № 1, но я перечисляю его отдельно, потому что # 1 касается только сложности с пониманием , что у вас есть изменение equals(...), тогда как эта проблема немного сложна, чтобы адресовать даже , учитывая, что знания.)

  3. Неповторимость. Существует не так много причин ожидать, что это произойдет очень часто, что два экземпляра получают equals(...) -компрометированные, когда один уже был инициализирован ленивым, а другой - нет; поэтому дополнительная сложность кода и недостатки, упомянутые выше, вряд ли будут стоить того. (Помните: код не является бесплатным. Для того, чтобы пройти стоимость анализа, преимущества части кода должны превышать затраты на тестирование, понимание, поддержание и поддержку в будущем.) Если стоит поделиться этими ленивыми -инициализированные значения между эквивалентными экземплярами, то это должно быть сделано более ясным и более организованным способом, который не зависит от случайности. (Например, вы можете сделать конструктор приватным классовым, и есть static фабрика-метод, который проверяет staticWeakHashMap для существующего экземпляра до создания и возвращения нового.)

+0

Спасибо за ваш ответ. Я согласен с пунктами 1 и 3 - код не поддерживается, а «WeakHashMap» - это разумный маршрут кэширования. В пункте 2 я не уверен. Я только на странице 3 Java Concurrency In Practice, поэтому то, что я собираюсь спросить, вероятно, раскрывает полное незнание ... но почему имеет значение, если переменная 'string' не' volatile'?Разумеется, все это означает, что значение, назначенное ему другим потоком, может не отображаться, что может привести к тому, что 'дорогое вычисление()' будет выполняться без необходимости в более поздней точке. Но это не означает, что класс не является потокобезопасным, или не так ли? –

+0

@pbabcdefp: Хм, хороший вопрос. Спецификация не позволяет воспринимать полностью ложные записи, поэтому, если поток # 1 обновляет 'string' от' null' до ненулевой ссылки, тогда поток # 2 может видеть только «null» или эту ссылку; и класс 'String' использует' final' внутри, чтобы получить специальные гарантии, гарантирующие, что любой поток, который наблюдает за ненулевой ссылкой типа 'String', будет наблюдать полностью инициализированный объект' String'. (См. Раздел 17.5 JLS.) Я написал этот параграф в предположении, что вы бы не хотели, чтобы 'дорогойCalculation()' вызывался несколько * [продолжение] * – ruakh

+0

* [продолжение] * раз, но если вы на самом деле ОК с такими дублирующимися вызовами, то да, я думаю, что существующий код является потокобезопасным. (Но я все равно согласен с этим пунктом. Если вы решили, что хотите получить более надежную гарантию безопасности нитей, или если вы изменили это поле на нечто, отличное от 'String', и поэтому пришлось использовать более явный механизм защиты от потоков или что-то еще , было бы немного сложно держать 'equals (...)' в соответствии с этим, не рискуя взаимоблокировками.) – ruakh