Глубокие мысли о методе HashCode в Java

Java

предисловие

Недавно я изучаю язык Go.В языке Go есть объекты-указатели.Переменная-указатель указывает на адрес памяти значения. Друзья-обезьяны, изучившие язык C, должны знать концепцию указателей. Синтаксис языка Go похож на C, можно сказать, что это C-подобный язык программирования, поэтому наличие указателей в языке Go является нормальным явлением. Мы можем взять символ адреса&Поместив его перед переменной, вы получите адрес памяти соответствующей переменной.

 1package main
2
3import "fmt"
4
5func main() {
6   var a int= 20   /* 声明实际变量 */
7   var ip *int        /* 声明指针变量 */
8
9   ip = &a  /* 指针变量的存储地址 */
10   fmt.Printf("a 变量的地址是: %x\n", &a  )
11
12   /* 指针变量的存储地址 */
13   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
14   /* 使用指针访问值 */
15   fmt.Printf("*ip 变量的值: %d\n", *ip )
16}

Поскольку мой основной язык разработки — Java, я думаю, что в Java нет указателя, так как же получить адрес памяти переменной в Java?

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

Многие люди говорят, что метод HashCode объекта возвращает адрес памяти объекта, в том числе я также нашел в главе 5 «Java Core Programming Volume I», что это HashCode, а его значение — адрес памяти объекта.

Но действительно ли метод HashCode является адресом памяти?Прежде чем ответить на этот вопрос, давайте рассмотрим некоторые основы.

== и равно

Сравнение двух объектов на предмет равенства в Java выполняется в основном с помощью==число, которое сравнивает их адреса хранения в памяти. Класс Object является суперклассом в Java и по умолчанию наследуется всеми классами.equalsметод, затем черезequalsМетод также может определить, являются ли два объекта одинаковыми, потому что он проходит через==быть реализованным.

1//Indicates whether some other object is "equal to" this one.
2public boolean equals(Object obj) {
3    return (this == obj);
4}

Советы:Вот дополнительное объяснение

Когда мы изучаем Java, мы знаем, что наследование Java является одиночным наследованием.Если все классы наследуют класс Object, почему мы можем расширять другие классы при создании класса?

Это связано с проблемой прямого наследования и косвенного наследования, когда созданный класс не передает ключевое словоextendКогда указанный класс отображается, класс напрямую наследует Object по умолчанию, A --> Object. Когда класс создается с помощью ключевого словаextendКогда указанный класс отображается, он косвенно наследует класс Object, A --> B --> Object.

То же самое здесь означает, являются ли два сравниваемых объекта одним и тем же объектом, то есть равны ли адреса в памяти. И нам иногда нужно сравнить, является ли содержание двух объектов одинаковым, то есть класс имеет свое собственное понятие «логического равенства», а не знать, относятся ли они к одному и тому же объекту.

Например, сравните, являются ли следующие две строки одинаковыми.String a = "Hello"иString b = new String("Hello"), здесь есть два случая одного и того же, это сравнить, являются ли a и b одним и тем же объектом (одинаков ли адрес памяти) или их содержимое одинаково? Как это отличить?

При использовании==Затем нужно сравнить, являются ли они одним и тем же объектом в памяти, но родительским классом объектов String по умолчанию также является Object, поэтому по умолчаниюequalsМетод также сравнивает адрес памяти, поэтому мы должны переписатьequalsметод, как написано в исходном коде String.

 1public boolean equals(Object anObject) {
2    if (this == anObject) {
3        return true;
4    }
5    if (anObject instanceof String) {
6        String anotherString = (String)anObject;
7        int n = value.length;
8        if (n == anotherString.value.length) {
9            char v1[] = value;
10            char v2[] = anotherString.value;
11            int i = 0;
12            while (n-- != 0) {
13                if (v1[i] != v2[i])
14                    return false;
15                i++;
16            }
17            return true;
18        }
19    }
20    return false;
21}

поэтому, когда мыa == bКогда судить о том, являются ли а и b одним и тем же объектом,a.equals(b)Это нужно для сравнения, одинаково ли содержимое a и b, что следует хорошо понимать.

В JDK не только класс String переписал метод equals, но также были переписаны типы данных Integer, Long, Double, Float и т.д.equalsметод. Поэтому, когда мы используем Long или Integer в качестве бизнес-параметров в коде, если мы хотим сравнить, равны ли они, не забудьте использоватьequalsметод вместо использования==.

