hashcode(),equal()方法深入解析

让人想犯罪 __ 提交于 2020-02-25 11:05:43

首先,想要明白hashCode的作用,必须要先知道Java中的集合。  

总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。 前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。

那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢? 这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。 也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。  

于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。 哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。  

这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。 所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。 

所以,Java对于eqauls方法和hashCode方法是这样规定的:

1、如果两个对象相同,那么它们的hashCode值一定要相同;

2、如果两个对象的hashCode相同,它们并不一定相同(上面说的对象相同指的是用eqauls方法比较。)  

你当然可以不按要求去做了,但你会发现,相同的对象可以出现在Set集合中。同时,增加新元素的效率会大大下降。

hashcode这个方法是用来鉴定2个对象是否相等的。 那你会说,不是还有equals这个方法吗? 不错,这2个方法都是用来判断2个对象是否相等的。但是他们是有区别的。 一般来讲,equals这个方法是给用户调用的,如果你想判断2个对象是否相等,你可以重写equals方法,然后在代码中调用,就可以判断他们是否相等 了。简单来讲,equals方法主要是用来判断从表面上看或者从内容上看,2个对象是不是相等。

举个例子,有个学生类,属性只有姓名和性别,那么我们可以 认为只要姓名和性别相等,那么就说这2个对象是相等的。 hashcode方法一般用户不会去调用,比如在hashmap中,由于key是不可以重复的,他在判断key是不是重复的时候就判断了hashcode 这个方法,而且也用到了equals方法。这里不可以重复是说equals和hashcode只要有一个不等就可以了!所以简单来讲,hashcode相 当于是一个对象的编码,就好像文件中的md5,他和equals不同就在于他返回的是int型的,比较起来不直观。我们一般在覆盖equals的同时也要 覆盖hashcode,让他们的逻辑一致。举个例子,还是刚刚的例子,如果姓名和性别相等就算2个对象相等的话,那么hashcode的方法也要返回姓名 的hashcode值加上性别的hashcode值,这样从逻辑上,他们就一致了。 要从物理上判断2个对象是否相等,用==就可以了。

在Java语言中,equals()和hashCode()两个函数的使用是紧密配合的,你要是自己设计其中一个,就要设计另外一个。在多数情况 下,这两个函数是不用考虑的,直接使用它们的默认设计就可以了。但是在一些情况下,这两个函数最好是自己设计,才能确保整个程序的正常运行。最常见的是当 一个对象被加入收集对象(collection object)时,这两个函数必须自己设计。更细化的定义是:如果你想将一个对象A放入另一个收集对象B里,或者使用这个对象A为查找一个元对象在收集对 象B里位置的钥匙,并支持是否容纳,删除收集对象B里的元对象这样的操作,那么,equals()和hashCode()函数必须开发者自己定义。其他情 况下,这两个函数是不需要定义的。

equals():

它是用于进行两个对象的比较的,是对象内容的比较,当然也能用于进行对象参阅值的比较。什么是对象参阅值的比较?就是两个参阅变量的值得比较,我们 都知道参阅变量的值其实就是一个数字,这个数字可以看成是鉴别不同对象的代号。两个对象参阅值的比较,就是两个数字的比较,两个代号的比较。这种比较是默 认的对象比较方式,在Object这个对象中,这种方式就已经设计好了。所以你也不用自己来重写,浪费不必要的时间。

对象内容的比较才是设计equals()的真正目的,Java语言对equals()的要求如下,这些要求是必须遵循的。否则,你就不该浪费时间:

•对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。

•反射性:x.equals(x)必须返回是“true”。

•类推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。

•还有一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。

•任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。

hashCode():
这个函数返回的就是一个用来进行赫希操作的整型代号,请不要把这个代号和前面所说的参阅变量所代表的代号弄混了。后者不仅仅是个代号还具有在内存中才查找对 象的位置的功能。hashCode()所返回的值是用来分类对象在一些特定的收集对象中的位置。这些对象是HashMap, Hashtable, HashSet,等等。这个函数和上面的equals()函数必须自己设计,用来协助HashMap, Hashtable, HashSet,等等对自己所收集的大量对象进行搜寻和定位。

