Кирилл Данилов (donz_ru) wrote,
Кирилл Данилов
donz_ru

Category:

java.land.Object. Парадокс с hashCode()

Первый класс, который рассматривается в любой книге по Java для начинающих, - java.lang.Object. Контракт на него должен быть полностью прочитан и понят каждым ява-разработчиком.
Но есть парадокс с hashCode(), являющимся основой многими "любимых" HashMap и HashSet. На самом деле отмазаться от понимания этого метода из-за отсутствия его в реальной жизни не получится даже у разработчиков, реализующих POJO объекты. Потому что даже если в самописной логике не используются коллекции, то в ORM и других сторонних библиотеках их предостаточно.

Первое условие в описании метода:

Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified.


Что в вольном переводе означает:

Когда бы и сколько бы раз этот метод ни был вызван у одного и того же объекта (один и тот же с точки зрения Java, а не логики самого объекта) во время выполнения ява приложения, он должен единообразно возвращать одно и то же значение integer, без предоставления какой-либо информации, используемой в методе equals, используемой для определения изменения состояния объекта в том случае, если поля, используемые для сравнения в equals, не изменялись.
 
Из этого следует, что хэш-код должен вычисляться только один раз при инициализации объекта (то есть в конструкторе). Если нужно использовать алгоритм, отличный от реализации hashCode() по умолчанию (базируется на адресе памяти, где располагается объект), значит для вычисления должны быть использованы значения, переданные в конструктор как параметры. И этот набор значений должен являться уникальным неизменяемым идентификатором объекта и сохранен в соответствующие неизменяемые поля объекта. И эти же поля должны в первую очередь анализироваться в переопределенном методе equals.

Парадокс в том, что на контракты equals и hashCode не просто забивают абсолютно все, начиная с самих же авторов основных пакетов Java, но и реализации многих библиотек и фреймворков подталкивают к ошибочному переопределению этих методов или просто не оставляют выбора. Тот же Hibernate обязывает разработчиков предоставить ему в POJO объекте конструктор по умолчанию. А значит поля, на основе которых вычисляется хеш, являются изменяемыми. 
В этом случае наиболее неправильный способ посчитать хэш, и он же самый распространенный, - вычислять его при каждом вызове hashCode(). Может привести к ошибкам в логике программы, а также к утечке памяти. Пример: создали объект, проинициализировали Id, положили в HashMap в качестве ключа, изменили Id объекта, попытались получить из HashMap ранее сохраненное значение, получили null, но ссылка на объект и соответствующее ему значение будет в HashMap до вызова clear или до уничтожения самого объекта HashMap. 
Если программист заморочился и вычислил хэш только один раз, игнорируя последующие изменения уникального идентификатора объекта (а это вполне может быть, раз у объекта есть сеттер этого поля), то получим просто возможную ошибку в логике программы и в реализации методов equals и hashCode. Пример: создали объект с Id = 1, создали второй объект с Id = 2, изменили у первого объекта Id на 2. Вызвали equals - получили true. Вызвали hashCode() получили различие, что противоречит спецификации.
Наиболее правильный способ в данной ситуации - позволять вызывать сеттер для идентификатора только один раз, бросая исключения при повторных вызовах. При этом остается надеяться, что hashCode() не будет вызван до первой инициализации идентификатора. Плюс ко всему такое поведение сеттера совершенно не очевидно. Навряд ли ORM или кто еще попытается еще раз вызвать сеттер для уникального ID объекта, но кто знает. Исключение будет неожиданностью для вызывающего.
 
 
Какие мысли? Как вы переопределяете hashCode()?
 
 
Пример ошибочной реализации hashCode() в самом классе HashMap и в его внутреннем классе Entry. После вызова метода startTest() получаем утечку памяти, а также некорректную логику программы в процессе работы.
 
package ru.donz.test;
 
import java.util.*;
 
public class HashCodeTest
{
private Set<Map> mapContainer = new HashSet<Map>();
private static void addNewEntry( Map<Integer, String> map )
{
map.put( new Integer( map.size() ), "test string " + map.size() );
}
private void putToContainerAndTest( Map map )
{
boolean isAlreadyExisted = !mapContainer.add( map );
System.out.println( "isAlreadyExisted = " + isAlreadyExisted );
}
private void removeFromContainerAndTest( Map map )
{
boolean wasContained = mapContainer.remove( map );
System.out.println( "wasContained = " + wasContained );
}
private void printSetSize()
{
System.out.println( "Size of map container = " + mapContainer.size() );
}
private void startTest()
{
Map<Integer, String> testMap = new HashMap<Integer, String>();
 
addNewEntry( testMap );
putToContainerAndTest( testMap );
printSetSize();
 
addNewEntry( testMap );
putToContainerAndTest( testMap );
printSetSize();
 
putToContainerAndTest( testMap );
printSetSize();
 
removeFromContainerAndTest( testMap );
printSetSize();
 
removeFromContainerAndTest( testMap );
printSetSize();
 
removeFromContainerAndTest( testMap );
printSetSize();
}
public static void main( String[] args )
{
HashCodeTest test = new HashCodeTest();
test.startTest();
System.out.println( "\nOut of startTest method" );
test.printSetSize();
}
 
 

UPD: ошибся в переводе, как заметили на ru_java. Так что со следованием спецификации все хорошо, зря на разработчиков наехал. Остается вопрос, как минимизировать ошибку, потенциально приводящую к мемори лику.
Tags: hash, it, java, программирование
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 2 comments