потому что использовать==Будут неожиданные провалы, и многие типы данных, подобные этому, будут инкапсулировать пул констант внутри, например IntegerCache, LongCache и т. д. Когда значение данных находится в пределах определенного диапазона, оно будет получено напрямую из пула констант без создания нового объекта.

Если вы хотите использовать==, вы можете преобразовать эти типы оболочки данных в примитивные типы, а затем передать==сравнивать, потому что примитивные типы передаются через==Значение сравнивается, но нужно обратить внимание на возникновение NPE (NullPointException) в процессе преобразования.

Хэш-код в объекте

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

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

Таким образом, мы уменьшаем количество сравнений запросов, оптимизируем эффективность запроса и сокращаем время запроса.

Этот метод кодирования является методом hashCode в Java, который по умолчанию определен в классе Object.Это локальный метод, измененный в собственном коде, и возвращаемое значение имеет тип int.

 1/**
2 * Returns a hash code value for the object. This method is
3 * supported for the benefit of hash tables such as those provided by
4 * {@link java.util.HashMap}.
5 * ...
6 * As much as is reasonably practical, the hashCode method defined by
7 * class {@code Object} does return distinct integers for distinct
8 * objects. (This is typically implemented by converting the internal
9 * address of the object into an integer, but this implementation
10 * technique is not required by the
11 * Java™ programming language.)
12 *
13 * @return  a hash code value for this object.
14 * @see     java.lang.Object#equals(java.lang.Object)
15 * @see     java.lang.System#identityHashCode
16 */
17public native int hashCode();

Как видно из описания аннотации, метод hashCode возвращает значение хеш-кода объекта. Это может быть полезно для хеш-таблиц, таких как HashMap. Метод hashCode, определенный в классе Object, возвращает разные целочисленные значения для разных объектов. Там, где есть сбивающее с толку возражениеThis is typically implemented by converting the internal address of the object into an integerЭто предложение означает, что обычно оно реализуется путем преобразования внутреннего адреса объекта в целочисленное значение.

Если не вникать, то можно подумать, что он возвращает адрес памяти объекта.Можно продолжить смотреть на его реализацию, но поскольку это нативный метод, то мы не можем напрямую посмотреть, как он реализован внутри . Сам собственный метод не реализован в java. Если вы хотите увидеть исходный код, вы можете загрузить только полный исходный код jdk. JDK Oracle не виден. OpenJDK или другая JRE с открытым исходным кодом могут найти соответствующий код C/C++. Находим в OpenJDKObject.cфайл, вы можете видеть, что метод hashCode указывает наJVM_IHashCodeметод обработки.

1static JNINativeMethod methods[] = {
2    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
3    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
4    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
5    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
6    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
7};

иJVM_IHashCodeметод реализован вjvm.cppопределяется как:

1JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))  
2  JVMWrapper("JVM_IHashCode");  
3  // as implemented in the classic virtual machine; return 0 if object is NULL  
4  return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;  
5JVM_END 

Вот троичное выражение, реальный расчет для получения значения hashCodeObjectSynchronizer::FastHashCode, который специально реализован вsynchronizer.cpp, перехватите некоторые ключевые фрагменты кода.

 1intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
2  if (UseBiasedLocking) {
3
4  ......
5
6  // Inflate the monitor to set hash code
7  monitor = ObjectSynchronizer::inflate(Self, obj);
8  // Load displaced header and check it has hash code
9  mark = monitor->header();
10  assert (mark->is_neutral(), "invariant") ;
11  hash = mark->hash();
12  if (hash == 0) {
13    hash = get_next_hash(Self, obj);
14    temp = mark->copy_set_hash(hash); // merge hash code into header
15    assert (temp->is_neutral(), "invariant") ;
16    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
17    if (test != mark) {
18      // The only update to the header in the monitor (outside GC)
19      // is install the hash code. If someone add new usage of
20      // displaced header, please update this code
21      hash = test->hash();
22      assert (test->is_neutral(), "invariant") ;
23      assert (hash != 0, "Trivial unexpected object/monitor header usage.");
24    }
25  }
26  // We finally get the hash
27  return hash;
28}