这些收集对象究竟如何工作的,想象每个元对象hashCode是一个箱子的 编码,按照编码,每个元对象就是根据hashCode()提供的代号归入相应的箱子里。所有的箱子加起来就是一个HashSet,HashMap,或 Hashtable对象,我们需要寻找一个元对象时,先看它的代码,就是hashCode()返回的整型值,这样我们找到它所在的箱子,然后在箱子里,每 个元对象都拿出来一个个和我们要找的对象进行对比,如果两个对象的内容相等,我们的搜寻也就结束。这种操作需要两个重要的信息,一是对象的 hashCode(),还有一个是对象内容对比的结果。

hashCode()的返回值和equals()的关系如下:

•如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。

•如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。

为什么这两个规则是这样的,原因其实很简单,拿HashSet来说吧,HashSet可以拥有一个或更多的箱子,在同一个箱子中可以有一个 或更多的独特元对象(HashSet所容纳的必须是独特的元对象)。这个例子说明一个元对象可以和其他不同的元对象拥有相同的hashCode。但是一个 元对象只能和拥有同样内容的元对象相等。所以这两个规则必须成立。

设计这两个函数所要注意到的:
如果你设计的对象类型并不使用于收集性对象,那么没有必要自己再设计这两个函数的处理方式。这是正确的面向对象设计方法,任何用户一时用不到的功能,就先不要设计,以免给日后功能扩展带来麻烦。

如果你在设计时想别出心裁,不遵守以上的两套规则,那么劝你还是不要做这样想入非非的事。我还没有遇到过哪一个开发者和我说设计这两个函数要违背前面说的两个规则,我碰到这些违反规则的情况时,都是作为设计错误处理。

当一个对象类型作为收集型对象的元对象时,这个对象应该拥有自己处理equals(),和/或处理hashCode()的设计,而且要遵守前面所说 的两种原则。equals()先要查null和是否是同一类型。查同一类型是为了避免出现ClassCastException这样的异常给丢出来。查 null是为了避免出现NullPointerException这样的异常给丢出来。

如果你的对象里面容纳的数据过多,那么这两个函数 equals()和hashCode()将会变得效率低。如果对象中拥有无法serialized的数据,equals()有可能在操作中出现错误。想象 一个对象x,它的一个整型数据是transient型(不能被serialize成二进制数据流)。然而equals()和hashCode()都有依靠 这个整型数据,那么,这个对象在serialization之前和之后,是否一样?答案是不一样。因为serialization之前的整型数据是有效的 数据,在serialization之后,这个整型数据的值并没有存储下来,再重新由二进制数据流转换成对象后,两者(对象在serialization 之前和之后)的状态已经不同了。这也是要注意的。

知道以上这些能够帮助你:

1. 进行更好的设计和开发。

2. 进行更好的测试案例开发。

3. 在面试过程中让面试者对你的学识渊博感到满意。

您可能感兴趣的文章:

 

数组的特点:寻址容易,插入和删除困难。

链表的特点是:寻址困难,插入和删除容易。

ArrayList的底层实现就是通过动态数组来实现的,LinkedLIst底层实现就是通过链表来实现的,所以直接答出数组和链表的特点就ok

面试题: hashMap是怎样实现key-value这样键值对的保存?

HashMap中有一个内部类Entry,

 

1
2
3
4
5
6
7
static class Entry<k,v> implements Map.Entry<k,v> {
        final K key;
        V value;
        Entry<k,v> next;
        int hash;
        //.....
}</k,v></k,v></k,v>

主要有4个属性,key ,hash,value,指向下一个节点的引用next ,看到这个实体类就明白了,在HashMap中存放的key-value实质是通过实体类Entry来保存的

 

面试题: hashMap的实现原理?

HashMap使用到的数据类型主要就是数组和链表,首先看原理图

 