Из приведенного выше фрагмента кода видно, что фактический расчет hashCodeget_next_hash, также в этом документе мы ищемget_next_hash, получить его код ключа.

 1static inline intptr_t get_next_hash(Thread * Self, oop obj) {
2  intptr_t value = 0 ;
3  if (hashCode == 0) {
4     // This form uses an unguarded global Park-Miller RNG,
5     // so it's possible for two threads to race and generate the same RNG.
6     // On MP system we'll have lots of RW access to a global, so the
7     // mechanism induces lots of coherency traffic.
8     value = os::random() ;
9  } else
10  if (hashCode == 1) {
11     // This variation has the property of being stable (idempotent)
12     // between STW operations.  This can be useful in some of the 1-0
13     // synchronization schemes.
14     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
15     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
16  } else
17  if (hashCode == 2) {
18     value = 1 ;            // for sensitivity testing
19  } else
20  if (hashCode == 3) {
21     value = ++GVars.hcSequence ;
22  } else
23  if (hashCode == 4) {
24     value = cast_from_oop<intptr_t>(obj) ;
25  } else {
26     // Marsaglia's xor-shift scheme with thread-specific state
27     // This is probably the best overall implementation -- we'll
28     // likely make this the default in future releases.
29     unsigned t = Self->_hashStateX ;
30     t ^= (t << 11) ;
31     Self->_hashStateX = Self->_hashStateY ;
32     Self->_hashStateY = Self->_hashStateZ ;
33     Self->_hashStateZ = Self->_hashStateW ;
34     unsigned v = Self->_hashStateW ;
35     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
36     Self->_hashStateW = v ;
37     value = v ;
38  }
39
40  value &= markOopDesc::hash_mask;
41  if (value == 0) value = 0xBAD ;
42  assert (value != markOopDesc::no_hash, "invariant") ;
43  TEVENT (hashCode: GENERATE) ;
44  return value;
45}

отget_next_hashИз метода видно, что если начать отсчет с 0, то здесь 6 схем вычисления хеш-значения, включая самоувеличающуюся последовательность, случайное число, ассоциированный адрес памяти и т. д. Официально по умолчанию используется последняя. , генерация случайных чисел. Видно, что hashCode может быть связан с адресом памяти, но напрямую адрес памяти не представляет, это зависит от версии и настроек виртуальной машины.

равно и hashCode

Методы equals и hashCode принадлежат классу Object, а содержимое, выводимое методом toString в классе Object, также включает шестнадцатеричное значение hashCode без знака.

1public String toString() {
2    return getClass().getName() + "@" + Integer.toHexString(hashCode());
3}

Поскольку нам нужно сравнить содержимое объекта, мы обычно переопределяем метод equals,Но переписывание метода equals также требует переписывания метода hashCode Вы когда-нибудь задумывались, почему?

В противном случае это нарушит общее соглашение hashCode и не позволит этому классу правильно работать со всеми коллекциями на основе хэшей, включая HashMap и HashSet.

здесьобщая конвенция, из комментариев метода hashCode класса Object можно понять, что он в основном включает в себя следующие аспекты:

  • Во время выполнения приложения метод hashCode должен всегда возвращать одно и то же значение при нескольких вызовах одного и того же объекта, если информация, используемая для операции сравнения метода equals объекта, не была изменена.

  • Если два объекта сравниваются как равные в соответствии с методом equals, то вызов метода hashCode для обоих объектов должен дать один и тот же целочисленный результат.

  • Если два объекта сравниваются неравными в соответствии с методом equals, то вызывающая сторона метода hashCode в двух объектах не обязательно требует, чтобы метод hashCode давал разные результаты. Но можно улучшить производительность хеш-таблиц, генерируя разные целочисленные значения хеш-функции для неравных объектов.

Теоретически, если вы переопределяете метод equals, не переопределяя метод hashCode, это нарушает второй пункт вышеуказанного соглашения.Равные объекты должны иметь одинаковые хэш-значения.

Но правила — это молчаливое согласие всех, если мы предпочитаем идти другим путем и не переопределяем метод hashCode после переопределения метода equals, будут ли какие-то последствия?

Мы настраиваем класс Student и переопределяем метод equals, но не переопределяем метод hashCode, поэтому при вызове метода hashCode класса Student по умолчанию вызывается метод hashCode объекта суперкласса, а целое число возвращается в соответствии с значение типа случайного числа.

 1public class Student {
2
3    private String name;
4
5    private String gender;
6
7    public Student(String name, String gender) {
8        this.name = name;
9        this.gender = gender;
10    }
11
12    //省略 Setter,Gettter
13
14    @Override
15    public boolean equals(Object anObject) {
16        if (this == anObject) {
17            return true;
18        }
19        if (anObject instanceof Student) {
20            Student anotherStudent = (Student) anObject;
21
22            if (this.getName() == anotherStudent.getName()
23                    || this.getGender() == anotherStudent.getGender())
24                return true;
25        }
26        return false;
27    }
28}

Мы создаем два объекта и устанавливаем значения свойств одинаковыми, и тестируем результаты:

1public static void main(String[] args) {
2
3    Student student1 = new Student("小明", "male");
4    Student student2 = new Student("小明", "male");
5
6    System.out.println("equals结果:" + student1.equals(student2));
7    System.out.println("对象1的散列值:" + student1.hashCode() + ",对象2的散列值:" + student2.hashCode());
8}

Полученные результаты

1equals结果:true
2对象1的散列值:1058025095,对象2的散列值:665576141

Мы переписали метод equals, чтобы судить о том, равно ли содержимое объекта по атрибутам имени и пола, но hashCode печатает два неравных целочисленных значения, потому что он вызывает метод hashCode класса Object.

Если этот объект хранится в HashMap и объект используется в качестве ключа, учащиеся, знакомые с принципом HashMap, должны знать, что HashMap состоит из структуры массива + связанного списка.В результате, поскольку их хэш-коды не равны, они размещаются в разных индексах массива. , когда мы запрашиваем по ключу, результат нулевой.

 1public static void main(String[] args) {
2
3    Student student1 = new Student("小明", "male");
4    Student student2 = new Student("小明", "male");
5
6    HashMap<Student, String> hashMap = new HashMap<>();
7    hashMap.put(student1, "小明");
8
9    String value = hashMap.get(student2);
10    System.out.println(value); 
11}

выходной результат

1null

Нас точно не устраивают полученные результаты, хотя адреса памяти у student1 и student2 разные, но их логическое содержимое одинаковое, и мы думаем, что они должны быть одинаковыми.

Если здесь сложно понять, друзья-обезьяны могут заменить класс Student на класс String и подумать об этом. Класс String часто используется в качестве значения Key для HashMap. Представьте, если класс String только переписывает метод equals, не переписывая Метод HashCode, здесь поместите строкуnew String("s")в качестве ключа, а затем поставить значение, но затем в соответствии сnew String("s")Когда вы переходите к Get, вы получаете нулевой результат, что неприемлемо.

Итак, будь то теоретическое соглашение или практическое программирование, когда мы переписываем метод equals, нам всегда приходится переписывать метод hashCode, пожалуйста, помните об этом..

Хотя метод hashCode был переопределен, если мы хотим получить хеш-код в исходном классе Object, мы можем передатьSystem.identityHashCode(Object a)Для получения этот метод возвращает значение метода hashCode объекта по умолчанию, даже если метод hashCode объекта переопределен, это не влияет.

1public static native int identityHashCode(Object x);

Суммировать

Если HashCode не является адресом памяти, как получить адрес памяти в Java? Я поискал и обнаружил, что прямого доступного метода нет.

Если подумать об этом позже, может быть, это потому, что авторы языка Java считают, что нет необходимости напрямую получать адрес памяти, потому что Java — это язык высокого уровня.По сравнению с машинным языком ассемблера или языком C, он более абстрактный и скрывает сложность, потому что все-таки дело в дальнейшей инкапсуляции на основе C и C++. Более того, из-за механизма автоматической сборки мусора и проблемы генерации возраста объекта адрес объекта в Java будет меняться, поэтому получать актуальный адрес памяти не имеет смысла.

Разумеется, вышеизложенное является мнением самого блогера.Если у вас есть другие разные мнения или мнения, вы можете оставить сообщение и обсудить его вместе.


Личный общедоступный номер: Сяо Цай И Ню

Добро пожаловать, нажмите и удерживайте изображение, чтобы подписаться на общедоступный номер: Xiao Cai Yi Niu!

Регулярно предоставлять вам пояснения и анализ связанных технологий первоклассных интернет-компаний, таких как распределенные и микросервисы.