在hashMap的原理图中,左边是通过数组来存放链表的第一个节点,看懂这个图这个问题就ok

面试题: hashMap的put过程?

面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。比如说: 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。也就是说数组中存储的是最后插入的元素。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<k,v> e = table[i]; e != null; e = e.next) {//循环判断插入的key是否已经存在,若存在就更新key对应的value
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
 
    modCount++;
    addEntry(hash, key, value, i);//key不存在,那么插入新的key-value
    return null;
}</k,v>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
 
    createEntry(hash, key, value, bucketIndex);
}
 
void createEntry(int hash, K key, V value, int bucketIndex) {//这个方法就验证了上面说的<strong>数组中存储的是最后插入的元素</strong>
    Entry<k,v> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}</k,v>

面试题: hashMap的get过程?
这个过程比较简单,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        //先定位到数组元素,再遍历该元素处的链表
        for (Entry<k,v> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
}</k,v>

面试题: hashMap存取的时候是如何定位数组下标的?
找到indexFor这个方法,hashMap就是通过这个方法获取数组下标的:

 

 

1
2
3
static int indexFor(int h, int length) {
        return h & (length-1);
    }

通过key的hash值和数组长度求&,这意味着数组下标相同,并不表示hashCode相同。所以在Entry中存有一个hash值,在比较Entry的时候都是想比较hash值
面试题: hashMap中数组的初始化大小过程?

 

找到hashMap的构造方法:

 

1
2
3
4
5
6
7
8
9
10
public HashMap(int initialCapacity, float loadFactor) {
       .....// Find a power of 2 >= initialCapacity
       int capacity = 1;
       while (capacity < initialCapacity)
           capacity <<= 1//相当于capacity = capacity * 2
       this.loadFactor = loadFactor;
       threshold = (int)(capacity * loadFactor);
       table = new Entry[capacity];
       init();
   }

从上面的代码我们可以看出数组的初始大小并不是构造函数中的initialCapacity!!而是2的n次方
面试题: hashMap什么时候开始rehash?
在hashMap中有一个加载因子loadFactor,默认值是0.75,当数组的实际存入值的大小 > 数组的长度×loadFactor 时候就会rehash,重新创建一个新的表,将原表的映射到新表中,这个过程很费时。

 

 

9.list移除某个特定值

package com.bjs.test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class TestList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        list.remove("a");
        Iterator<String> iter = list.iterator();
        while (iter.hasNext()) {
            String s = iter.next();
            if (s.equals("a")) {
                iter.remove();
            }
        }
        for (String ele : list) {
            System.out.println(ele);
        }
    }
}

map 的iterate 里面移除里面的key  再获得map.get()会拿到值吗
Student类可以做map的key值吗 如果能请写出Student的类
null可以做map的key吗?

 

6.为什么String, Interger这样的wrapper类适合作为键?

String, Interger这样的wrapper类是final类型的,具有不可变性,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。 

 

7.ConcurrentHashMap和Hashtable的区别

Hashtable和ConcurrentHashMap有什么分别呢?它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

 

 

8.HashMap的遍历

第一种:
  Map map = new HashMap();
  Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
  Map.Entry entry = (Map.Entry) iter.next();
  Object key = entry.getKey();
  Object val = entry.getValue();
  }
  效率高,以后一定要使用此种方式!
第二种:
  Map map = new HashMap();
  Iterator iter = map.keySet().iterator();
  while (iter.hasNext()) {
  Object key = iter.next();

Object val = map.get(key);
  }
  效率低,以后尽量少使用!

可是为什么第一种比第二种方法效率更高呢?

HashMap这两种遍历方法是分别对keyset及entryset来进行遍历,但是对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,它把key和value都放到了entry中,即键值对,所以就快了。

1.HashMap与Hashtable的区别:

HashMap可以接受null键值和值,而Hashtable则不能。

Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。

 

5.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办 

HashMap默认的负载因子大小为0.75,也就是说,当一个map填满了75%的空间的时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的数组,来重新调整map的大小,并将原来的对象放入新的数组中。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!